diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp index 802744d0c0..194a9d00cb 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.cpp @@ -4,6 +4,7 @@ #include "DolphinQt/Config/Mapping/IOWindow.h" +#include #include #include @@ -11,7 +12,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -25,22 +28,174 @@ #include "DolphinQt/QtUtils/BlockUserInputFilter.h" #include "InputCommon/ControlReference/ControlReference.h" +#include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerEmu/ControllerEmu.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" constexpr int SLIDER_TICK_COUNT = 100; +namespace +{ +// TODO: Make sure these functions return colors that will be visible in the current theme. +QTextCharFormat GetSpecialCharFormat() +{ + QTextCharFormat format; + format.setFontWeight(QFont::Weight::Bold); + return format; +} + +QTextCharFormat GetLiteralCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkMagenta}); + return format; +} + +QTextCharFormat GetInvalidCharFormat() +{ + QTextCharFormat format; + format.setUnderlineStyle(QTextCharFormat::WaveUnderline); + format.setUnderlineColor(Qt::darkRed); + return format; +} + +QTextCharFormat GetControlCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkGreen}); + return format; +} + +QTextCharFormat GetVariableCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkYellow}); + return format; +} + +QTextCharFormat GetBarewordCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkBlue}); + return format; +} + +QTextCharFormat GetCommentCharFormat() +{ + QTextCharFormat format; + format.setForeground(QBrush{Qt::darkGray}); + return format; +} +} // namespace + +ControlExpressionSyntaxHighlighter::ControlExpressionSyntaxHighlighter(QTextDocument* parent, + QLineEdit* result) + : QSyntaxHighlighter(parent), m_result_text(result) +{ +} + +void ControlExpressionSyntaxHighlighter::highlightBlock(const QString&) +{ + // TODO: This is going to result in improper highlighting with non-ascii characters: + ciface::ExpressionParser::Lexer lexer(document()->toPlainText().toStdString()); + + std::vector tokens; + const auto tokenize_status = lexer.Tokenize(tokens); + + using ciface::ExpressionParser::TokenType; + + const auto set_block_format = [this](int start, int count, const QTextCharFormat& format) { + if (start + count <= currentBlock().position() || + start >= currentBlock().position() + currentBlock().length()) + { + // This range is not within the current block. + return; + } + + int block_start = start - currentBlock().position(); + + if (block_start < 0) + { + count += block_start; + block_start = 0; + } + + setFormat(block_start, count, format); + }; + + for (auto& token : tokens) + { + std::optional char_format; + + switch (token.type) + { + case TokenType::TOK_INVALID: + char_format = GetInvalidCharFormat(); + break; + case TokenType::TOK_LPAREN: + case TokenType::TOK_RPAREN: + case TokenType::TOK_COMMA: + char_format = GetSpecialCharFormat(); + break; + case TokenType::TOK_LITERAL: + char_format = GetLiteralCharFormat(); + break; + case TokenType::TOK_CONTROL: + char_format = GetControlCharFormat(); + break; + case TokenType::TOK_BAREWORD: + char_format = GetBarewordCharFormat(); + break; + case TokenType::TOK_VARIABLE: + char_format = GetVariableCharFormat(); + break; + case TokenType::TOK_COMMENT: + char_format = GetCommentCharFormat(); + break; + default: + if (token.IsBinaryOperator()) + char_format = GetSpecialCharFormat(); + break; + } + + if (char_format.has_value()) + set_block_format(int(token.string_position), int(token.string_length), *char_format); + } + + // This doesn't need to be run for every "block", but it works. + if (ciface::ExpressionParser::ParseStatus::Successful != tokenize_status) + { + m_result_text->setText(tr("Invalid Token.")); + } + else + { + ciface::ExpressionParser::RemoveInertTokens(&tokens); + const auto parse_status = ciface::ExpressionParser::ParseTokens(tokens); + + m_result_text->setText( + QString::fromStdString(parse_status.description.value_or(_trans("Success.")))); + + if (ciface::ExpressionParser::ParseStatus::Successful != parse_status.status) + { + const auto token = *parse_status.token; + set_block_format(int(token.string_position), int(token.string_length), + GetInvalidCharFormat()); + } + } +} + IOWindow::IOWindow(QWidget* parent, ControllerEmu::EmulatedController* controller, ControlReference* ref, IOWindow::Type type) : QDialog(parent), m_reference(ref), m_controller(controller), m_type(type) { CreateMainLayout(); - ConnectWidgets(); setWindowTitle(type == IOWindow::Type::Input ? tr("Configure Input") : tr("Configure Output")); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ConfigChanged(); + + ConnectWidgets(); } void IOWindow::CreateMainLayout() @@ -51,18 +206,54 @@ void IOWindow::CreateMainLayout() m_option_list = new QListWidget(); m_select_button = new QPushButton(tr("Select")); m_detect_button = new QPushButton(tr("Detect")); - m_or_button = new QPushButton(tr("| OR")); - m_and_button = new QPushButton(tr("&& AND")); - m_add_button = new QPushButton(tr("+ ADD")); - m_not_button = new QPushButton(tr("! NOT")); m_test_button = new QPushButton(tr("Test")); - m_expression_text = new QPlainTextEdit(); m_button_box = new QDialogButtonBox(); m_clear_button = new QPushButton(tr("Clear")); m_apply_button = new QPushButton(tr("Apply")); m_range_slider = new QSlider(Qt::Horizontal); m_range_spinbox = new QSpinBox(); + m_parse_text = new QLineEdit(); + m_parse_text->setReadOnly(true); + + m_expression_text = new QPlainTextEdit(); + m_expression_text->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + new ControlExpressionSyntaxHighlighter(m_expression_text->document(), m_parse_text); + + m_operators_combo = new QComboBox(); + m_operators_combo->addItem(tr("Operators")); + m_operators_combo->insertSeparator(1); + if (m_type == Type::Input) + { + m_operators_combo->addItem(tr("! Not")); + m_operators_combo->addItem(tr("* Multiply")); + m_operators_combo->addItem(tr("/ Divide")); + m_operators_combo->addItem(tr("% Modulo")); + m_operators_combo->addItem(tr("+ Add")); + m_operators_combo->addItem(tr("- Subtract")); + m_operators_combo->addItem(tr("> Greater-than")); + m_operators_combo->addItem(tr("< Less-than")); + m_operators_combo->addItem(tr("& And")); + } + m_operators_combo->addItem(tr("| Or")); + if (m_type == Type::Input) + { + m_operators_combo->addItem(tr(", Comma")); + } + + m_functions_combo = new QComboBox(); + m_functions_combo->addItem(tr("Functions")); + m_functions_combo->insertSeparator(1); + m_functions_combo->addItem(QStringLiteral("if")); + m_functions_combo->addItem(QStringLiteral("timer")); + m_functions_combo->addItem(QStringLiteral("toggle")); + m_functions_combo->addItem(QStringLiteral("deadzone")); + m_functions_combo->addItem(QStringLiteral("smooth")); + m_functions_combo->addItem(QStringLiteral("hold")); + m_functions_combo->addItem(QStringLiteral("tap")); + m_functions_combo->addItem(QStringLiteral("relative")); + m_functions_combo->addItem(QStringLiteral("pulse")); + // Devices m_main_layout->addWidget(m_devices_combo); @@ -77,16 +268,6 @@ void IOWindow::CreateMainLayout() m_range_spinbox->setMaximum(500); m_main_layout->addLayout(range_hbox); - // Options (Buttons, Outputs) and action buttons - // macOS style doesn't support expanding buttons -#ifndef __APPLE__ - for (QPushButton* button : {m_select_button, m_detect_button, m_or_button, m_and_button, - m_add_button, m_not_button, m_test_button}) - { - button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - } -#endif - auto* hbox = new QHBoxLayout(); auto* button_vbox = new QVBoxLayout(); hbox->addWidget(m_option_list, 8); @@ -94,17 +275,15 @@ void IOWindow::CreateMainLayout() button_vbox->addWidget(m_select_button); button_vbox->addWidget(m_type == Type::Input ? m_detect_button : m_test_button); - button_vbox->addWidget(m_or_button); - + button_vbox->addWidget(m_operators_combo); if (m_type == Type::Input) { - button_vbox->addWidget(m_and_button); - button_vbox->addWidget(m_add_button); - button_vbox->addWidget(m_not_button); + button_vbox->addWidget(m_functions_combo); } m_main_layout->addLayout(hbox, 2); m_main_layout->addWidget(m_expression_text, 1); + m_main_layout->addWidget(m_parse_text); // Button Box m_main_layout->addWidget(m_button_box); @@ -132,11 +311,7 @@ void IOWindow::ConfigChanged() void IOWindow::ConnectWidgets() { - connect(m_select_button, &QPushButton::clicked, [this] { AppendSelectedOption(""); }); - connect(m_add_button, &QPushButton::clicked, [this] { AppendSelectedOption(" + "); }); - connect(m_and_button, &QPushButton::clicked, [this] { AppendSelectedOption(" & "); }); - connect(m_or_button, &QPushButton::clicked, [this] { AppendSelectedOption(" | "); }); - connect(m_not_button, &QPushButton::clicked, [this] { AppendSelectedOption("!"); }); + connect(m_select_button, &QPushButton::clicked, [this] { AppendSelectedOption(); }); connect(m_detect_button, &QPushButton::clicked, this, &IOWindow::OnDetectButtonPressed); connect(m_test_button, &QPushButton::clicked, this, &IOWindow::OnTestButtonPressed); @@ -147,17 +322,38 @@ void IOWindow::ConnectWidgets() this, &IOWindow::OnRangeChanged); connect(m_range_slider, static_cast(&QSlider::valueChanged), this, &IOWindow::OnRangeChanged); + + connect(m_expression_text, &QPlainTextEdit::textChanged, [this] { + m_apply_button->setText(m_apply_button->text().remove(QStringLiteral("*"))); + m_apply_button->setText(m_apply_button->text() + QStringLiteral("*")); + }); + + connect(m_operators_combo, QOverload::of(&QComboBox::activated), [this](int index) { + if (0 == index) + return; + + m_expression_text->insertPlainText(m_operators_combo->currentText().left(1)); + + m_operators_combo->setCurrentIndex(0); + }); + + connect(m_functions_combo, QOverload::of(&QComboBox::activated), [this](int index) { + if (0 == index) + return; + + m_expression_text->insertPlainText(m_functions_combo->currentText() + QStringLiteral("()")); + + m_functions_combo->setCurrentIndex(0); + }); } -void IOWindow::AppendSelectedOption(const std::string& prefix) +void IOWindow::AppendSelectedOption() { if (m_option_list->currentItem() == nullptr) return; - m_expression_text->insertPlainText( - QString::fromStdString(prefix) + - MappingCommon::GetExpressionForControl(m_option_list->currentItem()->text(), m_devq, - m_controller->GetDefaultDevice())); + m_expression_text->insertPlainText(MappingCommon::GetExpressionForControl( + m_option_list->currentItem()->text(), m_devq, m_controller->GetDefaultDevice())); } void IOWindow::OnDeviceChanged(const QString& device) @@ -175,7 +371,19 @@ void IOWindow::OnDialogButtonPressed(QAbstractButton* button) } m_reference->SetExpression(m_expression_text->toPlainText().toStdString()); - m_controller->UpdateReferences(g_controller_interface); + m_controller->UpdateSingleControlReference(g_controller_interface, m_reference); + + m_apply_button->setText(m_apply_button->text().remove(QStringLiteral("*"))); + + if (ciface::ExpressionParser::ParseStatus::SyntaxError == m_reference->GetParseStatus()) + { + QMessageBox error(this); + error.setIcon(QMessageBox::Critical); + error.setWindowTitle(tr("Error")); + error.setText(tr("The expression contains a syntax error.")); + error.setWindowModality(Qt::WindowModal); + error.exec(); + } if (button != m_apply_button) accept(); diff --git a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h index 5921b14424..1d8a3f4f2f 100644 --- a/Source/Core/DolphinQt/Config/Mapping/IOWindow.h +++ b/Source/Core/DolphinQt/Config/Mapping/IOWindow.h @@ -6,6 +6,7 @@ #include #include +#include #include "Common/Flag.h" #include "InputCommon/ControllerInterface/Device.h" @@ -14,6 +15,7 @@ class ControlReference; class QAbstractButton; class QComboBox; class QDialogButtonBox; +class QLineEdit; class QListWidget; class QVBoxLayout; class QWidget; @@ -27,6 +29,19 @@ namespace ControllerEmu class EmulatedController; } +class ControlExpressionSyntaxHighlighter final : public QSyntaxHighlighter +{ + Q_OBJECT +public: + ControlExpressionSyntaxHighlighter(QTextDocument* parent, QLineEdit* result); + +protected: + void highlightBlock(const QString& text) final override; + +private: + QLineEdit* const m_result_text; +}; + class IOWindow final : public QDialog { Q_OBJECT @@ -51,7 +66,7 @@ private: void OnTestButtonPressed(); void OnRangeChanged(int range); - void AppendSelectedOption(const std::string& prefix); + void AppendSelectedOption(); void UpdateOptionList(); void UpdateDeviceList(); @@ -70,19 +85,18 @@ private: // Shared actions QPushButton* m_select_button; - QPushButton* m_or_button; + QComboBox* m_operators_combo; // Input actions QPushButton* m_detect_button; - QPushButton* m_and_button; - QPushButton* m_not_button; - QPushButton* m_add_button; + QComboBox* m_functions_combo; // Output actions QPushButton* m_test_button; // Textarea QPlainTextEdit* m_expression_text; + QLineEdit* m_parse_text; // Buttonbox QDialogButtonBox* m_button_box; diff --git a/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp b/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp index 435033c8c1..a9a142276f 100644 --- a/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp +++ b/Source/Core/DolphinQt/Config/Mapping/MappingButton.cpp @@ -100,7 +100,7 @@ void MappingButton::Detect() return; m_reference->SetExpression(expression.toStdString()); - m_parent->GetController()->UpdateReferences(g_controller_interface); + m_parent->GetController()->UpdateSingleControlReference(g_controller_interface, m_reference); ConfigChanged(); m_parent->SaveSettings(); @@ -111,7 +111,7 @@ void MappingButton::Clear() m_reference->range = 100.0 / SLIDER_TICK_COUNT; m_reference->SetExpression(""); - m_parent->GetController()->UpdateReferences(g_controller_interface); + m_parent->GetController()->UpdateSingleControlReference(g_controller_interface, m_reference); m_parent->SaveSettings(); ConfigChanged(); diff --git a/Source/Core/InputCommon/CMakeLists.txt b/Source/Core/InputCommon/CMakeLists.txt index df8fa6423c..b47503a9ae 100644 --- a/Source/Core/InputCommon/CMakeLists.txt +++ b/Source/Core/InputCommon/CMakeLists.txt @@ -45,6 +45,8 @@ add_library(inputcommon ControlReference/ControlReference.h ControlReference/ExpressionParser.cpp ControlReference/ExpressionParser.h + ControlReference/FunctionExpression.cpp + ControlReference/FunctionExpression.h ) target_link_libraries(inputcommon PUBLIC diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.cpp b/Source/Core/InputCommon/ControlReference/ControlReference.cpp index cfc6fa658e..af7536547c 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.cpp +++ b/Source/Core/InputCommon/ControlReference/ControlReference.cpp @@ -25,12 +25,12 @@ bool ControlReference::InputGateOn() // Updates a controlreference's binded devices/controls // need to call this to re-bind a control reference after changing its expression // -void ControlReference::UpdateReference(const ciface::Core::DeviceContainer& devices, - const ciface::Core::DeviceQualifier& default_device) +void ControlReference::UpdateReference(ciface::ExpressionParser::ControlEnvironment& env) { - ControlFinder finder(devices, default_device, IsInput()); if (m_parsed_expression) - m_parsed_expression->UpdateReferences(finder); + { + m_parsed_expression->UpdateReferences(env); + } } int ControlReference::BoundCount() const @@ -54,7 +54,9 @@ std::string ControlReference::GetExpression() const void ControlReference::SetExpression(std::string expr) { m_expression = std::move(expr); - std::tie(m_parse_status, m_parsed_expression) = ParseExpression(m_expression); + auto parse_result = ParseExpression(m_expression); + m_parse_status = parse_result.status; + m_parsed_expression = std::move(parse_result.expr); } ControlReference::ControlReference() : range(1), m_parsed_expression(nullptr) diff --git a/Source/Core/InputCommon/ControlReference/ControlReference.h b/Source/Core/InputCommon/ControlReference/ControlReference.h index 83fa288676..3f2c205501 100644 --- a/Source/Core/InputCommon/ControlReference/ControlReference.h +++ b/Source/Core/InputCommon/ControlReference/ControlReference.h @@ -30,8 +30,7 @@ public: int BoundCount() const; ciface::ExpressionParser::ParseStatus GetParseStatus() const; - void UpdateReference(const ciface::Core::DeviceContainer& devices, - const ciface::Core::DeviceQualifier& default_device); + void UpdateReference(ciface::ExpressionParser::ControlEnvironment& env); std::string GetExpression() const; void SetExpression(std::string expr); diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp index c864b22a20..00afc5e036 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.cpp @@ -2,222 +2,201 @@ // Licensed under GPLv2+ // Refer to the license.txt file included. -#include #include +#include #include -#include #include +#include #include +#include #include +#include "Common/Common.h" #include "Common/StringUtil.h" + #include "InputCommon/ControlReference/ExpressionParser.h" +#include "InputCommon/ControlReference/FunctionExpression.h" namespace ciface::ExpressionParser { using namespace ciface::Core; -enum TokenType +Token::Token(TokenType type_) : type(type_) { - TOK_DISCARD, - TOK_INVALID, - TOK_EOF, - TOK_LPAREN, - TOK_RPAREN, - TOK_AND, - TOK_OR, - TOK_NOT, - TOK_ADD, - TOK_CONTROL, -}; +} -inline std::string OpName(TokenType op) +Token::Token(TokenType type_, std::string data_) : type(type_), data(std::move(data_)) { - switch (op) +} + +bool Token::IsBinaryOperator() const +{ + return type >= TOK_BINARY_OPS_BEGIN && type < TOK_BINARY_OPS_END; +} + +Lexer::Lexer(std::string expr_) : expr(std::move(expr_)) +{ + it = expr.begin(); +} + +std::string Lexer::FetchDelimString(char delim) +{ + const std::string result = FetchCharsWhile([delim](char c) { return c != delim; }); + if (it != expr.end()) + ++it; + return result; +} + +std::string Lexer::FetchWordChars() +{ + // Valid word characters: + std::regex rx(R"([a-z\d_])", std::regex_constants::icase); + + return FetchCharsWhile([&rx](char c) { return std::regex_match(std::string(1, c), rx); }); +} + +Token Lexer::GetDelimitedLiteral() +{ + return Token(TOK_LITERAL, FetchDelimString('\'')); +} + +Token Lexer::GetVariable() +{ + return Token(TOK_VARIABLE, FetchWordChars()); +} + +Token Lexer::GetFullyQualifiedControl() +{ + return Token(TOK_CONTROL, FetchDelimString('`')); +} + +Token Lexer::GetBareword(char first_char) +{ + return Token(TOK_BAREWORD, first_char + FetchWordChars()); +} + +Token Lexer::GetRealLiteral(char first_char) +{ + std::string value; + value += first_char; + value += FetchCharsWhile([](char c) { return isdigit(c, std::locale::classic()) || ('.' == c); }); + + if (std::regex_match(value, std::regex(R"(\d+(\.\d+)?)"))) + return Token(TOK_LITERAL, value); + + return Token(TOK_INVALID); +} + +Token Lexer::PeekToken() +{ + const auto old_it = it; + const auto tok = NextToken(); + it = old_it; + return tok; +} + +Token Lexer::NextToken() +{ + if (it == expr.end()) + return Token(TOK_EOF); + + char c = *it++; + switch (c) { - case TOK_AND: - return "And"; - case TOK_OR: - return "Or"; - case TOK_NOT: - return "Not"; - case TOK_ADD: - return "Add"; + case ' ': + case '\t': + case '\n': + case '\r': + return Token(TOK_WHITESPACE); + case '(': + return Token(TOK_LPAREN); + case ')': + return Token(TOK_RPAREN); + case '&': + return Token(TOK_AND); + case '|': + return Token(TOK_OR); + case '!': + return Token(TOK_NOT); + case '+': + return Token(TOK_ADD); + case '-': + return Token(TOK_SUB); + case '*': + return Token(TOK_MUL); + case '/': + return Token(TOK_DIV); + case '%': + return Token(TOK_MOD); + case '=': + return Token(TOK_ASSIGN); + case '<': + return Token(TOK_LTHAN); + case '>': + return Token(TOK_GTHAN); + case ',': + return Token(TOK_COMMA); + case '\'': + return GetDelimitedLiteral(); + case '$': + return GetVariable(); + case '`': + return GetFullyQualifiedControl(); default: - assert(false); - return ""; + if (isalpha(c, std::locale::classic())) + return GetBareword(c); + else if (isdigit(c, std::locale::classic())) + return GetRealLiteral(c); + else + return Token(TOK_INVALID); } } -class Token +ParseStatus Lexer::Tokenize(std::vector& tokens) { -public: - TokenType type; - ControlQualifier qualifier; - - Token(TokenType type_) : type(type_) {} - Token(TokenType type_, ControlQualifier qualifier_) : type(type_), qualifier(qualifier_) {} - operator std::string() const + while (true) { - switch (type) + const std::size_t string_position = it - expr.begin(); + Token tok = NextToken(); + + tok.string_position = string_position; + tok.string_length = it - expr.begin(); + + // Handle /* */ style comments. + if (tok.type == TOK_DIV && PeekToken().type == TOK_MUL) { - case TOK_DISCARD: - return "Discard"; - case TOK_EOF: - return "EOF"; - case TOK_LPAREN: - return "("; - case TOK_RPAREN: - return ")"; - case TOK_AND: - return "&"; - case TOK_OR: - return "|"; - case TOK_NOT: - return "!"; - case TOK_ADD: - return "+"; - case TOK_CONTROL: - return "Device(" + (std::string)qualifier + ")"; - case TOK_INVALID: - break; - } + const auto end_of_comment = expr.find("*/", it - expr.begin()); - return "Invalid"; - } -}; - -class Lexer -{ -public: - std::string expr; - std::string::iterator it; - - Lexer(const std::string& expr_) : expr(expr_) { it = expr.begin(); } - bool FetchBacktickString(std::string& value, char otherDelim = 0) - { - value = ""; - while (it != expr.end()) - { - char c = *it; - ++it; - if (c == '`') - return false; - if (c > 0 && c == otherDelim) - return true; - value += c; - } - return false; - } - - Token GetFullyQualifiedControl() - { - ControlQualifier qualifier; - std::string value; - - if (FetchBacktickString(value, ':')) - { - // Found colon, this is the device name - qualifier.has_device = true; - qualifier.device_qualifier.FromString(value); - FetchBacktickString(value); - } - - qualifier.control_name = value; - - return Token(TOK_CONTROL, qualifier); - } - - Token GetBarewordsControl(char c) - { - std::string name; - name += c; - - while (it != expr.end()) - { - c = *it; - if (!isalpha(c)) - break; - name += c; - ++it; - } - - ControlQualifier qualifier; - qualifier.control_name = name; - return Token(TOK_CONTROL, qualifier); - } - - Token NextToken() - { - if (it == expr.end()) - return Token(TOK_EOF); - - char c = *it++; - switch (c) - { - case ' ': - case '\t': - case '\n': - case '\r': - return Token(TOK_DISCARD); - case '(': - return Token(TOK_LPAREN); - case ')': - return Token(TOK_RPAREN); - case '&': - return Token(TOK_AND); - case '|': - return Token(TOK_OR); - case '!': - return Token(TOK_NOT); - case '+': - return Token(TOK_ADD); - case '`': - return GetFullyQualifiedControl(); - default: - if (isalpha(c)) - return GetBarewordsControl(c); - else - return Token(TOK_INVALID); - } - } - - ParseStatus Tokenize(std::vector& tokens) - { - while (true) - { - Token tok = NextToken(); - - if (tok.type == TOK_DISCARD) - continue; - - if (tok.type == TOK_INVALID) - { - tokens.clear(); + if (end_of_comment == std::string::npos) return ParseStatus::SyntaxError; - } - tokens.push_back(tok); + tok.type = TOK_COMMENT; + tok.string_length = end_of_comment + 4; - if (tok.type == TOK_EOF) - break; + it = expr.begin() + end_of_comment + 2; } - return ParseStatus::Successful; + + tokens.push_back(tok); + + if (tok.type == TOK_INVALID) + return ParseStatus::SyntaxError; + + if (tok.type == TOK_EOF) + break; } -}; + return ParseStatus::Successful; +} class ControlExpression : public Expression { public: - ControlQualifier qualifier; - Device::Control* control = nullptr; - // Keep a shared_ptr to the device so the control pointer doesn't become invalid + // Keep a shared_ptr to the device so the control pointer doesn't become invalid. std::shared_ptr m_device; explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {} ControlState GetValue() const override { - if (!control) + if (!input) return 0.0; // Note: Inputs may return negative values in situations where opposing directions are @@ -226,20 +205,25 @@ public: // FYI: Clamping values greater than 1.0 is purposely not done to support unbounded values in // the future. (e.g. raw accelerometer/gyro data) - return std::max(0.0, control->ToInput()->GetState()); + return std::max(0.0, input->GetState()); } void SetValue(ControlState value) override { - if (control) - control->ToOutput()->SetState(value); + if (output) + output->SetState(value); } - int CountNumControls() const override { return control ? 1 : 0; } - void UpdateReferences(ControlFinder& finder) override + int CountNumControls() const override { return (input || output) ? 1 : 0; } + void UpdateReferences(ControlEnvironment& env) override { - m_device = finder.FindDevice(qualifier); - control = finder.FindControl(qualifier); + m_device = env.FindDevice(qualifier); + input = env.FindInput(qualifier); + output = env.FindOutput(qualifier); } - operator std::string() const override { return "`" + static_cast(qualifier) + "`"; } + +private: + ControlQualifier qualifier; + Device::Input* input = nullptr; + Device::Output* output = nullptr; }; class BinaryExpression : public Expression @@ -257,16 +241,43 @@ public: ControlState GetValue() const override { - ControlState lhsValue = lhs->GetValue(); - ControlState rhsValue = rhs->GetValue(); switch (op) { case TOK_AND: - return std::min(lhsValue, rhsValue); + return std::min(lhs->GetValue(), rhs->GetValue()); case TOK_OR: - return std::max(lhsValue, rhsValue); + return std::max(lhs->GetValue(), rhs->GetValue()); case TOK_ADD: - return std::min(lhsValue + rhsValue, 1.0); + return lhs->GetValue() + rhs->GetValue(); + case TOK_SUB: + return lhs->GetValue() - rhs->GetValue(); + case TOK_MUL: + return lhs->GetValue() * rhs->GetValue(); + case TOK_DIV: + { + const ControlState result = lhs->GetValue() / rhs->GetValue(); + return std::isinf(result) ? 0.0 : result; + } + case TOK_MOD: + { + const ControlState result = std::fmod(lhs->GetValue(), rhs->GetValue()); + return std::isnan(result) ? 0.0 : result; + } + case TOK_ASSIGN: + { + lhs->SetValue(rhs->GetValue()); + return lhs->GetValue(); + } + case TOK_LTHAN: + return lhs->GetValue() < rhs->GetValue(); + case TOK_GTHAN: + return lhs->GetValue() > rhs->GetValue(); + case TOK_COMMA: + { + // Eval and discard lhs: + lhs->GetValue(); + return rhs->GetValue(); + } default: assert(false); return 0; @@ -286,57 +297,73 @@ public: return lhs->CountNumControls() + rhs->CountNumControls(); } - void UpdateReferences(ControlFinder& finder) override + void UpdateReferences(ControlEnvironment& env) override { - lhs->UpdateReferences(finder); - rhs->UpdateReferences(finder); - } - - operator std::string() const override - { - return OpName(op) + "(" + (std::string)(*lhs) + ", " + (std::string)(*rhs) + ")"; + lhs->UpdateReferences(env); + rhs->UpdateReferences(env); } }; -class UnaryExpression : public Expression +class LiteralExpression : public Expression { public: - TokenType op; - std::unique_ptr inner; - - UnaryExpression(TokenType op_, std::unique_ptr&& inner_) - : op(op_), inner(std::move(inner_)) + void SetValue(ControlState) override { - } - ControlState GetValue() const override - { - ControlState value = inner->GetValue(); - switch (op) - { - case TOK_NOT: - return 1.0 - value; - default: - assert(false); - return 0; - } + // Do nothing. } - void SetValue(ControlState value) override - { - switch (op) - { - case TOK_NOT: - inner->SetValue(1.0 - value); - break; + int CountNumControls() const override { return 1; } - default: - assert(false); - } + void UpdateReferences(ControlEnvironment&) override + { + // Nothing needed. } - int CountNumControls() const override { return inner->CountNumControls(); } - void UpdateReferences(ControlFinder& finder) override { inner->UpdateReferences(finder); } - operator std::string() const override { return OpName(op) + "(" + (std::string)(*inner) + ")"; } +protected: + virtual std::string GetName() const = 0; +}; + +class LiteralReal : public LiteralExpression +{ +public: + LiteralReal(ControlState value) : m_value(value) {} + + ControlState GetValue() const override { return m_value; } + + std::string GetName() const override { return ValueToString(m_value); } + +private: + const ControlState m_value{}; +}; + +ParseResult MakeLiteralExpression(Token token) +{ + ControlState val{}; + if (TryParse(token.data, &val)) + return ParseResult::MakeSuccessfulResult(std::make_unique(val)); + else + return ParseResult::MakeErrorResult(token, _trans("Invalid literal.")); +} + +class VariableExpression : public Expression +{ +public: + VariableExpression(std::string name) : m_name(name) {} + + ControlState GetValue() const override { return *m_value_ptr; } + + void SetValue(ControlState value) override { *m_value_ptr = value; } + + int CountNumControls() const override { return 1; } + + void UpdateReferences(ControlEnvironment& env) override + { + m_value_ptr = env.GetVariablePtr(m_name); + } + +protected: + const std::string m_name; + ControlState* m_value_ptr{}; }; // This class proxies all methods to its either left-hand child if it has bound controls, or its @@ -353,16 +380,10 @@ public: void SetValue(ControlState value) override { GetActiveChild()->SetValue(value); } int CountNumControls() const override { return GetActiveChild()->CountNumControls(); } - operator std::string() const override + void UpdateReferences(ControlEnvironment& env) override { - return "Coalesce(" + static_cast(*m_lhs) + ", " + - static_cast(*m_rhs) + ')'; - } - - void UpdateReferences(ControlFinder& finder) override - { - m_lhs->UpdateReferences(finder); - m_rhs->UpdateReferences(finder); + m_lhs->UpdateReferences(env); + m_rhs->UpdateReferences(env); } private: @@ -375,7 +396,7 @@ private: std::unique_ptr m_rhs; }; -std::shared_ptr ControlFinder::FindDevice(ControlQualifier qualifier) const +std::shared_ptr ControlEnvironment::FindDevice(ControlQualifier qualifier) const { if (qualifier.has_device) return container.FindDevice(qualifier.device_qualifier); @@ -383,150 +404,303 @@ std::shared_ptr ControlFinder::FindDevice(ControlQualifier qualifier) co return container.FindDevice(default_device); } -Device::Control* ControlFinder::FindControl(ControlQualifier qualifier) const +Device::Input* ControlEnvironment::FindInput(ControlQualifier qualifier) const { const std::shared_ptr device = FindDevice(qualifier); if (!device) return nullptr; - if (is_input) - return device->FindInput(qualifier.control_name); - else - return device->FindOutput(qualifier.control_name); + return device->FindInput(qualifier.control_name); } -struct ParseResult +Device::Output* ControlEnvironment::FindOutput(ControlQualifier qualifier) const { - ParseResult(ParseStatus status_, std::unique_ptr&& expr_ = {}) - : status(status_), expr(std::move(expr_)) - { - } + const std::shared_ptr device = FindDevice(qualifier); + if (!device) + return nullptr; - ParseStatus status; - std::unique_ptr expr; -}; + return device->FindOutput(qualifier.control_name); +} + +ControlState* ControlEnvironment::GetVariablePtr(const std::string& name) +{ + return &m_variables[name]; +} + +ParseResult ParseResult::MakeEmptyResult() +{ + ParseResult result; + result.status = ParseStatus::EmptyExpression; + return result; +} + +ParseResult ParseResult::MakeSuccessfulResult(std::unique_ptr&& expr) +{ + ParseResult result; + result.status = ParseStatus::Successful; + result.expr = std::move(expr); + return result; +} + +ParseResult ParseResult::MakeErrorResult(Token token, std::string description) +{ + ParseResult result; + result.status = ParseStatus::SyntaxError; + result.token = std::move(token); + result.description = std::move(description); + return result; +} class Parser { public: - explicit Parser(std::vector tokens_) : tokens(tokens_) { m_it = tokens.begin(); } - ParseResult Parse() { return Toplevel(); } + explicit Parser(const std::vector& tokens_) : tokens(tokens_) { m_it = tokens.begin(); } + ParseResult Parse() + { + ParseResult result = ParseToplevel(); + + if (ParseStatus::Successful != result.status) + return result; + + if (Peek().type == TOK_EOF) + return result; + + return ParseResult::MakeErrorResult(Peek(), _trans("Expected EOF.")); + } private: - std::vector tokens; - std::vector::iterator m_it; + const std::vector& tokens; + std::vector::const_iterator m_it; + + Token Chew() + { + const Token tok = Peek(); + if (TOK_EOF != tok.type) + ++m_it; + return tok; + } - Token Chew() { return *m_it++; } Token Peek() { return *m_it; } + bool Expects(TokenType type) { Token tok = Chew(); return tok.type == type; } - ParseResult Atom() + ParseResult ParseFunctionArguments(const std::string_view& func_name, + std::unique_ptr&& func, + const Token& func_tok) + { + std::vector> args; + + if (TOK_LPAREN != Peek().type) + { + // Single argument with no parens (useful for unary ! function) + const auto tok = Chew(); + auto arg = ParseAtom(tok); + if (ParseStatus::Successful != arg.status) + return arg; + + args.emplace_back(std::move(arg.expr)); + } + else + { + // Chew the L-Paren + Chew(); + + // Check for empty argument list: + if (TOK_RPAREN == Peek().type) + { + Chew(); + } + else + { + while (true) + { + // Read one argument. + // Grab an expression, but stop at comma. + auto arg = ParseBinary(BinaryOperatorPrecedence(TOK_COMMA)); + if (ParseStatus::Successful != arg.status) + return arg; + + args.emplace_back(std::move(arg.expr)); + + // Right paren is the end of our arguments. + const Token tok = Chew(); + if (TOK_RPAREN == tok.type) + break; + + // Comma before the next argument. + if (TOK_COMMA != tok.type) + return ParseResult::MakeErrorResult(tok, _trans("Expected comma.")); + }; + } + } + + const auto argument_validation = func->SetArguments(std::move(args)); + + if (std::holds_alternative(argument_validation)) + { + const auto text = std::string(func_name) + '(' + + std::get(argument_validation).text + + ')'; + + return ParseResult::MakeErrorResult(func_tok, _trans("Expected arguments: " + text)); + } + + return ParseResult::MakeSuccessfulResult(std::move(func)); + } + + ParseResult ParseAtom(const Token& tok) { - Token tok = Chew(); switch (tok.type) { - case TOK_CONTROL: - return {ParseStatus::Successful, std::make_unique(tok.qualifier)}; - case TOK_LPAREN: - return Paren(); - default: - return {ParseStatus::SyntaxError}; - } - } - - bool IsUnaryExpression(TokenType type) - { - switch (type) + case TOK_BAREWORD: { - case TOK_NOT: - return true; - default: - return false; - } - } + auto func = MakeFunctionExpression(tok.data); - ParseResult Unary() - { - if (IsUnaryExpression(Peek().type)) - { - Token tok = Chew(); - ParseResult result = Atom(); - if (result.status == ParseStatus::SyntaxError) - return result; - return {ParseStatus::Successful, - std::make_unique(tok.type, std::move(result.expr))}; - } - - return Atom(); - } - - bool IsBinaryToken(TokenType type) - { - switch (type) - { - case TOK_AND: - case TOK_OR: - case TOK_ADD: - return true; - default: - return false; - } - } - - ParseResult Binary() - { - ParseResult result = Unary(); - if (result.status == ParseStatus::SyntaxError) - return result; - - std::unique_ptr expr = std::move(result.expr); - while (IsBinaryToken(Peek().type)) - { - Token tok = Chew(); - ParseResult unary_result = Unary(); - if (unary_result.status == ParseStatus::SyntaxError) + if (!func) { - return unary_result; + // Invalid function, interpret this as a bareword control. + Token control_tok(tok); + control_tok.type = TOK_CONTROL; + return ParseAtom(control_tok); } - expr = std::make_unique(tok.type, std::move(expr), - std::move(unary_result.expr)); + return ParseFunctionArguments(tok.data, std::move(func), tok); + } + case TOK_CONTROL: + { + ControlQualifier cq; + cq.FromString(tok.data); + return ParseResult::MakeSuccessfulResult(std::make_unique(cq)); + } + case TOK_NOT: + { + return ParseFunctionArguments("not", MakeFunctionExpression("not"), tok); + } + case TOK_LITERAL: + { + return MakeLiteralExpression(tok); + } + case TOK_VARIABLE: + { + return ParseResult::MakeSuccessfulResult(std::make_unique(tok.data)); + } + case TOK_LPAREN: + { + return ParseParens(); + } + case TOK_SUB: + { + // An atom was expected but we got a subtraction symbol. + // Interpret it as a unary minus function. + return ParseFunctionArguments("minus", MakeFunctionExpression("minus"), tok); + } + default: + { + return ParseResult::MakeErrorResult(tok, _trans("Expected start of expression.")); + } } - - return {ParseStatus::Successful, std::move(expr)}; } - ParseResult Paren() + static int BinaryOperatorPrecedence(TokenType type) + { + switch (type) + { + case TOK_MUL: + case TOK_DIV: + case TOK_MOD: + return 1; + case TOK_ADD: + case TOK_SUB: + return 2; + case TOK_GTHAN: + case TOK_LTHAN: + return 3; + case TOK_AND: + return 4; + case TOK_OR: + return 5; + case TOK_ASSIGN: + return 6; + case TOK_COMMA: + return 7; + default: + assert(false); + return 0; + } + } + + ParseResult ParseBinary(int precedence = 999) + { + ParseResult lhs = ParseAtom(Chew()); + + if (lhs.status == ParseStatus::SyntaxError) + return lhs; + + std::unique_ptr expr = std::move(lhs.expr); + + // TODO: handle LTR/RTL associativity? + while (Peek().IsBinaryOperator() && BinaryOperatorPrecedence(Peek().type) < precedence) + { + const Token tok = Chew(); + ParseResult rhs = ParseBinary(BinaryOperatorPrecedence(tok.type)); + if (rhs.status == ParseStatus::SyntaxError) + { + return rhs; + } + + expr = std::make_unique(tok.type, std::move(expr), std::move(rhs.expr)); + } + + return ParseResult::MakeSuccessfulResult(std::move(expr)); + } + + ParseResult ParseParens() { // lparen already chewed - ParseResult result = Toplevel(); + ParseResult result = ParseToplevel(); if (result.status != ParseStatus::Successful) return result; - if (!Expects(TOK_RPAREN)) + const auto rparen = Chew(); + if (rparen.type != TOK_RPAREN) { - return {ParseStatus::SyntaxError}; + return ParseResult::MakeErrorResult(rparen, _trans("Expected closing paren.")); } return result; } - ParseResult Toplevel() { return Binary(); } -}; + ParseResult ParseToplevel() { return ParseBinary(); } +}; // namespace ExpressionParser + +ParseResult ParseTokens(const std::vector& tokens) +{ + return Parser(tokens).Parse(); +} static ParseResult ParseComplexExpression(const std::string& str) { Lexer l(str); std::vector tokens; - ParseStatus tokenize_status = l.Tokenize(tokens); + const ParseStatus tokenize_status = l.Tokenize(tokens); if (tokenize_status != ParseStatus::Successful) - return {tokenize_status}; + return ParseResult::MakeErrorResult(Token(TOK_INVALID), _trans("Tokenizing failed.")); - return Parser(std::move(tokens)).Parse(); + RemoveInertTokens(&tokens); + return ParseTokens(tokens); +} + +void RemoveInertTokens(std::vector* tokens) +{ + tokens->erase(std::remove_if(tokens->begin(), tokens->end(), + [](const Token& tok) { + return tok.type == TOK_COMMENT || tok.type == TOK_WHITESPACE; + }), + tokens->end()); } static std::unique_ptr ParseBarewordExpression(const std::string& str) @@ -538,21 +712,24 @@ static std::unique_ptr ParseBarewordExpression(const std::string& st return std::make_unique(qualifier); } -std::pair> ParseExpression(const std::string& str) +ParseResult ParseExpression(const std::string& str) { if (StripSpaces(str).empty()) - return std::make_pair(ParseStatus::EmptyExpression, nullptr); + return ParseResult::MakeEmptyResult(); auto bareword_expr = ParseBarewordExpression(str); ParseResult complex_result = ParseComplexExpression(str); if (complex_result.status != ParseStatus::Successful) { - return std::make_pair(complex_result.status, std::move(bareword_expr)); + // This is a bit odd. + // Return the error status of the complex expression with the fallback barewords expression. + complex_result.expr = std::move(bareword_expr); + return complex_result; } - auto combined_expr = std::make_unique(std::move(bareword_expr), - std::move(complex_result.expr)); - return std::make_pair(complex_result.status, std::move(combined_expr)); + complex_result.expr = std::make_unique(std::move(bareword_expr), + std::move(complex_result.expr)); + return complex_result; } } // namespace ciface::ExpressionParser diff --git a/Source/Core/InputCommon/ControlReference/ExpressionParser.h b/Source/Core/InputCommon/ControlReference/ExpressionParser.h index 56c0340f49..cab6096c0e 100644 --- a/Source/Core/InputCommon/ControlReference/ExpressionParser.h +++ b/Source/Core/InputCommon/ControlReference/ExpressionParser.h @@ -4,56 +4,58 @@ #pragma once +#include #include +#include #include -#include + #include "InputCommon/ControllerInterface/Device.h" namespace ciface::ExpressionParser { -class ControlQualifier +enum TokenType { -public: - bool has_device; - Core::DeviceQualifier device_qualifier; - std::string control_name; - - ControlQualifier() : has_device(false) {} - operator std::string() const - { - if (has_device) - return device_qualifier.ToString() + ":" + control_name; - else - return control_name; - } + TOK_WHITESPACE, + TOK_INVALID, + TOK_EOF, + TOK_LPAREN, + TOK_RPAREN, + TOK_NOT, + TOK_CONTROL, + TOK_LITERAL, + TOK_VARIABLE, + TOK_BAREWORD, + TOK_COMMENT, + // Binary Ops: + TOK_BINARY_OPS_BEGIN, + TOK_AND = TOK_BINARY_OPS_BEGIN, + TOK_OR, + TOK_ADD, + TOK_SUB, + TOK_MUL, + TOK_DIV, + TOK_MOD, + TOK_ASSIGN, + TOK_LTHAN, + TOK_GTHAN, + TOK_COMMA, + TOK_BINARY_OPS_END, }; -class ControlFinder +class Token { public: - ControlFinder(const Core::DeviceContainer& container_, const Core::DeviceQualifier& default_, - const bool is_input_) - : container(container_), default_device(default_), is_input(is_input_) - { - } - std::shared_ptr FindDevice(ControlQualifier qualifier) const; - Core::Device::Control* FindControl(ControlQualifier qualifier) const; + TokenType type; + std::string data; -private: - const Core::DeviceContainer& container; - const Core::DeviceQualifier& default_device; - bool is_input; -}; + // Position in the input string: + std::size_t string_position = 0; + std::size_t string_length = 0; -class Expression -{ -public: - virtual ~Expression() = default; - virtual ControlState GetValue() const = 0; - virtual void SetValue(ControlState state) = 0; - virtual int CountNumControls() const = 0; - virtual void UpdateReferences(ControlFinder& finder) = 0; - virtual operator std::string() const = 0; + explicit Token(TokenType type_); + Token(TokenType type_, std::string data_); + + bool IsBinaryOperator() const; }; enum class ParseStatus @@ -63,5 +65,129 @@ enum class ParseStatus EmptyExpression, }; -std::pair> ParseExpression(const std::string& expr); +class Lexer +{ +public: + std::string expr; + std::string::iterator it; + + explicit Lexer(std::string expr_); + + ParseStatus Tokenize(std::vector& tokens); + +private: + template + std::string FetchCharsWhile(F&& func) + { + std::string value; + while (it != expr.end() && func(*it)) + { + value += *it; + ++it; + } + return value; + } + + std::string FetchDelimString(char delim); + std::string FetchWordChars(); + Token GetDelimitedLiteral(); + Token GetVariable(); + Token GetFullyQualifiedControl(); + Token GetBareword(char c); + Token GetRealLiteral(char c); + + Token PeekToken(); + Token NextToken(); +}; + +class ControlQualifier +{ +public: + bool has_device; + Core::DeviceQualifier device_qualifier; + std::string control_name; + + ControlQualifier() : has_device(false) {} + + operator std::string() const + { + if (has_device) + return device_qualifier.ToString() + ":" + control_name; + else + return control_name; + } + + void FromString(const std::string& str) + { + const auto col_pos = str.find_last_of(':'); + + has_device = (str.npos != col_pos); + if (has_device) + { + device_qualifier.FromString(str.substr(0, col_pos)); + control_name = str.substr(col_pos + 1); + } + else + { + device_qualifier.FromString(""); + control_name = str; + } + } +}; + +class ControlEnvironment +{ +public: + using VariableContainer = std::map; + + ControlEnvironment(const Core::DeviceContainer& container_, const Core::DeviceQualifier& default_, + VariableContainer& vars) + : m_variables(vars), container(container_), default_device(default_) + { + } + + std::shared_ptr FindDevice(ControlQualifier qualifier) const; + Core::Device::Input* FindInput(ControlQualifier qualifier) const; + Core::Device::Output* FindOutput(ControlQualifier qualifier) const; + ControlState* GetVariablePtr(const std::string& name); + +private: + VariableContainer& m_variables; + const Core::DeviceContainer& container; + const Core::DeviceQualifier& default_device; +}; + +class Expression +{ +public: + virtual ~Expression() = default; + virtual ControlState GetValue() const = 0; + virtual void SetValue(ControlState state) = 0; + virtual int CountNumControls() const = 0; + virtual void UpdateReferences(ControlEnvironment& finder) = 0; +}; + +class ParseResult +{ +public: + static ParseResult MakeEmptyResult(); + static ParseResult MakeSuccessfulResult(std::unique_ptr&& expr); + static ParseResult MakeErrorResult(Token token, std::string description); + + ParseStatus status; + std::unique_ptr expr; + + // Used for parse errors: + // TODO: This should probably be moved elsewhere: + std::optional token; + std::optional description; + +private: + ParseResult() = default; +}; + +ParseResult ParseExpression(const std::string& expr); +ParseResult ParseTokens(const std::vector& tokens); +void RemoveInertTokens(std::vector* tokens); + } // namespace ciface::ExpressionParser diff --git a/Source/Core/InputCommon/ControlReference/FunctionExpression.cpp b/Source/Core/InputCommon/ControlReference/FunctionExpression.cpp new file mode 100644 index 0000000000..18b98c5e19 --- /dev/null +++ b/Source/Core/InputCommon/ControlReference/FunctionExpression.cpp @@ -0,0 +1,526 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include + +#include "InputCommon/ControlReference/FunctionExpression.h" + +namespace ciface +{ +namespace ExpressionParser +{ +constexpr int LOOP_MAX_REPS = 10000; +constexpr ControlState CONDITION_THRESHOLD = 0.5; + +using Clock = std::chrono::steady_clock; +using FSec = std::chrono::duration; + +// usage: toggle(toggle_state_input, [clear_state_input]) +class ToggleExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + // Optional 2nd argument for clearing state: + if (1 == args.size() || 2 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"toggle_state_input, [clear_state_input]"}; + } + + ControlState GetValue() const override + { + const ControlState inner_value = GetArg(0).GetValue(); + + if (inner_value < CONDITION_THRESHOLD) + { + m_released = true; + } + else if (m_released && inner_value > CONDITION_THRESHOLD) + { + m_released = false; + m_state ^= true; + } + + if (2 == GetArgCount() && GetArg(1).GetValue() > CONDITION_THRESHOLD) + { + m_state = false; + } + + return m_state; + } + + mutable bool m_released{}; + mutable bool m_state{}; +}; + +// usage: not(expression) +class NotExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (1 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"expression"}; + } + + ControlState GetValue() const override { return 1.0 - GetArg(0).GetValue(); } + void SetValue(ControlState value) override { GetArg(0).SetValue(1.0 - value); } +}; + +// usage: sin(expression) +class SinExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (1 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"expression"}; + } + + ControlState GetValue() const override { return std::sin(GetArg(0).GetValue()); } +}; + +// usage: timer(seconds) +class TimerExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (1 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"seconds"}; + } + + ControlState GetValue() const override + { + const auto now = Clock::now(); + const auto elapsed = now - m_start_time; + + const ControlState val = GetArg(0).GetValue(); + + ControlState progress = std::chrono::duration_cast(elapsed).count() / val; + + if (std::isinf(progress) || progress < 0.0) + { + // User configured a non-positive timer. Reset the timer and return 0.0. + progress = 0.0; + m_start_time = now; + } + else if (progress >= 1.0) + { + const ControlState reset_count = std::floor(progress); + + m_start_time += std::chrono::duration_cast(FSec(val * reset_count)); + progress -= reset_count; + } + + return progress; + } + +private: + mutable Clock::time_point m_start_time = Clock::now(); +}; + +// usage: if(condition, true_expression, false_expression) +class IfExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (3 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"condition, true_expression, false_expression"}; + } + + ControlState GetValue() const override + { + return (GetArg(0).GetValue() > CONDITION_THRESHOLD) ? GetArg(1).GetValue() : + GetArg(2).GetValue(); + } +}; + +// usage: minus(expression) +class UnaryMinusExpression : public FunctionExpression +{ +private: + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (1 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"expression"}; + } + + ControlState GetValue() const override + { + // Subtraction for clarity: + return 0.0 - GetArg(0).GetValue(); + } +}; + +// usage: deadzone(input, amount) +class DeadzoneExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (2 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, amount"}; + } + + ControlState GetValue() const override + { + const ControlState val = GetArg(0).GetValue(); + const ControlState deadzone = GetArg(1).GetValue(); + return std::copysign(std::max(0.0, std::abs(val) - deadzone) / (1.0 - deadzone), val); + } +}; + +// usage: smooth(input, seconds_up, seconds_down = seconds_up) +// seconds is seconds to change from 0.0 to 1.0 +class SmoothExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (2 == args.size() || 3 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, seconds_up, seconds_down = seconds_up"}; + } + + ControlState GetValue() const override + { + const auto now = Clock::now(); + const auto elapsed = now - m_last_update; + m_last_update = now; + + const ControlState desired_value = GetArg(0).GetValue(); + + const ControlState smooth_up = GetArg(1).GetValue(); + const ControlState smooth_down = (3 == GetArgCount() ? GetArg(2).GetValue() : smooth_up); + + const ControlState smooth = (desired_value < m_value) ? smooth_down : smooth_up; + const ControlState max_move = std::chrono::duration_cast(elapsed).count() / smooth; + + if (std::isinf(max_move)) + { + m_value = desired_value; + } + else + { + const ControlState diff = desired_value - m_value; + m_value += std::copysign(std::min(max_move, std::abs(diff)), diff); + } + + return m_value; + } + +private: + mutable ControlState m_value = 0.0; + mutable Clock::time_point m_last_update = Clock::now(); +}; + +// usage: hold(input, seconds) +class HoldExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (2 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, seconds"}; + } + + ControlState GetValue() const override + { + const auto now = Clock::now(); + + const ControlState input = GetArg(0).GetValue(); + + if (input < CONDITION_THRESHOLD) + { + m_state = false; + m_start_time = Clock::now(); + } + else if (!m_state) + { + const auto hold_time = now - m_start_time; + + if (std::chrono::duration_cast(hold_time).count() >= GetArg(1).GetValue()) + m_state = true; + } + + return m_state; + } + +private: + mutable bool m_state = false; + mutable Clock::time_point m_start_time = Clock::now(); +}; + +// usage: tap(input, seconds, taps=2) +class TapExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (2 == args.size() || 3 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, seconds, taps = 2"}; + } + + ControlState GetValue() const override + { + const auto now = Clock::now(); + + const auto elapsed = std::chrono::duration_cast(now - m_start_time).count(); + + const ControlState input = GetArg(0).GetValue(); + const ControlState seconds = GetArg(1).GetValue(); + + const bool is_time_up = elapsed > seconds; + + const u32 desired_taps = (3 == GetArgCount()) ? u32(GetArg(2).GetValue() + 0.5) : 2; + + if (input < CONDITION_THRESHOLD) + { + m_released = true; + + if (m_taps > 0 && is_time_up) + { + m_taps = 0; + } + } + else + { + if (m_released) + { + if (!m_taps) + { + m_start_time = now; + } + + ++m_taps; + m_released = false; + } + + return desired_taps == m_taps; + } + + return 0.0; + } + +private: + mutable bool m_released = true; + mutable u32 m_taps = 0; + mutable Clock::time_point m_start_time = Clock::now(); +}; + +// usage: relative(input, speed, [max_abs_value, [shared_state]]) +// speed is max movement per second +class RelativeExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (args.size() >= 2 && args.size() <= 4) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, speed, [max_abs_value, [shared_state]]"}; + } + + ControlState GetValue() const override + { + // There is a lot of funky math in this function but it allows for a variety of uses: + // + // e.g. A single mapping with a relatively adjusted value between 0.0 and 1.0 + // Potentially useful for a trigger input + // relative(`Up` - `Down`, 2.0) + // + // e.g. A value with two mappings (such as analog stick Up/Down) + // The shared state allows the two mappings to work together. + // This mapping (for up) returns a value clamped between 0.0 and 1.0 + // relative(`Up`, 2.0, 1.0, $y) + // This mapping (for down) returns the negative value clamped between 0.0 and 1.0 + // (Adjustments created by `Down` are applied negatively to the shared state) + // relative(`Down`, 2.0, -1.0, $y) + + const auto now = Clock::now(); + + if (GetArgCount() >= 4) + m_state = GetArg(3).GetValue(); + + const auto elapsed = std::chrono::duration_cast(now - m_last_update).count(); + m_last_update = now; + + const ControlState input = GetArg(0).GetValue(); + const ControlState speed = GetArg(1).GetValue(); + + const ControlState max_abs_value = (GetArgCount() >= 3) ? GetArg(2).GetValue() : 1.0; + + const ControlState max_move = input * elapsed * speed; + const ControlState diff_from_zero = std::abs(0.0 - m_state); + const ControlState diff_from_max = std::abs(max_abs_value - m_state); + + m_state += std::min(std::max(max_move, -diff_from_zero), diff_from_max) * + std::copysign(1.0, max_abs_value); + + if (GetArgCount() >= 4) + const_cast(GetArg(3)).SetValue(m_state); + + return std::max(0.0, m_state * std::copysign(1.0, max_abs_value)); + } + +private: + mutable ControlState m_state = 0.0; + mutable Clock::time_point m_last_update = Clock::now(); +}; + +// usage: pulse(input, seconds) +class PulseExpression : public FunctionExpression +{ + ArgumentValidation + ValidateArguments(const std::vector>& args) override + { + if (2 == args.size()) + return ArgumentsAreValid{}; + else + return ExpectedArguments{"input, seconds"}; + } + + ControlState GetValue() const override + { + const auto now = Clock::now(); + + const ControlState input = GetArg(0).GetValue(); + + if (input < CONDITION_THRESHOLD) + { + m_released = true; + } + else if (m_released) + { + m_released = false; + + const auto seconds = std::chrono::duration_cast(FSec(GetArg(1).GetValue())); + + if (m_state) + { + m_release_time += seconds; + } + else + { + m_state = true; + m_release_time = now + seconds; + } + } + + if (m_state && now >= m_release_time) + { + m_state = false; + } + + return m_state; + } + +private: + mutable bool m_released = false; + mutable bool m_state = false; + mutable Clock::time_point m_release_time = Clock::now(); +}; + +std::unique_ptr MakeFunctionExpression(std::string name) +{ + if ("not" == name) + return std::make_unique(); + else if ("if" == name) + return std::make_unique(); + else if ("sin" == name) + return std::make_unique(); + else if ("timer" == name) + return std::make_unique(); + else if ("toggle" == name) + return std::make_unique(); + else if ("minus" == name) + return std::make_unique(); + else if ("deadzone" == name) + return std::make_unique(); + else if ("smooth" == name) + return std::make_unique(); + else if ("hold" == name) + return std::make_unique(); + else if ("tap" == name) + return std::make_unique(); + else if ("relative" == name) + return std::make_unique(); + else if ("pulse" == name) + return std::make_unique(); + else + return nullptr; +} + +int FunctionExpression::CountNumControls() const +{ + int result = 0; + + for (auto& arg : m_args) + result += arg->CountNumControls(); + + return result; +} + +void FunctionExpression::UpdateReferences(ControlEnvironment& env) +{ + for (auto& arg : m_args) + arg->UpdateReferences(env); +} + +FunctionExpression::ArgumentValidation +FunctionExpression::SetArguments(std::vector>&& args) +{ + m_args = std::move(args); + + return ValidateArguments(m_args); +} + +Expression& FunctionExpression::GetArg(u32 number) +{ + return *m_args[number]; +} + +const Expression& FunctionExpression::GetArg(u32 number) const +{ + return *m_args[number]; +} + +u32 FunctionExpression::GetArgCount() const +{ + return u32(m_args.size()); +} + +void FunctionExpression::SetValue(ControlState) +{ +} + +} // namespace ExpressionParser +} // namespace ciface diff --git a/Source/Core/InputCommon/ControlReference/FunctionExpression.h b/Source/Core/InputCommon/ControlReference/FunctionExpression.h new file mode 100644 index 0000000000..f5332a0d25 --- /dev/null +++ b/Source/Core/InputCommon/ControlReference/FunctionExpression.h @@ -0,0 +1,55 @@ +// Copyright 2019 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +#include "InputCommon/ControlReference/ExpressionParser.h" +#include "InputCommon/ControlReference/FunctionExpression.h" + +namespace ciface +{ +namespace ExpressionParser +{ +class FunctionExpression : public Expression +{ +public: + struct ArgumentsAreValid + { + }; + + struct ExpectedArguments + { + std::string text; + }; + + using ArgumentValidation = std::variant; + + int CountNumControls() const override; + void UpdateReferences(ControlEnvironment& env) override; + + ArgumentValidation SetArguments(std::vector>&& args); + + void SetValue(ControlState value) override; + +protected: + virtual ArgumentValidation + ValidateArguments(const std::vector>& args) = 0; + + Expression& GetArg(u32 number); + const Expression& GetArg(u32 number) const; + u32 GetArgCount() const; + +private: + std::vector> m_args; +}; + +std::unique_ptr MakeFunctionExpression(std::string name); + +} // namespace ExpressionParser +} // namespace ciface diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp index de0579a2b2..c5ff0705a2 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.cpp @@ -38,23 +38,38 @@ std::unique_lock EmulatedController::GetStateLock() void EmulatedController::UpdateReferences(const ControllerInterface& devi) { - const auto lock = GetStateLock(); m_default_device_is_connected = devi.HasConnectedDevice(m_default_device); + ciface::ExpressionParser::ControlEnvironment env(devi, GetDefaultDevice(), m_expression_vars); + + UpdateReferences(env); +} + +void EmulatedController::UpdateReferences(ciface::ExpressionParser::ControlEnvironment& env) +{ + const auto lock = GetStateLock(); + for (auto& ctrlGroup : groups) { for (auto& control : ctrlGroup->controls) - control->control_ref.get()->UpdateReference(devi, GetDefaultDevice()); + control->control_ref->UpdateReference(env); // Attachments: if (ctrlGroup->type == GroupType::Attachments) { for (auto& attachment : static_cast(ctrlGroup.get())->GetAttachmentList()) - attachment->UpdateReferences(devi); + attachment->UpdateReferences(env); } } } +void EmulatedController::UpdateSingleControlReference(const ControllerInterface& devi, + ControlReference* ref) +{ + ciface::ExpressionParser::ControlEnvironment env(devi, GetDefaultDevice(), m_expression_vars); + ref->UpdateReference(env); +} + bool EmulatedController::IsDefaultDeviceConnected() const { return m_default_device_is_connected; diff --git a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h index bf2ff666fa..2611880ee9 100644 --- a/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h +++ b/Source/Core/InputCommon/ControllerEmu/ControllerEmu.h @@ -13,6 +13,7 @@ #include "Common/Common.h" #include "Common/IniFile.h" +#include "InputCommon/ControlReference/ExpressionParser.h" #include "InputCommon/ControllerInterface/Device.h" class ControllerInterface; @@ -20,6 +21,8 @@ class ControllerInterface; const char* const named_directions[] = {_trans("Up"), _trans("Down"), _trans("Left"), _trans("Right")}; +class ControlReference; + namespace ControllerEmu { class ControlGroup; @@ -43,6 +46,7 @@ public: void SetDefaultDevice(ciface::Core::DeviceQualifier devq); void UpdateReferences(const ControllerInterface& devi); + void UpdateSingleControlReference(const ControllerInterface& devi, ControlReference* ref); // This returns a lock that should be held before calling State() on any control // references and GetState(), by extension. This prevents a race condition @@ -75,6 +79,12 @@ public: return T(std::lround((zero_value - neg_1_value) * input_value + zero_value)); } +protected: + // TODO: Wiimote attachment has its own member that isn't being used.. + ciface::ExpressionParser::ControlEnvironment::VariableContainer m_expression_vars; + + void UpdateReferences(ciface::ExpressionParser::ControlEnvironment& env); + private: ciface::Core::DeviceQualifier m_default_device; bool m_default_device_is_connected{false}; diff --git a/Source/Core/InputCommon/InputCommon.vcxproj b/Source/Core/InputCommon/InputCommon.vcxproj index 8d0b0b3759..a22ad21394 100644 --- a/Source/Core/InputCommon/InputCommon.vcxproj +++ b/Source/Core/InputCommon/InputCommon.vcxproj @@ -64,6 +64,7 @@ +