sf_api/command.rs
1#![allow(deprecated)]
2use enum_map::Enum;
3use log::warn;
4use num_derive::FromPrimitive;
5use strum::EnumIter;
6
7use crate::{
8 PlayerId,
9 gamestate::{
10 character::*,
11 dungeons::{CompanionClass, Dungeon},
12 fortress::*,
13 guild::{Emblem, GuildSkill},
14 idle::IdleBuildingType,
15 items::*,
16 social::Relationship,
17 underworld::*,
18 unlockables::{
19 EnchantmentIdent, HabitatType, HellevatorTreatType, Unlockable,
20 },
21 },
22};
23
24#[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, Default)]
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 #[default]
830 Weapon = 3,
831 Magic = 4,
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
835#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
836#[allow(missing_docs)]
837/// The "currency" you want to use to skip a quest
838pub enum TimeSkip {
839 Mushroom = 1,
840 Glass = 2,
841}
842
843impl Command {
844 /// Returns the unencrypted string, that has to be send to the server to to
845 /// perform the request
846 #[allow(deprecated, clippy::useless_format)]
847 #[cfg(feature = "session")]
848 pub(crate) fn request_string(
849 &self,
850 ) -> Result<String, crate::error::SFError> {
851 const APP_VERSION: &str = "2700000000000";
852 use crate::{
853 error::SFError,
854 gamestate::dungeons::{LightDungeon, ShadowDungeon},
855 misc::{HASH_CONST, sha1_hash, to_sf_string},
856 };
857
858 Ok(match self {
859 Command::Custom {
860 cmd_name,
861 arguments: values,
862 } => {
863 format!("{cmd_name}:{}", values.join("/"))
864 }
865 Command::Login {
866 username,
867 pw_hash,
868 login_count,
869 } => {
870 let full_hash = sha1_hash(&format!("{pw_hash}{login_count}"));
871 format!(
872 "AccountLogin:{username}/{full_hash}/{login_count}/\
873 unity3d_webglplayer//{APP_VERSION}///0/"
874 )
875 }
876 #[cfg(feature = "sso")]
877 Command::SSOLogin {
878 uuid, character_id, ..
879 } => format!(
880 "SFAccountCharLogin:{uuid}/{character_id}/unity3d_webglplayer/\
881 /{APP_VERSION}"
882 ),
883 Command::Register {
884 username,
885 password,
886 gender,
887 race,
888 class,
889 } => {
890 // TODO: Custom portrait
891 format!(
892 "AccountCreate:{username}/{password}/{username}@playa.sso/\
893 {}/{}/{}/8,203,201,6,199,3,1,2,1/0//en",
894 *gender as usize + 1,
895 *race as usize,
896 *class as usize + 1
897 )
898 }
899 Command::Update => "Poll:".to_string(),
900 Command::HallOfFamePage { page } => {
901 let per_page = 51;
902 let pos = 26 + (per_page * page);
903 format!("PlayerGetHallOfFame:{pos}//25/25")
904 }
905 Command::HallOfFameFortressPage { page } => {
906 let per_page = 51;
907 let pos = 26 + (per_page * page);
908 format!("FortressGetHallOfFame:{pos}//25/25")
909 }
910 Command::HallOfFameGroupPage { page } => {
911 let per_page = 51;
912 let pos = 26 + (per_page * page);
913 format!("GroupGetHallOfFame:{pos}//25/25")
914 }
915 Command::HallOfFameUnderworldPage { page } => {
916 let per_page = 51;
917 let pos = 26 + (per_page * page);
918 format!("UnderworldGetHallOfFame:{pos}//25/25")
919 }
920 Command::HallOfFamePetsPage { page } => {
921 let per_page = 51;
922 let pos = 26 + (per_page * page);
923 format!("PetsGetHallOfFame:{pos}//25/25")
924 }
925 Command::ViewPlayer { ident } => format!("PlayerLookAt:{ident}"),
926 Command::BuyBeer => format!("PlayerBeerBuy:"),
927 Command::StartQuest {
928 quest_pos,
929 overwrite_inv,
930 } => {
931 format!(
932 "PlayerAdventureStart:{}/{}",
933 quest_pos + 1,
934 u8::from(*overwrite_inv)
935 )
936 }
937 Command::CancelQuest => format!("PlayerAdventureStop:"),
938 Command::FinishQuest { skip } => {
939 format!(
940 "PlayerAdventureFinished:{}",
941 skip.map(|a| a as u8).unwrap_or(0)
942 )
943 }
944 Command::StartWork { hours } => format!("PlayerWorkStart:{hours}"),
945 Command::CancelWork => format!("PlayerWorkStop:"),
946 Command::FinishWork => format!("PlayerWorkFinished:"),
947 Command::CheckNameAvailable { name } => {
948 format!("AccountCheck:{name}")
949 }
950 Command::BuyMount { mount } => {
951 format!("PlayerMountBuy:{}", *mount as usize)
952 }
953 Command::IncreaseAttribute {
954 attribute,
955 increase_to,
956 } => format!(
957 "PlayerAttributIncrease:{}/{increase_to}",
958 *attribute as u8
959 ),
960 Command::RemovePotion { pos } => {
961 format!("PlayerPotionKill:{}", pos + 1)
962 }
963 Command::CheckArena => format!("PlayerArenaEnemy:"),
964 Command::Fight { name, use_mushroom } => {
965 format!("PlayerArenaFight:{name}/{}", u8::from(*use_mushroom))
966 }
967 Command::CollectCalendar => format!("PlayerOpenCalender:"),
968 Command::UpgradeSkill {
969 attribute,
970 next_attribute,
971 } => format!(
972 "PlayerAttributIncrease:{}/{next_attribute}",
973 *attribute as i64
974 ),
975 Command::RefreshShop { shop } => {
976 format!("PlayerNewWares:{}", *shop as usize - 2)
977 }
978 Command::ViewGuild { guild_ident } => {
979 format!("GroupLookAt:{guild_ident}")
980 }
981 Command::GuildFound { name } => format!("GroupFound:{name}"),
982 Command::GuildInvitePlayer { name } => {
983 format!("GroupInviteMember:{name}")
984 }
985 Command::GuildKickPlayer { name } => {
986 format!("GroupRemoveMember:{name}")
987 }
988 Command::GuildSetLeader { name } => {
989 format!("GroupSetLeader:{name}")
990 }
991 Command::GuildToggleOfficer { name } => {
992 format!("GroupSetOfficer:{name}")
993 }
994 Command::GuildLoadMushrooms => {
995 format!("GroupIncreaseBuilding:0")
996 }
997 Command::GuildIncreaseSkill { skill, current } => {
998 format!("GroupSkillIncrease:{}/{current}", *skill as usize)
999 }
1000 Command::GuildJoinAttack => format!("GroupReadyAttack:"),
1001 Command::GuildJoinDefense => format!("GroupReadyDefense:"),
1002 Command::GuildAttack { guild } => {
1003 format!("GroupAttackDeclare:{guild}")
1004 }
1005 Command::GuildRaid => format!("GroupRaidDeclare:"),
1006 Command::ToiletFlush => format!("PlayerToilettFlush:"),
1007 Command::ToiletOpen => format!("PlayerToilettOpenWithKey:"),
1008 Command::FightTower {
1009 current_level: progress,
1010 use_mush,
1011 } => {
1012 format!("PlayerTowerBattle:{progress}/{}", u8::from(*use_mush))
1013 }
1014 Command::ToiletDrop { inventory, pos } => {
1015 format!("PlayerToilettLoad:{}/{}", *inventory as usize, pos + 1)
1016 }
1017 Command::GuildPortalBattle => format!("GroupPortalBattle:"),
1018 Command::GuildGetFightableTargets => {
1019 format!("GroupFightableTargets:")
1020 }
1021 Command::FightPortal => format!("PlayerPortalBattle:"),
1022 Command::MessageOpen { pos: index } => {
1023 format!("PlayerMessageView:{}", *index + 1)
1024 }
1025 Command::MessageDelete { pos: index } => format!(
1026 "PlayerMessageDelete:{}",
1027 match index {
1028 -1 => -1,
1029 x => *x + 1,
1030 }
1031 ),
1032 Command::ViewScrapbook => format!("PlayerPollScrapbook:"),
1033 Command::ViewPet { pet_id: pet_index } => {
1034 format!("PetsGetStats:{pet_index}")
1035 }
1036 Command::BuyShop {
1037 shop_type,
1038 shop_pos,
1039 inventory,
1040 inventory_pos,
1041 } => format!(
1042 "PlayerItemMove:{}/{}/{}/{}",
1043 *shop_type as usize,
1044 *shop_pos + 1,
1045 *inventory as usize,
1046 *inventory_pos + 1
1047 ),
1048 Command::SellShop {
1049 inventory,
1050 inventory_pos,
1051 } => {
1052 let mut rng = fastrand::Rng::new();
1053 let shop = if rng.bool() {
1054 ShopType::Magic
1055 } else {
1056 ShopType::Weapon
1057 };
1058 let shop_pos = rng.u32(0..6);
1059 format!(
1060 "PlayerItemMove:{}/{}/{}/{}",
1061 *inventory as usize,
1062 *inventory_pos + 1,
1063 shop as usize,
1064 shop_pos + 1,
1065 )
1066 }
1067 Command::InventoryMove {
1068 inventory_from,
1069 inventory_from_pos,
1070 inventory_to,
1071 inventory_to_pos,
1072 } => format!(
1073 "PlayerItemMove:{}/{}/{}/{}",
1074 *inventory_from as usize,
1075 *inventory_from_pos + 1,
1076 *inventory_to as usize,
1077 *inventory_to_pos + 1
1078 ),
1079 Command::ItemMove {
1080 from,
1081 from_pos,
1082 to,
1083 to_pos,
1084 } => format!(
1085 "PlayerItemMove:{}/{}/{}/{}",
1086 *from as usize,
1087 *from_pos + 1,
1088 *to as usize,
1089 *to_pos + 1
1090 ),
1091 Command::UsePotion { from, from_pos } => {
1092 format!(
1093 "PlayerItemMove:{}/{}/1/0/",
1094 *from as usize,
1095 *from_pos + 1
1096 )
1097 }
1098 Command::UnlockFeature { unlockable } => format!(
1099 "UnlockFeature:{}/{}",
1100 unlockable.main_ident, unlockable.sub_ident
1101 ),
1102 Command::GuildSetInfo {
1103 description,
1104 emblem,
1105 } => format!(
1106 "GroupSetDescription:{}ยง{}",
1107 emblem.server_encode(),
1108 to_sf_string(description)
1109 ),
1110 Command::SetDescription { description } => {
1111 format!("PlayerSetDescription:{}", &to_sf_string(description))
1112 }
1113 Command::GuildSendChat { message } => {
1114 format!("GroupChat:{}", &to_sf_string(message))
1115 }
1116 Command::GambleSilver { amount } => {
1117 format!("PlayerGambleGold:{amount}")
1118 }
1119 Command::GambleMushrooms { amount } => {
1120 format!("PlayerGambleCoins:{amount}")
1121 }
1122 Command::SendMessage { to, msg } => {
1123 format!("PlayerMessageSend:{to}/{}", to_sf_string(msg))
1124 }
1125 Command::WitchDropCauldron {
1126 inventory_t,
1127 position,
1128 } => format!(
1129 "PlayerWitchSpendItem:{}/{}",
1130 *inventory_t as usize,
1131 position + 1
1132 ),
1133 Command::Blacksmith {
1134 inventory_t,
1135 position,
1136 action,
1137 } => format!(
1138 "PlayerItemMove:{}/{}/{}/-1",
1139 *inventory_t as usize,
1140 position + 1,
1141 *action as usize
1142 ),
1143 Command::WitchEnchant { enchantment } => {
1144 format!("PlayerWitchEnchantItem:{}/1", enchantment.0)
1145 }
1146 Command::SpinWheelOfFortune {
1147 payment: fortune_payment,
1148 } => {
1149 format!("WheelOfFortune:{}", *fortune_payment as usize)
1150 }
1151 Command::FortressGather { resource } => {
1152 format!("FortressGather:{}", *resource as usize + 1)
1153 }
1154 Command::FortressGatherSecretStorage { stone, wood } => {
1155 format!("FortressGatherTreasure:{wood}/{stone}")
1156 }
1157 Command::EquipCompanion {
1158 from_inventory,
1159 from_pos,
1160 to_slot,
1161 to_companion,
1162 } => format!(
1163 "PlayerItemMove:{}/{}/{}/{}",
1164 *from_inventory as usize,
1165 *from_pos,
1166 *to_companion as u8 + 101,
1167 *to_slot as usize
1168 ),
1169 Command::FortressBuild { f_type } => {
1170 format!("FortressBuildStart:{}/0", *f_type as usize + 1)
1171 }
1172 Command::FortressBuildCancel { f_type } => {
1173 format!("FortressBuildStop:{}", *f_type as usize + 1)
1174 }
1175 Command::FortressBuildFinish { f_type, mushrooms } => format!(
1176 "FortressBuildFinished:{}/{mushrooms}",
1177 *f_type as usize + 1
1178 ),
1179 Command::FortressBuildUnit { unit, count } => {
1180 format!("FortressBuildUnitStart:{}/{count}", *unit as usize + 1)
1181 }
1182 Command::FortressGemStoneSearch => {
1183 format!("FortressGemstoneStart:",)
1184 }
1185 Command::FortressGemStoneSearchCancel => {
1186 format!("FortressGemStoneStop:0")
1187 }
1188 Command::FortressGemStoneSearchFinish { mushrooms } => {
1189 format!("FortressGemstoneFinished:{mushrooms}",)
1190 }
1191 Command::FortressAttack { soldiers } => {
1192 format!("FortressAttack:{soldiers}")
1193 }
1194 Command::FortressNewEnemy { use_mushroom: pay } => {
1195 format!("FortressEnemy:{}", usize::from(*pay))
1196 }
1197 Command::FortressSetCAEnemy { msg_id } => {
1198 format!("FortressEnemy:0/{}", *msg_id)
1199 }
1200 Command::FortressUpgradeHallOfKnights => {
1201 format!("FortressGroupBonusUpgrade:")
1202 }
1203 Command::Whisper {
1204 player_name: player,
1205 message,
1206 } => format!(
1207 "PlayerMessageWhisper:{}/{}",
1208 player,
1209 to_sf_string(message)
1210 ),
1211 Command::UnderworldCollect {
1212 resource: resource_t,
1213 } => {
1214 format!("UnderworldGather:{}", *resource_t as usize + 1)
1215 }
1216 Command::UnderworldUnitUpgrade { unit: unit_t } => {
1217 format!("UnderworldUpgradeUnit:{}", *unit_t as usize + 1)
1218 }
1219 Command::UnderworldUpgradeStart {
1220 building,
1221 mushrooms,
1222 } => format!(
1223 "UnderworldBuildStart:{}/{mushrooms}",
1224 *building as usize + 1
1225 ),
1226 Command::UnderworldUpgradeCancel { building } => {
1227 format!("UnderworldBuildStop:{}", *building as usize + 1)
1228 }
1229 Command::UnderworldUpgradeFinish {
1230 building,
1231 mushrooms,
1232 } => {
1233 format!(
1234 "UnderworldBuildFinished:{}/{mushrooms}",
1235 *building as usize + 1
1236 )
1237 }
1238 Command::UnderworldAttack { player_id } => {
1239 format!("UnderworldAttack:{player_id}")
1240 }
1241 Command::RollDice { payment, dices } => {
1242 let mut dices = dices.iter().fold(String::new(), |mut a, b| {
1243 if !a.is_empty() {
1244 a.push('/');
1245 }
1246 a.push((*b as u8 + b'0') as char);
1247 a
1248 });
1249
1250 if dices.is_empty() {
1251 // FIXME: This is dead code, right?
1252 dices = "0/0/0/0/0".to_string();
1253 }
1254 format!("RollDice:{}/{}", *payment as usize, dices)
1255 }
1256 Command::PetFeed { pet_id, fruit_idx } => {
1257 format!("PlayerPetFeed:{pet_id}/{fruit_idx}")
1258 }
1259 Command::GuildPetBattle { use_mushroom } => {
1260 format!("GroupPetBattle:{}", usize::from(*use_mushroom))
1261 }
1262 Command::IdleUpgrade { typ: kind, amount } => {
1263 format!("IdleIncrease:{}/{}", *kind as usize, amount)
1264 }
1265 Command::IdleSacrifice => format!("IdlePrestige:0"),
1266 Command::SwapManequin => format!("PlayerDummySwap:301/1"),
1267 Command::UpdateFlag { flag } => format!(
1268 "PlayerSetFlag:{}",
1269 flag.map(Flag::code).unwrap_or_default()
1270 ),
1271 Command::BlockGuildInvites { block_invites } => {
1272 format!("PlayerSetNoGroupInvite:{}", u8::from(*block_invites))
1273 }
1274 Command::ShowTips { show_tips } => {
1275 #[allow(clippy::unreadable_literal)]
1276 {
1277 format!(
1278 "PlayerTutorialStatus:{}",
1279 if *show_tips { 0 } else { 0xFFFFFFF }
1280 )
1281 }
1282 }
1283 Command::ChangePassword { username, old, new } => {
1284 let old = sha1_hash(&format!("{old}{HASH_CONST}"));
1285 let new = sha1_hash(&format!("{new}{HASH_CONST}"));
1286 format!("AccountPasswordChange:{username}/{old}/106/{new}/")
1287 }
1288 Command::ChangeMailAddress {
1289 old_mail,
1290 new_mail,
1291 password,
1292 username,
1293 } => {
1294 let pass = sha1_hash(&format!("{password}{HASH_CONST}"));
1295 format!(
1296 "AccountMailChange:{old_mail}/{new_mail}/{username}/\
1297 {pass}/106"
1298 )
1299 }
1300 Command::SetLanguage { language } => {
1301 format!("AccountSetLanguage:{language}")
1302 }
1303 Command::SetPlayerRelation {
1304 player_id,
1305 relation,
1306 } => {
1307 format!("PlayerFriendSet:{player_id}/{}", *relation as i32)
1308 }
1309 Command::SetPortraitFrame { portrait_id } => {
1310 format!("PlayerSetActiveFrame:{portrait_id}")
1311 }
1312 Command::CollectDailyQuestReward { pos } => {
1313 format!("DailyTaskClaim:1/{}", pos + 1)
1314 }
1315 Command::CollectEventTaskReward { pos } => {
1316 format!("DailyTaskClaim:2/{}", pos + 1)
1317 }
1318 Command::SwapRunes {
1319 from,
1320 from_pos,
1321 to,
1322 to_pos,
1323 } => {
1324 format!(
1325 "PlayerSmithSwapRunes:{}/{}/{}/{}",
1326 *from as usize,
1327 *from_pos + 1,
1328 *to as usize,
1329 *to_pos + 1
1330 )
1331 }
1332 Command::ChangeItemLook {
1333 inv,
1334 pos,
1335 raw_model_id: model_id,
1336 } => {
1337 format!(
1338 "ItemChangePicture:{}/{}/{}",
1339 *inv as usize,
1340 pos + 1,
1341 model_id
1342 )
1343 }
1344 Command::ExpeditionPickEncounter { pos } => {
1345 format!("ExpeditionProceed:{}", pos + 1)
1346 }
1347 Command::ExpeditionContinue => format!("ExpeditionProceed:1"),
1348 Command::ExpeditionPickReward { pos } => {
1349 format!("ExpeditionProceed:{}", pos + 1)
1350 }
1351 Command::ExpeditionStart { pos } => {
1352 format!("ExpeditionStart:{}", pos + 1)
1353 }
1354 Command::FightDungeon {
1355 dungeon,
1356 use_mushroom,
1357 } => match dungeon {
1358 Dungeon::Light(name) => {
1359 if *name == LightDungeon::Tower {
1360 return Err(SFError::InvalidRequest(
1361 "The tower must be fought with the FightTower \
1362 command",
1363 ));
1364 }
1365 format!(
1366 "PlayerDungeonBattle:{}/{}",
1367 *name as usize + 1,
1368 u8::from(*use_mushroom)
1369 )
1370 }
1371 Dungeon::Shadow(name) => {
1372 if *name == ShadowDungeon::Twister {
1373 format!(
1374 "PlayerDungeonBattle:{}/{}",
1375 LightDungeon::Tower as u32 + 1,
1376 u8::from(*use_mushroom)
1377 )
1378 } else {
1379 format!(
1380 "PlayerShadowBattle:{}/{}",
1381 *name as u32 + 1,
1382 u8::from(*use_mushroom)
1383 )
1384 }
1385 }
1386 },
1387 Command::FightPetOpponent {
1388 opponent_id,
1389 habitat: element,
1390 } => {
1391 format!("PetsPvPFight:0/{opponent_id}/{}", *element as u32 + 1)
1392 }
1393 Command::FightPetDungeon {
1394 use_mush,
1395 habitat: element,
1396 enemy_pos,
1397 player_pet_id,
1398 } => {
1399 format!(
1400 "PetsDungeonFight:{}/{}/{enemy_pos}/{player_pet_id}",
1401 u8::from(*use_mush),
1402 *element as u8 + 1,
1403 )
1404 }
1405 Command::ExpeditionSkipWait { typ } => {
1406 format!("ExpeditionTimeSkip:{}", *typ as u8)
1407 }
1408 Command::SetQuestsInsteadOfExpeditions { value } => {
1409 let value = match value {
1410 ExpeditionSetting::PreferExpeditions => 'a',
1411 ExpeditionSetting::PreferQuests => 'b',
1412 };
1413 format!("UserSettingsUpdate:5/{value}")
1414 }
1415 Command::HellevatorEnter => format!("GroupTournamentJoin:"),
1416 Command::HellevatorViewGuildRanking => {
1417 format!("GroupTournamentRankingOwnGroup")
1418 }
1419 Command::HellevatorFight { use_mushroom } => {
1420 format!("GroupTournamentBattle:{}", u8::from(*use_mushroom))
1421 }
1422 Command::HellevatorBuy {
1423 position,
1424 typ,
1425 price,
1426 use_mushroom,
1427 } => {
1428 format!(
1429 "GroupTournamentMerchantBuy:{position}/{}/{price}/{}",
1430 *typ as u32,
1431 if *use_mushroom { 2 } else { 1 }
1432 )
1433 }
1434 Command::HellevatorRefreshShop => {
1435 format!("GroupTournamentMerchantReroll:")
1436 }
1437 Command::HallOfFameHellevatorPage { page } => {
1438 let per_page = 51;
1439 let pos = 26 + (per_page * page);
1440 format!("GroupTournamentRankingAllGroups:{pos}//25/25")
1441 }
1442 Command::HellevatorJoinHellAttack {
1443 use_mushroom,
1444 plain: pos,
1445 } => {
1446 format!(
1447 "GroupTournamentRaidParticipant:{}/{}",
1448 u8::from(*use_mushroom),
1449 *pos + 1
1450 )
1451 }
1452 Command::HellevatorClaimDaily => {
1453 format!("GroupTournamentClaimDaily:")
1454 }
1455 Command::HellevatorClaimDailyYesterday => {
1456 format!("GroupTournamentClaimDailyYesterday:")
1457 }
1458 Command::HellevatorPreviewRewards => {
1459 format!("GroupTournamentPreview:")
1460 }
1461 Command::HellevatorClaimFinal => format!("GroupTournamentClaim:"),
1462 Command::ClaimablePreview { msg_id } => {
1463 format!("PendingRewardView:{msg_id}")
1464 }
1465 Command::ClaimableClaim { msg_id } => {
1466 format!("PendingRewardClaim:{msg_id}")
1467 }
1468 Command::BuyGoldFrame => {
1469 format!("PlayerGoldFrameBuy:")
1470 }
1471 })
1472 }
1473}
1474
1475macro_rules! generate_flag_enum {
1476 ($($variant:ident => $code:expr),*) => {
1477 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
1478 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1479 #[allow(missing_docs)]
1480 /// The flag of a country, that will be visible in the Hall of Fame
1481 pub enum Flag {
1482 $(
1483 $variant,
1484 )*
1485 }
1486
1487 impl Flag {
1488 pub(crate) fn code(self) -> &'static str {
1489 match self {
1490 $(
1491 Flag::$variant => $code,
1492 )*
1493 }
1494 }
1495
1496 pub(crate) fn parse(value: &str) -> Option<Self> {
1497 if value.is_empty() {
1498 return None;
1499 }
1500
1501 // Mapping from string codes to enum variants
1502 match value {
1503 $(
1504 $code => Some(Flag::$variant),
1505 )*
1506
1507 _ => {
1508 warn!("Invalid flag value: {value}");
1509 None
1510 }
1511 }
1512 }
1513 }
1514 };
1515}
1516
1517// Use the macro to generate the Flag enum and its methods
1518// Source: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
1519generate_flag_enum! {
1520 Argentina => "ar",
1521 Australia => "au",
1522 Austria => "at",
1523 Belgium => "be",
1524 Bolivia => "bo",
1525 Brazil => "br",
1526 Bulgaria => "bg",
1527 Canada => "ca",
1528 Chile => "cl",
1529 China => "cn",
1530 Colombia => "co",
1531 CostaRica => "cr",
1532 Czechia => "cz",
1533 Denmark => "dk",
1534 DominicanRepublic => "do",
1535 Ecuador => "ec",
1536 ElSalvador =>"sv",
1537 Finland => "fi",
1538 France => "fr",
1539 Germany => "de",
1540 GreatBritain => "gb",
1541 Greece => "gr",
1542 Honduras => "hn",
1543 Hungary => "hu",
1544 India => "in",
1545 Italy => "it",
1546 Japan => "jp",
1547 Lithuania => "lt",
1548 Mexico => "mx",
1549 Netherlands => "nl",
1550 Panama => "pa",
1551 Paraguay => "py",
1552 Peru => "pe",
1553 Philippines => "ph",
1554 Poland => "pl",
1555 Portugal => "pt",
1556 Romania => "ro",
1557 Russia => "ru",
1558 SaudiArabia => "sa",
1559 Slovakia => "sk",
1560 SouthKorea => "kr",
1561 Spain => "es",
1562 Sweden => "se",
1563 Switzerland => "ch",
1564 Thailand => "th",
1565 Turkey => "tr",
1566 Ukraine => "ua",
1567 UnitedArabEmirates => "ae",
1568 UnitedStates => "us",
1569 Uruguay => "uy",
1570 Venezuela => "ve",
1571 Vietnam => "vn"
1572}