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