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