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