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