Skip to main content

flatland_play_loop/
actions.rs

1//! Input action and overlay menu dispatch for any [`GameClient`] session.
2
3use flatland_client_lib::{
4    AutoNavigator, ClientKeyBindings, GameClient, PlayConnection, RotationEditorMode,
5};
6use flatland_client_ui::{
7    editor_ability_choices, next_custom_preset, preset_deletable, InputAction, UiKeyCode,
8    UiKeyEvent, UiKeyEventKind,
9};
10
11pub struct ActionCtx<'a> {
12    pub block_active: &'a mut bool,
13    pub auto_nav: &'a mut Option<AutoNavigator>,
14}
15
16/// Returns true if the key was consumed by a menu handler.
17pub async fn dispatch_menu_key<S: PlayConnection>(
18    client: &mut GameClient<S>,
19    key: UiKeyEvent,
20) -> bool {
21    if key.kind != UiKeyEventKind::Press {
22        return false;
23    }
24
25    if client.state.show_worker_rename {
26        match key.code {
27            UiKeyCode::Esc => client.cancel_worker_rename(),
28            UiKeyCode::Enter => {
29                if let Err(err) = client.confirm_worker_rename().await {
30                    client.state.push_log(format!("Rename: {err}"));
31                }
32            }
33            UiKeyCode::Backspace => {
34                client.state.rename_buffer.pop();
35            }
36            UiKeyCode::Char(c) => {
37                if client.state.rename_buffer.chars().count() < 32 {
38                    client.state.rename_buffer.push(c);
39                }
40            }
41            _ => {}
42        }
43        return true;
44    }
45
46    if client.state.show_equip_menu {
47        match key.code {
48            UiKeyCode::Char('p') | UiKeyCode::Esc => {
49                client.toggle_equip_menu();
50            }
51            UiKeyCode::Up | UiKeyCode::Char('k') => {
52                if client.state.equip_menu_index > 0 {
53                    client.state.equip_menu_index -= 1;
54                }
55            }
56            UiKeyCode::Down | UiKeyCode::Char('j') => {
57                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
58                if n > 0 {
59                    client.state.equip_menu_index = (client.state.equip_menu_index + 1).min(n - 1);
60                }
61            }
62            UiKeyCode::PageUp => {
63                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
64                client.state.equip_menu_index =
65                    flatland_client_lib::page_list_index(client.state.equip_menu_index, -1, n);
66            }
67            UiKeyCode::PageDown => {
68                let n = flatland_client_lib::equip_paperdoll_rows(&client.state).len();
69                client.state.equip_menu_index =
70                    flatland_client_lib::page_list_index(client.state.equip_menu_index, 1, n);
71            }
72            UiKeyCode::Enter => {
73                if let Err(err) = client.activate_equip_selection().await {
74                    client.state.push_log(format!("Equip: {err}"));
75                }
76            }
77            _ => {}
78        }
79        return true;
80    }
81
82    if client.state.show_inventory_menu {
83        if client.state.show_rename_prompt {
84            match key.code {
85                UiKeyCode::Esc => client.cancel_rename_prompt(),
86                UiKeyCode::Enter => {
87                    if let Err(err) = client.confirm_rename_prompt().await {
88                        client.state.push_log(format!("Rename: {err}"));
89                    }
90                }
91                UiKeyCode::Backspace => {
92                    client.state.rename_buffer.pop();
93                }
94                UiKeyCode::Char(c) => {
95                    if client.state.rename_buffer.len() < 32 {
96                        client.state.rename_buffer.push(c);
97                    }
98                }
99                _ => {}
100            }
101        } else if client.state.show_destroy_picker {
102            match key.code {
103                UiKeyCode::Esc => {
104                    if client.state.destroy_confirm_pending {
105                        client.cancel_destroy_confirm();
106                    } else {
107                        client.close_destroy_picker();
108                    }
109                }
110                UiKeyCode::Char('[') | UiKeyCode::Char('-') => {
111                    if !client.state.destroy_confirm_pending {
112                        client.destroy_picker_adjust_quantity(-1);
113                    }
114                }
115                UiKeyCode::Char(']') | UiKeyCode::Char('=') => {
116                    if !client.state.destroy_confirm_pending {
117                        client.destroy_picker_adjust_quantity(1);
118                    }
119                }
120                UiKeyCode::Char('a') => {
121                    if !client.state.destroy_confirm_pending {
122                        client.destroy_picker_set_quantity_max();
123                    }
124                }
125                UiKeyCode::Enter => {
126                    if let Err(err) = client.activate_inventory_selection().await {
127                        client.state.push_log(format!("Destroy failed: {err}"));
128                    }
129                }
130                _ => {}
131            }
132        } else if client.state.show_grant_picker {
133            let filter_focused = client
134                .state
135                .grant_picker
136                .as_ref()
137                .map(|p| p.filter_focused)
138                .unwrap_or(false);
139            match key.code {
140                UiKeyCode::Esc => {
141                    if !client.clear_or_blur_inventory_filter() {
142                        client.close_grant_picker();
143                    }
144                }
145                UiKeyCode::PageUp => client.inventory_menu_page(-1),
146                UiKeyCode::PageDown => client.inventory_menu_page(1),
147                UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
148                UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
149                UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
150                UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
151                    client.inventory_menu_move(-1)
152                }
153                UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
154                    client.inventory_menu_move(1)
155                }
156                UiKeyCode::Enter if !filter_focused => {
157                    if let Err(err) = client.activate_inventory_selection().await {
158                        client.state.push_log(format!("Grant failed: {err}"));
159                    }
160                }
161                _ => {}
162            }
163        } else if client.state.show_move_picker {
164            let filter_focused = client
165                .state
166                .move_picker
167                .as_ref()
168                .map(|p| p.filter_focused)
169                .unwrap_or(false);
170            match key.code {
171                UiKeyCode::Esc => {
172                    if !client.clear_or_blur_inventory_filter() {
173                        client.close_move_picker();
174                    }
175                }
176                UiKeyCode::PageUp => client.inventory_menu_page(-1),
177                UiKeyCode::PageDown => client.inventory_menu_page(1),
178                UiKeyCode::Char('/') if !filter_focused => client.focus_inventory_filter(),
179                UiKeyCode::Backspace if filter_focused => client.inventory_filter_backspace(),
180                UiKeyCode::Char(c) if filter_focused => client.append_inventory_filter_char(c),
181                UiKeyCode::Up | UiKeyCode::Char('k') if !filter_focused => {
182                    client.inventory_menu_move(-1)
183                }
184                UiKeyCode::Down | UiKeyCode::Char('j') if !filter_focused => {
185                    client.inventory_menu_move(1)
186                }
187                UiKeyCode::Char('[') | UiKeyCode::Char('-') if !filter_focused => {
188                    client.move_picker_adjust_quantity(-1);
189                }
190                UiKeyCode::Char(']') | UiKeyCode::Char('=') if !filter_focused => {
191                    client.move_picker_adjust_quantity(1);
192                }
193                UiKeyCode::Char('a') if !filter_focused => client.move_picker_set_quantity_max(),
194                UiKeyCode::Enter if !filter_focused => {
195                    if let Err(err) = client.activate_inventory_selection().await {
196                        client.state.push_log(format!("Move failed: {err}"));
197                    }
198                }
199                _ => {}
200            }
201        } else if client.state.inventory_filter_focused {
202            match key.code {
203                UiKeyCode::Esc => {
204                    let _ = client.clear_or_blur_inventory_filter();
205                }
206                UiKeyCode::Enter => {
207                    client.state.inventory_filter_focused = false;
208                }
209                UiKeyCode::Backspace => client.inventory_filter_backspace(),
210                UiKeyCode::Char(c) => client.append_inventory_filter_char(c),
211                _ => {}
212            }
213        } else {
214            match key.code {
215                UiKeyCode::Esc => {
216                    if !client.clear_or_blur_inventory_filter() {
217                        client.close_inventory_menu();
218                    }
219                }
220                UiKeyCode::Char('b') => client.close_inventory_menu(),
221                UiKeyCode::Tab => client.cycle_inventory_tab(true),
222                UiKeyCode::BackTab => client.cycle_inventory_tab(false),
223                UiKeyCode::PageUp => client.inventory_menu_page(-1),
224                UiKeyCode::PageDown => client.inventory_menu_page(1),
225                UiKeyCode::Char('/') => client.focus_inventory_filter(),
226                UiKeyCode::Up | UiKeyCode::Char('k') => client.inventory_menu_move(-1),
227                UiKeyCode::Down | UiKeyCode::Char('j') => client.inventory_menu_move(1),
228                UiKeyCode::Enter => {
229                    if let Err(err) = client.activate_inventory_selection().await {
230                        client.state.push_log(format!("{err}"));
231                    }
232                }
233                UiKeyCode::Char('m') => {
234                    if let Err(err) = client.open_move_picker() {
235                        client.state.push_log(format!("{err}"));
236                    }
237                }
238                UiKeyCode::Char('e') => {
239                    if let Err(err) = client.use_selected_consumable().await {
240                        client.state.push_log(format!("Use: {err}"));
241                    }
242                }
243                UiKeyCode::Char('n') => {
244                    if let Err(err) = client.open_rename_prompt() {
245                        client.state.push_log(format!("{err}"));
246                    }
247                }
248                UiKeyCode::Char('d') => {
249                    if let Err(err) = client.drop_selected().await {
250                        client.state.push_log(format!("Drop: {err}"));
251                    }
252                }
253                UiKeyCode::Char('x') => {
254                    if let Err(err) = client.open_destroy_picker() {
255                        client.state.push_log(format!("Destroy: {err}"));
256                    }
257                }
258                UiKeyCode::Char('l') => {
259                    if let Err(err) = client.toggle_chest_lock_for_selection().await {
260                        client.state.push_log(format!("Lock: {err}"));
261                    }
262                }
263                UiKeyCode::Char('g') => {
264                    if let Err(err) = client.give_selected_inventory_to_worker().await {
265                        client.state.push_log(format!("Give: {err}"));
266                    }
267                }
268                UiKeyCode::Char('u') => {
269                    if let Err(err) = client.unequip_mainhand().await {
270                        client.state.push_log(format!("Unequip: {err}"));
271                    }
272                }
273                _ => {}
274            }
275        }
276        return true;
277    }
278
279    if client.state.show_keychain_menu {
280        match key.code {
281            UiKeyCode::Esc | UiKeyCode::Char(',') => client.close_keychain_menu(),
282            UiKeyCode::Up | UiKeyCode::Char('w') | UiKeyCode::Char('k') => {
283                client.keychain_menu_move(-1);
284            }
285            UiKeyCode::Down | UiKeyCode::Char('s') | UiKeyCode::Char('j') => {
286                client.keychain_menu_move(1);
287            }
288            UiKeyCode::PageUp => client.keychain_menu_page(-1),
289            UiKeyCode::PageDown => client.keychain_menu_page(1),
290            UiKeyCode::Enter => {
291                if let Err(err) = client.activate_keychain_selection().await {
292                    client.state.push_log(format!("Keychain: {err}"));
293                }
294            }
295            _ => {}
296        }
297        return true;
298    }
299
300    if client.state.show_craft_menu {
301        match key.code {
302            UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
303                client.close_craft_menu()
304            }
305            UiKeyCode::Up | UiKeyCode::Char('k') => client.craft_menu_move(-1),
306            UiKeyCode::Down | UiKeyCode::Char('j') => client.craft_menu_move(1),
307            UiKeyCode::PageUp => client.craft_menu_page(-1),
308            UiKeyCode::PageDown => client.craft_menu_page(1),
309            UiKeyCode::Char('-') => client.craft_batch_adjust_quantity(-1),
310            UiKeyCode::Char('=') => client.craft_batch_adjust_quantity(1),
311            UiKeyCode::Char('a') => client.craft_batch_set_max(),
312            UiKeyCode::Enter => {
313                if let Err(err) = client.craft_menu_selection().await {
314                    client.state.push_log(format!("Craft failed: {err}"));
315                }
316            }
317            _ => {}
318        }
319        return true;
320    }
321
322    if client.state.show_quest_offer {
323        match key.code {
324            UiKeyCode::Esc => client.quest_offer_decline(),
325            UiKeyCode::Enter => {
326                if let Err(err) = client.quest_offer_accept().await {
327                    client.state.push_log(format!("Quest: {err}"));
328                }
329            }
330            _ => {}
331        }
332        return true;
333    }
334
335    if client.state.show_npc_chat {
336        match key.code {
337            UiKeyCode::Esc => {
338                if let Err(err) = client.npc_talk_close().await {
339                    client.state.push_log(format!("Talk close failed: {err}"));
340                }
341            }
342            UiKeyCode::Enter => {
343                if let Err(err) = client.npc_talk_send().await {
344                    client.state.push_log(format!("Talk failed: {err}"));
345                }
346            }
347            UiKeyCode::Backspace => {
348                if let Some(chat) = client.state.npc_chat.as_mut() {
349                    chat.input.pop();
350                }
351            }
352            UiKeyCode::Char(c) => {
353                if let Some(chat) = client.state.npc_chat.as_mut() {
354                    if !chat.pending && chat.input.len() < 300 {
355                        chat.input.push(c);
356                    }
357                }
358            }
359            _ => {}
360        }
361        return true;
362    }
363
364    if client.state.show_npc_verb_menu {
365        match key.code {
366            UiKeyCode::Esc => {
367                client.state.show_npc_verb_menu = false;
368                client.state.npc_verb_target = None;
369            }
370            UiKeyCode::Up | UiKeyCode::Char('k') => {
371                if client.state.npc_verb_index > 0 {
372                    client.state.npc_verb_index -= 1;
373                }
374            }
375            UiKeyCode::Down | UiKeyCode::Char('j') => {
376                let max = client.npc_verb_options().len().saturating_sub(1);
377                if client.state.npc_verb_index < max {
378                    client.state.npc_verb_index += 1;
379                }
380            }
381            UiKeyCode::Enter => {
382                if let Err(err) = client.confirm_npc_verb().await {
383                    client.state.push_log(format!("Interact failed: {err}"));
384                }
385            }
386            _ => {}
387        }
388        return true;
389    }
390
391    if client.state.bank_panel.is_some() {
392        match &client.state.bank_ui_mode {
393            flatland_client_lib::BankUiMode::Menu => match key.code {
394                UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
395                    if let Err(err) = client.close_bank_panel().await {
396                        client.state.push_log(format!("Bank close failed: {err}"));
397                    }
398                }
399                UiKeyCode::Up | UiKeyCode::Char('k') => client.bank_menu_move(-1),
400                UiKeyCode::Down | UiKeyCode::Char('j') => client.bank_menu_move(1),
401                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
402                    if let Err(err) = client.confirm_bank_menu().await {
403                        client.state.push_log(format!("Bank action failed: {err}"));
404                    }
405                }
406                UiKeyCode::Char('d') => {
407                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::DepositAmount {
408                        input: String::new(),
409                    };
410                }
411                UiKeyCode::Char('w') => {
412                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::WithdrawAmount {
413                        input: String::new(),
414                    };
415                }
416                UiKeyCode::Char('t') => {
417                    client.state.bank_ui_mode = flatland_client_lib::BankUiMode::TransferName {
418                        input: String::new(),
419                    };
420                }
421                _ => {}
422            },
423            _ => match key.code {
424                UiKeyCode::Esc | UiKeyCode::Char('[') => client.bank_transfer_back(),
425                UiKeyCode::Enter => {
426                    if let Err(err) = client.confirm_bank_menu().await {
427                        client.state.push_log(format!("Bank action failed: {err}"));
428                    }
429                }
430                UiKeyCode::Backspace => client.bank_transfer_backspace(),
431                UiKeyCode::Char(c) => client.bank_transfer_append_char(c),
432                _ => {}
433            },
434        }
435        return true;
436    }
437
438    if client.state.storage_panel.is_some() {
439        match &client.state.storage_ui_mode {
440            flatland_client_lib::StorageUiMode::Menu => match key.code {
441                UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('c') => {
442                    if let Err(err) = client.close_storage_panel().await {
443                        client.state.push_log(format!("Storage close failed: {err}"));
444                    }
445                }
446                UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_menu_move(-1),
447                UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_menu_move(1),
448                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
449                    if let Err(err) = client.confirm_storage_menu().await {
450                        client.state.push_log(format!("Storage action failed: {err}"));
451                    }
452                }
453                UiKeyCode::Char('s') => {
454                    let opts = client.state.storage_store_options();
455                    if opts.is_empty() {
456                        client.state.push_log("Nothing loose to store.");
457                    } else {
458                        client.state.storage_ui_mode =
459                            flatland_client_lib::StorageUiMode::StorePick { index: 0 };
460                    }
461                }
462                UiKeyCode::Char('t') => {
463                    let opts = client.state.storage_vault_options();
464                    if opts.is_empty() {
465                        client.state.push_log("Vault is empty.");
466                    } else {
467                        client.state.storage_ui_mode =
468                            flatland_client_lib::StorageUiMode::TakePick { index: 0 };
469                    }
470                }
471                _ => {}
472            },
473            flatland_client_lib::StorageUiMode::StoreAmount { .. }
474            | flatland_client_lib::StorageUiMode::TakeAmount { .. }
475            | flatland_client_lib::StorageUiMode::ShipAmount { .. } => match key.code {
476                UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
477                UiKeyCode::Enter => {
478                    if let Err(err) = client.confirm_storage_menu().await {
479                        client.state.push_log(format!("Storage action failed: {err}"));
480                    }
481                }
482                UiKeyCode::Backspace => client.storage_amount_backspace(),
483                UiKeyCode::Char(c) => client.storage_amount_append_char(c),
484                _ => {}
485            },
486            _ => match key.code {
487                UiKeyCode::Esc | UiKeyCode::Char('[') => client.storage_ui_back(),
488                UiKeyCode::Up | UiKeyCode::Char('k') => client.storage_pick_move(-1),
489                UiKeyCode::Down | UiKeyCode::Char('j') => client.storage_pick_move(1),
490                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
491                    if let Err(err) = client.confirm_storage_menu().await {
492                        client.state.push_log(format!("Storage action failed: {err}"));
493                    }
494                }
495                _ => {}
496            },
497        }
498        return true;
499    }
500
501    if client.state.show_shop_menu {
502        match key.code {
503            UiKeyCode::Esc | UiKeyCode::Char('[') | UiKeyCode::Char('n') | UiKeyCode::Char('c') => {
504                if let Err(err) = client.back_from_shop_menu().await {
505                    client.state.push_log(format!("Shop close failed: {err}"));
506                }
507            }
508            UiKeyCode::Tab => client.shop_tab_toggle(),
509            UiKeyCode::Up | UiKeyCode::Char('k') => client.shop_menu_move(-1),
510            UiKeyCode::Down | UiKeyCode::Char('j') => client.shop_menu_move(1),
511            UiKeyCode::PageUp => client.shop_menu_page(-1),
512            UiKeyCode::PageDown => client.shop_menu_page(1),
513            UiKeyCode::Char('-') => client.shop_quantity_adjust(-1),
514            UiKeyCode::Char('=') => client.shop_quantity_adjust(1),
515            UiKeyCode::Char('a') => client.shop_quantity_set_max(),
516            UiKeyCode::Enter => {
517                if let Err(err) = client.shop_confirm().await {
518                    client.state.push_log(format!("Trade failed: {err}"));
519                }
520            }
521            _ => {}
522        }
523        return true;
524    }
525
526    if client.state.show_quest_menu {
527        match key.code {
528            UiKeyCode::Esc => {
529                if client.state.quest_withdraw_confirm {
530                    client.state.quest_withdraw_confirm = false;
531                } else {
532                    client.state.show_quest_menu = false;
533                }
534            }
535            UiKeyCode::Up | UiKeyCode::Char('k') => client.quest_menu_move(-1),
536            UiKeyCode::Down | UiKeyCode::Char('j') => client.quest_menu_move(1),
537            UiKeyCode::PageUp => client.quest_menu_page(-1),
538            UiKeyCode::PageDown => client.quest_menu_page(1),
539            UiKeyCode::Char('x') => client.quest_request_withdraw(),
540            UiKeyCode::Enter => {
541                if let Err(err) = client.quest_confirm_action().await {
542                    client.state.push_log(format!("Quest: {err}"));
543                }
544            }
545            _ => {}
546        }
547        return true;
548    }
549
550    if client.state.worker_route_editor.is_some() {
551        let at_root = client.re_at_root_sheet();
552        let sheet_index = client.re_sheet_index();
553        if at_root {
554            // Root: the ordered stop list.
555            match key.code {
556                UiKeyCode::Esc => client.close_worker_route_editor(),
557                UiKeyCode::Char('s') => {
558                    if let Err(err) = client.worker_route_editor_save().await {
559                        client.state.push_log(format!("Route: {err}"));
560                    }
561                }
562                UiKeyCode::Down => {
563                    if key.modifiers.contains_control() {
564                        client.worker_route_editor_move_selected(1);
565                    } else {
566                        client.worker_route_editor_select(1);
567                    }
568                }
569                UiKeyCode::Up => {
570                    if key.modifiers.contains_control() {
571                        client.worker_route_editor_move_selected(-1);
572                    } else {
573                        client.worker_route_editor_select(-1);
574                    }
575                }
576                // Select: vim j/k. Reorder: Ctrl+j = earlier (up), Ctrl+k = later (down)
577                // — matches "k moves down" and keeps arrow+Ctrl natural.
578                UiKeyCode::Char('j') => {
579                    if key.modifiers.contains_control() {
580                        client.worker_route_editor_move_selected(-1);
581                    } else {
582                        client.worker_route_editor_select(1);
583                    }
584                }
585                UiKeyCode::Char('k') => {
586                    if key.modifiers.contains_control() {
587                        client.worker_route_editor_move_selected(1);
588                    } else {
589                        client.worker_route_editor_select(-1);
590                    }
591                }
592                UiKeyCode::Char('d') | UiKeyCode::Delete => {
593                    client.worker_route_editor_delete_selected()
594                }
595                UiKeyCode::Char('x') => client.worker_route_editor_clear_stops(),
596                UiKeyCode::Char('a') => client.re_open_add_menu(),
597                UiKeyCode::Char('l') => client.re_open_bed_picker(),
598                UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
599                UiKeyCode::Enter => client.re_edit_selected_stop(),
600                _ => {}
601            }
602        } else {
603            // Inside a setup sheet: uniform picker keys.
604            match key.code {
605                UiKeyCode::Esc => client.re_sheet_back(),
606                UiKeyCode::Char('z') => client.worker_route_editor_toggle_panel(),
607                UiKeyCode::Char('j') | UiKeyCode::Down => client.re_sheet_move(1),
608                UiKeyCode::Char('k') | UiKeyCode::Up => client.re_sheet_move(-1),
609                UiKeyCode::Enter | UiKeyCode::Char(' ') => {
610                    client.re_sheet_row_activate(sheet_index)
611                }
612                UiKeyCode::Char('[') | UiKeyCode::Char('-') => client.re_sheet_adjust(-1),
613                UiKeyCode::Char(']') | UiKeyCode::Char('=') => client.re_sheet_adjust(1),
614                _ => {}
615            }
616        }
617        return true;
618    }
619
620    if client.state.show_worker_give_picker {
621        match key.code {
622            UiKeyCode::Esc => client.close_worker_give_picker(),
623            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_picker_move(-1),
624            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_picker_move(1),
625            UiKeyCode::Enter => {
626                if let Err(err) = client.confirm_worker_give_picker().await {
627                    client.state.push_log(format!("Give: {err}"));
628                }
629            }
630            _ => {}
631        }
632        return true;
633    }
634
635    if client.state.show_worker_give_target_picker {
636        match key.code {
637            UiKeyCode::Esc => client.close_worker_give_target_picker(),
638            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_give_target_picker_move(-1),
639            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_give_target_picker_move(1),
640            UiKeyCode::Enter => {
641                if let Err(err) = client.confirm_worker_give_target_picker().await {
642                    client.state.push_log(format!("Give: {err}"));
643                }
644            }
645            _ => {}
646        }
647        return true;
648    }
649
650    if client.state.show_worker_take_picker {
651        match key.code {
652            UiKeyCode::Esc => client.close_worker_take_picker(),
653            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_take_picker_move(-1),
654            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_take_picker_move(1),
655            UiKeyCode::Enter => {
656                if let Err(err) = client.confirm_worker_take_picker().await {
657                    client.state.push_log(format!("Take: {err}"));
658                }
659            }
660            _ => {}
661        }
662        return true;
663    }
664
665    if client.state.show_worker_teach_picker {
666        match key.code {
667            UiKeyCode::Esc => client.close_worker_teach_picker(),
668            UiKeyCode::Up | UiKeyCode::Char('k') => client.worker_teach_picker_move(-1),
669            UiKeyCode::Down | UiKeyCode::Char('j') => client.worker_teach_picker_move(1),
670            UiKeyCode::Enter => {
671                if let Err(err) = client.confirm_worker_teach_picker().await {
672                    client.state.push_log(format!("Teach: {err}"));
673                }
674            }
675            _ => {}
676        }
677        return true;
678    }
679
680    if client.state.show_workers_menu {
681        match key.code {
682            UiKeyCode::Esc => client.state.show_workers_menu = false,
683            UiKeyCode::Up | UiKeyCode::Char('k') => client.workers_menu_move(-1),
684            UiKeyCode::Down | UiKeyCode::Char('j') => client.workers_menu_move(1),
685            UiKeyCode::PageUp => client.workers_menu_page(-1),
686            UiKeyCode::PageDown => client.workers_menu_page(1),
687            UiKeyCode::Char('d') => {
688                if let Err(err) = client.workers_dismiss_selected().await {
689                    client.state.push_log(format!("Worker: {err}"));
690                }
691            }
692            UiKeyCode::Char('r') => {
693                if let Err(err) = client.hire_worker_laborer().await {
694                    client.state.push_log(format!("Hire: {err}"));
695                }
696            }
697            UiKeyCode::Char('e') => {
698                if let Err(err) = client.open_worker_route_editor_for_selected() {
699                    client.state.push_log(format!("Route: {err}"));
700                }
701            }
702            UiKeyCode::Char('n') => {
703                if let Err(err) = client.open_worker_rename() {
704                    client.state.push_log(format!("Rename: {err}"));
705                }
706            }
707            UiKeyCode::Char('g') => {
708                if let Err(err) = client.open_worker_give_picker() {
709                    client.state.push_log(format!("Give: {err}"));
710                }
711            }
712            UiKeyCode::Char('i') => {
713                if let Err(err) = client.open_worker_take_picker() {
714                    client.state.push_log(format!("Take: {err}"));
715                }
716            }
717            UiKeyCode::Char('t') => {
718                if let Err(err) = client.open_worker_teach_picker() {
719                    client.state.push_log(format!("Teach: {err}"));
720                }
721            }
722            UiKeyCode::Char('c') => client.toggle_workers_menu_compact(),
723            UiKeyCode::Enter => {
724                if let Err(err) = client.workers_confirm_action().await {
725                    client.state.push_log(format!("Worker: {err}"));
726                }
727            }
728            _ => {}
729        }
730        return true;
731    }
732
733    false
734}
735
736pub async fn dispatch_action<S: PlayConnection>(
737    client: &mut GameClient<S>,
738    _keys: &ClientKeyBindings,
739    action: InputAction,
740    ctx: &mut ActionCtx<'_>,
741) {
742    match action {
743        InputAction::Harvest => {
744            if let Err(err) = client.harvest_nearest().await {
745                client.state.push_log(format!("Harvest failed: {err}"));
746            }
747        }
748        InputAction::Pickup => {
749            if client.pickup_nearest().await.is_err() {
750                if let Err(err) = client.pickup_nearest_container().await {
751                    client.state.push_log(format!("Pickup: {err}"));
752                }
753            }
754        }
755        InputAction::Craft => client.open_craft_menu(),
756        InputAction::Interact | InputAction::UseWorld => {
757            if let Err(err) = client.use_nearest().await {
758                client.state.push_log(format!("Use: {err}"));
759            }
760        }
761        InputAction::TestDamage => {
762            if let Err(err) = client.test_damage(25.0).await {
763                client.state.push_log(format!("Damage failed: {err}"));
764            }
765        }
766        InputAction::CycleCombatTarget { reverse } => {
767            if let Err(err) = client.cycle_combat_target(reverse).await {
768                client.state.push_log(format!("T1 target: {err}"));
769            }
770        }
771        InputAction::CycleCombatTargetT2 { reverse } => {
772            if let Err(err) = client.cycle_combat_target_slot(2, reverse).await {
773                client.state.push_log(format!("T2 target: {err}"));
774            }
775        }
776        InputAction::AdvanceRotationT1 => {
777            if let Err(err) = client.advance_rotation(1).await {
778                client.state.push_log(format!("T1 step: {err}"));
779            }
780        }
781        InputAction::AdvanceRotationT2 => {
782            if let Err(err) = client.advance_rotation(2).await {
783                client.state.push_log(format!("T2 step: {err}"));
784            }
785        }
786        InputAction::ToggleAutoT1 => {
787            if let Err(err) = client.toggle_auto_attack_slot(1).await {
788                client.state.push_log(format!("T1 auto: {err}"));
789            }
790        }
791        InputAction::ToggleAutoT2 => {
792            if let Err(err) = client.toggle_auto_attack_slot(2).await {
793                client.state.push_log(format!("T2 auto: {err}"));
794            }
795        }
796        InputAction::ToggleLoadout => {
797            client.state.show_rotation_editor = false;
798            client.state.rotation_editor.reset();
799            client.state.show_loadout_menu = !client.state.show_loadout_menu;
800            if client.state.show_loadout_menu {
801                client.state.loadout_focus_presets = true;
802                client.state.loadout_hotbar_slot = client.state.loadout_hotbar_slot.clamp(1, 9);
803                let ability_len = client.state.loadout_hotbar_choices().len();
804                if ability_len > 0 {
805                    client.state.loadout_ability_index =
806                        client.state.loadout_ability_index.min(ability_len - 1);
807                } else {
808                    client.state.loadout_ability_index = 0;
809                }
810                let preset_len = client.state.rotation_presets.len();
811                if preset_len > 0 {
812                    client.state.loadout_menu_index =
813                        client.state.loadout_menu_index.min(preset_len - 1);
814                } else {
815                    client.state.loadout_menu_index = 0;
816                }
817                *ctx.auto_nav = None;
818                let _ = client.stop().await;
819            }
820        }
821        InputAction::ToggleRotationEditor => {
822            client.state.show_loadout_menu = false;
823            if client.state.show_rotation_editor {
824                client.state.show_rotation_editor = false;
825                client.state.rotation_editor.reset();
826            } else {
827                client.state.show_rotation_editor = true;
828                client.state.rotation_editor.reset();
829                let max = client.state.rotation_presets.len();
830                if max > 0 {
831                    client.state.rotation_editor.list_index =
832                        client.state.rotation_editor.list_index.min(max - 1);
833                }
834            }
835            if client.state.show_rotation_editor {
836                *ctx.auto_nav = None;
837                let _ = client.stop().await;
838            }
839        }
840        InputAction::LoadoutMenuUp => {
841            if !client.state.show_loadout_menu {
842                return;
843            }
844            if client.state.loadout_focus_presets {
845                if client.state.loadout_menu_index > 0 {
846                    client.state.loadout_menu_index -= 1;
847                }
848            } else if client.state.loadout_ability_index > 0 {
849                client.state.loadout_ability_index -= 1;
850            }
851        }
852        InputAction::LoadoutMenuDown => {
853            if !client.state.show_loadout_menu {
854                return;
855            }
856            if client.state.loadout_focus_presets {
857                let max = client.state.rotation_presets.len();
858                if max > 0 {
859                    client.state.loadout_menu_index =
860                        (client.state.loadout_menu_index + 1).min(max - 1);
861                }
862            } else {
863                let max = client.state.loadout_hotbar_choices().len();
864                if max > 0 {
865                    client.state.loadout_ability_index =
866                        (client.state.loadout_ability_index + 1).min(max - 1);
867                }
868            }
869        }
870        InputAction::LoadoutHotbarPrev => {
871            if client.state.show_loadout_menu {
872                let slot = client.state.loadout_hotbar_slot;
873                client.state.loadout_hotbar_slot = if slot <= 1 { 9 } else { slot - 1 };
874            }
875        }
876        InputAction::LoadoutHotbarNext => {
877            if client.state.show_loadout_menu {
878                let slot = client.state.loadout_hotbar_slot;
879                client.state.loadout_hotbar_slot = if slot >= 9 { 1 } else { slot + 1 };
880            }
881        }
882        InputAction::LoadoutToggleFocus => {
883            if client.state.show_loadout_menu {
884                client.state.loadout_focus_presets = !client.state.loadout_focus_presets;
885            }
886        }
887        InputAction::LoadoutBindHotbar => {
888            if !client.state.show_loadout_menu {
889                return;
890            }
891            let slot = client.state.loadout_hotbar_slot;
892            let binding = {
893                let choices = client.state.loadout_hotbar_choices();
894                choices
895                    .get(client.state.loadout_ability_index)
896                    .map(|c| c.binding.clone())
897            };
898            if let Some(binding) = binding {
899                if let Err(err) = client.set_hotbar_slot(slot, Some(&binding)).await {
900                    client.state.push_log(format!("Hotbar: {err}"));
901                }
902            } else {
903                client.state.push_log("No ability/consumable selected to bind");
904            }
905        }
906        InputAction::LoadoutClearHotbar => {
907            if !client.state.show_loadout_menu {
908                return;
909            }
910            let slot = client.state.loadout_hotbar_slot;
911            if let Err(err) = client.set_hotbar_slot(slot, None).await {
912                client.state.push_log(format!("Hotbar: {err}"));
913            }
914        }
915        InputAction::LoadoutAssignT1 => {
916            if !client.state.show_loadout_menu {
917                return;
918            }
919            let preset = {
920                let idx = client.state.loadout_menu_index;
921                client.state.rotation_presets.get(idx).cloned()
922            };
923            if let Some(preset) = preset {
924                let already = client
925                    .state
926                    .combat_slots
927                    .iter()
928                    .find(|s| s.slot_index == 1)
929                    .and_then(|s| s.preset_id.as_deref())
930                    == Some(preset.id.as_str());
931                if already {
932                    client.state.push_log(format!(
933                        "T1 already uses {} (equip weapons from inventory — Weapon auto tracks mainhand)",
934                        preset.label
935                    ));
936                } else if let Err(err) = client.assign_slot_preset(1, &preset.id).await {
937                    client.state.push_log(format!("Loadout: {err}"));
938                } else {
939                    client
940                        .state
941                        .push_log(format!("T1 ← {}", preset.label));
942                }
943            }
944        }
945        InputAction::LoadoutAssignT2 => {
946            if !client.state.show_loadout_menu {
947                return;
948            }
949            let preset = {
950                let idx = client.state.loadout_menu_index;
951                client.state.rotation_presets.get(idx).cloned()
952            };
953            if let Some(preset) = preset {
954                let already = client
955                    .state
956                    .combat_slots
957                    .iter()
958                    .find(|s| s.slot_index == 2)
959                    .and_then(|s| s.preset_id.as_deref())
960                    == Some(preset.id.as_str());
961                if already {
962                    client
963                        .state
964                        .push_log(format!("T2 already uses {}", preset.label));
965                } else if let Err(err) = client.assign_slot_preset(2, &preset.id).await {
966                    client.state.push_log(format!("Loadout: {err}"));
967                } else {
968                    client
969                        .state
970                        .push_log(format!("T2 ← {}", preset.label));
971                }
972            }
973        }
974        InputAction::CloseOverlay => {
975            client.state.show_loadout_menu = false;
976            client.state.show_rotation_editor = false;
977            client.state.rotation_editor.reset();
978        }
979        InputAction::ClearCombatTarget => {
980            if let Err(err) = client.clear_combat_target().await {
981                client.state.push_log(format!("Target: {err}"));
982            }
983        }
984        InputAction::ClearCombatTargetT2 => {
985            if let Err(err) = client.clear_combat_target_slot(2).await {
986                client.state.push_log(format!("T2 target: {err}"));
987            }
988        }
989        InputAction::Dodge => {
990            *ctx.auto_nav = None;
991            if let Err(err) = client.dodge().await {
992                client.state.push_log(format!("Dodge: {err}"));
993            }
994        }
995        InputAction::Lunge => {
996            *ctx.auto_nav = None;
997            let _ = client.stop().await;
998            if let Err(err) = client.lunge().await {
999                client.state.push_log(format!("Lunge: {err}"));
1000            }
1001        }
1002        InputAction::DirectionalJump { forward, strafe } => {
1003            *ctx.auto_nav = None;
1004            let _ = client.stop().await;
1005            if let Err(err) = client.directional_jump(forward, strafe).await {
1006                client.state.push_log(format!("Jump: {err}"));
1007            }
1008        }
1009        InputAction::CastHotbar { slot } => {
1010            if let Err(err) = client.cast_hotbar_ability(slot).await {
1011                client.state.push_log(format!("Hotbar {slot}: {err}"));
1012            }
1013        }
1014        InputAction::ToggleBlock => {
1015            *ctx.block_active = !*ctx.block_active;
1016            if let Err(err) = client.set_block(*ctx.block_active).await {
1017                client.state.push_log(format!("Block: {err}"));
1018                *ctx.block_active = false;
1019            }
1020        }
1021        InputAction::ToggleStats => client.toggle_stats(),
1022        InputAction::ToggleEquip => client.toggle_equip_menu(),
1023        InputAction::CycleCharacterSheetTab => client.cycle_character_sheet_tab(),
1024        InputAction::LedgerPeriodDigit(c) => client.set_ledger_period_digit(c),
1025        InputAction::ToggleInventory => client.toggle_inventory_menu(),
1026        InputAction::ToggleKeychain => client.toggle_keychain_menu(),
1027        InputAction::ToggleQuestMenu => client.toggle_quest_menu(),
1028        InputAction::ToggleWorkersMenu => client.toggle_workers_menu(),
1029        InputAction::QuestMenuUp => client.quest_menu_move(-1),
1030        InputAction::QuestMenuDown => client.quest_menu_move(1),
1031        InputAction::QuestWithdraw => client.quest_request_withdraw(),
1032        InputAction::RotationEditorBack => {
1033            let _ = client.back_on_esc();
1034        }
1035        InputAction::RotationEditorListUp
1036        | InputAction::RotationEditorListDown
1037        | InputAction::RotationEditorEdit
1038        | InputAction::RotationEditorNew
1039        | InputAction::RotationEditorDelete
1040        | InputAction::RotationEditorAddAbility
1041        | InputAction::RotationEditorRemoveAbility
1042        | InputAction::RotationEditorMoveAbilityUp
1043        | InputAction::RotationEditorMoveAbilityDown
1044        | InputAction::RotationEditorAbilityUp
1045        | InputAction::RotationEditorAbilityDown
1046        | InputAction::RotationEditorPickerUp
1047        | InputAction::RotationEditorPickerDown
1048        | InputAction::RotationEditorPickAbility
1049        | InputAction::RotationEditorRename
1050        | InputAction::RotationEditorConfirmLabel
1051        | InputAction::RotationEditorLabelBackspace
1052        | InputAction::RotationEditorLabelChar(_)
1053        | InputAction::RotationEditorSave => {
1054            handle_rotation_editor_action(client, action).await;
1055        }
1056        InputAction::None
1057        | InputAction::Quit
1058        | InputAction::ToggleHelp
1059        | InputAction::CycleHudView
1060        | InputAction::StartChat { .. }
1061        | InputAction::SubmitChat
1062        | InputAction::CancelChat
1063        | InputAction::ToggleSprintMode
1064        | InputAction::ToggleMapTarget
1065        | InputAction::ConfirmMapTarget
1066        | InputAction::CancelMapTarget
1067        | InputAction::MapTargetNudge { .. }
1068        | InputAction::CancelAutoNav
1069        | InputAction::StopMovement => {}
1070    }
1071}
1072
1073async fn handle_rotation_editor_action<S: PlayConnection>(
1074    client: &mut GameClient<S>,
1075    action: InputAction,
1076) {
1077    match action {
1078        InputAction::RotationEditorListUp => {
1079            if client.state.rotation_editor.list_index > 0 {
1080                client.state.rotation_editor.list_index -= 1;
1081            }
1082        }
1083        InputAction::RotationEditorListDown => {
1084            let max = client.state.rotation_presets.len();
1085            if max > 0 {
1086                client.state.rotation_editor.list_index =
1087                    (client.state.rotation_editor.list_index + 1).min(max - 1);
1088            }
1089        }
1090        InputAction::RotationEditorEdit => {
1091            let idx = client.state.rotation_editor.list_index;
1092            if let Some(preset) = client.state.rotation_presets.get(idx).cloned() {
1093                client.state.rotation_editor.draft = Some(preset);
1094                client.state.rotation_editor.ability_index = 0;
1095                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1096            }
1097        }
1098        InputAction::RotationEditorNew => {
1099            let preset = next_custom_preset(&client.state.rotation_presets);
1100            client.state.rotation_editor.list_index = client.state.rotation_presets.len();
1101            client.state.rotation_editor.draft = Some(preset);
1102            client.state.rotation_editor.ability_index = 0;
1103            client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1104        }
1105        InputAction::RotationEditorDelete => {
1106            let idx = client.state.rotation_editor.list_index;
1107            let preset_id = client.state.rotation_presets.get(idx).map(|p| p.id.clone());
1108            if let Some(id) = preset_id {
1109                if preset_deletable(&id) {
1110                    if let Err(err) = client.delete_rotation_preset(&id).await {
1111                        client.state.push_log(format!("Delete: {err}"));
1112                    } else {
1113                        let max = client.state.rotation_presets.len();
1114                        client.state.rotation_editor.list_index = if max == 0 {
1115                            0
1116                        } else {
1117                            client.state.rotation_editor.list_index.min(max - 1)
1118                        };
1119                    }
1120                } else {
1121                    client.state.push_log("Cannot delete built-in preset");
1122                }
1123            }
1124        }
1125        InputAction::RotationEditorBack => match client.state.rotation_editor.mode {
1126            RotationEditorMode::EditLabel => {
1127                client.state.rotation_editor.label_buffer.clear();
1128                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1129            }
1130            RotationEditorMode::PickAbility => {
1131                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1132            }
1133            RotationEditorMode::EditSequence => {
1134                client.state.rotation_editor.draft = None;
1135                client.state.rotation_editor.mode = RotationEditorMode::List;
1136            }
1137            RotationEditorMode::List => {}
1138        },
1139        InputAction::RotationEditorAddAbility => {
1140            client.state.rotation_editor.picker_index = 0;
1141            client.state.rotation_editor.mode = RotationEditorMode::PickAbility;
1142        }
1143        InputAction::RotationEditorRemoveAbility => {
1144            let idx = client.state.rotation_editor.ability_index;
1145            if let Some(draft) = &mut client.state.rotation_editor.draft {
1146                if idx < draft.abilities.len() {
1147                    draft.abilities.remove(idx);
1148                    if client.state.rotation_editor.ability_index >= draft.abilities.len()
1149                        && client.state.rotation_editor.ability_index > 0
1150                    {
1151                        client.state.rotation_editor.ability_index -= 1;
1152                    }
1153                }
1154            }
1155        }
1156        InputAction::RotationEditorMoveAbilityUp => {
1157            let i = client.state.rotation_editor.ability_index;
1158            if let Some(draft) = &mut client.state.rotation_editor.draft {
1159                if i > 0 && i < draft.abilities.len() {
1160                    draft.abilities.swap(i, i - 1);
1161                    client.state.rotation_editor.ability_index -= 1;
1162                }
1163            }
1164        }
1165        InputAction::RotationEditorMoveAbilityDown => {
1166            let i = client.state.rotation_editor.ability_index;
1167            if let Some(draft) = &mut client.state.rotation_editor.draft {
1168                if i + 1 < draft.abilities.len() {
1169                    draft.abilities.swap(i, i + 1);
1170                    client.state.rotation_editor.ability_index += 1;
1171                }
1172            }
1173        }
1174        InputAction::RotationEditorAbilityUp => {
1175            if client.state.rotation_editor.ability_index > 0 {
1176                client.state.rotation_editor.ability_index -= 1;
1177            }
1178        }
1179        InputAction::RotationEditorAbilityDown => {
1180            if let Some(draft) = &client.state.rotation_editor.draft {
1181                if !draft.abilities.is_empty() {
1182                    client.state.rotation_editor.ability_index =
1183                        (client.state.rotation_editor.ability_index + 1)
1184                            .min(draft.abilities.len() - 1);
1185                }
1186            }
1187        }
1188        InputAction::RotationEditorPickerUp => {
1189            if client.state.rotation_editor.picker_index > 0 {
1190                client.state.rotation_editor.picker_index -= 1;
1191            }
1192        }
1193        InputAction::RotationEditorPickerDown => {
1194            let choices = editor_ability_choices(
1195                &client.state.known_abilities,
1196                &client.state.weapon_ability_id,
1197            );
1198            if !choices.is_empty() {
1199                client.state.rotation_editor.picker_index =
1200                    (client.state.rotation_editor.picker_index + 1).min(choices.len() - 1);
1201            }
1202        }
1203        InputAction::RotationEditorPickAbility => {
1204            let choices = editor_ability_choices(
1205                &client.state.known_abilities,
1206                &client.state.weapon_ability_id,
1207            );
1208            let pick = client.state.rotation_editor.picker_index;
1209            if let Some(ability) = choices.get(pick) {
1210                if let Some(draft) = &mut client.state.rotation_editor.draft {
1211                    let max = client.state.max_abilities_per_rotation.max(1) as usize;
1212                    if draft.abilities.len() >= max {
1213                        client
1214                            .state
1215                            .push_log(format!("Rotation full (max {max} from INT+WIS)"));
1216                    } else {
1217                        draft.abilities.push(ability.clone());
1218                        client.state.rotation_editor.ability_index =
1219                            draft.abilities.len().saturating_sub(1);
1220                    }
1221                }
1222                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1223            }
1224        }
1225        InputAction::RotationEditorRename => {
1226            if let Some(draft) = &client.state.rotation_editor.draft {
1227                client.state.rotation_editor.label_buffer = draft.label.clone();
1228                client.state.rotation_editor.mode = RotationEditorMode::EditLabel;
1229            }
1230        }
1231        InputAction::RotationEditorConfirmLabel => {
1232            let label = client.state.rotation_editor.label_buffer.trim().to_string();
1233            if label.is_empty() {
1234                client.state.push_log("Label cannot be empty");
1235            } else if let Some(draft) = &mut client.state.rotation_editor.draft {
1236                draft.label = label;
1237                client.state.rotation_editor.label_buffer.clear();
1238                client.state.rotation_editor.mode = RotationEditorMode::EditSequence;
1239            }
1240        }
1241        InputAction::RotationEditorLabelBackspace => {
1242            client.state.rotation_editor.label_buffer.pop();
1243        }
1244        InputAction::RotationEditorLabelChar(c) => {
1245            if client.state.rotation_editor.label_buffer.len() < 32 {
1246                client.state.rotation_editor.label_buffer.push(c);
1247            }
1248        }
1249        InputAction::RotationEditorSave => {
1250            let draft = match client.state.rotation_editor.draft.clone() {
1251                Some(d) => d,
1252                None => return,
1253            };
1254            if draft.abilities.is_empty() {
1255                client.state.push_log("Rotation needs at least one ability");
1256                return;
1257            }
1258            let saved_id = draft.id.clone();
1259            if let Err(err) = client.upsert_rotation_preset(draft).await {
1260                client.state.push_log(format!("Save: {err}"));
1261            } else {
1262                client.state.rotation_editor.mode = RotationEditorMode::List;
1263                client.state.rotation_editor.draft = None;
1264                if let Some(i) = client
1265                    .state
1266                    .rotation_presets
1267                    .iter()
1268                    .position(|p| p.id == saved_id)
1269                {
1270                    client.state.rotation_editor.list_index = i;
1271                }
1272            }
1273        }
1274        _ => {}
1275    }
1276}