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