1#include "editor_server_settings.h"
2
3#include "editor.h"
4
5#include <base/color.h>
6#include <base/system.h>
7
8#include <engine/keys.h>
9#include <engine/shared/config.h>
10#include <engine/textrender.h>
11
12#include <game/client/gameclient.h>
13#include <game/client/lineinput.h>
14#include <game/client/ui.h>
15#include <game/client/ui_listbox.h>
16#include <game/editor/editor_actions.h>
17#include <game/editor/editor_history.h>
18
19#include <iterator>
20
21using namespace FontIcons;
22
23static const int FONT_SIZE = 12.0f;
24
25struct IMapSetting
26{
27 enum EType
28 {
29 SETTING_INT,
30 SETTING_COMMAND,
31 };
32 const char *m_pName;
33 const char *m_pHelp;
34 EType m_Type;
35
36 IMapSetting(const char *pName, const char *pHelp, EType Type) :
37 m_pName(pName), m_pHelp(pHelp), m_Type(Type) {}
38};
39struct SMapSettingInt : public IMapSetting
40{
41 int m_Default;
42 int m_Min;
43 int m_Max;
44
45 SMapSettingInt(const char *pName, const char *pHelp, int Default, int Min, int Max) :
46 IMapSetting(pName, pHelp, IMapSetting::SETTING_INT), m_Default(Default), m_Min(Min), m_Max(Max) {}
47};
48struct SMapSettingCommand : public IMapSetting
49{
50 const char *m_pArgs;
51
52 SMapSettingCommand(const char *pName, const char *pHelp, const char *pArgs) :
53 IMapSetting(pName, pHelp, IMapSetting::SETTING_COMMAND), m_pArgs(pArgs) {}
54};
55
56void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast)
57{
58 static int s_CommandSelectedIndex = -1;
59 static CListBox s_ListBox;
60 s_ListBox.SetActive(!m_MapSettingsCommandContext.m_DropdownContext.m_ListBox.Active() && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen());
61
62 bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size();
63 const bool CurrentInputValid = m_MapSettingsCommandContext.Valid(); // Use the context to validate the input
64
65 CUIRect ToolBar, Button, Label, List, DragBar;
66 View.HSplitTop(Cut: 22.0f, pTop: &DragBar, pBottom: nullptr);
67 DragBar.y -= 2.0f;
68 DragBar.w += 2.0f;
69 DragBar.h += 4.0f;
70 DoEditorDragBar(View, pDragBar: &DragBar, Side: EDragSide::SIDE_TOP, pValue: &m_aExtraEditorSplits[EXTRAEDITOR_SERVER_SETTINGS]);
71 View.HSplitTop(Cut: 20.0f, pTop: &ToolBar, pBottom: &View);
72 View.HSplitTop(Cut: 2.0f, pTop: nullptr, pBottom: &List);
73 ToolBar.HMargin(Cut: 2.0f, pOtherRect: &ToolBar);
74
75 // delete button
76 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
77 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
78 static int s_DeleteButton = 0;
79 if(DoButton_FontIcon(pId: &s_DeleteButton, pText: FONT_ICON_TRASH, Checked: GotSelection ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Delete] Delete the selected command from the command list.", Corners: IGraphics::CORNER_ALL, FontSize: 9.0f) || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DELETE)))
80 {
81 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand));
82
83 m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + s_CommandSelectedIndex);
84 if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size())
85 s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1;
86 if(s_CommandSelectedIndex >= 0)
87 m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand);
88 else
89 m_SettingsCommandInput.Clear();
90 m_Map.OnModify();
91 m_MapSettingsCommandContext.Update();
92 s_ListBox.ScrollToSelected();
93 }
94
95 // move down button
96 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
97 const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1;
98 static int s_DownButton = 0;
99 if(DoButton_FontIcon(pId: &s_DownButton, pText: FONT_ICON_SORT_DOWN, Checked: CanMoveDown ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Alt+Down] Move the selected command down.", Corners: IGraphics::CORNER_R, FontSize: 11.0f) || (CanMoveDown && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_DOWN)))
100 {
101 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::MOVE_DOWN, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex));
102
103 std::swap(a&: m_Map.m_vSettings[s_CommandSelectedIndex], b&: m_Map.m_vSettings[s_CommandSelectedIndex + 1]);
104 s_CommandSelectedIndex++;
105 m_Map.OnModify();
106 s_ListBox.ScrollToSelected();
107 }
108
109 // move up button
110 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
111 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
112 const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0;
113 static int s_UpButton = 0;
114 if(DoButton_FontIcon(pId: &s_UpButton, pText: FONT_ICON_SORT_UP, Checked: CanMoveUp ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Alt+Up] Move the selected command up.", Corners: IGraphics::CORNER_L, FontSize: 11.0f) || (CanMoveUp && Input()->AltIsPressed() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_UP)))
115 {
116 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::MOVE_UP, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex));
117
118 std::swap(a&: m_Map.m_vSettings[s_CommandSelectedIndex], b&: m_Map.m_vSettings[s_CommandSelectedIndex - 1]);
119 s_CommandSelectedIndex--;
120 m_Map.OnModify();
121 s_ListBox.ScrollToSelected();
122 }
123
124 // redo button
125 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
126 static int s_RedoButton = 0;
127 if(DoButton_FontIcon(pId: &s_RedoButton, pText: FONT_ICON_REDO, Checked: m_Map.m_ServerSettingsHistory.CanRedo() ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Y] Redo the last command edit.", Corners: IGraphics::CORNER_R, FontSize: 11.0f))
128 {
129 m_Map.m_ServerSettingsHistory.Redo();
130 }
131
132 // undo button
133 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
134 ToolBar.VSplitRight(Cut: 5.0f, pLeft: &ToolBar, pRight: nullptr);
135 static int s_UndoButton = 0;
136 if(DoButton_FontIcon(pId: &s_UndoButton, pText: FONT_ICON_UNDO, Checked: m_Map.m_ServerSettingsHistory.CanUndo() ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Ctrl+Z] Undo the last command edit.", Corners: IGraphics::CORNER_L, FontSize: 11.0f))
137 {
138 m_Map.m_ServerSettingsHistory.Undo();
139 }
140
141 GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size();
142
143 int CollidingCommandIndex = -1;
144 ECollisionCheckResult CheckResult = ECollisionCheckResult::ERROR;
145 if(CurrentInputValid)
146 CollidingCommandIndex = m_MapSettingsCommandContext.CheckCollision(Result&: CheckResult);
147
148 // update button
149 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
150 const bool CanAdd = CheckResult == ECollisionCheckResult::ADD;
151 const bool CanReplace = CheckResult == ECollisionCheckResult::REPLACE;
152
153 const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(a: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, b: m_SettingsCommandInput.GetString()) != 0;
154
155 static int s_UpdateButton = 0;
156 if(DoButton_FontIcon(pId: &s_UpdateButton, pText: FONT_ICON_PENCIL, Checked: CanUpdate ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: "[Alt+Enter] Update the selected command based on the entered value.", Corners: IGraphics::CORNER_R, FontSize: 9.0f) || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && m_SettingsCommandInput.IsActive() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
157 {
158 if(CollidingCommandIndex == -1)
159 {
160 bool Found = false;
161 int i;
162 for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i)
163 {
164 if(i != s_CommandSelectedIndex && !str_comp(a: m_Map.m_vSettings[i].m_aCommand, b: m_SettingsCommandInput.GetString()))
165 {
166 Found = true;
167 break;
168 }
169 }
170 if(Found)
171 {
172 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand));
173 m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + s_CommandSelectedIndex);
174 s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i;
175 }
176 else
177 {
178 const char *pStr = m_SettingsCommandInput.GetString();
179 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr));
180 str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr);
181 }
182 }
183 else
184 {
185 if(s_CommandSelectedIndex == CollidingCommandIndex)
186 { // If we are editing the currently collinding line, then we can just call EDIT on it
187 const char *pStr = m_SettingsCommandInput.GetString();
188 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr));
189 str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr);
190 }
191 else
192 { // If not, then editing the current selected line will result in the deletion of the colliding line, and the editing of the selected line
193 const char *pStr = m_SettingsCommandInput.GetString();
194
195 char aBuf[256];
196 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "Delete command %d; Edit command %d", CollidingCommandIndex, s_CommandSelectedIndex);
197
198 m_Map.m_ServerSettingsHistory.BeginBulk();
199 // Delete the colliding command
200 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::DELETE, args: &s_CommandSelectedIndex, args&: CollidingCommandIndex, args&: m_Map.m_vSettings[CollidingCommandIndex].m_aCommand));
201 m_Map.m_vSettings.erase(position: m_Map.m_vSettings.begin() + CollidingCommandIndex);
202 // Edit the selected command
203 s_CommandSelectedIndex = s_CommandSelectedIndex > CollidingCommandIndex ? s_CommandSelectedIndex - 1 : s_CommandSelectedIndex;
204 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr));
205 str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr);
206
207 m_Map.m_ServerSettingsHistory.EndBulk(pDisplay: aBuf);
208 }
209 }
210
211 m_Map.OnModify();
212 s_ListBox.ScrollToSelected();
213 m_SettingsCommandInput.Clear();
214 m_MapSettingsCommandContext.Reset(); // Reset context
215 Ui()->SetActiveItem(&m_SettingsCommandInput);
216 }
217
218 // add button
219 ToolBar.VSplitRight(Cut: 25.0f, pLeft: &ToolBar, pRight: &Button);
220 ToolBar.VSplitRight(Cut: 100.0f, pLeft: &ToolBar, pRight: nullptr);
221
222 static int s_AddButton = 0;
223 if(DoButton_FontIcon(pId: &s_AddButton, pText: CanReplace ? FONT_ICON_ARROWS_ROTATE : FONT_ICON_PLUS, Checked: CanAdd || CanReplace ? 0 : -1, pRect: &Button, Flags: BUTTONFLAG_LEFT, pToolTip: CanReplace ? "[Enter] Replace the corresponding command in the command list." : "[Enter] Add a command to the command list.", Corners: IGraphics::CORNER_L) || ((CanAdd || CanReplace) && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && m_SettingsCommandInput.IsActive() && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
224 {
225 if(CanReplace)
226 {
227 dbg_assert(CollidingCommandIndex != -1, "Could not replace command");
228 s_CommandSelectedIndex = CollidingCommandIndex;
229
230 const char *pStr = m_SettingsCommandInput.GetString();
231 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::EDIT, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, args&: pStr));
232 str_copy(dst&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, src: pStr);
233 }
234 else if(CanAdd)
235 {
236 m_Map.m_vSettings.emplace_back(args: m_SettingsCommandInput.GetString());
237 s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1;
238 m_Map.m_ServerSettingsHistory.RecordAction(pAction: std::make_shared<CEditorCommandAction>(args: &m_Map, args: CEditorCommandAction::EType::ADD, args: &s_CommandSelectedIndex, args&: s_CommandSelectedIndex, args&: m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand));
239 }
240
241 m_Map.OnModify();
242 s_ListBox.ScrollToSelected();
243 m_SettingsCommandInput.Clear();
244 m_MapSettingsCommandContext.Reset(); // Reset context
245 Ui()->SetActiveItem(&m_SettingsCommandInput);
246 }
247
248 // command input (use remaining toolbar width)
249 if(!ShowServerSettingsEditorLast) // Just activated
250 Ui()->SetActiveItem(&m_SettingsCommandInput);
251 m_SettingsCommandInput.SetEmptyText("Command");
252
253 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
254
255 // command list
256 s_ListBox.DoStart(RowHeight: 15.0f, NumItems: m_Map.m_vSettings.size(), ItemsPerRow: 1, RowsPerScroll: 3, SelectedIndex: s_CommandSelectedIndex, pRect: &List);
257
258 for(size_t i = 0; i < m_Map.m_vSettings.size(); i++)
259 {
260 const CListboxItem Item = s_ListBox.DoNextItem(pId: &m_Map.m_vSettings[i], Selected: s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i);
261 if(!Item.m_Visible)
262 continue;
263
264 Item.m_Rect.VMargin(Cut: 5.0f, pOtherRect: &Label);
265
266 SLabelProperties Props;
267 Props.m_MaxWidth = Label.w;
268 Props.m_EllipsisAtEnd = true;
269 Ui()->DoLabel(pRect: &Label, pText: m_Map.m_vSettings[i].m_aCommand, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
270 }
271
272 const int NewSelected = s_ListBox.DoEnd();
273 if(s_CommandSelectedIndex != NewSelected || s_ListBox.WasItemSelected())
274 {
275 s_CommandSelectedIndex = NewSelected;
276 if(m_SettingsCommandInput.IsEmpty() || !Input()->ModifierIsPressed()) // Allow ctrl+click to only change selection
277 {
278 m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand);
279 m_MapSettingsCommandContext.Update();
280 m_MapSettingsCommandContext.UpdateCursor(Force: true);
281 }
282 m_MapSettingsCommandContext.m_DropdownContext.m_ShouldHide = true;
283 Ui()->SetActiveItem(&m_SettingsCommandInput);
284 }
285
286 // Map setting input
287 DoMapSettingsEditBox(pContext: &m_MapSettingsCommandContext, pRect: &ToolBar, FontSize: FONT_SIZE, DropdownMaxHeight: List.h);
288}
289
290void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners, const char *pToolTip)
291{
292 // Main method to do the full featured map settings edit box
293
294 auto *pLineInput = pContext->LineInput();
295 auto &Context = *pContext;
296 Context.SetFontSize(FontSize);
297
298 // Small utility to render a floating part above the input rect.
299 // Use to display either the error or the current argument name
300 const float PartMargin = 4.0f;
301 auto &&RenderFloatingPart = [&](CUIRect *pInputRect, float x, const char *pStr) {
302 CUIRect Background;
303 Background.x = x - PartMargin;
304 Background.y = pInputRect->y - pInputRect->h - 6.0f;
305 Background.w = TextRender()->TextWidth(Size: FontSize, pText: pStr) + 2 * PartMargin;
306 Background.h = pInputRect->h;
307 Background.Draw(Color: ColorRGBA(0, 0, 0, 0.9f), Corners: IGraphics::CORNER_ALL, Rounding: 3.0f);
308
309 CUIRect Label;
310 Background.VSplitLeft(Cut: PartMargin, pLeft: nullptr, pRight: &Label);
311 TextRender()->TextColor(r: 0.8f, g: 0.8f, b: 0.8f, a: 1.0f);
312 Ui()->DoLabel(pRect: &Label, pText: pStr, Size: FontSize, Align: TEXTALIGN_ML);
313 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
314 };
315
316 // If we have a valid command, display the help in the tooltip
317 if(Context.CommandIsValid() && pLineInput->IsActive() && Ui()->HotItem() == nullptr)
318 {
319 Context.GetCommandHelpText(pStr: m_aTooltip, Length: sizeof(m_aTooltip));
320 str_append(dst&: m_aTooltip, src: ".");
321 }
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: BUTTONFLAG_LEFT, pToolTip: "Disallow/allow unknown or invalid 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. Press ctrl+space to show available settings.", 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 = std::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, pfnMatchCallback: 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
401template<typename T>
402int CEditor::DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector<T> &vData, const FDropdownRenderCallback<T> &pfnMatchCallback)
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 = std::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, pfnMatchCallback);
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
462template<typename T>
463int CEditor::RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector<T> &vData, const FDropdownRenderCallback<T> &pfnMatchCallback)
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 pfnMatchCallback(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
547void 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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(Color: 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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: BUTTONFLAG_LEFT, 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(Color);
893 Ui()->DoLabel(pRect: &Label, pText: Duplicate.m_aSetting, Size: 10.0f, Align: TEXTALIGN_ML, LabelProps: Props);
894 TextRender()->TextColor(Color: 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 ConfirmButton, CancelButton, FixAllUnknownButton;
920 ButtonBar.VSplitLeft(Cut: 110.0f, pLeft: &CancelButton, pRight: &ButtonBar);
921 ButtonBar.VSplitRight(Cut: 110.0f, pLeft: &ButtonBar, pRight: &ConfirmButton);
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: BUTTONFLAG_LEFT, 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: &ConfirmButton, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr) || (CanConfirm && Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ENTER)))
1006 {
1007 Execute();
1008 OnDialogClose();
1009 }
1010
1011 // Cancel - we load a new empty map
1012 if(DoButton_Editor(pId: &s_CancelButton, pText: "Cancel", Checked: 0, pRect: &CancelButton, Flags: BUTTONFLAG_LEFT, pToolTip: nullptr) || (Ui()->ConsumeHotkey(Hotkey: CUi::HOTKEY_ESCAPE)))
1013 {
1014 Reset();
1015 m_aFilename[0] = 0;
1016 OnDialogClose();
1017 }
1018}
1019
1020void 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
1050void CMapSettingsBackend::OnInit(CEditor *pEditor)
1051{
1052 CEditorComponent::OnInit(pEditor);
1053
1054 // Register values loader
1055 InitValueLoaders();
1056
1057 // Load settings/commands
1058 LoadAllMapSettings();
1059
1060 CValuesBuilder Builder(&m_PossibleValuesPerCommand);
1061
1062 // Load and parse static map settings so we can use them here
1063 for(auto &pSetting : m_vpMapSettings)
1064 {
1065 // We want to parse the arguments of each map setting so we can autocomplete them later
1066 // But that depends on the type of the setting.
1067 // If we have a INT setting, then we know we can only ever have 1 argument which is a integer value
1068 // If we have a COMMAND setting, then we need to parse its arguments
1069 if(pSetting->m_Type == IMapSetting::SETTING_INT)
1070 LoadSettingInt(pSetting: std::static_pointer_cast<SMapSettingInt>(r: pSetting));
1071 else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND)
1072 LoadSettingCommand(pSetting: std::static_pointer_cast<SMapSettingCommand>(r: pSetting));
1073
1074 LoadPossibleValues(Builder: Builder(pSetting->m_pName), pSetting);
1075 }
1076
1077 // Init constraints
1078 LoadConstraints();
1079}
1080
1081void CMapSettingsBackend::LoadAllMapSettings()
1082{
1083 // Gather all config variables having the flag CFGFLAG_GAME
1084 Editor()->ConfigManager()->PossibleConfigVariables(pStr: "", FlagMask: CFGFLAG_GAME, pfnCallback: PossibleConfigVariableCallback, pUserData: this);
1085
1086 // Load list of commands
1087 LoadCommand(pName: "tune", pArgs: "s[tuning] f[value]", pHelp: "Tune variable to value or show current value");
1088 LoadCommand(pName: "tune_zone", pArgs: "i[zone] s[tuning] f[value]", pHelp: "Tune in zone a variable to value");
1089 LoadCommand(pName: "tune_zone_enter", pArgs: "i[zone] r[message]", pHelp: "Which message to display on zone enter; use 0 for normal area");
1090 LoadCommand(pName: "tune_zone_leave", pArgs: "i[zone] r[message]", pHelp: "Which message to display on zone leave; use 0 for normal area");
1091 LoadCommand(pName: "mapbug", pArgs: "s[mapbug]", pHelp: "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)");
1092 LoadCommand(pName: "switch_open", pArgs: "i[switch]", pHelp: "Whether a switch is deactivated by default (otherwise activated)");
1093}
1094
1095void CMapSettingsBackend::LoadCommand(const char *pName, const char *pArgs, const char *pHelp)
1096{
1097 m_vpMapSettings.emplace_back(args: std::make_shared<SMapSettingCommand>(args&: pName, args&: pHelp, args&: pArgs));
1098}
1099
1100void CMapSettingsBackend::LoadSettingInt(const std::shared_ptr<SMapSettingInt> &pSetting)
1101{
1102 // We load an int argument here
1103 m_ParsedCommandArgs[pSetting].emplace_back();
1104 auto &Arg = m_ParsedCommandArgs[pSetting].back();
1105 str_copy(dst&: Arg.m_aName, src: "value");
1106 Arg.m_Type = 'i';
1107}
1108
1109void CMapSettingsBackend::LoadSettingCommand(const std::shared_ptr<SMapSettingCommand> &pSetting)
1110{
1111 // This method parses a setting into its arguments (name and type) so we can later
1112 // use them to validate the current input as well as display the current argument value
1113 // over the line input.
1114
1115 m_ParsedCommandArgs[pSetting].clear();
1116 const char *pIterator = pSetting->m_pArgs;
1117
1118 char Type;
1119
1120 while(*pIterator)
1121 {
1122 if(*pIterator == '?') // Skip optional values as a map setting should not have optional values
1123 pIterator++;
1124
1125 Type = *pIterator;
1126 pIterator++;
1127 while(*pIterator && *pIterator != '[')
1128 pIterator++;
1129 pIterator++; // skip '['
1130
1131 const char *pNameStart = pIterator;
1132
1133 while(*pIterator && *pIterator != ']')
1134 pIterator++;
1135
1136 size_t Len = pIterator - pNameStart;
1137 pIterator++; // Skip ']'
1138
1139 dbg_assert(Len + 1 < sizeof(SParsedMapSettingArg::m_aName), "Length of server setting name exceeds limit.");
1140
1141 // Append parsed arg
1142 m_ParsedCommandArgs[pSetting].emplace_back();
1143 auto &Arg = m_ParsedCommandArgs[pSetting].back();
1144 str_copy(dst: Arg.m_aName, src: pNameStart, dst_size: Len + 1);
1145 Arg.m_Type = Type;
1146
1147 pIterator = str_skip_whitespaces_const(str: pIterator);
1148 }
1149}
1150
1151void CMapSettingsBackend::LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr<IMapSetting> &pSetting)
1152{
1153 // Call the value loader for that setting
1154 auto Iter = m_LoaderFunctions.find(x: pSetting->m_pName);
1155 if(Iter == m_LoaderFunctions.end())
1156 return;
1157
1158 (*Iter->second)(Builder);
1159}
1160
1161void CMapSettingsBackend::RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader)
1162{
1163 // Registers a value loader function for a specific setting name
1164 m_LoaderFunctions[pSettingName] = pfnLoader;
1165}
1166
1167void CMapSettingsBackend::LoadConstraints()
1168{
1169 // Make an instance of constraint builder
1170 CCommandArgumentConstraintBuilder Command(&m_ArgConstraintsPerCommand);
1171
1172 // Define constraints like this
1173 // This is still a bit sad as we have to do it manually here.
1174 Command("tune", 2).Unique(Arg: 0);
1175 Command("tune_zone", 3).Multiple(Arg: 0).Unique(Arg: 1);
1176 Command("tune_zone_enter", 2).Unique(Arg: 0);
1177 Command("tune_zone_leave", 2).Unique(Arg: 0);
1178 Command("switch_open", 1).Unique(Arg: 0);
1179 Command("mapbug", 1).Unique(Arg: 0);
1180}
1181
1182void CMapSettingsBackend::PossibleConfigVariableCallback(const SConfigVariable *pVariable, void *pUserData)
1183{
1184 CMapSettingsBackend *pBackend = (CMapSettingsBackend *)pUserData;
1185
1186 if(pVariable->m_Type == SConfigVariable::VAR_INT)
1187 {
1188 SIntConfigVariable *pIntVariable = (SIntConfigVariable *)pVariable;
1189 pBackend->m_vpMapSettings.emplace_back(args: std::make_shared<SMapSettingInt>(
1190 args&: pIntVariable->m_pScriptName,
1191 args&: pIntVariable->m_pHelp,
1192 args&: pIntVariable->m_Default,
1193 args&: pIntVariable->m_Min,
1194 args&: pIntVariable->m_Max));
1195 }
1196}
1197
1198void CMapSettingsBackend::CContext::Reset()
1199{
1200 m_LastCursorOffset = 0;
1201 m_CursorArgIndex = -1;
1202 m_pCurrentSetting = nullptr;
1203 m_vCurrentArgs.clear();
1204 m_aCommand[0] = '\0';
1205 m_DropdownContext.m_Selected = -1;
1206 m_CurrentCompletionIndex = -1;
1207 m_DropdownContext.m_ShortcutUsed = false;
1208 m_DropdownContext.m_MousePressedInside = false;
1209 m_DropdownContext.m_Visible = false;
1210 m_DropdownContext.m_ShouldHide = false;
1211 m_CommentOffset = -1;
1212
1213 ClearError();
1214}
1215
1216void CMapSettingsBackend::CContext::Update()
1217{
1218 UpdateFromString(pStr: InputString());
1219}
1220
1221void CMapSettingsBackend::CContext::UpdateFromString(const char *pStr)
1222{
1223 // This is the main method that does all the argument parsing and validating.
1224 // It fills pretty much all the context values, the arguments, their position,
1225 // if they are valid or not, etc.
1226
1227 m_pCurrentSetting = nullptr;
1228 m_vCurrentArgs.clear();
1229 m_CommentOffset = -1;
1230
1231 const char *pIterator = pStr;
1232
1233 // Check for comment
1234 const char *pEnd = pStr;
1235 int InString = 0;
1236
1237 while(*pEnd)
1238 {
1239 if(*pEnd == '"')
1240 InString ^= 1;
1241 else if(*pEnd == '\\') // Escape sequences
1242 {
1243 if(pEnd[1] == '"')
1244 pEnd++;
1245 }
1246 else if(!InString)
1247 {
1248 if(*pEnd == '#') // Found comment
1249 {
1250 m_CommentOffset = pEnd - pStr;
1251 break;
1252 }
1253 }
1254
1255 pEnd++;
1256 }
1257
1258 if(m_CommentOffset == 0)
1259 return;
1260
1261 // End command at start of comment, if any
1262 char aInputString[256];
1263 str_copy(dst: aInputString, src: pStr, dst_size: m_CommentOffset != -1 ? m_CommentOffset + 1 : sizeof(aInputString));
1264 pIterator = aInputString;
1265
1266 // Get the command/setting
1267 m_aCommand[0] = '\0';
1268 while(pIterator && *pIterator != ' ' && *pIterator != '\0')
1269 pIterator++;
1270
1271 str_copy(dst: m_aCommand, src: aInputString, dst_size: (pIterator - aInputString) + 1);
1272
1273 // Get the command if it is a recognized one
1274 for(auto &pSetting : m_pBackend->m_vpMapSettings)
1275 {
1276 if(str_comp_nocase(a: m_aCommand, b: pSetting->m_pName) == 0)
1277 {
1278 m_pCurrentSetting = pSetting;
1279 break;
1280 }
1281 }
1282
1283 // Parse args
1284 ParseArgs(pLineInputStr: aInputString, pStr: pIterator);
1285}
1286
1287void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const char *pStr)
1288{
1289 // This method parses the arguments of the current command, starting at pStr
1290
1291 ClearError();
1292
1293 const char *pIterator = pStr;
1294
1295 if(!pStr || *pStr == '\0')
1296 return; // No arguments
1297
1298 // NextArg is used to get the contents of the current argument and go to the next argument position
1299 // It outputs the length of the argument in pLength and returns a boolean indicating if the parsing
1300 // of that argument is valid or not (only the case when using strings with quotes ("))
1301 auto &&NextArg = [&](const char *pArg, int *pLength) {
1302 if(*pIterator == '"')
1303 {
1304 pIterator++;
1305 bool Valid = true;
1306 bool IsEscape = false;
1307
1308 while(true)
1309 {
1310 if(pIterator[0] == '"' && !IsEscape)
1311 break;
1312 else if(pIterator[0] == 0)
1313 {
1314 Valid = false;
1315 break;
1316 }
1317
1318 if(pIterator[0] == '\\' && !IsEscape)
1319 IsEscape = true;
1320 else if(IsEscape)
1321 IsEscape = false;
1322
1323 pIterator++;
1324 }
1325 const char *pEnd = ++pIterator;
1326 pIterator = str_skip_to_whitespace_const(str: pIterator);
1327
1328 // Make sure there are no other characters at the end, otherwise the string is invalid.
1329 // E.g. "abcd"ef is invalid
1330 Valid = Valid && pIterator == pEnd;
1331 *pLength = pEnd - pArg;
1332
1333 return Valid;
1334 }
1335 else
1336 {
1337 pIterator = str_skip_to_whitespace_const(str: pIterator);
1338 *pLength = pIterator - pArg;
1339 return true;
1340 }
1341 };
1342
1343 // Simple validation of string. Checks that it does not contain unescaped " in the middle of it.
1344 auto &&ValidateStr = [](const char *pString) -> bool {
1345 const char *pIt = pString;
1346 bool IsEscape = false;
1347 while(*pIt)
1348 {
1349 if(pIt[0] == '"' && !IsEscape)
1350 return false;
1351
1352 if(pIt[0] == '\\' && !IsEscape)
1353 IsEscape = true;
1354 else if(IsEscape)
1355 IsEscape = false;
1356
1357 pIt++;
1358 }
1359 return true;
1360 };
1361
1362 const int CommandArgCount = m_pCurrentSetting != nullptr ? m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting).size() : 0;
1363 int ArgIndex = 0;
1364 SCommandParseError::EErrorType Error = SCommandParseError::ERROR_NONE;
1365
1366 // Also keep track of the visual X position of each argument within the input
1367 float PosX = 0;
1368 const float WW = m_pLineInput != nullptr ? m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: " ") : 0.0f;
1369 PosX += m_pLineInput != nullptr ? m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: m_aCommand) : 0.0f;
1370
1371 // Parsing beings
1372 while(*pIterator)
1373 {
1374 Error = SCommandParseError::ERROR_NONE;
1375 pIterator++; // Skip whitespace
1376 PosX += WW; // Add whitespace width
1377
1378 // Insert argument here
1379 char Char = *pIterator;
1380 const char *pArgStart = pIterator;
1381 int Length;
1382 bool Valid = NextArg(pArgStart, &Length); // Get contents and go to next argument position
1383 size_t Offset = pArgStart - pLineInputStr; // Compute offset from the start of the input
1384
1385 // Add new argument, copy the argument contents
1386 m_vCurrentArgs.emplace_back();
1387 auto &NewArg = m_vCurrentArgs.back();
1388 // Fill argument value, with a maximum length of 256
1389 str_copy(dst: NewArg.m_aValue, src: pArgStart, dst_size: minimum(a: (int)sizeof(SCurrentSettingArg::m_aValue), b: Length + 1));
1390
1391 // Validate argument from the parsed argument of the current setting.
1392 // If current setting is not valid, then there are no arguments which results in an error.
1393
1394 char Type = 'u'; // u = unknown
1395 if(ArgIndex < CommandArgCount)
1396 {
1397 SParsedMapSettingArg &Arg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(n: ArgIndex);
1398 if(Arg.m_Type == 'r')
1399 {
1400 // Rest of string, should add all the string if there was no quotes
1401 // Otherwise, only get the contents in the quotes, and consider content after that as other arguments
1402 if(Char != '"')
1403 {
1404 while(*pIterator)
1405 pIterator++;
1406 Length = pIterator - pArgStart;
1407 str_copy(dst: NewArg.m_aValue, src: pArgStart, dst_size: Length + 1);
1408 }
1409
1410 if(!Valid)
1411 Error = SCommandParseError::ERROR_INVALID_VALUE;
1412 }
1413 else if(Arg.m_Type == 'i')
1414 {
1415 // Validate int
1416 if(!str_toint(str: NewArg.m_aValue, out: nullptr))
1417 Error = SCommandParseError::ERROR_INVALID_VALUE;
1418 }
1419 else if(Arg.m_Type == 'f')
1420 {
1421 // Validate float
1422 if(!str_tofloat(str: NewArg.m_aValue, out: nullptr))
1423 Error = SCommandParseError::ERROR_INVALID_VALUE;
1424 }
1425 else if(Arg.m_Type == 's')
1426 {
1427 // Validate string
1428 if(!Valid || (Char != '"' && !ValidateStr(NewArg.m_aValue)))
1429 Error = SCommandParseError::ERROR_INVALID_VALUE;
1430 }
1431
1432 // Extended argument validation:
1433 // for int settings it checks that the value is in range
1434 // for command settings, it checks that the value is one of the possible values if there are any
1435 EValidationResult Result = ValidateArg(Index: ArgIndex, pArg: NewArg.m_aValue);
1436 if(Length && !Error && Result != EValidationResult::VALID)
1437 {
1438 if(Result == EValidationResult::ERROR)
1439 Error = SCommandParseError::ERROR_INVALID_VALUE; // Invalid argument value (invalid int, invalid float)
1440 else if(Result == EValidationResult::UNKNOWN)
1441 Error = SCommandParseError::ERROR_UNKNOWN_VALUE; // Unknown argument value
1442 else if(Result == EValidationResult::INCOMPLETE)
1443 Error = SCommandParseError::ERROR_INCOMPLETE; // Incomplete argument in case of possible values
1444 else if(Result == EValidationResult::OUT_OF_RANGE)
1445 Error = SCommandParseError::ERROR_OUT_OF_RANGE; // Out of range argument value in case of int settings
1446 else
1447 Error = SCommandParseError::ERROR_UNKNOWN; // Unknown error
1448 }
1449
1450 Type = Arg.m_Type;
1451 }
1452 else
1453 {
1454 // Error: too many arguments if no comment after
1455 if(m_CommentOffset == -1)
1456 Error = SCommandParseError::ERROR_TOO_MANY_ARGS;
1457 else
1458 { // Otherwise, check if there are any arguments left between this argument and the comment
1459 const char *pSubIt = pArgStart;
1460 pSubIt = str_skip_whitespaces_const(str: pSubIt);
1461 if(*pSubIt != '\0')
1462 { // If there aren't only spaces between the last argument and the comment, then this is an error
1463 Error = SCommandParseError::ERROR_TOO_MANY_ARGS;
1464 }
1465 else // If there are, then just exit the loop to avoid getting an error
1466 {
1467 m_vCurrentArgs.pop_back();
1468 break;
1469 }
1470 }
1471 }
1472
1473 // Fill argument information
1474 NewArg.m_X = PosX;
1475 NewArg.m_Start = Offset;
1476 NewArg.m_End = Offset + Length;
1477 NewArg.m_Error = Error != SCommandParseError::ERROR_NONE || Length == 0 || m_Error.m_Type != SCommandParseError::ERROR_NONE;
1478 NewArg.m_ExpectedType = Type;
1479
1480 // Check error and fill the error field with different messages
1481 if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_OUT_OF_RANGE || Error == SCommandParseError::ERROR_INCOMPLETE)
1482 {
1483 // Only keep first error
1484 if(!m_Error.m_aMessage[0])
1485 {
1486 int ErrorArgIndex = (int)m_vCurrentArgs.size() - 1;
1487 SCurrentSettingArg &ErrorArg = m_vCurrentArgs.back();
1488 SParsedMapSettingArg &SettingArg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(n: ArgIndex);
1489 char aFormattedValue[256];
1490 FormatDisplayValue(pValue: ErrorArg.m_aValue, aOut&: aFormattedValue);
1491
1492 if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_INCOMPLETE)
1493 {
1494 static const std::map<int, const char *> s_Names = {
1495 {SCommandParseError::ERROR_INVALID_VALUE, "Invalid"},
1496 {SCommandParseError::ERROR_UNKNOWN_VALUE, "Unknown"},
1497 {SCommandParseError::ERROR_INCOMPLETE, "Incomplete"},
1498 };
1499 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);
1500 }
1501 else
1502 {
1503 std::shared_ptr<SMapSettingInt> pSettingInt = std::static_pointer_cast<SMapSettingInt>(r: m_pCurrentSetting);
1504 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);
1505 }
1506 m_Error.m_ArgIndex = ErrorArgIndex;
1507 m_Error.m_Type = Error;
1508 }
1509 }
1510 else if(Error == SCommandParseError::ERROR_TOO_MANY_ARGS)
1511 {
1512 // Only keep first error
1513 if(!m_Error.m_aMessage[0])
1514 {
1515 if(m_pCurrentSetting != nullptr)
1516 {
1517 str_copy(dst&: m_Error.m_aMessage, src: "Too many arguments");
1518 m_Error.m_ArgIndex = ArgIndex;
1519 break;
1520 }
1521 else
1522 {
1523 char aFormattedValue[256];
1524 FormatDisplayValue(pValue: m_aCommand, aOut&: aFormattedValue);
1525 str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "Unknown server setting: %s", aFormattedValue);
1526 m_Error.m_ArgIndex = -1;
1527 break;
1528 }
1529 m_Error.m_Type = Error;
1530 }
1531 }
1532
1533 PosX += m_pLineInput != nullptr ? m_pBackend->TextRender()->TextWidth(Size: m_FontSize, pText: pArgStart, StrLength: Length) : 0.0f; // Advance argument position
1534 ArgIndex++;
1535 }
1536}
1537
1538void CMapSettingsBackend::CContext::ClearError()
1539{
1540 m_Error.m_aMessage[0] = '\0';
1541 m_Error.m_Type = SCommandParseError::ERROR_NONE;
1542}
1543
1544bool CMapSettingsBackend::CContext::UpdateCursor(bool Force)
1545{
1546 // This method updates the cursor offset in this class from
1547 // the cursor offset of the line input.
1548 // It also updates the argument index where the cursor is at
1549 // and the possible values matches if the argument index changes.
1550 // Returns true in case the cursor changed position
1551
1552 if(!m_pLineInput)
1553 return false;
1554
1555 size_t Offset = m_pLineInput->GetCursorOffset();
1556 if(Offset == m_LastCursorOffset && !Force)
1557 return false;
1558
1559 m_LastCursorOffset = Offset;
1560 int NewArg = m_CursorArgIndex;
1561
1562 // Update current argument under cursor
1563 if(m_CommentOffset != -1 && Offset >= (size_t)m_CommentOffset)
1564 {
1565 NewArg = (int)m_vCurrentArgs.size();
1566 }
1567 else
1568 {
1569 bool FoundArg = false;
1570 for(int i = (int)m_vCurrentArgs.size() - 1; i >= 0; i--)
1571 {
1572 if(Offset >= m_vCurrentArgs[i].m_Start)
1573 {
1574 NewArg = i;
1575 FoundArg = true;
1576 break;
1577 }
1578 }
1579
1580 if(!FoundArg)
1581 NewArg = -1;
1582 }
1583
1584 bool ShouldUpdate = NewArg != m_CursorArgIndex;
1585 m_CursorArgIndex = NewArg;
1586
1587 // Do not show error if current argument is incomplete, as we are editing it
1588 if(m_pLineInput != nullptr)
1589 {
1590 if(Offset == m_pLineInput->GetLength() && m_Error.m_aMessage[0] && m_Error.m_ArgIndex == m_CursorArgIndex && m_Error.m_Type == SCommandParseError::ERROR_INCOMPLETE)
1591 ClearError();
1592 }
1593
1594 if(m_DropdownContext.m_Selected == -1 || ShouldUpdate || Force)
1595 {
1596 // Update possible commands from cursor
1597 UpdatePossibleMatches();
1598 }
1599
1600 return true;
1601}
1602
1603EValidationResult CMapSettingsBackend::CContext::ValidateArg(int Index, const char *pArg)
1604{
1605 if(!m_pCurrentSetting)
1606 return EValidationResult::ERROR;
1607
1608 // Check if this argument is valid against current argument
1609 if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT)
1610 {
1611 std::shared_ptr<SMapSettingInt> pSetting = std::static_pointer_cast<SMapSettingInt>(r: m_pCurrentSetting);
1612 if(Index > 0)
1613 return EValidationResult::ERROR;
1614
1615 int Value;
1616 if(!str_toint(str: pArg, out: &Value)) // Try parse the integer
1617 return EValidationResult::ERROR;
1618
1619 return Value >= pSetting->m_Min && Value <= pSetting->m_Max ? EValidationResult::VALID : EValidationResult::OUT_OF_RANGE;
1620 }
1621 else if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_COMMAND)
1622 {
1623 auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting);
1624 if(Index < (int)vArgs.size())
1625 {
1626 auto It = m_pBackend->m_PossibleValuesPerCommand.find(x: m_pCurrentSetting->m_pName);
1627 if(It != m_pBackend->m_PossibleValuesPerCommand.end())
1628 {
1629 auto ValuesIt = It->second.find(x: Index);
1630 if(ValuesIt != It->second.end())
1631 {
1632 // This means that we have possible values for this argument for this setting
1633 // In order to validate such arg, we have to check if it matches any of the possible values
1634 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; });
1635
1636 // If equals, then argument is valid
1637 if(EqualsAny)
1638 return EValidationResult::VALID;
1639
1640 // Here we check if argument is incomplete
1641 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; });
1642 if(StartsAny)
1643 return EValidationResult::INCOMPLETE;
1644
1645 return EValidationResult::UNKNOWN;
1646 }
1647 }
1648 }
1649
1650 // If we get here, it means there are no possible values for that specific argument.
1651 // The validation for specific types such as int and floats were done earlier so if we get here
1652 // we know the argument is valid.
1653 // String and "rest of string" types are valid by default.
1654 return EValidationResult::VALID;
1655 }
1656
1657 return EValidationResult::ERROR;
1658}
1659
1660void CMapSettingsBackend::CContext::UpdatePossibleMatches()
1661{
1662 // This method updates the possible values matches based on the cursor position within the current argument in the line input.
1663 // For example ("|" is the cursor):
1664 // - Typing "sv_deep|" will show "sv_deepfly" as a possible match in the dropdown
1665 // Moving the cursor: "sv_|deep" will show all possible commands starting with "sv_"
1666 // - Typing "tune ground_frict|" will show "ground_friction" as possible match
1667 // Moving the cursor: "tune ground_|frict" will show all possible values starting with "ground_" for that argument (argument 0 of "tune" setting)
1668
1669 m_vPossibleMatches.clear();
1670 m_DropdownContext.m_Selected = -1;
1671
1672 if(m_CommentOffset == 0 || (m_aCommand[0] == '\0' && !m_DropdownContext.m_ShortcutUsed))
1673 return;
1674
1675 // First case: argument index under cursor is -1 => we're on the command/setting name
1676 if(m_CursorArgIndex == -1)
1677 {
1678 // Use a substring from the start of the input to the cursor offset
1679 char aSubString[128];
1680 str_copy(dst: aSubString, src: m_aCommand, dst_size: minimum(a: m_LastCursorOffset + 1, b: sizeof(aSubString)));
1681
1682 // Iterate through available map settings and find those which the beginning matches with the command/setting name we are writing
1683 for(auto &pSetting : m_pBackend->m_vpMapSettings)
1684 {
1685 if(str_startswith_nocase(str: pSetting->m_pName, prefix: aSubString))
1686 {
1687 m_vPossibleMatches.emplace_back(args: SPossibleValueMatch{
1688 .m_pValue: pSetting->m_pName,
1689 .m_ArgIndex: m_CursorArgIndex,
1690 .m_pData: pSetting.get(),
1691 });
1692 }
1693 }
1694
1695 // If there are no matches, then the command is unknown
1696 if(m_vPossibleMatches.empty())
1697 {
1698 // Fill the error if we do not allow unknown commands
1699 char aFormattedValue[256];
1700 FormatDisplayValue(pValue: m_aCommand, aOut&: aFormattedValue);
1701 str_format(buffer: m_Error.m_aMessage, buffer_size: sizeof(m_Error.m_aMessage), format: "Unknown server setting: %s", aFormattedValue);
1702 m_Error.m_ArgIndex = -1;
1703 }
1704 }
1705 else
1706 {
1707 // Second case: we are on an argument
1708 if(!m_pCurrentSetting) // If we are on an argument of an unknown setting, we can't handle it => no possible values, ever.
1709 return;
1710
1711 if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT)
1712 {
1713 // No possible values for int settings.
1714 // Maybe we can add "0" and "1" as possible values for settings that are binary.
1715 }
1716 else
1717 {
1718 // Get the parsed arguments for the current setting
1719 auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting);
1720 // Make sure we are not out of bounds
1721 if(m_CursorArgIndex < (int)vArgs.size() && m_CursorArgIndex < (int)m_vCurrentArgs.size())
1722 {
1723 // Check if there are possible values for this command
1724 auto It = m_pBackend->m_PossibleValuesPerCommand.find(x: m_pCurrentSetting->m_pName);
1725 if(It != m_pBackend->m_PossibleValuesPerCommand.end())
1726 {
1727 // If that's the case, then check if there are possible values for the current argument index the cursor is on
1728 auto ValuesIt = It->second.find(x: m_CursorArgIndex);
1729 if(ValuesIt != It->second.end())
1730 {
1731 // If that's the case, then do the same as previously, we check for each value if they match
1732 // with the current argument value
1733
1734 auto &CurrentArg = m_vCurrentArgs.at(n: m_CursorArgIndex);
1735 int SubstringLength = minimum(a: m_LastCursorOffset, b: CurrentArg.m_End) - CurrentArg.m_Start;
1736
1737 // Substring based on the cursor position inside that argument
1738 char aSubString[256];
1739 str_copy(dst: aSubString, src: CurrentArg.m_aValue, dst_size: SubstringLength + 1);
1740
1741 for(auto &pValue : ValuesIt->second)
1742 {
1743 if(str_startswith_nocase(str: pValue, prefix: aSubString))
1744 {
1745 m_vPossibleMatches.emplace_back(args: SPossibleValueMatch{
1746 .m_pValue: pValue,
1747 .m_ArgIndex: m_CursorArgIndex,
1748 .m_pData: nullptr,
1749 });
1750 }
1751 }
1752 }
1753 }
1754 }
1755 }
1756 }
1757}
1758
1759bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event)
1760{
1761 if(!m_pLineInput)
1762 return false;
1763
1764 if(!m_pLineInput->IsActive())
1765 return false;
1766
1767 if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT) && !m_pBackend->Input()->ModifierIsPressed() && !m_pBackend->Input()->AltIsPressed())
1768 {
1769 // How to make this better?
1770 // This checks when we press any key that is not handled by the dropdown
1771 // When that's the case, it means we confirm the completion if we have a valid completion index
1772 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))
1773 {
1774 if(m_CurrentCompletionIndex != -1)
1775 {
1776 m_CurrentCompletionIndex = -1;
1777 m_DropdownContext.m_Selected = -1;
1778 Update();
1779 UpdateCursor(Force: true);
1780 }
1781 }
1782 }
1783
1784 return false;
1785}
1786
1787const char *CMapSettingsBackend::CContext::InputString() const
1788{
1789 if(!m_pLineInput)
1790 return nullptr;
1791 return m_pBackend->Input()->HasComposition() ? m_CompositionStringBuffer.c_str() : m_pLineInput->GetString();
1792}
1793
1794void CMapSettingsBackend::CContext::ColorArguments(std::vector<STextColorSplit> &vColorSplits) const
1795{
1796 // Get argument color based on its type
1797 auto &&GetArgumentColor = [](char Type) -> ColorRGBA {
1798 if(Type == 'u')
1799 return ms_ArgumentUnknownColor;
1800 else if(Type == 's' || Type == 'r')
1801 return ms_ArgumentStringColor;
1802 else if(Type == 'i' || Type == 'f')
1803 return ms_ArgumentNumberColor;
1804 return ms_ErrorColor; // Invalid arg type
1805 };
1806
1807 // Iterate through all the current arguments and color them
1808 for(int i = 0; i < ArgCount(); i++)
1809 {
1810 const auto &Argument = Arg(Index: i);
1811 // Color is based on the error flag and the type of the argument
1812 auto Color = Argument.m_Error ? ms_ErrorColor : GetArgumentColor(Argument.m_ExpectedType);
1813 vColorSplits.emplace_back(args: Argument.m_Start, args: Argument.m_End - Argument.m_Start, args&: Color);
1814 }
1815
1816 if(m_pLineInput && !m_pLineInput->IsEmpty())
1817 {
1818 if(!CommandIsValid() && m_CommentOffset != 0)
1819 {
1820 // If command is invalid, override color splits with red, but not comment
1821 int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset;
1822 vColorSplits = {{0, ErrorLength, ms_ErrorColor}};
1823 }
1824 else if(HasError())
1825 {
1826 // If there is an error, then color the wrong part of the input, excluding comment
1827 int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset - ErrorOffset();
1828 vColorSplits.emplace_back(args: ErrorOffset(), args&: ErrorLength, args: ms_ErrorColor);
1829 }
1830 if(m_CommentOffset != -1)
1831 { // Color comment if there is one
1832 vColorSplits.emplace_back(args: m_CommentOffset, args: -1, args: ms_CommentColor);
1833 }
1834 }
1835
1836 std::sort(first: vColorSplits.begin(), last: vColorSplits.end(), comp: [](const STextColorSplit &a, const STextColorSplit &b) {
1837 return a.m_CharIndex < b.m_CharIndex;
1838 });
1839}
1840
1841int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) const
1842{
1843 return CheckCollision(vSettings: m_pBackend->Editor()->m_Map.m_vSettings, Result);
1844}
1845
1846int CMapSettingsBackend::CContext::CheckCollision(const std::vector<CEditorMapSetting> &vSettings, ECollisionCheckResult &Result) const
1847{
1848 return CheckCollision(pInputString: InputString(), vSettings, Result);
1849}
1850
1851int CMapSettingsBackend::CContext::CheckCollision(const char *pInputString, const std::vector<CEditorMapSetting> &vSettings, ECollisionCheckResult &Result) const
1852{
1853 // Checks for a collision with the current map settings.
1854 // A collision is when a setting with the same arguments already exists and that it can't be added multiple times.
1855 // For this, we use argument constraints that we define in CMapSettingsCommandObject::LoadConstraints().
1856 // For example, the "tune" command can be added multiple times, but only if the actual tune argument is different, thus
1857 // the tune argument must be defined as UNIQUE.
1858 // This method CheckCollision(ECollisionCheckResult&) returns an integer which is the index of the colliding line. If no
1859 // colliding line was found, then it returns -1.
1860
1861 const int InputLength = str_length(str: pInputString);
1862
1863 if(m_CommentOffset == 0 || InputLength == 0)
1864 { // Ignore comments
1865 Result = ECollisionCheckResult::ADD;
1866 return -1;
1867 }
1868
1869 struct SArgument
1870 {
1871 char m_aValue[128];
1872 SArgument(const char *pStr)
1873 {
1874 str_copy(dst&: m_aValue, src: pStr);
1875 }
1876 };
1877
1878 struct SLineArgs
1879 {
1880 int m_Index;
1881 std::vector<SArgument> m_vArgs;
1882 };
1883
1884 // For now we split each map setting corresponding to the setting we want to add by spaces
1885 auto &&SplitSetting = [](const char *pStr) {
1886 std::vector<SArgument> vaArgs;
1887 const char *pIt = pStr;
1888 char aBuffer[128];
1889 while((pIt = str_next_token(str: pIt, delim: " ", buffer: aBuffer, buffer_size: sizeof(aBuffer))))
1890 vaArgs.emplace_back(args&: aBuffer);
1891 return vaArgs;
1892 };
1893
1894 // Define the result of the check
1895 Result = ECollisionCheckResult::ERROR;
1896
1897 // First case: the command is not a valid (recognized) command.
1898 if(!CommandIsValid())
1899 {
1900 // If we don't allow unknown commands, then we know there is no collision
1901 // and the check results in an error.
1902 if(!m_AllowUnknownCommands)
1903 return -1;
1904
1905 // If we get here, it means we allow unknown commands.
1906 // For them, we need to check if a similar exact command exists or not in the settings list.
1907 // If it does, then we found a collision, and the result is REPLACE.
1908 for(int i = 0; i < (int)vSettings.size(); i++)
1909 {
1910 if(str_comp_nocase(a: vSettings[i].m_aCommand, b: pInputString) == 0)
1911 {
1912 Result = ECollisionCheckResult::REPLACE;
1913 return i;
1914 }
1915 }
1916
1917 // If nothing was found, then we must ensure that the command, although unknown, is somewhat valid
1918 // by checking if the command contains a space and that there is at least one non-empty argument.
1919 const char *pSpace = str_find(haystack: pInputString, needle: " ");
1920 if(!pSpace || !*(pSpace + 1))
1921 Result = ECollisionCheckResult::ERROR;
1922 else
1923 Result = ECollisionCheckResult::ADD;
1924
1925 return -1; // No collision
1926 }
1927
1928 // Second case: the command is valid.
1929 // In this case, we know we have a valid setting name, which means we can use everything we have in this class which are
1930 // related to valid map settings, such as parsed command arguments, etc.
1931
1932 const std::shared_ptr<IMapSetting> &pSetting = Setting();
1933 if(pSetting->m_Type == IMapSetting::SETTING_INT)
1934 {
1935 // For integer settings, the check is quite simple as we know
1936 // we can only ever have 1 argument.
1937
1938 // The integer setting cannot be added multiple times, which means if a collision was found, then the only result we
1939 // can have is REPLACE.
1940 // In this case, the collision is found only by checking the command name for every setting in the current map settings.
1941 char aBuffer[256];
1942 auto It = std::find_if(first: vSettings.begin(), last: vSettings.end(), pred: [&](const CEditorMapSetting &Setting) {
1943 const char *pLineSettingValue = Setting.m_aCommand; // Get the map setting command
1944 pLineSettingValue = str_next_token(str: pLineSettingValue, delim: " ", buffer: aBuffer, buffer_size: sizeof(aBuffer)); // Get the first token before the first space
1945 return str_comp_nocase(a: aBuffer, b: pSetting->m_pName) == 0; // Check if that equals our current command
1946 });
1947
1948 if(It == vSettings.end())
1949 {
1950 // If nothing was found, then there is no collision and we can add that command to the list
1951 Result = ECollisionCheckResult::ADD;
1952 return -1;
1953 }
1954 else
1955 {
1956 // Otherwise, we can only replace it
1957 Result = ECollisionCheckResult::REPLACE;
1958 return It - vSettings.begin(); // This is the index of the colliding line
1959 }
1960 }
1961 else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND)
1962 {
1963 // For "command" settings, this is a bit more complex as we have to use argument constraints.
1964 // The general idea is to split every map setting in their arguments separated by spaces.
1965 // Then, for each argument, we check if it collides with any of the map settings. When that's the case,
1966 // we need to check the constraint of the argument. If set to UNIQUE, then that's a collision and we can only
1967 // replace the command in the list.
1968 // If set to anything else, we consider that it is not a collision and we move to the next argument.
1969 // This system is simple and somewhat flexible as we only need to declare the constraints, the rest should be
1970 // handled automatically.
1971
1972 std::shared_ptr<SMapSettingCommand> pSettingCommand = std::static_pointer_cast<SMapSettingCommand>(r: pSetting);
1973 // Get matching lines for that command
1974 std::vector<SLineArgs> vLineArgs;
1975 for(int i = 0; i < (int)vSettings.size(); i++)
1976 {
1977 const auto &Setting = vSettings.at(n: i);
1978
1979 // Split this setting into its arguments
1980 std::vector<SArgument> vArgs = SplitSetting(Setting.m_aCommand);
1981 // Only keep settings that match with the current input setting name
1982 if(!vArgs.empty() && str_comp_nocase(a: vArgs[0].m_aValue, b: pSettingCommand->m_pName) == 0)
1983 {
1984 // When that's the case, we save them
1985 vArgs.erase(position: vArgs.begin());
1986 vLineArgs.push_back(x: SLineArgs{
1987 .m_Index: i,
1988 .m_vArgs: vArgs,
1989 });
1990 }
1991 }
1992
1993 // Here is the simple algorithm to check for collisions according to argument constraints
1994 bool Error = false;
1995 int CollidingLineIndex = -1;
1996 for(int ArgIndex = 0; ArgIndex < ArgCount(); ArgIndex++)
1997 {
1998 bool Collide = false;
1999 const char *pValue = Arg(Index: ArgIndex).m_aValue;
2000 for(auto &Line : vLineArgs)
2001 {
2002 // Check first colliding line
2003 if(str_comp_nocase(a: pValue, b: Line.m_vArgs[ArgIndex].m_aValue) == 0)
2004 {
2005 Collide = true;
2006 CollidingLineIndex = Line.m_Index;
2007 Error = m_pBackend->ArgConstraint(pSettingName: pSetting->m_pName, Arg: ArgIndex) == CMapSettingsBackend::EArgConstraint::UNIQUE;
2008 }
2009 if(Error)
2010 break;
2011 }
2012
2013 // If we did not collide with any of the lines for that argument, we're good to go
2014 // (or if we had an error)
2015 if(!Collide || Error)
2016 break;
2017
2018 // Otherwise, remove non-colliding args from the list
2019 vLineArgs.erase(
2020 first: std::remove_if(first: vLineArgs.begin(), last: vLineArgs.end(), pred: [&](const SLineArgs &Line) {
2021 return str_comp_nocase(a: pValue, b: Line.m_vArgs[ArgIndex].m_aValue) != 0;
2022 }),
2023 last: vLineArgs.end());
2024 }
2025
2026 // The result is either REPLACE when we found a collision, or ADD
2027 Result = Error ? ECollisionCheckResult::REPLACE : ECollisionCheckResult::ADD;
2028 return CollidingLineIndex;
2029 }
2030
2031 return -1;
2032}
2033
2034bool CMapSettingsBackend::CContext::Valid() const
2035{
2036 // Check if the entire setting is valid or not
2037
2038 // We don't need to check whether a command is valid if we allow unknown commands
2039 if(m_AllowUnknownCommands)
2040 return true;
2041
2042 if(m_CommentOffset == 0 || m_aCommand[0] == '\0')
2043 return true; // A "comment" setting is considered valid.
2044
2045 // Check if command is valid
2046 if(m_pCurrentSetting)
2047 {
2048 // Check if all arguments are valid
2049 const bool ArgumentsValid = std::all_of(first: m_vCurrentArgs.begin(), last: m_vCurrentArgs.end(), pred: [](const SCurrentSettingArg &Arg) {
2050 return !Arg.m_Error;
2051 });
2052
2053 if(!ArgumentsValid)
2054 return false;
2055
2056 // Check that we have the same number of arguments
2057 return m_vCurrentArgs.size() == m_pBackend->m_ParsedCommandArgs.at(k: m_pCurrentSetting).size();
2058 }
2059 else
2060 {
2061 return false;
2062 }
2063}
2064
2065void CMapSettingsBackend::CContext::GetCommandHelpText(char *pStr, int Length) const
2066{
2067 if(!m_pCurrentSetting)
2068 return;
2069
2070 str_copy(dst: pStr, src: m_pCurrentSetting->m_pHelp, dst_size: Length);
2071}
2072
2073template<int N>
2074void CMapSettingsBackend::CContext::FormatDisplayValue(const char *pValue, char (&aOut)[N])
2075{
2076 const int MaxLength = 32;
2077 if(str_length(str: pValue) > MaxLength)
2078 {
2079 str_copy(aOut, pValue, MaxLength);
2080 str_append(aOut, "...");
2081 }
2082 else
2083 {
2084 str_copy(aOut, pValue);
2085 }
2086}
2087
2088void CMapSettingsBackend::OnMapLoad()
2089{
2090 // Load & validate all map settings
2091 m_LoadedMapSettings.Reset();
2092
2093 auto &vLoadedMapSettings = Editor()->m_Map.m_vSettings;
2094
2095 // Keep a vector of valid map settings, to check collision against: m_vValidLoadedMapSettings
2096
2097 // Create a local context with no lineinput, only used to parse the commands
2098 CContext LocalContext = NewContext(pLineInput: nullptr);
2099
2100 // Iterate through map settings
2101 // Two steps:
2102 // 1. Save valid and invalid settings
2103 // 2. Check for duplicates
2104
2105 std::vector<std::tuple<int, bool, CEditorMapSetting>> vSettingsInvalid;
2106
2107 for(int i = 0; i < (int)vLoadedMapSettings.size(); i++)
2108 {
2109 CEditorMapSetting &Setting = vLoadedMapSettings.at(n: i);
2110 // Parse the setting using the context
2111 LocalContext.UpdateFromString(pStr: Setting.m_aCommand);
2112
2113 bool Valid = LocalContext.Valid();
2114 ECollisionCheckResult Result = ECollisionCheckResult::ERROR;
2115 LocalContext.CheckCollision(pInputString: Setting.m_aCommand, vSettings: m_LoadedMapSettings.m_vSettingsValid, Result);
2116
2117 if(Valid && Result == ECollisionCheckResult::ADD)
2118 m_LoadedMapSettings.m_vSettingsValid.emplace_back(args&: Setting);
2119 else
2120 vSettingsInvalid.emplace_back(args&: i, args&: Valid, args&: Setting);
2121
2122 LocalContext.Reset();
2123
2124 // Empty duplicates for this line, might be filled later
2125 m_LoadedMapSettings.m_SettingsDuplicate.insert(x: {i, {}});
2126 }
2127
2128 for(const auto &[Index, Valid, Setting] : vSettingsInvalid)
2129 {
2130 LocalContext.UpdateFromString(pStr: Setting.m_aCommand);
2131
2132 ECollisionCheckResult Result = ECollisionCheckResult::ERROR;
2133 int CollidingLineIndex = LocalContext.CheckCollision(pInputString: Setting.m_aCommand, vSettings: m_LoadedMapSettings.m_vSettingsValid, Result);
2134 int RealCollidingLineIndex = CollidingLineIndex;
2135
2136 if(CollidingLineIndex != -1)
2137 RealCollidingLineIndex = std::find_if(first: vLoadedMapSettings.begin(), last: vLoadedMapSettings.end(), pred: [&](const CEditorMapSetting &MapSetting) {
2138 return str_comp_nocase(a: MapSetting.m_aCommand, b: m_LoadedMapSettings.m_vSettingsValid.at(n: CollidingLineIndex).m_aCommand) == 0;
2139 }) - vLoadedMapSettings.begin();
2140
2141 int Type = 0;
2142 if(!Valid)
2143 Type |= SInvalidSetting::TYPE_INVALID;
2144 if(Result == ECollisionCheckResult::REPLACE)
2145 Type |= SInvalidSetting::TYPE_DUPLICATE;
2146
2147 m_LoadedMapSettings.m_vSettingsInvalid.emplace_back(args: Index, args: Setting.m_aCommand, args&: Type, args&: RealCollidingLineIndex, args: !Valid || !LocalContext.CommandIsValid());
2148 if(Type & SInvalidSetting::TYPE_DUPLICATE)
2149 m_LoadedMapSettings.m_SettingsDuplicate[RealCollidingLineIndex].emplace_back(args: m_LoadedMapSettings.m_vSettingsInvalid.size() - 1);
2150
2151 LocalContext.Reset();
2152 }
2153
2154 if(!m_LoadedMapSettings.m_vSettingsInvalid.empty())
2155 Editor()->m_Dialog = DIALOG_MAPSETTINGS_ERROR;
2156}
2157
2158// ------ loaders
2159
2160void CMapSettingsBackend::InitValueLoaders()
2161{
2162 // Load the different possible values for some specific settings
2163 RegisterLoader(pSettingName: "tune", pfnLoader: SValueLoader::LoadTuneValues);
2164 RegisterLoader(pSettingName: "tune_zone", pfnLoader: SValueLoader::LoadTuneZoneValues);
2165 RegisterLoader(pSettingName: "mapbug", pfnLoader: SValueLoader::LoadMapBugs);
2166}
2167
2168void SValueLoader::LoadTuneValues(const CSettingValuesBuilder &TuneBuilder)
2169{
2170 // Add available tuning names to argument 0 of setting "tune"
2171 LoadArgumentTuneValues(ArgBuilder: TuneBuilder.Argument(Arg: 0));
2172}
2173
2174void SValueLoader::LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder)
2175{
2176 // Add available tuning names to argument 1 of setting "tune_zone"
2177 LoadArgumentTuneValues(ArgBuilder: TuneZoneBuilder.Argument(Arg: 1));
2178}
2179
2180void SValueLoader::LoadMapBugs(const CSettingValuesBuilder &BugBuilder)
2181{
2182 // Get argument 0 of setting "mapbug"
2183 auto ArgBuilder = BugBuilder.Argument(Arg: 0);
2184 // Add available map bugs options
2185 ArgBuilder.Add(pString: "grenade-doubleexplosion@ddnet.tw");
2186}
2187
2188void SValueLoader::LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder)
2189{
2190 // Iterate through available tunings add their name to the list
2191 for(int i = 0; i < CTuningParams::Num(); i++)
2192 {
2193 ArgBuilder.Add(pString: CTuningParams::Name(Index: i));
2194 }
2195}
2196