1 | #include "editor_server_settings.h" |
2 | #include "editor.h" |
3 | |
4 | #include <engine/keys.h> |
5 | #include <engine/shared/config.h> |
6 | #include <engine/textrender.h> |
7 | |
8 | #include <game/client/gameclient.h> |
9 | #include <game/client/lineinput.h> |
10 | #include <game/client/ui.h> |
11 | #include <game/client/ui_listbox.h> |
12 | #include <game/editor/editor_actions.h> |
13 | #include <game/editor/editor_history.h> |
14 | |
15 | #include <base/color.h> |
16 | #include <base/system.h> |
17 | |
18 | #include <iterator> |
19 | |
20 | using namespace FontIcons; |
21 | |
22 | static const int FONT_SIZE = 12.0f; |
23 | |
24 | struct IMapSetting |
25 | { |
26 | enum EType |
27 | { |
28 | SETTING_INT, |
29 | SETTING_COMMAND, |
30 | }; |
31 | const char *m_pName; |
32 | const char *m_pHelp; |
33 | EType m_Type; |
34 | |
35 | IMapSetting(const char *pName, const char *pHelp, EType Type) : |
36 | m_pName(pName), m_pHelp(pHelp), m_Type(Type) {} |
37 | }; |
38 | struct SMapSettingInt : public IMapSetting |
39 | { |
40 | int m_Default; |
41 | int m_Min; |
42 | int m_Max; |
43 | |
44 | SMapSettingInt(const char *pName, const char *pHelp, int Default, int Min, int Max) : |
45 | IMapSetting(pName, pHelp, IMapSetting::SETTING_INT), m_Default(Default), m_Min(Min), m_Max(Max) {} |
46 | }; |
47 | struct SMapSettingCommand : public IMapSetting |
48 | { |
49 | const char *m_pArgs; |
50 | |
51 | SMapSettingCommand(const char *pName, const char *pHelp, const char *pArgs) : |
52 | IMapSetting(pName, pHelp, IMapSetting::SETTING_COMMAND), m_pArgs(pArgs) {} |
53 | }; |
54 | |
55 | void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast) |
56 | { |
57 | static int s_CommandSelectedIndex = -1; |
58 | static CListBox s_ListBox; |
59 | s_ListBox.SetActive(!m_MapSettingsCommandContext.m_DropdownContext.m_ListBox.Active() && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen()); |
60 | |
61 | bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); |
62 | const bool CurrentInputValid = m_MapSettingsCommandContext.Valid(); // Use the context to validate the input |
63 | |
64 | CUIRect ToolBar, Button, Label, List, DragBar; |
65 | View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr); |
66 | DragBar.y -= 2.0f; |
67 | DragBar.w += 2.0f; |
68 | DragBar.h += 4.0f; |
69 | DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_SERVER_SETTINGS]); |
70 | View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View); |
71 | View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List); |
72 | ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar); |
73 | |
74 | // delete button |
75 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
76 | ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr); |
77 | static int s_DeleteButton = 0; |
78 | if(DoButton_FontIcon(pId: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: GotSelection ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Delete] Delete the selected command from the command list." , Corners: IGraphics::CORNER_ALL, FontSize: 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE))) |
79 | { |
80 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); |
81 | |
82 | m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + s_CommandSelectedIndex); |
83 | if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) |
84 | s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; |
85 | if(s_CommandSelectedIndex >= 0) |
86 | m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); |
87 | else |
88 | m_SettingsCommandInput.Clear(); |
89 | m_Map.OnModify(); |
90 | m_MapSettingsCommandContext.Update(); |
91 | s_ListBox.ScrollToSelected(); |
92 | } |
93 | |
94 | // move down button |
95 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
96 | const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1; |
97 | static int s_DownButton = 0; |
98 | if(DoButton_FontIcon(pId: &s_DownButton, pText: FONT_ICON_SORT_DOWN, Checked: CanMoveDown ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Alt+Down] Move the selected command down." , Corners: IGraphics::CORNER_R, FontSize: 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DOWN))) |
99 | { |
100 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::MOVE_DOWN, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex)); |
101 | |
102 | std::swap(a&: m_Map.m_vSettings[s_CommandSelectedIndex], b&: m_Map.m_vSettings[s_CommandSelectedIndex + 1]); |
103 | s_CommandSelectedIndex++; |
104 | m_Map.OnModify(); |
105 | s_ListBox.ScrollToSelected(); |
106 | } |
107 | |
108 | // move up button |
109 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
110 | ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr); |
111 | const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0; |
112 | static int s_UpButton = 0; |
113 | if(DoButton_FontIcon(pId: &s_UpButton, pText: FONT_ICON_SORT_UP, Checked: CanMoveUp ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Alt+Up] Move the selected command up." , Corners: IGraphics::CORNER_L, FontSize: 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_UP))) |
114 | { |
115 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::MOVE_UP, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex)); |
116 | |
117 | std::swap(a&: m_Map.m_vSettings[s_CommandSelectedIndex], b&: m_Map.m_vSettings[s_CommandSelectedIndex - 1]); |
118 | s_CommandSelectedIndex--; |
119 | m_Map.OnModify(); |
120 | s_ListBox.ScrollToSelected(); |
121 | } |
122 | |
123 | // redo button |
124 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
125 | static int s_RedoButton = 0; |
126 | if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_ServerSettingsHistory.CanRedo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Y] Redo command edit" , Corners: IGraphics::CORNER_R, FontSize: 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DOWN))) |
127 | { |
128 | m_ServerSettingsHistory.Redo(); |
129 | } |
130 | |
131 | // undo button |
132 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
133 | ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr); |
134 | static int s_UndoButton = 0; |
135 | if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_ServerSettingsHistory.CanUndo() ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Ctrl+Z] Undo command edit" , Corners: IGraphics::CORNER_L, FontSize: 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_UP))) |
136 | { |
137 | m_ServerSettingsHistory.Undo(); |
138 | } |
139 | |
140 | GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); |
141 | |
142 | int CollidingCommandIndex = -1; |
143 | ECollisionCheckResult CheckResult = ECollisionCheckResult::ERROR; |
144 | if(CurrentInputValid) |
145 | CollidingCommandIndex = m_MapSettingsCommandContext.CheckCollision(Result&: CheckResult); |
146 | |
147 | // update button |
148 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
149 | const bool CanAdd = CheckResult == ECollisionCheckResult::ADD; |
150 | const bool CanReplace = CheckResult == ECollisionCheckResult::REPLACE; |
151 | |
152 | const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(a: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, b: m_SettingsCommandInput.GetString()) != 0; |
153 | |
154 | static int s_UpdateButton = 0; |
155 | if(DoButton_FontIcon(pId: &s_UpdateButton, pText: FONT_ICON_PENCIL, Checked: CanUpdate ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: "[Alt+Enter] Update the selected command based on the entered value." , Corners: IGraphics::CORNER_R, FontSize: 9.0f) == 1 || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))) |
156 | { |
157 | if(CollidingCommandIndex == -1) |
158 | { |
159 | bool Found = false; |
160 | int i; |
161 | for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i) |
162 | { |
163 | if(i != s_CommandSelectedIndex && !str_comp(a: m_Map.m_vSettings[i].m_aCommand, b: m_SettingsCommandInput.GetString())) |
164 | { |
165 | Found = true; |
166 | break; |
167 | } |
168 | } |
169 | if(Found) |
170 | { |
171 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); |
172 | m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + s_CommandSelectedIndex); |
173 | s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; |
174 | } |
175 | else |
176 | { |
177 | const char *pStr = m_SettingsCommandInput.GetString(); |
178 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr)); |
179 | str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr); |
180 | } |
181 | } |
182 | else |
183 | { |
184 | if(s_CommandSelectedIndex == CollidingCommandIndex) |
185 | { // If we are editing the currently collinding line, then we can just call EDIT on it |
186 | const char *pStr = m_SettingsCommandInput.GetString(); |
187 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr)); |
188 | str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr); |
189 | } |
190 | else |
191 | { // If not, then editing the current selected line will result in the deletion of the colliding line, and the editing of the selected line |
192 | const char *pStr = m_SettingsCommandInput.GetString(); |
193 | |
194 | char aBuf[256]; |
195 | str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Delete command %d; Edit command %d" , CollidingCommandIndex, s_CommandSelectedIndex); |
196 | |
197 | m_ServerSettingsHistory.BeginBulk(); |
198 | // Delete the colliding command |
199 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: CollidingCommandIndex, args&: m_Map.m_vSettings[CollidingCommandIndex].m_aCommand)); |
200 | m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + CollidingCommandIndex); |
201 | // Edit the selected command |
202 | s_CommandSelectedIndex = s_CommandSelectedIndex > CollidingCommandIndex ? s_CommandSelectedIndex - 1 : s_CommandSelectedIndex; |
203 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr)); |
204 | str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr); |
205 | |
206 | m_ServerSettingsHistory.EndBulk(pDisplay: aBuf); |
207 | } |
208 | } |
209 | |
210 | m_Map.OnModify(); |
211 | s_ListBox.ScrollToSelected(); |
212 | m_SettingsCommandInput.Clear(); |
213 | m_MapSettingsCommandContext.Reset(); // Reset context |
214 | Ui()->SetActiveItem(&m_SettingsCommandInput); |
215 | } |
216 | |
217 | // add button |
218 | ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button); |
219 | ToolBar.VSplitRight(Cut: 100.0f, pLeft: &ToolBar, pRight: nullptr); |
220 | |
221 | static int s_AddButton = 0; |
222 | if(DoButton_FontIcon(pId: &s_AddButton, pText: CanReplace ? FONT_ICON_ARROWS_ROTATE : FONT_ICON_PLUS, Checked: CanAdd || CanReplace ? 0 : -1, pRect: &Button, Flags: 0, pToolTip: CanReplace ? "[Enter] Replace the corresponding command in the command list." : "[Enter] Add a command to the command list." , Corners: IGraphics::CORNER_L) == 1 || ((CanAdd || CanReplace) && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))) |
223 | { |
224 | if(CanReplace) |
225 | { |
226 | dbg_assert(CollidingCommandIndex != -1, "Could not replace command" ); |
227 | s_CommandSelectedIndex = CollidingCommandIndex; |
228 | |
229 | const char *pStr = m_SettingsCommandInput.GetString(); |
230 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr)); |
231 | str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr); |
232 | } |
233 | else if(CanAdd) |
234 | { |
235 | m_Map.m_vSettings.emplace_back(args: m_SettingsCommandInput.GetString()); |
236 | s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; |
237 | m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: this, args: CEditorCommandAction::EType::ADD, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); |
238 | } |
239 | |
240 | m_Map.OnModify(); |
241 | s_ListBox.ScrollToSelected(); |
242 | m_SettingsCommandInput.Clear(); |
243 | m_MapSettingsCommandContext.Reset(); // Reset context |
244 | Ui()->SetActiveItem(&m_SettingsCommandInput); |
245 | } |
246 | |
247 | // command input (use remaining toolbar width) |
248 | if(!ShowServerSettingsEditorLast) // Just activated |
249 | Ui()->SetActiveItem(&m_SettingsCommandInput); |
250 | m_SettingsCommandInput.SetEmptyText("Command" ); |
251 | |
252 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
253 | |
254 | // command list |
255 | s_ListBox.DoStart(RowHeight: 15.0f, NumItems: m_Map.m_vSettings.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_CommandSelectedIndex, pRect: &List); |
256 | |
257 | for(size_t i = 0; i < m_Map.m_vSettings.size(); i++) |
258 | { |
259 | const CListboxItem Item = s_ListBox.DoNextItem(pId: &m_Map.m_vSettings[i], Selected: s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i); |
260 | if(!Item.m_Visible) |
261 | continue; |
262 | |
263 | Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label); |
264 | |
265 | SLabelProperties Props; |
266 | Props.m_MaxWidth = Label.w; |
267 | Props.m_EllipsisAtEnd = true; |
268 | Ui()->DoLabel(pRect: &Label, pText: m_Map.m_vSettings[i].m_aCommand, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
269 | } |
270 | |
271 | const int NewSelected = s_ListBox.DoEnd(); |
272 | if(s_CommandSelectedIndex != NewSelected || s_ListBox.WasItemSelected()) |
273 | { |
274 | s_CommandSelectedIndex = NewSelected; |
275 | if(m_SettingsCommandInput.IsEmpty() || !Input()->ModifierIsPressed()) // Allow ctrl+click to only change selection |
276 | { |
277 | m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); |
278 | m_MapSettingsCommandContext.Update(); |
279 | m_MapSettingsCommandContext.UpdateCursor(Force: true); |
280 | } |
281 | m_MapSettingsCommandContext.m_DropdownContext.m_ShouldHide = true; |
282 | Ui()->SetActiveItem(&m_SettingsCommandInput); |
283 | } |
284 | |
285 | // Map setting input |
286 | DoMapSettingsEditBox(pContext: &m_MapSettingsCommandContext, pRect: &ToolBar, FontSize: FONT_SIZE, DropdownMaxHeight: List.h); |
287 | } |
288 | |
289 | void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners, const char *pToolTip) |
290 | { |
291 | // Main method to do the full featured map settings edit box |
292 | |
293 | auto *pLineInput = pContext->LineInput(); |
294 | auto &Context = *pContext; |
295 | Context.SetFontSize(FontSize); |
296 | |
297 | // Set current active context if input is active |
298 | if(pLineInput->IsActive()) |
299 | CMapSettingsBackend::ms_pActiveContext = pContext; |
300 | |
301 | // Small utility to render a floating part above the input rect. |
302 | // Use to display either the error or the current argument name |
303 | const float PartMargin = 4.0f; |
304 | auto &&RenderFloatingPart = [&](CUIRect *pInputRect, float x, const char *pStr) { |
305 | CUIRect Background; |
306 | Background.x = x - PartMargin; |
307 | Background.y = pInputRect->y - pInputRect->h - 6.0f; |
308 | Background.w = TextRender()->TextWidth(Size: FontSize, pText: pStr) + 2 * PartMargin; |
309 | Background.h = pInputRect->h; |
310 | Background.Draw(Color: ColorRGBA(0, 0, 0, 0.9f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f); |
311 | |
312 | CUIRect Label; |
313 | Background.VSplitLeft(Cut: PartMargin, pLeft: nullptr, pRight: &Label); |
314 | TextRender()->TextColor(r: 0.8f, g: 0.8f, b: 0.8f, a: 1.0f); |
315 | Ui()->DoLabel(pRect: &Label, pText: pStr, Size: FontSize, Align: TEXTALIGN_ML); |
316 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
317 | }; |
318 | |
319 | // If we have a valid command, display the help in the tooltip |
320 | if(Context.CommandIsValid()) |
321 | Context.GetCommandHelpText(pStr: m_aTooltip, Length: sizeof(m_aTooltip)); |
322 | |
323 | CUIRect ToolBar = *pRect; |
324 | CUIRect Button; |
325 | ToolBar.VSplitRight(Cut: ToolBar.h, pLeft: &ToolBar, pRight: &Button); |
326 | |
327 | // Do the unknown command toggle button |
328 | if(DoButton_FontIcon(pId: &Context.m_AllowUnknownCommands, pText: FONT_ICON_QUESTION, Checked: Context.m_AllowUnknownCommands, pRect: &Button, Flags: 0, pToolTip: "Disallow/allow unknown commands" , Corners: IGraphics::CORNER_R)) |
329 | { |
330 | Context.m_AllowUnknownCommands = !Context.m_AllowUnknownCommands; |
331 | Context.Update(); |
332 | } |
333 | |
334 | // Color the arguments |
335 | std::vector<STextColorSplit> vColorSplits; |
336 | Context.ColorArguments(vColorSplits); |
337 | |
338 | // Do and render clearable edit box with the colors |
339 | if(DoClearableEditBox(pLineInput, pRect: &ToolBar, FontSize, Corners: IGraphics::CORNER_L, pToolTip: "Enter a server setting." , vColorSplits)) |
340 | { |
341 | Context.Update(); // Update the context when contents change |
342 | Context.m_DropdownContext.m_ShouldHide = false; |
343 | } |
344 | |
345 | // Update/track the cursor |
346 | if(Context.UpdateCursor()) |
347 | Context.m_DropdownContext.m_ShouldHide = false; |
348 | |
349 | // Calculate x position of the dropdown and the floating part |
350 | float x = ToolBar.x + Context.CurrentArgPos() - pLineInput->GetScrollOffset(); |
351 | x = clamp(val: x, lo: ToolBar.x + PartMargin, hi: ToolBar.x + ToolBar.w); |
352 | |
353 | if(pLineInput->IsActive()) |
354 | { |
355 | // If line input is active, let's display a floating part for either the current argument name |
356 | // or for the error, if any. The error is only displayed when the cursor is at the end of the input. |
357 | const bool IsAtEnd = pLineInput->GetCursorOffset() >= (m_MapSettingsCommandContext.CommentOffset() != -1 ? m_MapSettingsCommandContext.CommentOffset() : pLineInput->GetLength()); |
358 | |
359 | if(Context.CurrentArgName() && (!Context.HasError() || !IsAtEnd)) // Render argument name |
360 | RenderFloatingPart(&ToolBar, x, Context.CurrentArgName()); |
361 | else if(Context.HasError() && IsAtEnd) // Render error |
362 | RenderFloatingPart(&ToolBar, ToolBar.x + PartMargin, Context.Error()); |
363 | } |
364 | |
365 | // If we have possible matches for the current argument, let's display an editbox suggestions dropdown |
366 | const auto &vPossibleCommands = Context.PossibleMatches(); |
367 | int Selected = DoEditBoxDropdown<SPossibleValueMatch>(pDropdown: &Context.m_DropdownContext, pLineInput, pEditBoxRect: &ToolBar, x: x - PartMargin, MaxHeight: DropdownMaxHeight, AutoWidth: Context.CurrentArg() >= 0, vData: vPossibleCommands, fnMatchCallback: MapSettingsDropdownRenderCallback); |
368 | |
369 | // If the dropdown just became visible, update the context |
370 | // This is needed when input loses focus and then we click a command in the map settings list |
371 | if(Context.m_DropdownContext.m_DidBecomeVisible) |
372 | { |
373 | Context.Update(); |
374 | Context.UpdateCursor(Force: true); |
375 | } |
376 | |
377 | if(!vPossibleCommands.empty()) |
378 | { |
379 | // Check if the completion index has changed |
380 | if(Selected != pContext->m_CurrentCompletionIndex) |
381 | { |
382 | // If so, we should autocomplete the selected option |
383 | if(Selected != -1) |
384 | { |
385 | const char *pStr = vPossibleCommands[Selected].m_pValue; |
386 | int Len = pContext->m_CurrentCompletionIndex == -1 ? str_length(str: Context.CurrentArgValue()) : (pContext->m_CurrentCompletionIndex < (int)vPossibleCommands.size() ? str_length(str: vPossibleCommands[pContext->m_CurrentCompletionIndex].m_pValue) : 0); |
387 | size_t Start = Context.CurrentArgOffset(); |
388 | size_t End = Start + Len; |
389 | pLineInput->SetRange(pString: pStr, Begin: Start, End); |
390 | } |
391 | |
392 | pContext->m_CurrentCompletionIndex = Selected; |
393 | } |
394 | } |
395 | else |
396 | { |
397 | Context.m_DropdownContext.m_ListBox.SetActive(false); |
398 | } |
399 | } |
400 | |
401 | template<typename T> |
402 | int CEditor::DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector<T> &vData, const FDropdownRenderCallback<T> &fnMatchCallback) |
403 | { |
404 | // Do an edit box with a possible dropdown |
405 | // This is a generic method which can display any data we want |
406 | |
407 | pDropdown->m_Selected = clamp(val: pDropdown->m_Selected, lo: -1, hi: (int)vData.size() - 1); |
408 | |
409 | if(Input()->KeyPress(Key: KEY_SPACE) && Input()->ModifierIsPressed()) |
410 | { // Handle Ctrl+Space to show available options |
411 | pDropdown->m_ShortcutUsed = true; |
412 | // Remove inserted space |
413 | pLineInput->SetRange(pString: "" , Begin: pLineInput->GetCursorOffset() - 1, End: pLineInput->GetCursorOffset()); |
414 | } |
415 | |
416 | if((!pDropdown->m_ShouldHide && !pLineInput->IsEmpty() && (pLineInput->IsActive() || pDropdown->m_MousePressedInside)) || pDropdown->m_ShortcutUsed) |
417 | { |
418 | if(!pDropdown->m_Visible) |
419 | { |
420 | pDropdown->m_DidBecomeVisible = true; |
421 | pDropdown->m_Visible = true; |
422 | } |
423 | else if(pDropdown->m_DidBecomeVisible) |
424 | pDropdown->m_DidBecomeVisible = false; |
425 | |
426 | if(!pLineInput->IsEmpty() || !pLineInput->IsActive()) |
427 | pDropdown->m_ShortcutUsed = false; |
428 | |
429 | int CurrentSelected = pDropdown->m_Selected; |
430 | |
431 | // Use tab to navigate through entries |
432 | if(Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_TAB) && !vData.empty()) |
433 | { |
434 | int Direction = Input()->ShiftIsPressed() ? -1 : 1; |
435 | |
436 | pDropdown->m_Selected += Direction; |
437 | if(pDropdown->m_Selected < 0) |
438 | pDropdown->m_Selected = (int)vData.size() - 1; |
439 | pDropdown->m_Selected %= vData.size(); |
440 | } |
441 | |
442 | int Selected = RenderEditBoxDropdown<T>(pDropdown, *pEditBoxRect, pLineInput, x, MaxHeight, AutoWidth, vData, fnMatchCallback); |
443 | if(Selected != -1) |
444 | pDropdown->m_Selected = Selected; |
445 | |
446 | if(CurrentSelected != pDropdown->m_Selected) |
447 | pDropdown->m_ListBox.ScrollToSelected(); |
448 | |
449 | return pDropdown->m_Selected; |
450 | } |
451 | else |
452 | { |
453 | pDropdown->m_ShortcutUsed = false; |
454 | pDropdown->m_Visible = false; |
455 | pDropdown->m_ListBox.SetActive(false); |
456 | pDropdown->m_Selected = -1; |
457 | } |
458 | |
459 | return -1; |
460 | } |
461 | |
462 | template<typename T> |
463 | int CEditor::RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector<T> &vData, const FDropdownRenderCallback<T> &fnMatchCallback) |
464 | { |
465 | // Render a dropdown tied to an edit box/line input |
466 | auto *pListBox = &pDropdown->m_ListBox; |
467 | |
468 | pListBox->SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && pLineInput->IsActive()); |
469 | pListBox->SetScrollbarWidth(15.0f); |
470 | |
471 | const int NumEntries = vData.size(); |
472 | |
473 | // Setup the rect |
474 | CUIRect CommandsDropdown = View; |
475 | CommandsDropdown.y += View.h + 0.1f; |
476 | CommandsDropdown.x = x; |
477 | if(AutoWidth) |
478 | CommandsDropdown.w = pDropdown->m_Width + pListBox->ScrollbarWidth(); |
479 | |
480 | pListBox->SetActive(NumEntries > 0); |
481 | if(NumEntries > 0) |
482 | { |
483 | // Draw the background |
484 | CommandsDropdown.h = minimum(a: NumEntries * 15.0f + 1.0f, b: MaxHeight); |
485 | CommandsDropdown.Draw(Color: ColorRGBA(0.1f, 0.1f, 0.1f, 0.9f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f); |
486 | |
487 | if(Ui()->MouseButton(Index: 0) && Ui()->MouseInside(pRect: &CommandsDropdown)) |
488 | pDropdown->m_MousePressedInside = true; |
489 | |
490 | // Do the list box |
491 | int Selected = pDropdown->m_Selected; |
492 | pListBox->DoStart(RowHeight: 15.0f, NumItems: NumEntries, ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: Selected, pRect: &CommandsDropdown); |
493 | CUIRect Label; |
494 | |
495 | int NewIndex = Selected; |
496 | float LargestWidth = 0; |
497 | for(int i = 0; i < NumEntries; i++) |
498 | { |
499 | const CListboxItem Item = pListBox->DoNextItem(pId: &vData[i], Selected: Selected == i); |
500 | |
501 | Item.m_Rect.VMargin(Cut: 4.0f, pOtherRect: &Label); |
502 | |
503 | SLabelProperties Props; |
504 | Props.m_MaxWidth = Label.w; |
505 | Props.m_EllipsisAtEnd = true; |
506 | |
507 | // Call the callback to fill the current line string |
508 | char aBuf[128]; |
509 | fnMatchCallback(vData.at(i), aBuf, Props.m_vColorSplits); |
510 | |
511 | LargestWidth = maximum(a: LargestWidth, b: TextRender()->TextWidth(Size: 12.0f, pText: aBuf) + 10.0f); |
512 | if(!Item.m_Visible) |
513 | continue; |
514 | |
515 | Ui()->DoLabel(pRect: &Label, pText: aBuf, Size: 12.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
516 | |
517 | if(Ui()->ActiveItem() == &vData[i]) |
518 | { |
519 | // If we selected an item (by clicking on it for example), then set the active item back to the |
520 | // line input so we don't loose focus |
521 | NewIndex = i; |
522 | Ui()->SetActiveItem(pLineInput); |
523 | } |
524 | } |
525 | |
526 | pDropdown->m_Width = LargestWidth; |
527 | |
528 | int EndIndex = pListBox->DoEnd(); |
529 | if(NewIndex == Selected) |
530 | NewIndex = EndIndex; |
531 | |
532 | if(pDropdown->m_MousePressedInside && !Ui()->MouseButton(Index: 0)) |
533 | { |
534 | Ui()->SetActiveItem(pLineInput); |
535 | pDropdown->m_MousePressedInside = false; |
536 | } |
537 | |
538 | if(NewIndex != Selected) |
539 | { |
540 | Ui()->SetActiveItem(pLineInput); |
541 | return NewIndex; |
542 | } |
543 | } |
544 | return -1; |
545 | } |
546 | |
547 | void CEditor::RenderMapSettingsErrorDialog() |
548 | { |
549 | auto &LoadedMapSettings = m_MapSettingsBackend.m_LoadedMapSettings; |
550 | auto &vSettingsInvalid = LoadedMapSettings.m_vSettingsInvalid; |
551 | auto &vSettingsValid = LoadedMapSettings.m_vSettingsValid; |
552 | auto &SettingsDuplicate = LoadedMapSettings.m_SettingsDuplicate; |
553 | |
554 | Ui()->MapScreen(); |
555 | CUIRect Overlay = *Ui()->Screen(); |
556 | |
557 | Overlay.Draw(Color: ColorRGBA(0, 0, 0, 0.33f), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f); |
558 | CUIRect Background; |
559 | Overlay.VMargin(Cut: 150.0f, pOtherRect: &Background); |
560 | Background.HMargin(Cut: 50.0f, pOtherRect: &Background); |
561 | Background.Draw(Color: ColorRGBA(0, 0, 0, 0.80f), Corners: IGraphics::CORNER_ALL, Rounding: 5.0f); |
562 | |
563 | CUIRect View; |
564 | Background.Margin(Cut: 10.0f, pOtherRect: &View); |
565 | |
566 | CUIRect Title, ButtonBar, Label; |
567 | View.HSplitTop(Cut: 18.0f, pTop: &Title, pBottom: &View); |
568 | View.HSplitTop(Cut: 5.0f, pTop: nullptr, pBottom: &View); // some spacing |
569 | View.HSplitBottom(Cut: 18.0f, pTop: &View, pBottom: &ButtonBar); |
570 | View.HSplitBottom(Cut: 10.0f, pTop: &View, pBottom: nullptr); // some spacing |
571 | |
572 | // title bar |
573 | Title.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 4.0f); |
574 | Title.VMargin(Cut: 10.0f, pOtherRect: &Title); |
575 | Ui()->DoLabel(pRect: &Title, pText: "Map settings error" , Size: 12.0f, Align: TEXTALIGN_ML); |
576 | |
577 | // Render body |
578 | { |
579 | static CLineInputBuffered<256> s_Input; |
580 | static CMapSettingsBackend::CContext s_Context = m_MapSettingsBackend.NewContext(pLineInput: &s_Input); |
581 | |
582 | // Some text |
583 | SLabelProperties Props; |
584 | CUIRect Text; |
585 | View.HSplitTop(Cut: 30.0f, pTop: &Text, pBottom: &View); |
586 | Props.m_MaxWidth = Text.w; |
587 | Ui()->DoLabel(pRect: &Text, pText: "Below is a report of the invalid map settings found when loading the map. Please fix them before proceeding further." , Size: 10.0f, Align: TEXTALIGN_MC, LabelProps: Props); |
588 | |
589 | // Mixed list |
590 | CUIRect List = View; |
591 | View.Draw(Color: ColorRGBA(1, 1, 1, 0.25f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f); |
592 | |
593 | const float RowHeight = 18.0f; |
594 | static CScrollRegion s_ScrollRegion; |
595 | vec2 ScrollOffset(0.0f, 0.0f); |
596 | CScrollRegionParams ScrollParams; |
597 | ScrollParams.m_ScrollUnit = 120.0f; |
598 | s_ScrollRegion.Begin(pClipRect: &List, pOutOffset: &ScrollOffset, pParams: &ScrollParams); |
599 | const float EndY = List.y + List.h; |
600 | List.y += ScrollOffset.y; |
601 | |
602 | List.HSplitTop(Cut: 20.0f, pTop: nullptr, pBottom: &List); |
603 | |
604 | static int s_FixingCommandIndex = -1; |
605 | |
606 | auto &&SetInput = [&](const char *pString) { |
607 | s_Input.Set(pString); |
608 | s_Context.Update(); |
609 | s_Context.UpdateCursor(Force: true); |
610 | Ui()->SetActiveItem(&s_Input); |
611 | }; |
612 | |
613 | CUIRect FixInput; |
614 | bool DisplayFixInput = false; |
615 | float DropdownHeight = 110.0f; |
616 | |
617 | for(int i = 0; i < (int)m_Map.m_vSettings.size(); i++) |
618 | { |
619 | CUIRect Slot; |
620 | |
621 | auto pInvalidSetting = std::find_if(first: vSettingsInvalid.begin(), last: vSettingsInvalid.end(), pred: [i](const SInvalidSetting &Setting) { return Setting.m_Index == i; }); |
622 | if(pInvalidSetting != vSettingsInvalid.end()) |
623 | { // This setting is invalid, only display it if its not a duplicate |
624 | if(!(pInvalidSetting->m_Type & SInvalidSetting::TYPE_DUPLICATE)) |
625 | { |
626 | bool IsFixing = s_FixingCommandIndex == i; |
627 | List.HSplitTop(Cut: RowHeight, pTop: &Slot, pBottom: &List); |
628 | |
629 | // Draw a reddish background if setting is marked as deleted |
630 | if(pInvalidSetting->m_Context.m_Deleted) |
631 | Slot.Draw(Color: ColorRGBA(0.85f, 0.0f, 0.0f, 0.15f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f); |
632 | |
633 | Slot.VMargin(Cut: 5.0f, pOtherRect: &Slot); |
634 | Slot.HMargin(Cut: 1.0f, pOtherRect: &Slot); |
635 | |
636 | if(!IsFixing && !pInvalidSetting->m_Context.m_Fixed) |
637 | { // Display "Fix" and "delete" buttons if we're not fixing the command and the command has not been fixed |
638 | CUIRect FixBtn, DelBtn; |
639 | Slot.VSplitRight(Cut: 30.0f, pLeft: &Slot, pRight: &DelBtn); |
640 | Slot.VSplitRight(Cut: 5.0f, pLeft: &Slot, pRight: nullptr); |
641 | DelBtn.HMargin(Cut: 1.0f, pOtherRect: &DelBtn); |
642 | |
643 | Slot.VSplitRight(Cut: 30.0f, pLeft: &Slot, pRight: &FixBtn); |
644 | Slot.VSplitRight(Cut: 10.0f, pLeft: &Slot, pRight: nullptr); |
645 | FixBtn.HMargin(Cut: 1.0f, pOtherRect: &FixBtn); |
646 | |
647 | // Delete button |
648 | if(DoButton_FontIcon(pId: &pInvalidSetting->m_Context.m_Deleted, pText: FONT_ICON_TRASH, Checked: pInvalidSetting->m_Context.m_Deleted, pRect: &DelBtn, Flags: 0, pToolTip: "Delete this command" , Corners: IGraphics::CORNER_ALL, FontSize: 10.0f)) |
649 | pInvalidSetting->m_Context.m_Deleted = !pInvalidSetting->m_Context.m_Deleted; |
650 | |
651 | // Fix button |
652 | if(DoButton_Editor(pId: &pInvalidSetting->m_Context.m_Fixed, pText: "Fix" , Checked: !pInvalidSetting->m_Context.m_Deleted ? (s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1)) : -1, pRect: &FixBtn, Flags: 0, pToolTip: "Fix this command" )) |
653 | { |
654 | s_FixingCommandIndex = i; |
655 | SetInput(pInvalidSetting->m_aSetting); |
656 | } |
657 | } |
658 | else if(IsFixing) |
659 | { // If we're fixing this command, then display "Done" and "Cancel" buttons |
660 | // Also setup the input rect |
661 | CUIRect OkBtn, CancelBtn; |
662 | Slot.VSplitRight(Cut: 50.0f, pLeft: &Slot, pRight: &CancelBtn); |
663 | Slot.VSplitRight(Cut: 5.0f, pLeft: &Slot, pRight: nullptr); |
664 | CancelBtn.HMargin(Cut: 1.0f, pOtherRect: &CancelBtn); |
665 | |
666 | Slot.VSplitRight(Cut: 30.0f, pLeft: &Slot, pRight: &OkBtn); |
667 | Slot.VSplitRight(Cut: 10.0f, pLeft: &Slot, pRight: nullptr); |
668 | OkBtn.HMargin(Cut: 1.0f, pOtherRect: &OkBtn); |
669 | |
670 | // Buttons |
671 | static int s_Cancel = 0, s_Ok = 0; |
672 | if(DoButton_Editor(pId: &s_Cancel, pText: "Cancel" , Checked: 0, pRect: &CancelBtn, Flags: 0, pToolTip: "Cancel fixing this command" ) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)) |
673 | { |
674 | s_FixingCommandIndex = -1; |
675 | s_Input.Clear(); |
676 | } |
677 | |
678 | // "Done" button only enabled if the fixed setting is valid |
679 | // For that we use a local CContext s_Context and use it to check |
680 | // that the setting is valid and that it is not a duplicate |
681 | ECollisionCheckResult Res = ECollisionCheckResult::ERROR; |
682 | s_Context.CheckCollision(vSettings: vSettingsValid, Result&: Res); |
683 | bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::ADD; |
684 | |
685 | if(DoButton_Editor(pId: &s_Ok, pText: "Done" , Checked: Valid ? 0 : -1, pRect: &OkBtn, Flags: 0, pToolTip: "Confirm editing of this command" ) || (s_Input.IsActive() && Valid && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))) |
686 | { |
687 | // Mark the setting is being fixed |
688 | pInvalidSetting->m_Context.m_Fixed = true; |
689 | str_copy(dst&: pInvalidSetting->m_aSetting, src: s_Input.GetString()); |
690 | // Add it to the list for future collision checks |
691 | vSettingsValid.emplace_back(args: s_Input.GetString()); |
692 | |
693 | // Clear the input & fixing command index |
694 | s_FixingCommandIndex = -1; |
695 | s_Input.Clear(); |
696 | } |
697 | } |
698 | |
699 | Label = Slot; |
700 | Props.m_EllipsisAtEnd = true; |
701 | Props.m_MaxWidth = Label.w; |
702 | |
703 | if(IsFixing) |
704 | { |
705 | // Setup input rect, which will be used to draw the map settings input later |
706 | Label.HMargin(Cut: 1.0, pOtherRect: &FixInput); |
707 | DisplayFixInput = true; |
708 | DropdownHeight = minimum(a: DropdownHeight, b: EndY - FixInput.y - 16.0f); |
709 | } |
710 | else |
711 | { |
712 | // Draw label in case we're not fixing this setting. |
713 | // Deleted settings are shown in gray with a red line through them |
714 | // Fixed settings are shown in green |
715 | // Invalid settings are shown in red |
716 | if(!pInvalidSetting->m_Context.m_Deleted) |
717 | { |
718 | if(pInvalidSetting->m_Context.m_Fixed) |
719 | TextRender()->TextColor(r: 0.0f, g: 1.0f, b: 0.0f, a: 1.0f); |
720 | else |
721 | TextRender()->TextColor(r: 1.0f, g: 0.0f, b: 0.0f, a: 1.0f); |
722 | Ui()->DoLabel(pRect: &Label, pText: pInvalidSetting->m_aSetting, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
723 | } |
724 | else |
725 | { |
726 | TextRender()->TextColor(r: 0.3f, g: 0.3f, b: 0.3f, a: 1.0f); |
727 | Ui()->DoLabel(pRect: &Label, pText: pInvalidSetting->m_aSetting, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
728 | |
729 | CUIRect Line = Label; |
730 | Line.y = Label.y + Label.h / 2; |
731 | Line.h = 1; |
732 | Line.Draw(Color: ColorRGBA(1, 0, 0, 1), Corners: IGraphics::CORNER_NONE, Rounding: 0.0f); |
733 | } |
734 | } |
735 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
736 | } |
737 | } |
738 | else |
739 | { // This setting is valid |
740 | // Check for duplicates |
741 | const std::vector<int> &vDuplicates = SettingsDuplicate.at(k: i); |
742 | int Chosen = -1; // This is the chosen duplicate setting. -1 means the first valid setting that was found which was not a duplicate |
743 | for(int d = 0; d < (int)vDuplicates.size(); d++) |
744 | { |
745 | int DupIndex = vDuplicates[d]; |
746 | if(vSettingsInvalid[DupIndex].m_Context.m_Chosen) |
747 | { |
748 | Chosen = d; |
749 | break; |
750 | } |
751 | } |
752 | |
753 | List.HSplitTop(Cut: RowHeight * (vDuplicates.size() + 1) + 2.0f, pTop: &Slot, pBottom: &List); |
754 | Slot.HMargin(Cut: 1.0f, pOtherRect: &Slot); |
755 | |
756 | // Draw a background to highlight group of duplicates |
757 | if(!vDuplicates.empty()) |
758 | Slot.Draw(Color: ColorRGBA(1, 1, 1, 0.15f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f); |
759 | |
760 | Slot.VMargin(Cut: 5.0f, pOtherRect: &Slot); |
761 | Slot.HSplitTop(Cut: RowHeight, pTop: &Label, pBottom: &Slot); |
762 | Label.HMargin(Cut: 1.0f, pOtherRect: &Label); |
763 | |
764 | // Draw a "choose" button next to the label in case we have duplicates for this line |
765 | if(!vDuplicates.empty()) |
766 | { |
767 | CUIRect ChooseBtn; |
768 | Label.VSplitRight(Cut: 50.0f, pLeft: &Label, pRight: &ChooseBtn); |
769 | Label.VSplitRight(Cut: 5.0f, pLeft: &Label, pRight: nullptr); |
770 | ChooseBtn.HMargin(Cut: 1.0f, pOtherRect: &ChooseBtn); |
771 | if(DoButton_Editor(pId: &vDuplicates, pText: "Choose" , Checked: Chosen == -1, pRect: &ChooseBtn, Flags: 0, pToolTip: "Choose this command" )) |
772 | { |
773 | if(Chosen != -1) |
774 | vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; |
775 | Chosen = -1; // Choosing this means that we do not choose any of the duplicates |
776 | } |
777 | } |
778 | |
779 | // Draw the label |
780 | Props.m_MaxWidth = Label.w; |
781 | Ui()->DoLabel(pRect: &Label, pText: m_Map.m_vSettings[i].m_aCommand, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
782 | |
783 | // Draw the list of duplicates, with a "Choose" button for each duplicate |
784 | // In case a duplicate is also invalid, then we draw a "Fix" button which behaves like the fix button above |
785 | // Duplicate settings name are shown in light blue, or in purple if they are also invalid |
786 | Slot.VSplitLeft(Cut: 10.0f, pLeft: nullptr, pRight: &Slot); |
787 | for(int DuplicateIndex = 0; DuplicateIndex < (int)vDuplicates.size(); DuplicateIndex++) |
788 | { |
789 | auto &Duplicate = vSettingsInvalid.at(n: vDuplicates[DuplicateIndex]); |
790 | bool IsFixing = s_FixingCommandIndex == Duplicate.m_Index; |
791 | bool IsInvalid = Duplicate.m_Type & SInvalidSetting::TYPE_INVALID; |
792 | |
793 | ColorRGBA Color(0.329f, 0.714f, 0.859f, 1.0f); |
794 | CUIRect SubSlot; |
795 | Slot.HSplitTop(Cut: RowHeight, pTop: &SubSlot, pBottom: &Slot); |
796 | SubSlot.HMargin(Cut: 1.0f, pOtherRect: &SubSlot); |
797 | |
798 | if(!IsFixing) |
799 | { |
800 | // If not fixing, then display "Choose" and maybe "Fix" buttons. |
801 | |
802 | CUIRect ChooseBtn; |
803 | SubSlot.VSplitRight(Cut: 50.0f, pLeft: &SubSlot, pRight: &ChooseBtn); |
804 | SubSlot.VSplitRight(Cut: 5.0f, pLeft: &SubSlot, pRight: nullptr); |
805 | ChooseBtn.HMargin(Cut: 1.0f, pOtherRect: &ChooseBtn); |
806 | if(DoButton_Editor(pId: &Duplicate.m_Context.m_Chosen, pText: "Choose" , Checked: IsInvalid && !Duplicate.m_Context.m_Fixed ? -1 : Duplicate.m_Context.m_Chosen, pRect: &ChooseBtn, Flags: 0, pToolTip: "Override with this command" )) |
807 | { |
808 | Duplicate.m_Context.m_Chosen = !Duplicate.m_Context.m_Chosen; |
809 | if(Chosen != -1 && Chosen != DuplicateIndex) |
810 | vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; |
811 | Chosen = DuplicateIndex; |
812 | } |
813 | |
814 | if(IsInvalid) |
815 | { |
816 | if(!Duplicate.m_Context.m_Fixed) |
817 | { |
818 | Color = ColorRGBA(1, 0, 1, 1); |
819 | CUIRect FixBtn; |
820 | SubSlot.VSplitRight(Cut: 30.0f, pLeft: &SubSlot, pRight: &FixBtn); |
821 | SubSlot.VSplitRight(Cut: 10.0f, pLeft: &SubSlot, pRight: nullptr); |
822 | FixBtn.HMargin(Cut: 1.0f, pOtherRect: &FixBtn); |
823 | if(DoButton_Editor(pId: &Duplicate.m_Context.m_Fixed, pText: "Fix" , Checked: s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1), pRect: &FixBtn, Flags: 0, pToolTip: "Fix this command (needed before it can be chosen)" )) |
824 | { |
825 | s_FixingCommandIndex = Duplicate.m_Index; |
826 | SetInput(Duplicate.m_aSetting); |
827 | } |
828 | } |
829 | else |
830 | { |
831 | Color = ColorRGBA(0.329f, 0.714f, 0.859f, 1.0f); |
832 | } |
833 | } |
834 | } |
835 | else |
836 | { |
837 | // If we're fixing, display "Done" and "Cancel" buttons |
838 | CUIRect OkBtn, CancelBtn; |
839 | SubSlot.VSplitRight(Cut: 50.0f, pLeft: &SubSlot, pRight: &CancelBtn); |
840 | SubSlot.VSplitRight(Cut: 5.0f, pLeft: &SubSlot, pRight: nullptr); |
841 | CancelBtn.HMargin(Cut: 1.0f, pOtherRect: &CancelBtn); |
842 | |
843 | SubSlot.VSplitRight(Cut: 30.0f, pLeft: &SubSlot, pRight: &OkBtn); |
844 | SubSlot.VSplitRight(Cut: 10.0f, pLeft: &SubSlot, pRight: nullptr); |
845 | OkBtn.HMargin(Cut: 1.0f, pOtherRect: &OkBtn); |
846 | |
847 | static int s_Cancel = 0, s_Ok = 0; |
848 | if(DoButton_Editor(pId: &s_Cancel, pText: "Cancel" , Checked: 0, pRect: &CancelBtn, Flags: 0, pToolTip: "Cancel fixing this command" ) || Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)) |
849 | { |
850 | s_FixingCommandIndex = -1; |
851 | s_Input.Clear(); |
852 | } |
853 | |
854 | // Use the local CContext s_Context to validate the input |
855 | // We also need to make sure the fixed setting matches the initial duplicate setting |
856 | // For example: |
857 | // sv_deepfly 0 |
858 | // sv_deepfly 5 <- This is invalid and duplicate. We can only fix it by writing "sv_deepfly 0" or "sv_deepfly 1". |
859 | // If we write any other setting, like "sv_hit 1", it won't work as it does not match "sv_deepfly". |
860 | // To do that, we use the context and we check for collision with the current map setting |
861 | ECollisionCheckResult Res = ECollisionCheckResult::ERROR; |
862 | s_Context.CheckCollision(vSettings: {m_Map.m_vSettings[i]}, Result&: Res); |
863 | bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::REPLACE; |
864 | |
865 | if(DoButton_Editor(pId: &s_Ok, pText: "Done" , Checked: Valid ? 0 : -1, pRect: &OkBtn, Flags: 0, pToolTip: "Confirm editing of this command" ) || (s_Input.IsActive() && Valid && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))) |
866 | { |
867 | if(Valid) // Just to make sure |
868 | { |
869 | // Mark the setting as fixed |
870 | Duplicate.m_Context.m_Fixed = true; |
871 | str_copy(dst&: Duplicate.m_aSetting, src: s_Input.GetString()); |
872 | |
873 | s_FixingCommandIndex = -1; |
874 | s_Input.Clear(); |
875 | } |
876 | } |
877 | } |
878 | |
879 | Label = SubSlot; |
880 | Props.m_MaxWidth = Label.w; |
881 | |
882 | if(IsFixing) |
883 | { |
884 | // Setup input rect in case we are fixing the setting |
885 | Label.HMargin(Cut: 1.0, pOtherRect: &FixInput); |
886 | DisplayFixInput = true; |
887 | DropdownHeight = minimum(a: DropdownHeight, b: EndY - FixInput.y - 16.0f); |
888 | } |
889 | else |
890 | { |
891 | // Otherwise, render the setting label |
892 | TextRender()->TextColor(rgb: Color); |
893 | Ui()->DoLabel(pRect: &Label, pText: Duplicate.m_aSetting, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props); |
894 | TextRender()->TextColor(rgb: TextRender()->DefaultTextColor()); |
895 | } |
896 | } |
897 | } |
898 | |
899 | // Finally, add the slot to the scroll region |
900 | s_ScrollRegion.AddRect(Rect: Slot); |
901 | } |
902 | |
903 | // Add some padding to the bottom so the dropdown can actually display some values in case we |
904 | // fix an invalid setting at the bottom of the list |
905 | CUIRect PaddingBottom; |
906 | List.HSplitTop(Cut: 30.0f, pTop: &PaddingBottom, pBottom: &List); |
907 | s_ScrollRegion.AddRect(Rect: PaddingBottom); |
908 | |
909 | // Display the map settings edit box after having rendered all the lines, so the dropdown shows in |
910 | // front of everything, but is still being clipped by the scroll region. |
911 | if(DisplayFixInput) |
912 | DoMapSettingsEditBox(pContext: &s_Context, pRect: &FixInput, FontSize: 10.0f, DropdownMaxHeight: maximum(a: DropdownHeight, b: 30.0f)); |
913 | |
914 | s_ScrollRegion.End(); |
915 | } |
916 | |
917 | // Confirm button |
918 | static int s_ConfirmButton = 0, s_CancelButton = 0, s_FixAllButton = 0; |
919 | CUIRect ConfimButton, CancelButton, FixAllUnknownButton; |
920 | ButtonBar.VSplitLeft(Cut: 110.0f, pLeft: &CancelButton, pRight: &ButtonBar); |
921 | ButtonBar.VSplitRight(Cut: 110.0f, pLeft: &ButtonBar, pRight: &ConfimButton); |
922 | ButtonBar.VSplitRight(Cut: 5.0f, pLeft: &ButtonBar, pRight: nullptr); |
923 | ButtonBar.VSplitRight(Cut: 150.0f, pLeft: &ButtonBar, pRight: &FixAllUnknownButton); |
924 | |
925 | bool CanConfirm = true; |
926 | bool CanFixAllUnknown = false; |
927 | for(auto &InvalidSetting : vSettingsInvalid) |
928 | { |
929 | if(!InvalidSetting.m_Context.m_Fixed && !InvalidSetting.m_Context.m_Deleted && !(InvalidSetting.m_Type & SInvalidSetting::TYPE_DUPLICATE)) |
930 | { |
931 | CanConfirm = false; |
932 | if(InvalidSetting.m_Unknown) |
933 | CanFixAllUnknown = true; |
934 | break; |
935 | } |
936 | } |
937 | |
938 | auto &&Execute = [&]() { |
939 | // Execute will modify the actual map settings according to the fixes that were just made within the dialog. |
940 | |
941 | // Fix fixed settings, erase deleted settings |
942 | for(auto &FixedSetting : vSettingsInvalid) |
943 | { |
944 | if(FixedSetting.m_Context.m_Fixed) |
945 | { |
946 | str_copy(dst&: m_Map.m_vSettings[FixedSetting.m_Index].m_aCommand, src: FixedSetting.m_aSetting); |
947 | } |
948 | } |
949 | |
950 | // Choose chosen settings |
951 | // => Erase settings that don't match |
952 | // => Erase settings that were not chosen |
953 | std::vector<CEditorMapSetting> vSettingsToErase; |
954 | for(auto &Setting : vSettingsInvalid) |
955 | { |
956 | if(Setting.m_Type & SInvalidSetting::TYPE_DUPLICATE) |
957 | { |
958 | if(!Setting.m_Context.m_Chosen) |
959 | vSettingsToErase.emplace_back(args&: Setting.m_aSetting); |
960 | else |
961 | vSettingsToErase.emplace_back(args&: m_Map.m_vSettings[Setting.m_CollidingIndex].m_aCommand); |
962 | } |
963 | } |
964 | |
965 | // Erase deleted settings |
966 | for(auto &DeletedSetting : vSettingsInvalid) |
967 | { |
968 | if(DeletedSetting.m_Context.m_Deleted) |
969 | { |
970 | m_Map.m_vSettings.erase( |
971 | first: std::remove_if(first: m_Map.m_vSettings.begin(), last: m_Map.m_vSettings.end(), pred: [&](const CEditorMapSetting &MapSetting) { |
972 | return str_comp_nocase(a: MapSetting.m_aCommand, b: DeletedSetting.m_aSetting) == 0; |
973 | }), |
974 | last: m_Map.m_vSettings.end()); |
975 | } |
976 | } |
977 | |
978 | // Erase settings to erase |
979 | for(auto &Setting : vSettingsToErase) |
980 | { |
981 | m_Map.m_vSettings.erase( |
982 | first: std::remove_if(first: m_Map.m_vSettings.begin(), last: m_Map.m_vSettings.end(), pred: [&](const CEditorMapSetting &MapSetting) { |
983 | return str_comp_nocase(a: MapSetting.m_aCommand, b: Setting.m_aCommand) == 0; |
984 | }), |
985 | last: m_Map.m_vSettings.end()); |
986 | } |
987 | |
988 | m_Map.OnModify(); |
989 | }; |
990 | |
991 | auto &&FixAllUnknown = [&] { |
992 | // Mark unknown settings as fixed |
993 | for(auto &InvalidSetting : vSettingsInvalid) |
994 | if(!InvalidSetting.m_Context.m_Fixed && !InvalidSetting.m_Context.m_Deleted && !(InvalidSetting.m_Type & SInvalidSetting::TYPE_DUPLICATE) && InvalidSetting.m_Unknown) |
995 | InvalidSetting.m_Context.m_Fixed = true; |
996 | }; |
997 | |
998 | // Fix all unknown settings |
999 | if(DoButton_Editor(pId: &s_FixAllButton, pText: "Allow all unknown settings" , Checked: CanFixAllUnknown ? 0 : -1, pRect: &FixAllUnknownButton, Flags: 0, pToolTip: nullptr)) |
1000 | { |
1001 | FixAllUnknown(); |
1002 | } |
1003 | |
1004 | // Confirm - execute the fixes |
1005 | if(DoButton_Editor(pId: &s_ConfirmButton, pText: "Confirm" , Checked: CanConfirm ? 0 : -1, pRect: &ConfimButton, Flags: 0, pToolTip: nullptr) || (CanConfirm && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER))) |
1006 | { |
1007 | Execute(); |
1008 | m_Dialog = DIALOG_NONE; |
1009 | } |
1010 | |
1011 | // Cancel - we load a new empty map |
1012 | if(DoButton_Editor(pId: &s_CancelButton, pText: "Cancel" , Checked: 0, pRect: &CancelButton, Flags: 0, pToolTip: nullptr) || (Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE))) |
1013 | { |
1014 | Reset(); |
1015 | m_aFileName[0] = 0; |
1016 | m_Dialog = DIALOG_NONE; |
1017 | } |
1018 | } |
1019 | |
1020 | void CEditor::MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector<STextColorSplit> &vColorSplits) |
1021 | { |
1022 | // Check the match argument index. |
1023 | // If it's -1, we're displaying the list of available map settings names |
1024 | // If its >= 0, we're displaying the list of possible values matches for that argument |
1025 | if(Match.m_ArgIndex == -1) |
1026 | { |
1027 | IMapSetting *pInfo = (IMapSetting *)Match.m_pData; |
1028 | vColorSplits = { |
1029 | {str_length(str: pInfo->m_pName) + 1, -1, ColorRGBA(0.6f, 0.6f, 0.6f, 1)}, // Darker arguments |
1030 | }; |
1031 | |
1032 | if(pInfo->m_Type == IMapSetting::SETTING_INT) |
1033 | { |
1034 | str_format(buffer: aOutput, buffer_size: sizeof(aOutput), format: "%s i[value]" , pInfo->m_pName); |
1035 | } |
1036 | else if(pInfo->m_Type == IMapSetting::SETTING_COMMAND) |
1037 | { |
1038 | SMapSettingCommand *pCommand = (SMapSettingCommand *)pInfo; |
1039 | str_format(buffer: aOutput, buffer_size: sizeof(aOutput), format: "%s %s" , pCommand->m_pName, pCommand->m_pArgs); |
1040 | } |
1041 | } |
1042 | else |
1043 | { |
1044 | str_copy(dst&: aOutput, src: Match.m_pValue); |
1045 | } |
1046 | } |
1047 | |
1048 | // ---------------------------------------- |
1049 | |
1050 | CMapSettingsBackend::CContext *CMapSettingsBackend::ms_pActiveContext = nullptr; |
1051 | |
1052 | void CMapSettingsBackend::Init(CEditor *pEditor) |
1053 | { |
1054 | CEditorComponent::Init(pEditor); |
1055 | |
1056 | // Register values loader |
1057 | InitValueLoaders(); |
1058 | |
1059 | // Load settings/commands |
1060 | LoadAllMapSettings(); |
1061 | |
1062 | CValuesBuilder Builder(&m_PossibleValuesPerCommand); |
1063 | |
1064 | // Load and parse static map settings so we can use them here |
1065 | for(auto &pSetting : m_vpMapSettings) |
1066 | { |
1067 | // We want to parse the arguments of each map setting so we can autocomplete them later |
1068 | // But that depends on the type of the setting. |
1069 | // If we have a INT setting, then we know we can only ever have 1 argument which is a integer value |
1070 | // If we have a COMMAND setting, then we need to parse its arguments |
1071 | if(pSetting->m_Type == IMapSetting::SETTING_INT) |
1072 | LoadSettingInt(pSetting: std::static_pointer_cast<SMapSettingInt>(r: pSetting)); |
1073 | else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) |
1074 | LoadSettingCommand(pSetting: std::static_pointer_cast<SMapSettingCommand>(r: pSetting)); |
1075 | |
1076 | LoadPossibleValues(Builder: Builder(pSetting->m_pName), pSetting); |
1077 | } |
1078 | |
1079 | // Init constraints |
1080 | LoadConstraints(); |
1081 | } |
1082 | |
1083 | void CMapSettingsBackend::LoadAllMapSettings() |
1084 | { |
1085 | // Gather all config variables having the flag CFGFLAG_GAME |
1086 | Editor()->ConfigManager()->PossibleConfigVariables(pStr: "" , FlagMask: CFGFLAG_GAME, pfnCallback: PossibleConfigVariableCallback, pUserData: this); |
1087 | |
1088 | // Load list of commands |
1089 | LoadCommand(pName: "tune" , pArgs: "s[tuning] f[value]" , pHelp: "Tune variable to value or show current value" ); |
1090 | LoadCommand(pName: "tune_zone" , pArgs: "i[zone] s[tuning] f[value]" , pHelp: "Tune in zone a variable to value" ); |
1091 | LoadCommand(pName: "tune_zone_enter" , pArgs: "i[zone] r[message]" , pHelp: "which message to display on zone enter; use 0 for normal area" ); |
1092 | LoadCommand(pName: "tune_zone_leave" , pArgs: "i[zone] r[message]" , pHelp: "which message to display on zone leave; use 0 for normal area" ); |
1093 | LoadCommand(pName: "mapbug" , pArgs: "s[mapbug]" , pHelp: "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)" ); |
1094 | LoadCommand(pName: "switch_open" , pArgs: "i[switch]" , pHelp: "Whether a switch is deactivated by default (otherwise activated)" ); |
1095 | } |
1096 | |
1097 | void CMapSettingsBackend::LoadCommand(const char *pName, const char *pArgs, const char *pHelp) |
1098 | { |
1099 | m_vpMapSettings.emplace_back(args: std::make_shared<SMapSettingCommand>(args&: pName, args&: pHelp, args&: pArgs)); |
1100 | } |
1101 | |
1102 | void CMapSettingsBackend::LoadSettingInt(const std::shared_ptr<SMapSettingInt> &pSetting) |
1103 | { |
1104 | // We load an int argument here |
1105 | m_ParsedCommandArgs[pSetting].emplace_back(); |
1106 | auto &Arg = m_ParsedCommandArgs[pSetting].back(); |
1107 | str_copy(dst&: Arg.m_aName, src: "value" ); |
1108 | Arg.m_Type = 'i'; |
1109 | } |
1110 | |
1111 | void CMapSettingsBackend::LoadSettingCommand(const std::shared_ptr<SMapSettingCommand> &pSetting) |
1112 | { |
1113 | // This method parses a setting into its arguments (name and type) so we can later |
1114 | // use them to validate the current input as well as display the current argument value |
1115 | // over the line input. |
1116 | |
1117 | m_ParsedCommandArgs[pSetting].clear(); |
1118 | const char *pIterator = pSetting->m_pArgs; |
1119 | |
1120 | char Type; |
1121 | |
1122 | while(*pIterator) |
1123 | { |
1124 | if(*pIterator == '?') // Skip optional values as a map setting should not have optional values |
1125 | pIterator++; |
1126 | |
1127 | Type = *pIterator; |
1128 | pIterator++; |
1129 | while(*pIterator && *pIterator != '[') |
1130 | pIterator++; |
1131 | pIterator++; // skip '[' |
1132 | |
1133 | const char *pNameStart = pIterator; |
1134 | |
1135 | while(*pIterator && *pIterator != ']') |
1136 | pIterator++; |
1137 | |
1138 | size_t Len = pIterator - pNameStart; |
1139 | pIterator++; // Skip ']' |
1140 | |
1141 | dbg_assert(Len + 1 < sizeof(SParsedMapSettingArg::m_aName), "Length of server setting name exceeds limit." ); |
1142 | |
1143 | // Append parsed arg |
1144 | m_ParsedCommandArgs[pSetting].emplace_back(); |
1145 | auto &Arg = m_ParsedCommandArgs[pSetting].back(); |
1146 | str_copy(dst: Arg.m_aName, src: pNameStart, dst_size: Len + 1); |
1147 | Arg.m_Type = Type; |
1148 | |
1149 | pIterator = str_skip_whitespaces_const(str: pIterator); |
1150 | } |
1151 | } |
1152 | |
1153 | void CMapSettingsBackend::LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr<IMapSetting> &pSetting) |
1154 | { |
1155 | // Call the value loader for that setting |
1156 | auto Iter = m_LoaderFunctions.find(x: pSetting->m_pName); |
1157 | if(Iter == m_LoaderFunctions.end()) |
1158 | return; |
1159 | |
1160 | (*Iter->second)(Builder); |
1161 | } |
1162 | |
1163 | void CMapSettingsBackend::RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader) |
1164 | { |
1165 | // Registers a value loader function for a specific setting name |
1166 | m_LoaderFunctions[pSettingName] = pfnLoader; |
1167 | } |
1168 | |
1169 | void CMapSettingsBackend::LoadConstraints() |
1170 | { |
1171 | // Make an instance of constraint builder |
1172 | CCommandArgumentConstraintBuilder Command(&m_ArgConstraintsPerCommand); |
1173 | |
1174 | // Define constraints like this |
1175 | // This is still a bit sad as we have to do it manually here. |
1176 | Command("tune" , 2).Unique(Arg: 0); |
1177 | Command("tune_zone" , 3).Multiple(Arg: 0).Unique(Arg: 1); |
1178 | Command("tune_zone_enter" , 2).Unique(Arg: 0); |
1179 | Command("tune_zone_leave" , 2).Unique(Arg: 0); |
1180 | Command("switch_open" , 1).Unique(Arg: 0); |
1181 | Command("mapbug" , 1).Unique(Arg: 0); |
1182 | } |
1183 | |
1184 | void CMapSettingsBackend::PossibleConfigVariableCallback(const SConfigVariable *pVariable, void *pUserData) |
1185 | { |
1186 | CMapSettingsBackend *pBackend = (CMapSettingsBackend *)pUserData; |
1187 | |
1188 | if(pVariable->m_Type == SConfigVariable::VAR_INT) |
1189 | { |
1190 | SIntConfigVariable *pIntVariable = (SIntConfigVariable *)pVariable; |
1191 | pBackend->m_vpMapSettings.emplace_back(args: std::make_shared<SMapSettingInt>( |
1192 | args&: pIntVariable->m_pScriptName, |
1193 | args&: pIntVariable->m_pHelp, |
1194 | args&: pIntVariable->m_Default, |
1195 | args&: pIntVariable->m_Min, |
1196 | args&: pIntVariable->m_Max)); |
1197 | } |
1198 | } |
1199 | |
1200 | void CMapSettingsBackend::CContext::Reset() |
1201 | { |
1202 | m_LastCursorOffset = 0; |
1203 | m_CursorArgIndex = -1; |
1204 | m_pCurrentSetting = nullptr; |
1205 | m_vCurrentArgs.clear(); |
1206 | m_aCommand[0] = '\0'; |
1207 | m_DropdownContext.m_Selected = -1; |
1208 | m_CurrentCompletionIndex = -1; |
1209 | m_DropdownContext.m_ShortcutUsed = false; |
1210 | m_DropdownContext.m_MousePressedInside = false; |
1211 | m_DropdownContext.m_Visible = false; |
1212 | m_DropdownContext.m_ShouldHide = false; |
1213 | m_CommentOffset = -1; |
1214 | |
1215 | ClearError(); |
1216 | } |
1217 | |
1218 | void CMapSettingsBackend::CContext::Update() |
1219 | { |
1220 | UpdateFromString(pStr: InputString()); |
1221 | } |
1222 | |
1223 | void CMapSettingsBackend::CContext::UpdateFromString(const char *pStr) |
1224 | { |
1225 | // This is the main method that does all the argument parsing and validating. |
1226 | // It fills pretty much all the context values, the arguments, their position, |
1227 | // if they are valid or not, etc. |
1228 | |
1229 | m_pCurrentSetting = nullptr; |
1230 | m_vCurrentArgs.clear(); |
1231 | m_CommentOffset = -1; |
1232 | |
1233 | const char *pIterator = pStr; |
1234 | |
1235 | // Check for comment |
1236 | const char *pEnd = pStr; |
1237 | int InString = 0; |
1238 | |
1239 | while(*pEnd) |
1240 | { |
1241 | if(*pEnd == '"') |
1242 | InString ^= 1; |
1243 | else if(*pEnd == '\\') // Escape sequences |
1244 | { |
1245 | if(pEnd[1] == '"') |
1246 | pEnd++; |
1247 | } |
1248 | else if(!InString) |
1249 | { |
1250 | if(*pEnd == '#') // Found comment |
1251 | { |
1252 | m_CommentOffset = pEnd - pStr; |
1253 | break; |
1254 | } |
1255 | } |
1256 | |
1257 | pEnd++; |
1258 | } |
1259 | |
1260 | if(m_CommentOffset == 0) |
1261 | return; |
1262 | |
1263 | // End command at start of comment, if any |
1264 | char aInputString[256]; |
1265 | str_copy(dst: aInputString, src: pStr, dst_size: m_CommentOffset != -1 ? m_CommentOffset + 1 : sizeof(aInputString)); |
1266 | pIterator = aInputString; |
1267 | |
1268 | // Get the command/setting |
1269 | m_aCommand[0] = '\0'; |
1270 | while(pIterator && *pIterator != ' ' && *pIterator != '\0') |
1271 | pIterator++; |
1272 | |
1273 | str_copy(dst: m_aCommand, src: aInputString, dst_size: (pIterator - aInputString) + 1); |
1274 | |
1275 | // Get the command if it is a recognized one |
1276 | for(auto &pSetting : m_pBackend->m_vpMapSettings) |
1277 | { |
1278 | if(str_comp_nocase(a: m_aCommand, b: pSetting->m_pName) == 0) |
1279 | { |
1280 | m_pCurrentSetting = pSetting; |
1281 | break; |
1282 | } |
1283 | } |
1284 | |
1285 | // Parse args |
1286 | ParseArgs(pLineInputStr: aInputString, pStr: pIterator); |
1287 | } |
1288 | |
1289 | void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const char *pStr) |
1290 | { |
1291 | // This method parses the arguments of the current command, starting at pStr |
1292 | |
1293 | ClearError(); |
1294 | |
1295 | const char *pIterator = pStr; |
1296 | |
1297 | if(!pStr || *pStr == '\0') |
1298 | return; // No arguments |
1299 | |
1300 | // NextArg is used to get the contents of the current argument and go to the next argument position |
1301 | // It outputs the length of the argument in pLength and returns a boolean indicating if the parsing |
1302 | // of that argument is valid or not (only the case when using strings with quotes (")) |
1303 | auto &&NextArg = [&](const char *pArg, int *pLength) { |
1304 | if(*pIterator == '"') |
1305 | { |
1306 | pIterator++; |
1307 | bool Valid = true; |
1308 | bool IsEscape = false; |
1309 | |
1310 | while(true) |
1311 | { |
1312 | if(pIterator[0] == '"' && !IsEscape) |
1313 | break; |
1314 | else if(pIterator[0] == 0) |
1315 | { |
1316 | Valid = false; |
1317 | break; |
1318 | } |
1319 | |
1320 | if(pIterator[0] == '\\' && !IsEscape) |
1321 | IsEscape = true; |
1322 | else if(IsEscape) |
1323 | IsEscape = false; |
1324 | |
1325 | pIterator++; |
1326 | } |
1327 | const char *pEnd = ++pIterator; |
1328 | pIterator = str_skip_to_whitespace_const(str: pIterator); |
1329 | |
1330 | // Make sure there are no other characters at the end, otherwise the string is invalid. |
1331 | // E.g. "abcd"ef is invalid |
1332 | Valid = Valid && pIterator == pEnd; |
1333 | *pLength = pEnd - pArg; |
1334 | |
1335 | return Valid; |
1336 | } |
1337 | else |
1338 | { |
1339 | pIterator = str_skip_to_whitespace_const(str: pIterator); |
1340 | *pLength = pIterator - pArg; |
1341 | return true; |
1342 | } |
1343 | }; |
1344 | |
1345 | // Simple validation of string. Checks that it does not contain unescaped " in the middle of it. |
1346 | auto &&ValidateStr = [](const char *pString) -> bool { |
1347 | const char *pIt = pString; |
1348 | bool IsEscape = false; |
1349 | while(*pIt) |
1350 | { |
1351 | if(pIt[0] == '"' && !IsEscape) |
1352 | return false; |
1353 | |
1354 | if(pIt[0] == '\\' && !IsEscape) |
1355 | IsEscape = true; |
1356 | else if(IsEscape) |
1357 | IsEscape = false; |
1358 | |
1359 | pIt++; |
1360 | } |
1361 | return true; |
1362 | }; |
1363 | |
1364 | const int CommandArgCount = m_pCurrentSetting != nullptr ? m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting).size() : 0; |
1365 | int ArgIndex = 0; |
1366 | SCommandParseError::EErrorType Error = SCommandParseError::ERROR_NONE; |
1367 | |
1368 | // Also keep track of the visual X position of each argument within the input |
1369 | float PosX = 0; |
1370 | const float WW = m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: " " ); |
1371 | PosX += m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: m_aCommand); |
1372 | |
1373 | // Parsing beings |
1374 | while(*pIterator) |
1375 | { |
1376 | Error = SCommandParseError::ERROR_NONE; |
1377 | pIterator++; // Skip whitespace |
1378 | PosX += WW; // Add whitespace width |
1379 | |
1380 | // Insert argument here |
1381 | char Char = *pIterator; |
1382 | const char *pArgStart = pIterator; |
1383 | int Length; |
1384 | bool Valid = NextArg(pArgStart, &Length); // Get contents and go to next argument position |
1385 | size_t Offset = pArgStart - pLineInputStr; // Compute offset from the start of the input |
1386 | |
1387 | // Add new argument, copy the argument contents |
1388 | m_vCurrentArgs.emplace_back(); |
1389 | auto &NewArg = m_vCurrentArgs.back(); |
1390 | // Fill argument value, with a maximum length of 256 |
1391 | str_copy(dst: NewArg.m_aValue, src: pArgStart, dst_size: minimum(a: (int)sizeof(SCurrentSettingArg::m_aValue), b: Length + 1)); |
1392 | |
1393 | // Validate argument from the parsed argument of the current setting. |
1394 | // If current setting is not valid, then there are no arguments which results in an error. |
1395 | |
1396 | char Type = 'u'; // u = unknown, only possible for unknown commands when m_AllowUnknownCommands is true. |
1397 | if(ArgIndex < CommandArgCount) |
1398 | { |
1399 | SParsedMapSettingArg &Arg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(n: ArgIndex); |
1400 | if(Arg.m_Type == 'r') |
1401 | { |
1402 | // Rest of string, should add all the string if there was no quotes |
1403 | // Otherwise, only get the contents in the quotes, and consider content after that as other arguments |
1404 | if(Char != '"') |
1405 | { |
1406 | while(*pIterator) |
1407 | pIterator++; |
1408 | Length = pIterator - pArgStart; |
1409 | str_copy(dst: NewArg.m_aValue, src: pArgStart, dst_size: Length + 1); |
1410 | } |
1411 | |
1412 | if(!Valid) |
1413 | Error = SCommandParseError::ERROR_INVALID_VALUE; |
1414 | } |
1415 | else if(Arg.m_Type == 'i') |
1416 | { |
1417 | // Validate int |
1418 | if(!str_toint(str: NewArg.m_aValue, out: nullptr)) |
1419 | Error = SCommandParseError::ERROR_INVALID_VALUE; |
1420 | } |
1421 | else if(Arg.m_Type == 'f') |
1422 | { |
1423 | // Validate float |
1424 | if(!str_tofloat(str: NewArg.m_aValue, out: nullptr)) |
1425 | Error = SCommandParseError::ERROR_INVALID_VALUE; |
1426 | } |
1427 | else if(Arg.m_Type == 's') |
1428 | { |
1429 | // Validate string |
1430 | if(!Valid || (Char != '"' && !ValidateStr(NewArg.m_aValue))) |
1431 | Error = SCommandParseError::ERROR_INVALID_VALUE; |
1432 | } |
1433 | |
1434 | // Extended argument validation: |
1435 | // for int settings it checks that the value is in range |
1436 | // for command settings, it checks that the value is one of the possible values if there are any |
1437 | EValidationResult Result = ValidateArg(Index: ArgIndex, pArg: NewArg.m_aValue); |
1438 | if(Length && !Error && Result != EValidationResult::VALID) |
1439 | { |
1440 | if(Result == EValidationResult::ERROR) |
1441 | Error = SCommandParseError::ERROR_INVALID_VALUE; // Invalid argument value (invalid int, invalid float) |
1442 | else if(Result == EValidationResult::UNKNOWN) |
1443 | Error = SCommandParseError::ERROR_UNKNOWN_VALUE; // Unknown argument value |
1444 | else if(Result == EValidationResult::INCOMPLETE) |
1445 | Error = SCommandParseError::ERROR_INCOMPLETE; // Incomplete argument in case of possible values |
1446 | else if(Result == EValidationResult::OUT_OF_RANGE) |
1447 | Error = SCommandParseError::ERROR_OUT_OF_RANGE; // Out of range argument value in case of int settings |
1448 | else |
1449 | Error = SCommandParseError::ERROR_UNKNOWN; // Unknown error |
1450 | } |
1451 | |
1452 | Type = Arg.m_Type; |
1453 | } |
1454 | else |
1455 | { |
1456 | // Error: too many arguments if no comment after |
1457 | if(m_CommentOffset == -1) |
1458 | Error = SCommandParseError::ERROR_TOO_MANY_ARGS; |
1459 | else |
1460 | { // Otherwise, check if there are any arguments left between this argument and the comment |
1461 | const char *pSubIt = pArgStart; |
1462 | pSubIt = str_skip_whitespaces_const(str: pSubIt); |
1463 | if(*pSubIt != '\0') |
1464 | { // If there aren't only spaces between the last argument and the comment, then this is an error |
1465 | Error = SCommandParseError::ERROR_TOO_MANY_ARGS; |
1466 | } |
1467 | else // If there are, then just exit the loop to avoid getting an error |
1468 | { |
1469 | m_vCurrentArgs.pop_back(); |
1470 | break; |
1471 | } |
1472 | } |
1473 | } |
1474 | |
1475 | // Fill argument informations |
1476 | NewArg.m_X = PosX; |
1477 | NewArg.m_Start = Offset; |
1478 | NewArg.m_End = Offset + Length; |
1479 | NewArg.m_Error = Error != SCommandParseError::ERROR_NONE || Length == 0 || m_Error.m_Type != SCommandParseError::ERROR_NONE; |
1480 | NewArg.m_ExpectedType = Type; |
1481 | |
1482 | // Do not emit an error if we allow unknown commands and the current setting is invalid |
1483 | if(m_AllowUnknownCommands && m_pCurrentSetting == nullptr) |
1484 | NewArg.m_Error = false; |
1485 | |
1486 | // Check error and fill the error field with different messages |
1487 | if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_OUT_OF_RANGE || Error == SCommandParseError::ERROR_INCOMPLETE) |
1488 | { |
1489 | // Only keep first error |
1490 | if(!m_Error.m_aMessage[0]) |
1491 | { |
1492 | int ErrorArgIndex = (int)m_vCurrentArgs.size() - 1; |
1493 | SCurrentSettingArg &ErrorArg = m_vCurrentArgs.back(); |
1494 | SParsedMapSettingArg &SettingArg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(n: ArgIndex); |
1495 | char aFormattedValue[256]; |
1496 | FormatDisplayValue(pValue: ErrorArg.m_aValue, aOut&: aFormattedValue); |
1497 | |
1498 | if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_INCOMPLETE) |
1499 | { |
1500 | static const std::map<int, const char *> s_Names = { |
1501 | {SCommandParseError::ERROR_INVALID_VALUE, "Invalid" }, |
1502 | {SCommandParseError::ERROR_UNKNOWN_VALUE, "Unknown" }, |
1503 | {SCommandParseError::ERROR_INCOMPLETE, "Incomplete" }, |
1504 | }; |
1505 | str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "%s argument value: %s at position %d for argument '%s'" , s_Names.at(k: Error), aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName); |
1506 | } |
1507 | else |
1508 | { |
1509 | std::shared_ptr<SMapSettingInt> pSettingInt = std::static_pointer_cast<SMapSettingInt>(r: m_pCurrentSetting); |
1510 | str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "Invalid argument value: %s at position %d for argument '%s': out of range [%d, %d]" , aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName, pSettingInt->m_Min, pSettingInt->m_Max); |
1511 | } |
1512 | m_Error.m_ArgIndex = ErrorArgIndex; |
1513 | m_Error.m_Type = Error; |
1514 | } |
1515 | } |
1516 | else if(Error == SCommandParseError::ERROR_TOO_MANY_ARGS) |
1517 | { |
1518 | // Only keep first error |
1519 | if(!m_Error.m_aMessage[0]) |
1520 | { |
1521 | if(m_pCurrentSetting != nullptr) |
1522 | { |
1523 | str_copy(dst&: m_Error.m_aMessage, src: "Too many arguments" ); |
1524 | m_Error.m_ArgIndex = ArgIndex; |
1525 | break; |
1526 | } |
1527 | else if(!m_AllowUnknownCommands) |
1528 | { |
1529 | char aFormattedValue[256]; |
1530 | FormatDisplayValue(pValue: m_aCommand, aOut&: aFormattedValue); |
1531 | str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "Unknown server setting: %s" , aFormattedValue); |
1532 | m_Error.m_ArgIndex = -1; |
1533 | break; |
1534 | } |
1535 | m_Error.m_Type = Error; |
1536 | } |
1537 | } |
1538 | |
1539 | PosX += m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: pArgStart, StrLength: Length); // Advance argument position |
1540 | ArgIndex++; |
1541 | } |
1542 | } |
1543 | |
1544 | void CMapSettingsBackend::CContext::ClearError() |
1545 | { |
1546 | m_Error.m_aMessage[0] = '\0'; |
1547 | m_Error.m_Type = SCommandParseError::ERROR_NONE; |
1548 | } |
1549 | |
1550 | bool CMapSettingsBackend::CContext::UpdateCursor(bool Force) |
1551 | { |
1552 | // This method updates the cursor offset in this class from |
1553 | // the cursor offset of the line input. |
1554 | // It also updates the argument index where the cursor is at |
1555 | // and the possible values matches if the argument index changes. |
1556 | // Returns true in case the cursor changed position |
1557 | |
1558 | if(!m_pLineInput) |
1559 | return false; |
1560 | |
1561 | size_t Offset = m_pLineInput->GetCursorOffset(); |
1562 | if(Offset == m_LastCursorOffset && !Force) |
1563 | return false; |
1564 | |
1565 | m_LastCursorOffset = Offset; |
1566 | int NewArg = m_CursorArgIndex; |
1567 | |
1568 | // Update current argument under cursor |
1569 | if(m_CommentOffset != -1 && Offset >= (size_t)m_CommentOffset) |
1570 | { |
1571 | NewArg = (int)m_vCurrentArgs.size(); |
1572 | } |
1573 | else |
1574 | { |
1575 | bool FoundArg = false; |
1576 | for(int i = (int)m_vCurrentArgs.size() - 1; i >= 0; i--) |
1577 | { |
1578 | if(Offset >= m_vCurrentArgs[i].m_Start) |
1579 | { |
1580 | NewArg = i; |
1581 | FoundArg = true; |
1582 | break; |
1583 | } |
1584 | } |
1585 | |
1586 | if(!FoundArg) |
1587 | NewArg = -1; |
1588 | } |
1589 | |
1590 | bool ShouldUpdate = NewArg != m_CursorArgIndex; |
1591 | m_CursorArgIndex = NewArg; |
1592 | |
1593 | // Do not show error if current argument is incomplete, as we are editing it |
1594 | if(m_pLineInput != nullptr) |
1595 | { |
1596 | if(Offset == m_pLineInput->GetLength() && m_Error.m_aMessage[0] && m_Error.m_ArgIndex == m_CursorArgIndex && m_Error.m_Type == SCommandParseError::ERROR_INCOMPLETE) |
1597 | ClearError(); |
1598 | } |
1599 | |
1600 | if(m_DropdownContext.m_Selected == -1 || ShouldUpdate || Force) |
1601 | { |
1602 | // Update possible commands from cursor |
1603 | UpdatePossibleMatches(); |
1604 | } |
1605 | |
1606 | return true; |
1607 | } |
1608 | |
1609 | EValidationResult CMapSettingsBackend::CContext::ValidateArg(int Index, const char *pArg) |
1610 | { |
1611 | if(!m_pCurrentSetting) |
1612 | return EValidationResult::ERROR; |
1613 | |
1614 | // Check if this argument is valid against current argument |
1615 | if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) |
1616 | { |
1617 | std::shared_ptr<SMapSettingInt> pSetting = std::static_pointer_cast<SMapSettingInt>(r: m_pCurrentSetting); |
1618 | if(Index > 0) |
1619 | return EValidationResult::ERROR; |
1620 | |
1621 | int Value; |
1622 | if(!str_toint(str: pArg, out: &Value)) // Try parse the integer |
1623 | return EValidationResult::ERROR; |
1624 | |
1625 | return Value >= pSetting->m_Min && Value <= pSetting->m_Max ? EValidationResult::VALID : EValidationResult::OUT_OF_RANGE; |
1626 | } |
1627 | else if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_COMMAND) |
1628 | { |
1629 | auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting); |
1630 | if(Index < (int)vArgs.size()) |
1631 | { |
1632 | auto It = m_pBackend->m_PossibleValuesPerCommand.find(x: m_pCurrentSetting->m_pName); |
1633 | if(It != m_pBackend->m_PossibleValuesPerCommand.end()) |
1634 | { |
1635 | auto ValuesIt = It->second.find(x: Index); |
1636 | if(ValuesIt != It->second.end()) |
1637 | { |
1638 | // This means that we have possible values for this argument for this setting |
1639 | // In order to validate such arg, we have to check if it maches any of the possible values |
1640 | const bool EqualsAny = std::any_of(first: ValuesIt->second.begin(), last: ValuesIt->second.end(), pred: [pArg](auto *pValue) { return str_comp_nocase(pArg, pValue) == 0; }); |
1641 | |
1642 | // If equals, then argument is valid |
1643 | if(EqualsAny) |
1644 | return EValidationResult::VALID; |
1645 | |
1646 | // Here we check if argument is incomplete |
1647 | const bool StartsAny = std::any_of(first: ValuesIt->second.begin(), last: ValuesIt->second.end(), pred: [pArg](auto *pValue) { return str_startswith_nocase(pValue, pArg) != nullptr; }); |
1648 | if(StartsAny) |
1649 | return EValidationResult::INCOMPLETE; |
1650 | |
1651 | return EValidationResult::UNKNOWN; |
1652 | } |
1653 | } |
1654 | } |
1655 | |
1656 | // If we get here, it means there are no posssible values for that specific argument. |
1657 | // The validation for specific types such as int and floats were done earlier so if we get here |
1658 | // we know the argument is valid. |
1659 | // String and "rest of string" types are valid by default. |
1660 | return EValidationResult::VALID; |
1661 | } |
1662 | |
1663 | return EValidationResult::ERROR; |
1664 | } |
1665 | |
1666 | void CMapSettingsBackend::CContext::UpdatePossibleMatches() |
1667 | { |
1668 | // This method updates the possible values matches based on the cursor position within the current argument in the line input. |
1669 | // For example ("|" is the cursor): |
1670 | // - Typing "sv_deep|" will show "sv_deepfly" as a possible match in the dropdown |
1671 | // Moving the cursor: "sv_|deep" will show all possible commands starting with "sv_" |
1672 | // - Typing "tune ground_frict|" will show "ground_friction" as possible match |
1673 | // Moving the cursor: "tune ground_|frict" will show all possible values starting with "ground_" for that argument (argument 0 of "tune" setting) |
1674 | |
1675 | m_vPossibleMatches.clear(); |
1676 | m_DropdownContext.m_Selected = -1; |
1677 | |
1678 | if(m_CommentOffset == 0) |
1679 | return; |
1680 | |
1681 | // First case: argument index under cursor is -1 => we're on the command/setting name |
1682 | if(m_CursorArgIndex == -1) |
1683 | { |
1684 | // Use a substring from the start of the input to the cursor offset |
1685 | char aSubString[128]; |
1686 | str_copy(dst: aSubString, src: m_aCommand, dst_size: minimum(a: m_LastCursorOffset + 1, b: sizeof(aSubString))); |
1687 | |
1688 | // Iterate through available map settings and find those which the beginning matches with the command/setting name we are writing |
1689 | for(auto &pSetting : m_pBackend->m_vpMapSettings) |
1690 | { |
1691 | if(str_startswith_nocase(str: pSetting->m_pName, prefix: aSubString)) |
1692 | { |
1693 | m_vPossibleMatches.emplace_back(args: SPossibleValueMatch{ |
1694 | .m_pValue: pSetting->m_pName, |
1695 | .m_ArgIndex: m_CursorArgIndex, |
1696 | .m_pData: pSetting.get(), |
1697 | }); |
1698 | } |
1699 | } |
1700 | |
1701 | // If there are no matches, then the command is unknown |
1702 | if(m_vPossibleMatches.empty() && !m_AllowUnknownCommands) |
1703 | { |
1704 | // Fill the error if we do not allow unknown commands |
1705 | char aFormattedValue[256]; |
1706 | FormatDisplayValue(pValue: m_aCommand, aOut&: aFormattedValue); |
1707 | str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "Unknown server setting: %s" , aFormattedValue); |
1708 | m_Error.m_ArgIndex = -1; |
1709 | } |
1710 | } |
1711 | else |
1712 | { |
1713 | // Second case: we are on an argument |
1714 | if(!m_pCurrentSetting) // If we are on an argument of an unknown setting, we can't handle it => no possible values, ever. |
1715 | return; |
1716 | |
1717 | if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) |
1718 | { |
1719 | // No possible values for int settings. |
1720 | // Maybe we can add "0" and "1" as possible values for settings that are binary. |
1721 | } |
1722 | else |
1723 | { |
1724 | // Get the parsed arguments for the current setting |
1725 | auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting); |
1726 | // Make sure we are not out of bounds |
1727 | if(m_CursorArgIndex < (int)vArgs.size() && m_CursorArgIndex < (int)m_vCurrentArgs.size()) |
1728 | { |
1729 | // Check if there are possible values for this command |
1730 | auto It = m_pBackend->m_PossibleValuesPerCommand.find(x: m_pCurrentSetting->m_pName); |
1731 | if(It != m_pBackend->m_PossibleValuesPerCommand.end()) |
1732 | { |
1733 | // If that's the case, then check if there are possible values for the current argument index the cursor is on |
1734 | auto ValuesIt = It->second.find(x: m_CursorArgIndex); |
1735 | if(ValuesIt != It->second.end()) |
1736 | { |
1737 | // If that's the case, then do the same as previously, we check for each value if they match |
1738 | // with the current argument value |
1739 | |
1740 | auto &CurrentArg = m_vCurrentArgs.at(n: m_CursorArgIndex); |
1741 | int SubstringLength = minimum(a: m_LastCursorOffset, b: CurrentArg.m_End) - CurrentArg.m_Start; |
1742 | |
1743 | // Substring based on the cursor position inside that argument |
1744 | char aSubString[256]; |
1745 | str_copy(dst: aSubString, src: CurrentArg.m_aValue, dst_size: SubstringLength + 1); |
1746 | |
1747 | for(auto &pValue : ValuesIt->second) |
1748 | { |
1749 | if(str_startswith_nocase(str: pValue, prefix: aSubString)) |
1750 | { |
1751 | m_vPossibleMatches.emplace_back(args: SPossibleValueMatch{ |
1752 | .m_pValue: pValue, |
1753 | .m_ArgIndex: m_CursorArgIndex, |
1754 | .m_pData: nullptr, |
1755 | }); |
1756 | } |
1757 | } |
1758 | } |
1759 | } |
1760 | } |
1761 | } |
1762 | } |
1763 | } |
1764 | |
1765 | bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event) |
1766 | { |
1767 | if(!m_pLineInput) |
1768 | return false; |
1769 | |
1770 | if(!m_pLineInput->IsActive()) |
1771 | return false; |
1772 | |
1773 | if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT) && !m_pBackend->Input()->ModifierIsPressed() && !m_pBackend->Input()->AltIsPressed()) |
1774 | { |
1775 | // How to make this better? |
1776 | // This checks when we press any key that is not handled by the dropdown |
1777 | // When that's the case, it means we confirm the completion if we have a valid completion index |
1778 | if(Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT && Event.m_Key != KEY_UP && Event.m_Key != KEY_DOWN && !(Event.m_Key >= KEY_MOUSE_1 && Event.m_Key <= KEY_MOUSE_WHEEL_RIGHT)) |
1779 | { |
1780 | if(m_CurrentCompletionIndex != -1) |
1781 | { |
1782 | m_CurrentCompletionIndex = -1; |
1783 | m_DropdownContext.m_Selected = -1; |
1784 | Update(); |
1785 | UpdateCursor(Force: true); |
1786 | } |
1787 | } |
1788 | } |
1789 | |
1790 | return false; |
1791 | } |
1792 | |
1793 | const char *CMapSettingsBackend::CContext::InputString() const |
1794 | { |
1795 | if(!m_pLineInput) |
1796 | return nullptr; |
1797 | return m_pBackend->Input()->HasComposition() ? m_CompositionStringBuffer.c_str() : m_pLineInput->GetString(); |
1798 | } |
1799 | |
1800 | const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentStringColor = ColorRGBA(84 / 255.0f, 1.0f, 1.0f, 1.0f); |
1801 | const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentNumberColor = ColorRGBA(0.1f, 0.9f, 0.05f, 1.0f); |
1802 | const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentUnknownColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); |
1803 | const ColorRGBA CMapSettingsBackend::CContext:: = ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f); |
1804 | const ColorRGBA CMapSettingsBackend::CContext::ms_ErrorColor = ColorRGBA(240 / 255.0f, 70 / 255.0f, 70 / 255.0f, 1.0f); |
1805 | |
1806 | void CMapSettingsBackend::CContext::ColorArguments(std::vector<STextColorSplit> &vColorSplits) const |
1807 | { |
1808 | // Get argument color based on its type |
1809 | auto &&GetArgumentColor = [](char Type) -> ColorRGBA { |
1810 | if(Type == 'u') |
1811 | return ms_ArgumentUnknownColor; |
1812 | else if(Type == 's' || Type == 'r') |
1813 | return ms_ArgumentStringColor; |
1814 | else if(Type == 'i' || Type == 'f') |
1815 | return ms_ArgumentNumberColor; |
1816 | return ms_ErrorColor; // Invalid arg type |
1817 | }; |
1818 | |
1819 | // Iterate through all the current arguments and color them |
1820 | for(int i = 0; i < ArgCount(); i++) |
1821 | { |
1822 | const auto &Argument = Arg(Index: i); |
1823 | // Color is based on the error flag and the type of the argument |
1824 | auto Color = Argument.m_Error ? ms_ErrorColor : GetArgumentColor(Argument.m_ExpectedType); |
1825 | vColorSplits.emplace_back(args: Argument.m_Start, args: Argument.m_End - Argument.m_Start, args&: Color); |
1826 | } |
1827 | |
1828 | if(m_pLineInput && !m_pLineInput->IsEmpty()) |
1829 | { |
1830 | if(!CommandIsValid() && !m_AllowUnknownCommands && m_CommentOffset != 0) |
1831 | { |
1832 | // If command is invalid, override color splits with red, but not comment |
1833 | int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset; |
1834 | vColorSplits = {{0, ErrorLength, ms_ErrorColor}}; |
1835 | } |
1836 | else if(HasError()) |
1837 | { |
1838 | // If there is an error, then color the wrong part of the input, excluding comment |
1839 | int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset - ErrorOffset(); |
1840 | vColorSplits.emplace_back(args: ErrorOffset(), args&: ErrorLength, args: ms_ErrorColor); |
1841 | } |
1842 | if(m_CommentOffset != -1) |
1843 | { // Color comment if there is one |
1844 | vColorSplits.emplace_back(args: m_CommentOffset, args: -1, args: ms_CommentColor); |
1845 | } |
1846 | } |
1847 | |
1848 | std::sort(first: vColorSplits.begin(), last: vColorSplits.end(), comp: [](const STextColorSplit &a, const STextColorSplit &b) { |
1849 | return a.m_CharIndex < b.m_CharIndex; |
1850 | }); |
1851 | } |
1852 | |
1853 | int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) const |
1854 | { |
1855 | return CheckCollision(vSettings: m_pBackend->Editor()->m_Map.m_vSettings, Result); |
1856 | } |
1857 | |
1858 | int CMapSettingsBackend::CContext::CheckCollision(const std::vector<CEditorMapSetting> &vSettings, ECollisionCheckResult &Result) const |
1859 | { |
1860 | return CheckCollision(pInputString: InputString(), vSettings, Result); |
1861 | } |
1862 | |
1863 | int CMapSettingsBackend::CContext::CheckCollision(const char *pInputString, const std::vector<CEditorMapSetting> &vSettings, ECollisionCheckResult &Result) const |
1864 | { |
1865 | // Checks for a collision with the current map settings. |
1866 | // A collision is when a setting with the same arguments already exists and that it can't be added multiple times. |
1867 | // For this, we use argument constraints that we define in CMapSettingsCommandObject::LoadConstraints(). |
1868 | // For example, the "tune" command can be added multiple times, but only if the actual tune argument is different, thus |
1869 | // the tune argument must be defined as UNIQUE. |
1870 | // This method CheckCollision(ECollisionCheckResult&) returns an integer which is the index of the colliding line. If no |
1871 | // colliding line was found, then it returns -1. |
1872 | |
1873 | if(m_CommentOffset == 0) |
1874 | { // Ignore comments |
1875 | Result = ECollisionCheckResult::ADD; |
1876 | return -1; |
1877 | } |
1878 | |
1879 | const int InputLength = str_length(str: pInputString); |
1880 | |
1881 | struct SArgument |
1882 | { |
1883 | char m_aValue[128]; |
1884 | SArgument(const char *pStr) |
1885 | { |
1886 | str_copy(dst&: m_aValue, src: pStr); |
1887 | } |
1888 | }; |
1889 | |
1890 | struct SLineArgs |
1891 | { |
1892 | int m_Index; |
1893 | std::vector<SArgument> m_vArgs; |
1894 | }; |
1895 | |
1896 | // For now we split each map setting corresponding to the setting we want to add by spaces |
1897 | auto &&SplitSetting = [](const char *pStr) { |
1898 | std::vector<SArgument> vaArgs; |
1899 | const char *pIt = pStr; |
1900 | char aBuffer[128]; |
1901 | while((pIt = str_next_token(str: pIt, delim: " " , buffer: aBuffer, buffer_size: sizeof(aBuffer)))) |
1902 | vaArgs.emplace_back(args&: aBuffer); |
1903 | return vaArgs; |
1904 | }; |
1905 | |
1906 | // Define the result of the check |
1907 | Result = ECollisionCheckResult::ERROR; |
1908 | |
1909 | // First case: the command is not a valid (recognized) command. |
1910 | if(!CommandIsValid()) |
1911 | { |
1912 | // If we don't allow unknown commands, then we know there is no collision |
1913 | // and the check results in an error. |
1914 | if(!m_AllowUnknownCommands) |
1915 | return -1; |
1916 | |
1917 | if(InputLength == 0) |
1918 | return -1; |
1919 | |
1920 | // If we get here, it means we allow unknown commands. |
1921 | // For them, we need to check if a similar exact command exists or not in the settings list. |
1922 | // If it does, then we found a collision, and the result is REPLACE. |
1923 | for(int i = 0; i < (int)vSettings.size(); i++) |
1924 | { |
1925 | if(str_comp_nocase(a: vSettings[i].m_aCommand, b: pInputString) == 0) |
1926 | { |
1927 | Result = ECollisionCheckResult::REPLACE; |
1928 | return i; |
1929 | } |
1930 | } |
1931 | |
1932 | // If nothing was found, then we must ensure that the command, although unknown, is somewhat valid |
1933 | // by checking if the command contains a space and that there is at least one non-empty argument. |
1934 | const char *pSpace = str_find(haystack: pInputString, needle: " " ); |
1935 | if(!pSpace || !*(pSpace + 1)) |
1936 | Result = ECollisionCheckResult::ERROR; |
1937 | else |
1938 | Result = ECollisionCheckResult::ADD; |
1939 | |
1940 | return -1; // No collision |
1941 | } |
1942 | |
1943 | // Second case: the command is valid. |
1944 | // In this case, we know we have a valid setting name, which means we can use everything we have in this class which are |
1945 | // related to valid map settings, such as parsed command arguments, etc. |
1946 | |
1947 | const std::shared_ptr<IMapSetting> &pSetting = Setting(); |
1948 | if(pSetting->m_Type == IMapSetting::SETTING_INT) |
1949 | { |
1950 | // For integer settings, the check is quite simple as we know |
1951 | // we can only ever have 1 argument. |
1952 | |
1953 | // The integer setting cannot be added multiple times, which means if a collision was found, then the only result we |
1954 | // can have is REPLACE. |
1955 | // In this case, the collision is found only by checking the command name for every setting in the current map settings. |
1956 | char aBuffer[256]; |
1957 | auto It = std::find_if(first: vSettings.begin(), last: vSettings.end(), pred: [&](const CEditorMapSetting &Setting) { |
1958 | const char *pLineSettingValue = Setting.m_aCommand; // Get the map setting command |
1959 | pLineSettingValue = str_next_token(str: pLineSettingValue, delim: " " , buffer: aBuffer, buffer_size: sizeof(aBuffer)); // Get the first token before the first space |
1960 | return str_comp_nocase(a: aBuffer, b: pSetting->m_pName) == 0; // Check if that equals our current command |
1961 | }); |
1962 | |
1963 | if(It == vSettings.end()) |
1964 | { |
1965 | // If nothing was found, then there is no collision and we can add that command to the list |
1966 | Result = ECollisionCheckResult::ADD; |
1967 | return -1; |
1968 | } |
1969 | else |
1970 | { |
1971 | // Otherwise, we can only replace it |
1972 | Result = ECollisionCheckResult::REPLACE; |
1973 | return It - vSettings.begin(); // This is the index of the colliding line |
1974 | } |
1975 | } |
1976 | else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) |
1977 | { |
1978 | // For "command" settings, this is a bit more complex as we have to use argument constraints. |
1979 | // The general idea is to split every map setting in their arguments separated by spaces. |
1980 | // Then, for each argument, we check if it collides with any of the map settings. When that's the case, |
1981 | // we need to check the constraint of the argument. If set to UNIQUE, then that's a collision and we can only |
1982 | // replace the command in the list. |
1983 | // If set to anything else, we consider that it is not a collision and we move to the next argument. |
1984 | // This system is simple and somewhat flexible as we only need to declare the constraints, the rest should be |
1985 | // handled automatically. |
1986 | |
1987 | std::shared_ptr<SMapSettingCommand> pSettingCommand = std::static_pointer_cast<SMapSettingCommand>(r: pSetting); |
1988 | // Get matching lines for that command |
1989 | std::vector<SLineArgs> vLineArgs; |
1990 | for(int i = 0; i < (int)vSettings.size(); i++) |
1991 | { |
1992 | const auto &Setting = vSettings.at(n: i); |
1993 | |
1994 | // Split this setting into its arguments |
1995 | std::vector<SArgument> vArgs = SplitSetting(Setting.m_aCommand); |
1996 | // Only keep settings that match with the current input setting name |
1997 | if(!vArgs.empty() && str_comp_nocase(a: vArgs[0].m_aValue, b: pSettingCommand->m_pName) == 0) |
1998 | { |
1999 | // When that's the case, we save them |
2000 | vArgs.erase(position: vArgs.begin()); |
2001 | vLineArgs.push_back(x: SLineArgs{ |
2002 | .m_Index: i, |
2003 | .m_vArgs: vArgs, |
2004 | }); |
2005 | } |
2006 | } |
2007 | |
2008 | // Here is the simple algorithm to check for collisions according to argument constraints |
2009 | bool Error = false; |
2010 | int CollidingLineIndex = -1; |
2011 | for(int ArgIndex = 0; ArgIndex < ArgCount(); ArgIndex++) |
2012 | { |
2013 | bool Collide = false; |
2014 | const char *pValue = Arg(Index: ArgIndex).m_aValue; |
2015 | for(auto &Line : vLineArgs) |
2016 | { |
2017 | // Check first colliding line |
2018 | if(str_comp_nocase(a: pValue, b: Line.m_vArgs[ArgIndex].m_aValue) == 0) |
2019 | { |
2020 | Collide = true; |
2021 | CollidingLineIndex = Line.m_Index; |
2022 | Error = m_pBackend->ArgConstraint(pSettingName: pSetting->m_pName, Arg: ArgIndex) == CMapSettingsBackend::EArgConstraint::UNIQUE; |
2023 | } |
2024 | if(Error) |
2025 | break; |
2026 | } |
2027 | |
2028 | // If we did not collide with any of the lines for that argument, we're good to go |
2029 | // (or if we had an error) |
2030 | if(!Collide || Error) |
2031 | break; |
2032 | |
2033 | // Otherwise, remove non-colliding args from the list |
2034 | vLineArgs.erase( |
2035 | first: std::remove_if(first: vLineArgs.begin(), last: vLineArgs.end(), pred: [&](const SLineArgs &Line) { |
2036 | return str_comp_nocase(a: pValue, b: Line.m_vArgs[ArgIndex].m_aValue) != 0; |
2037 | }), |
2038 | last: vLineArgs.end()); |
2039 | } |
2040 | |
2041 | // The result is either REPLACE when we found a collision, or ADD |
2042 | Result = Error ? ECollisionCheckResult::REPLACE : ECollisionCheckResult::ADD; |
2043 | return CollidingLineIndex; |
2044 | } |
2045 | |
2046 | return -1; |
2047 | } |
2048 | |
2049 | bool CMapSettingsBackend::CContext::Valid() const |
2050 | { |
2051 | // Check if the entire setting is valid or not |
2052 | |
2053 | if(m_CommentOffset == 0) |
2054 | return true; // A "comment" setting is considered valid. |
2055 | |
2056 | // Check if command is valid |
2057 | if(m_pCurrentSetting) |
2058 | { |
2059 | // Check if all arguments are valid |
2060 | const bool ArgumentsValid = std::all_of(first: m_vCurrentArgs.begin(), last: m_vCurrentArgs.end(), pred: [](const SCurrentSettingArg &Arg) { |
2061 | return !Arg.m_Error; |
2062 | }); |
2063 | |
2064 | if(!ArgumentsValid) |
2065 | return false; |
2066 | |
2067 | // Check that we have the same number of arguments |
2068 | return m_vCurrentArgs.size() == m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting).size(); |
2069 | } |
2070 | else |
2071 | { |
2072 | // If we have an invalid setting, then we consider the entire setting as valid if we allow unknown commands |
2073 | // as we cannot handle them. |
2074 | return m_AllowUnknownCommands; |
2075 | } |
2076 | } |
2077 | |
2078 | void CMapSettingsBackend::CContext::GetCommandHelpText(char *pStr, int Length) const |
2079 | { |
2080 | if(!m_pCurrentSetting) |
2081 | return; |
2082 | |
2083 | str_copy(dst: pStr, src: m_pCurrentSetting->m_pHelp, dst_size: Length); |
2084 | } |
2085 | |
2086 | void CMapSettingsBackend::CContext::UpdateCompositionString() |
2087 | { |
2088 | if(!m_pLineInput) |
2089 | return; |
2090 | |
2091 | const bool HasComposition = m_pBackend->Input()->HasComposition(); |
2092 | |
2093 | if(HasComposition) |
2094 | { |
2095 | const size_t CursorOffset = m_pLineInput->GetCursorOffset(); |
2096 | const size_t DisplayCursorOffset = m_pLineInput->OffsetFromActualToDisplay(ActualOffset: CursorOffset); |
2097 | const std::string DisplayStr = std::string(m_pLineInput->GetString()); |
2098 | std::string CompositionBuffer = DisplayStr.substr(pos: 0, n: DisplayCursorOffset) + m_pBackend->Input()->GetComposition() + DisplayStr.substr(pos: DisplayCursorOffset); |
2099 | if(CompositionBuffer != m_CompositionStringBuffer) |
2100 | { |
2101 | m_CompositionStringBuffer = CompositionBuffer; |
2102 | Update(); |
2103 | UpdateCursor(); |
2104 | } |
2105 | } |
2106 | } |
2107 | |
2108 | template<int N> |
2109 | void CMapSettingsBackend::CContext::FormatDisplayValue(const char *pValue, char (&aOut)[N]) |
2110 | { |
2111 | const int MaxLength = 32; |
2112 | if(str_length(str: pValue) > MaxLength) |
2113 | { |
2114 | str_copy(aOut, pValue, MaxLength); |
2115 | str_append(aOut, "..." ); |
2116 | } |
2117 | else |
2118 | { |
2119 | str_copy(aOut, pValue); |
2120 | } |
2121 | } |
2122 | |
2123 | bool CMapSettingsBackend::OnInput(const IInput::CEvent &Event) |
2124 | { |
2125 | if(ms_pActiveContext) |
2126 | return ms_pActiveContext->OnInput(Event); |
2127 | |
2128 | return false; |
2129 | } |
2130 | |
2131 | void CMapSettingsBackend::OnUpdate() |
2132 | { |
2133 | if(ms_pActiveContext && ms_pActiveContext->m_pLineInput && ms_pActiveContext->m_pLineInput->IsActive()) |
2134 | ms_pActiveContext->UpdateCompositionString(); |
2135 | } |
2136 | |
2137 | void CMapSettingsBackend::OnMapLoad() |
2138 | { |
2139 | // Load & validate all map settings |
2140 | m_LoadedMapSettings.Reset(); |
2141 | |
2142 | auto &vLoadedMapSettings = Editor()->m_Map.m_vSettings; |
2143 | |
2144 | // Keep a vector of valid map settings, to check collision against: m_vValidLoadedMapSettings |
2145 | |
2146 | // Create a local context with no lineinput, only used to parse the commands |
2147 | CContext LocalContext = NewContext(pLineInput: nullptr); |
2148 | |
2149 | // Iterate through map settings |
2150 | // Two steps: |
2151 | // 1. Save valid and invalid settings |
2152 | // 2. Check for duplicates |
2153 | |
2154 | std::vector<std::tuple<int, bool, CEditorMapSetting>> vSettingsInvalid; |
2155 | |
2156 | for(int i = 0; i < (int)vLoadedMapSettings.size(); i++) |
2157 | { |
2158 | CEditorMapSetting &Setting = vLoadedMapSettings.at(n: i); |
2159 | // Parse the setting using the context |
2160 | LocalContext.UpdateFromString(pStr: Setting.m_aCommand); |
2161 | |
2162 | bool Valid = LocalContext.Valid(); |
2163 | ECollisionCheckResult Result = ECollisionCheckResult::ERROR; |
2164 | LocalContext.CheckCollision(pInputString: Setting.m_aCommand, vSettings: m_LoadedMapSettings.m_vSettingsValid, Result); |
2165 | |
2166 | if(Valid && Result == ECollisionCheckResult::ADD) |
2167 | m_LoadedMapSettings.m_vSettingsValid.emplace_back(args&: Setting); |
2168 | else |
2169 | vSettingsInvalid.emplace_back(args&: i, args&: Valid, args&: Setting); |
2170 | |
2171 | LocalContext.Reset(); |
2172 | |
2173 | // Empty duplicates for this line, might be filled later |
2174 | m_LoadedMapSettings.m_SettingsDuplicate.insert(x: {i, {}}); |
2175 | } |
2176 | |
2177 | for(const auto &[Index, Valid, Setting] : vSettingsInvalid) |
2178 | { |
2179 | LocalContext.UpdateFromString(pStr: Setting.m_aCommand); |
2180 | |
2181 | ECollisionCheckResult Result = ECollisionCheckResult::ERROR; |
2182 | int CollidingLineIndex = LocalContext.CheckCollision(pInputString: Setting.m_aCommand, vSettings: m_LoadedMapSettings.m_vSettingsValid, Result); |
2183 | int RealCollidingLineIndex = CollidingLineIndex; |
2184 | |
2185 | if(CollidingLineIndex != -1) |
2186 | RealCollidingLineIndex = std::find_if(first: vLoadedMapSettings.begin(), last: vLoadedMapSettings.end(), pred: [&](const CEditorMapSetting &MapSetting) { |
2187 | return str_comp_nocase(a: MapSetting.m_aCommand, b: m_LoadedMapSettings.m_vSettingsValid.at(n: CollidingLineIndex).m_aCommand) == 0; |
2188 | }) - vLoadedMapSettings.begin(); |
2189 | |
2190 | int Type = 0; |
2191 | if(!Valid) |
2192 | Type |= SInvalidSetting::TYPE_INVALID; |
2193 | if(Result == ECollisionCheckResult::REPLACE) |
2194 | Type |= SInvalidSetting::TYPE_DUPLICATE; |
2195 | |
2196 | m_LoadedMapSettings.m_vSettingsInvalid.emplace_back(args: Index, args: Setting.m_aCommand, args&: Type, args&: RealCollidingLineIndex, args: !LocalContext.CommandIsValid()); |
2197 | if(Type & SInvalidSetting::TYPE_DUPLICATE) |
2198 | m_LoadedMapSettings.m_SettingsDuplicate[RealCollidingLineIndex].emplace_back(args: m_LoadedMapSettings.m_vSettingsInvalid.size() - 1); |
2199 | |
2200 | LocalContext.Reset(); |
2201 | } |
2202 | |
2203 | if(!m_LoadedMapSettings.m_vSettingsInvalid.empty()) |
2204 | Editor()->m_Dialog = DIALOG_MAPSETTINGS_ERROR; |
2205 | } |
2206 | |
2207 | // ------ loaders |
2208 | |
2209 | void CMapSettingsBackend::InitValueLoaders() |
2210 | { |
2211 | // Load the different possible values for some specific settings |
2212 | RegisterLoader(pSettingName: "tune" , pfnLoader: SValueLoader::LoadTuneValues); |
2213 | RegisterLoader(pSettingName: "tune_zone" , pfnLoader: SValueLoader::LoadTuneZoneValues); |
2214 | RegisterLoader(pSettingName: "mapbug" , pfnLoader: SValueLoader::LoadMapBugs); |
2215 | } |
2216 | |
2217 | void SValueLoader::LoadTuneValues(const CSettingValuesBuilder &TuneBuilder) |
2218 | { |
2219 | // Add available tuning names to argument 0 of setting "tune" |
2220 | LoadArgumentTuneValues(ArgBuilder: TuneBuilder.Argument(Arg: 0)); |
2221 | } |
2222 | |
2223 | void SValueLoader::LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder) |
2224 | { |
2225 | // Add available tuning names to argument 1 of setting "tune_zone" |
2226 | LoadArgumentTuneValues(ArgBuilder: TuneZoneBuilder.Argument(Arg: 1)); |
2227 | } |
2228 | |
2229 | void SValueLoader::LoadMapBugs(const CSettingValuesBuilder &BugBuilder) |
2230 | { |
2231 | // Get argument 0 of setting "mapbug" |
2232 | auto ArgBuilder = BugBuilder.Argument(Arg: 0); |
2233 | // Add available map bugs options |
2234 | ArgBuilder.Add(pString: "grenade-doubleexplosion@ddnet.tw" ); |
2235 | } |
2236 | |
2237 | void SValueLoader::LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder) |
2238 | { |
2239 | // Iterate through available tunings add their name to the list |
2240 | for(int i = 0; i < CTuningParams::Num(); i++) |
2241 | { |
2242 | ArgBuilder.Add(pString: CTuningParams::Name(Index: i)); |
2243 | } |
2244 | } |
2245 | |