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