20 return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch())
27bool IsCursorTarget(
const std::shared_ptr<MenuOption>& opt)
29 return opt && opt->IsEnabled() && opt->IsSelectable();
34void StepCursor(
const std::vector<std::shared_ptr<MenuOption>>& items,
int& idx,
int step)
36 int n =
static_cast<int>(items.size());
43 idx = ((idx + step) % n + n) % n;
45 while (!IsCursorTarget(items[idx]) && --attempts > 0);
50void JumpPage(
const std::vector<std::shared_ptr<MenuOption>>& items,
int& idx,
int pageDelta)
52 int n =
static_cast<int>(items.size());
56 int pageCount = (n + ItemsPerPage - 1) / ItemsPerPage;
57 int currentPage = idx / ItemsPerPage;
58 int offset = idx % ItemsPerPage;
59 int newPage = ((currentPage + pageDelta) % pageCount + pageCount) % pageCount;
61 int pageStart = newPage * ItemsPerPage;
62 int pageEnd = std::min(n, pageStart + ItemsPerPage);
64 idx = std::min(pageStart + offset, pageEnd - 1);
65 int attempts = pageEnd - pageStart;
66 while (!IsCursorTarget(items[idx]) && --attempts > 0)
68 idx = (idx + 1 < pageEnd) ? idx + 1 : pageStart;
74void MenuManager::OpenMenu(
int slot, std::shared_ptr<Menu> menu)
76 if (slot < 0 || slot >= 64 || !menu)
79 auto& state = _states[slot];
80 state.MenuStack.push(std::move(menu));
81 state.SelectedIndex = 0;
84 auto* current = state.GetCurrentMenu();
89 if (!current->Items.empty() && !IsCursorTarget(current->Items[0]))
90 StepCursor(current->Items, state.SelectedIndex, +1);
92 Log::Info(
"Menu opened for slot {} (title: {}, items: {})", slot, current->Title, current->Items.size());
96void MenuManager::CloseMenu(
int slot)
98 if (slot < 0 || slot >= 64)
101 auto& state = _states[slot];
102 if (state.MenuStack.empty())
105 auto menu = state.MenuStack.top();
106 state.MenuStack.pop();
111 if (state.MenuStack.empty())
113 MessageSystem::Instance().ClearCenterHtml(slot);
118 state.SelectedIndex = 0;
119 if (
auto* parent = state.GetCurrentMenu(); parent && !parent->Items.empty() &&
120 !IsCursorTarget(parent->Items[0]))
122 StepCursor(parent->Items, state.SelectedIndex, +1);
127void MenuManager::CloseAllMenus(
int slot)
129 if (slot < 0 || slot >= 64)
132 auto& state = _states[slot];
134 MessageSystem::Instance().ClearCenterHtml(slot);
137bool MenuManager::HasActiveMenu(
int slot)
const
139 if (slot < 0 || slot >= 64)
142 return _states[slot].HasMenu();
145void MenuManager::OnGameFrame()
147 for (
int slot = 0; slot < 64; ++slot)
149 auto& state = _states[slot];
150 if (!state.HasMenu())
153 uint64_t buttons = EntitySystem::Instance().GetPlayerButtons(slot);
154 auto prev = state.PrevButtons;
155 state.PrevButtons = buttons;
157 HandleInput(slot, buttons, prev);
162void MenuManager::HandleInput(
int slot, uint64_t buttons, uint64_t prevButtons)
164 auto& state = _states[slot];
165 auto* menu = state.GetCurrentMenu();
169 uint64_t pressed = buttons & ~prevButtons;
174 if (now - state.LastInputTime < InputDebounceMs)
179 auto& capture = ChatInputCapture::Instance();
180 if (capture.IsCapturing(slot))
182 if (pressed & IN_RELOAD)
184 capture.CancelCapture(slot);
185 state.LastInputTime = now;
190 int itemCount =
static_cast<int>(menu->Items.size());
194 bool isPaginated = itemCount > ItemsPerPage;
195 bool inputHandled =
true;
197 auto& currentOption = menu->Items[state.SelectedIndex];
199 if (pressed & IN_FORWARD)
200 StepCursor(menu->Items, state.SelectedIndex, -1);
201 else if (pressed & IN_BACK)
202 StepCursor(menu->Items, state.SelectedIndex, +1);
203 else if (pressed & IN_MOVELEFT)
205 bool consumed = currentOption && currentOption->IsEnabled() && currentOption->OnHorizontal(slot, -1);
206 if (!consumed && isPaginated)
207 JumpPage(menu->Items, state.SelectedIndex, -1);
209 inputHandled =
false;
211 else if (pressed & IN_MOVERIGHT)
213 bool consumed = currentOption && currentOption->IsEnabled() && currentOption->OnHorizontal(slot, +1);
214 if (!consumed && isPaginated)
215 JumpPage(menu->Items, state.SelectedIndex, +1);
217 inputHandled =
false;
219 else if (pressed & IN_USE)
221 if (currentOption && currentOption->IsEnabled() && currentOption->IsSelectable())
222 currentOption->OnActivate(slot);
224 else if (pressed & IN_RELOAD)
227 inputHandled =
false;
230 state.LastInputTime = now;
233void MenuManager::RenderMenu(
int slot)
235 auto& state = _states[slot];
236 auto* menu = state.GetCurrentMenu();
241 if (
auto* prompt = ChatInputCapture::Instance().GetPrompt(slot); prompt !=
nullptr)
244 MessageSystem::Instance().SendCenterHtml(slot, html);
248 bool isSubmenu = state.MenuStack.size() > 1;
249 auto html =
RenderMenuHtml(menu, slot, state.SelectedIndex, isSubmenu);
250 MessageSystem::Instance().SendCenterHtml(slot, html);
253void MenuManager::OnPlayerDisconnect(
int slot)
255 if (slot < 0 || slot >= 64)
258 _states[slot].Reset();