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