diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..b8143ab4a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,766 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +ij_continuation_indent_size = 8 +ij_formatter_off_tag = @formatter:off +ij_formatter_on_tag = @formatter:on +ij_formatter_tags_enabled = false +ij_smart_tabs = false +ij_visual_guides = none +ij_wrap_on_typing = false + +[*.css] +ij_css_align_closing_brace_with_properties = false +ij_css_blank_lines_around_nested_selector = 1 +ij_css_blank_lines_between_blocks = 1 +ij_css_block_comment_add_space = false +ij_css_brace_placement = end_of_line +ij_css_enforce_quotes_on_format = false +ij_css_hex_color_long_format = false +ij_css_hex_color_lower_case = false +ij_css_hex_color_short_format = false +ij_css_hex_color_upper_case = false +ij_css_keep_blank_lines_in_code = 2 +ij_css_keep_indents_on_empty_lines = false +ij_css_keep_single_line_blocks = false +ij_css_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_css_space_after_colon = true +ij_css_space_before_opening_brace = true +ij_css_use_double_quotes = true +ij_css_value_alignment = do_not_align + +[*.feature] +indent_size = 2 +ij_gherkin_keep_indents_on_empty_lines = false + +[*.haml] +indent_size = 2 +ij_haml_keep_indents_on_empty_lines = false + +[*.less] +indent_size = 2 +ij_less_align_closing_brace_with_properties = false +ij_less_blank_lines_around_nested_selector = 1 +ij_less_blank_lines_between_blocks = 1 +ij_less_block_comment_add_space = false +ij_less_brace_placement = 0 +ij_less_enforce_quotes_on_format = false +ij_less_hex_color_long_format = false +ij_less_hex_color_lower_case = false +ij_less_hex_color_short_format = false +ij_less_hex_color_upper_case = false +ij_less_keep_blank_lines_in_code = 2 +ij_less_keep_indents_on_empty_lines = false +ij_less_keep_single_line_blocks = false +ij_less_line_comment_add_space = false +ij_less_line_comment_at_first_column = false +ij_less_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_less_space_after_colon = true +ij_less_space_before_opening_brace = true +ij_less_use_double_quotes = true +ij_less_value_alignment = 0 + +[*.pp] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_puppet_keep_indents_on_empty_lines = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[*.sass] +indent_size = 2 +ij_sass_align_closing_brace_with_properties = false +ij_sass_blank_lines_around_nested_selector = 1 +ij_sass_blank_lines_between_blocks = 1 +ij_sass_brace_placement = 0 +ij_sass_enforce_quotes_on_format = false +ij_sass_hex_color_long_format = false +ij_sass_hex_color_lower_case = false +ij_sass_hex_color_short_format = false +ij_sass_hex_color_upper_case = false +ij_sass_keep_blank_lines_in_code = 2 +ij_sass_keep_indents_on_empty_lines = false +ij_sass_keep_single_line_blocks = false +ij_sass_line_comment_add_space = false +ij_sass_line_comment_at_first_column = false +ij_sass_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_sass_space_after_colon = true +ij_sass_space_before_opening_brace = true +ij_sass_use_double_quotes = true +ij_sass_value_alignment = 0 + +[*.scss] +indent_size = 2 +ij_scss_align_closing_brace_with_properties = false +ij_scss_blank_lines_around_nested_selector = 1 +ij_scss_blank_lines_between_blocks = 1 +ij_scss_block_comment_add_space = false +ij_scss_brace_placement = 0 +ij_scss_enforce_quotes_on_format = false +ij_scss_hex_color_long_format = false +ij_scss_hex_color_lower_case = false +ij_scss_hex_color_short_format = false +ij_scss_hex_color_upper_case = false +ij_scss_keep_blank_lines_in_code = 2 +ij_scss_keep_indents_on_empty_lines = false +ij_scss_keep_single_line_blocks = false +ij_scss_line_comment_add_space = false +ij_scss_line_comment_at_first_column = false +ij_scss_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_scss_space_after_colon = true +ij_scss_space_before_opening_brace = true +ij_scss_use_double_quotes = true +ij_scss_value_alignment = 0 + +[*.styl] +indent_size = 2 +ij_stylus_align_closing_brace_with_properties = false +ij_stylus_blank_lines_around_nested_selector = 1 +ij_stylus_blank_lines_between_blocks = 1 +ij_stylus_brace_placement = 0 +ij_stylus_enforce_quotes_on_format = false +ij_stylus_hex_color_long_format = false +ij_stylus_hex_color_lower_case = false +ij_stylus_hex_color_short_format = false +ij_stylus_hex_color_upper_case = false +ij_stylus_keep_blank_lines_in_code = 2 +ij_stylus_keep_indents_on_empty_lines = false +ij_stylus_keep_single_line_blocks = false +ij_stylus_properties_order = font, font-family, font-size, font-weight, font-style, font-variant, font-size-adjust, font-stretch, line-height, position, z-index, top, right, bottom, left, display, visibility, float, clear, overflow, overflow-x, overflow-y, clip, zoom, align-content, align-items, align-self, flex, flex-flow, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, justify-content, order, box-sizing, width, min-width, max-width, height, min-height, max-height, margin, margin-top, margin-right, margin-bottom, margin-left, padding, padding-top, padding-right, padding-bottom, padding-left, table-layout, empty-cells, caption-side, border-spacing, border-collapse, list-style, list-style-position, list-style-type, list-style-image, content, quotes, counter-reset, counter-increment, resize, cursor, user-select, nav-index, nav-up, nav-right, nav-down, nav-left, transition, transition-delay, transition-timing-function, transition-duration, transition-property, transform, transform-origin, animation, animation-name, animation-duration, animation-play-state, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, text-align, text-align-last, vertical-align, white-space, text-decoration, text-emphasis, text-emphasis-color, text-emphasis-style, text-emphasis-position, text-indent, text-justify, letter-spacing, word-spacing, text-outline, text-transform, text-wrap, text-overflow, text-overflow-ellipsis, text-overflow-mode, word-wrap, word-break, tab-size, hyphens, pointer-events, opacity, color, border, border-width, border-style, border-color, border-top, border-top-width, border-top-style, border-top-color, border-right, border-right-width, border-right-style, border-right-color, border-bottom, border-bottom-width, border-bottom-style, border-bottom-color, border-left, border-left-width, border-left-style, border-left-color, border-radius, border-top-left-radius, border-top-right-radius, border-bottom-right-radius, border-bottom-left-radius, border-image, border-image-source, border-image-slice, border-image-width, border-image-outset, border-image-repeat, outline, outline-width, outline-style, outline-color, outline-offset, background, background-color, background-image, background-repeat, background-attachment, background-position, background-position-x, background-position-y, background-clip, background-origin, background-size, box-decoration-break, box-shadow, text-shadow +ij_stylus_space_after_colon = true +ij_stylus_space_before_opening_brace = true +ij_stylus_use_double_quotes = true +ij_stylus_value_alignment = 0 + +[.editorconfig] +ij_editorconfig_align_group_field_declarations = false +ij_editorconfig_space_after_colon = false +ij_editorconfig_space_after_comma = true +ij_editorconfig_space_before_colon = false +ij_editorconfig_space_before_comma = false +ij_editorconfig_spaces_around_assignment_operators = true + +[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.qrc,*.rng,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}] +ij_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.ats,*.cts,*.mts,*.ts}] +ij_continuation_indent_size = 4 +ij_typescript_align_imports = false +ij_typescript_align_multiline_array_initializer_expression = false +ij_typescript_align_multiline_binary_operation = false +ij_typescript_align_multiline_chained_methods = false +ij_typescript_align_multiline_extends_list = false +ij_typescript_align_multiline_for = true +ij_typescript_align_multiline_parameters = true +ij_typescript_align_multiline_parameters_in_calls = false +ij_typescript_align_multiline_ternary_operation = false +ij_typescript_align_object_properties = 0 +ij_typescript_align_union_types = false +ij_typescript_align_var_statements = 0 +ij_typescript_array_initializer_new_line_after_left_brace = false +ij_typescript_array_initializer_right_brace_on_new_line = false +ij_typescript_array_initializer_wrap = off +ij_typescript_assignment_wrap = off +ij_typescript_binary_operation_sign_on_next_line = false +ij_typescript_binary_operation_wrap = off +ij_typescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_typescript_blank_lines_after_imports = 1 +ij_typescript_blank_lines_around_class = 1 +ij_typescript_blank_lines_around_field = 0 +ij_typescript_blank_lines_around_field_in_interface = 0 +ij_typescript_blank_lines_around_function = 1 +ij_typescript_blank_lines_around_method = 1 +ij_typescript_blank_lines_around_method_in_interface = 1 +ij_typescript_block_brace_style = end_of_line +ij_typescript_block_comment_add_space = false +ij_typescript_block_comment_at_first_column = true +ij_typescript_call_parameters_new_line_after_left_paren = false +ij_typescript_call_parameters_right_paren_on_new_line = false +ij_typescript_call_parameters_wrap = off +ij_typescript_catch_on_new_line = false +ij_typescript_chained_call_dot_on_new_line = true +ij_typescript_class_brace_style = end_of_line +ij_typescript_comma_on_new_line = false +ij_typescript_do_while_brace_force = never +ij_typescript_else_on_new_line = false +ij_typescript_enforce_trailing_comma = keep +ij_typescript_enum_constants_wrap = on_every_item +ij_typescript_extends_keyword_wrap = off +ij_typescript_extends_list_wrap = off +ij_typescript_field_prefix = _ +ij_typescript_file_name_style = relaxed +ij_typescript_finally_on_new_line = false +ij_typescript_for_brace_force = never +ij_typescript_for_statement_new_line_after_left_paren = false +ij_typescript_for_statement_right_paren_on_new_line = false +ij_typescript_for_statement_wrap = off +ij_typescript_force_quote_style = false +ij_typescript_force_semicolon_style = false +ij_typescript_function_expression_brace_style = end_of_line +ij_typescript_if_brace_force = never +ij_typescript_import_merge_members = global +ij_typescript_import_prefer_absolute_path = global +ij_typescript_import_sort_members = true +ij_typescript_import_sort_module_name = false +ij_typescript_import_use_node_resolution = true +ij_typescript_imports_wrap = on_every_item +ij_typescript_indent_case_from_switch = true +ij_typescript_indent_chained_calls = true +ij_typescript_indent_package_children = 0 +ij_typescript_jsdoc_include_types = false +ij_typescript_jsx_attribute_value = braces +ij_typescript_keep_blank_lines_in_code = 2 +ij_typescript_keep_first_column_comment = true +ij_typescript_keep_indents_on_empty_lines = false +ij_typescript_keep_line_breaks = true +ij_typescript_keep_simple_blocks_in_one_line = false +ij_typescript_keep_simple_methods_in_one_line = false +ij_typescript_line_comment_add_space = true +ij_typescript_line_comment_at_first_column = false +ij_typescript_method_brace_style = end_of_line +ij_typescript_method_call_chain_wrap = off +ij_typescript_method_parameters_new_line_after_left_paren = false +ij_typescript_method_parameters_right_paren_on_new_line = false +ij_typescript_method_parameters_wrap = off +ij_typescript_object_literal_wrap = on_every_item +ij_typescript_parentheses_expression_new_line_after_left_paren = false +ij_typescript_parentheses_expression_right_paren_on_new_line = false +ij_typescript_place_assignment_sign_on_next_line = false +ij_typescript_prefer_as_type_cast = false +ij_typescript_prefer_explicit_types_function_expression_returns = false +ij_typescript_prefer_explicit_types_function_returns = false +ij_typescript_prefer_explicit_types_vars_fields = false +ij_typescript_prefer_parameters_wrap = false +ij_typescript_reformat_c_style_comments = false +ij_typescript_space_after_colon = true +ij_typescript_space_after_comma = true +ij_typescript_space_after_dots_in_rest_parameter = false +ij_typescript_space_after_generator_mult = true +ij_typescript_space_after_property_colon = true +ij_typescript_space_after_quest = true +ij_typescript_space_after_type_colon = true +ij_typescript_space_after_unary_not = false +ij_typescript_space_before_async_arrow_lparen = true +ij_typescript_space_before_catch_keyword = true +ij_typescript_space_before_catch_left_brace = true +ij_typescript_space_before_catch_parentheses = true +ij_typescript_space_before_class_lbrace = true +ij_typescript_space_before_class_left_brace = true +ij_typescript_space_before_colon = true +ij_typescript_space_before_comma = false +ij_typescript_space_before_do_left_brace = true +ij_typescript_space_before_else_keyword = true +ij_typescript_space_before_else_left_brace = true +ij_typescript_space_before_finally_keyword = true +ij_typescript_space_before_finally_left_brace = true +ij_typescript_space_before_for_left_brace = true +ij_typescript_space_before_for_parentheses = true +ij_typescript_space_before_for_semicolon = false +ij_typescript_space_before_function_left_parenth = true +ij_typescript_space_before_generator_mult = false +ij_typescript_space_before_if_left_brace = true +ij_typescript_space_before_if_parentheses = true +ij_typescript_space_before_method_call_parentheses = false +ij_typescript_space_before_method_left_brace = true +ij_typescript_space_before_method_parentheses = false +ij_typescript_space_before_property_colon = false +ij_typescript_space_before_quest = true +ij_typescript_space_before_switch_left_brace = true +ij_typescript_space_before_switch_parentheses = true +ij_typescript_space_before_try_left_brace = true +ij_typescript_space_before_type_colon = false +ij_typescript_space_before_unary_not = false +ij_typescript_space_before_while_keyword = true +ij_typescript_space_before_while_left_brace = true +ij_typescript_space_before_while_parentheses = true +ij_typescript_spaces_around_additive_operators = true +ij_typescript_spaces_around_arrow_function_operator = true +ij_typescript_spaces_around_assignment_operators = true +ij_typescript_spaces_around_bitwise_operators = true +ij_typescript_spaces_around_equality_operators = true +ij_typescript_spaces_around_logical_operators = true +ij_typescript_spaces_around_multiplicative_operators = true +ij_typescript_spaces_around_relational_operators = true +ij_typescript_spaces_around_shift_operators = true +ij_typescript_spaces_around_unary_operator = false +ij_typescript_spaces_within_array_initializer_brackets = false +ij_typescript_spaces_within_brackets = false +ij_typescript_spaces_within_catch_parentheses = false +ij_typescript_spaces_within_for_parentheses = false +ij_typescript_spaces_within_if_parentheses = false +ij_typescript_spaces_within_imports = false +ij_typescript_spaces_within_interpolation_expressions = false +ij_typescript_spaces_within_method_call_parentheses = false +ij_typescript_spaces_within_method_parentheses = false +ij_typescript_spaces_within_object_literal_braces = false +ij_typescript_spaces_within_object_type_braces = true +ij_typescript_spaces_within_parentheses = false +ij_typescript_spaces_within_switch_parentheses = false +ij_typescript_spaces_within_type_assertion = false +ij_typescript_spaces_within_union_types = true +ij_typescript_spaces_within_while_parentheses = false +ij_typescript_special_else_if_treatment = true +ij_typescript_ternary_operation_signs_on_next_line = false +ij_typescript_ternary_operation_wrap = off +ij_typescript_union_types_wrap = on_every_item +ij_typescript_use_chained_calls_group_indents = false +ij_typescript_use_double_quotes = true +ij_typescript_use_explicit_js_extension = auto +ij_typescript_use_path_mapping = always +ij_typescript_use_public_modifier = false +ij_typescript_use_semicolon_after_statement = true +ij_typescript_var_declaration_wrap = normal +ij_typescript_while_brace_force = never +ij_typescript_while_on_new_line = false +ij_typescript_wrap_comments = false + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 +ij_shell_binary_ops_start_line = false +ij_shell_keep_column_alignment_padding = false +ij_shell_minify_program = false +ij_shell_redirect_followed_by_space = false +ij_shell_switch_cases_indented = false +ij_shell_use_unix_line_separator = true + +[{*.cjs,*.js}] +ij_continuation_indent_size = 4 +ij_javascript_align_imports = false +ij_javascript_align_multiline_array_initializer_expression = false +ij_javascript_align_multiline_binary_operation = false +ij_javascript_align_multiline_chained_methods = false +ij_javascript_align_multiline_extends_list = false +ij_javascript_align_multiline_for = true +ij_javascript_align_multiline_parameters = true +ij_javascript_align_multiline_parameters_in_calls = false +ij_javascript_align_multiline_ternary_operation = false +ij_javascript_align_object_properties = 0 +ij_javascript_align_union_types = false +ij_javascript_align_var_statements = 0 +ij_javascript_array_initializer_new_line_after_left_brace = false +ij_javascript_array_initializer_right_brace_on_new_line = false +ij_javascript_array_initializer_wrap = off +ij_javascript_assignment_wrap = off +ij_javascript_binary_operation_sign_on_next_line = false +ij_javascript_binary_operation_wrap = off +ij_javascript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_javascript_blank_lines_after_imports = 1 +ij_javascript_blank_lines_around_class = 1 +ij_javascript_blank_lines_around_field = 0 +ij_javascript_blank_lines_around_function = 1 +ij_javascript_blank_lines_around_method = 1 +ij_javascript_block_brace_style = end_of_line +ij_javascript_block_comment_add_space = false +ij_javascript_block_comment_at_first_column = true +ij_javascript_call_parameters_new_line_after_left_paren = false +ij_javascript_call_parameters_right_paren_on_new_line = false +ij_javascript_call_parameters_wrap = off +ij_javascript_catch_on_new_line = false +ij_javascript_chained_call_dot_on_new_line = true +ij_javascript_class_brace_style = end_of_line +ij_javascript_comma_on_new_line = false +ij_javascript_do_while_brace_force = never +ij_javascript_else_on_new_line = false +ij_javascript_enforce_trailing_comma = keep +ij_javascript_extends_keyword_wrap = off +ij_javascript_extends_list_wrap = off +ij_javascript_field_prefix = _ +ij_javascript_file_name_style = relaxed +ij_javascript_finally_on_new_line = false +ij_javascript_for_brace_force = never +ij_javascript_for_statement_new_line_after_left_paren = false +ij_javascript_for_statement_right_paren_on_new_line = false +ij_javascript_for_statement_wrap = off +ij_javascript_force_quote_style = false +ij_javascript_force_semicolon_style = false +ij_javascript_function_expression_brace_style = end_of_line +ij_javascript_if_brace_force = never +ij_javascript_import_merge_members = global +ij_javascript_import_prefer_absolute_path = global +ij_javascript_import_sort_members = true +ij_javascript_import_sort_module_name = false +ij_javascript_import_use_node_resolution = true +ij_javascript_imports_wrap = on_every_item +ij_javascript_indent_case_from_switch = true +ij_javascript_indent_chained_calls = true +ij_javascript_indent_package_children = 0 +ij_javascript_jsx_attribute_value = braces +ij_javascript_keep_blank_lines_in_code = 2 +ij_javascript_keep_first_column_comment = true +ij_javascript_keep_indents_on_empty_lines = false +ij_javascript_keep_line_breaks = true +ij_javascript_keep_simple_blocks_in_one_line = false +ij_javascript_keep_simple_methods_in_one_line = false +ij_javascript_line_comment_add_space = true +ij_javascript_line_comment_at_first_column = false +ij_javascript_method_brace_style = end_of_line +ij_javascript_method_call_chain_wrap = off +ij_javascript_method_parameters_new_line_after_left_paren = false +ij_javascript_method_parameters_right_paren_on_new_line = false +ij_javascript_method_parameters_wrap = off +ij_javascript_object_literal_wrap = on_every_item +ij_javascript_parentheses_expression_new_line_after_left_paren = false +ij_javascript_parentheses_expression_right_paren_on_new_line = false +ij_javascript_place_assignment_sign_on_next_line = false +ij_javascript_prefer_as_type_cast = false +ij_javascript_prefer_explicit_types_function_expression_returns = false +ij_javascript_prefer_explicit_types_function_returns = false +ij_javascript_prefer_explicit_types_vars_fields = false +ij_javascript_prefer_parameters_wrap = false +ij_javascript_reformat_c_style_comments = false +ij_javascript_space_after_colon = true +ij_javascript_space_after_comma = true +ij_javascript_space_after_dots_in_rest_parameter = false +ij_javascript_space_after_generator_mult = true +ij_javascript_space_after_property_colon = true +ij_javascript_space_after_quest = true +ij_javascript_space_after_type_colon = true +ij_javascript_space_after_unary_not = false +ij_javascript_space_before_async_arrow_lparen = true +ij_javascript_space_before_catch_keyword = true +ij_javascript_space_before_catch_left_brace = true +ij_javascript_space_before_catch_parentheses = true +ij_javascript_space_before_class_lbrace = true +ij_javascript_space_before_class_left_brace = true +ij_javascript_space_before_colon = true +ij_javascript_space_before_comma = false +ij_javascript_space_before_do_left_brace = true +ij_javascript_space_before_else_keyword = true +ij_javascript_space_before_else_left_brace = true +ij_javascript_space_before_finally_keyword = true +ij_javascript_space_before_finally_left_brace = true +ij_javascript_space_before_for_left_brace = true +ij_javascript_space_before_for_parentheses = true +ij_javascript_space_before_for_semicolon = false +ij_javascript_space_before_function_left_parenth = true +ij_javascript_space_before_generator_mult = false +ij_javascript_space_before_if_left_brace = true +ij_javascript_space_before_if_parentheses = true +ij_javascript_space_before_method_call_parentheses = false +ij_javascript_space_before_method_left_brace = true +ij_javascript_space_before_method_parentheses = false +ij_javascript_space_before_property_colon = false +ij_javascript_space_before_quest = true +ij_javascript_space_before_switch_left_brace = true +ij_javascript_space_before_switch_parentheses = true +ij_javascript_space_before_try_left_brace = true +ij_javascript_space_before_type_colon = false +ij_javascript_space_before_unary_not = false +ij_javascript_space_before_while_keyword = true +ij_javascript_space_before_while_left_brace = true +ij_javascript_space_before_while_parentheses = true +ij_javascript_spaces_around_additive_operators = true +ij_javascript_spaces_around_arrow_function_operator = true +ij_javascript_spaces_around_assignment_operators = true +ij_javascript_spaces_around_bitwise_operators = true +ij_javascript_spaces_around_equality_operators = true +ij_javascript_spaces_around_logical_operators = true +ij_javascript_spaces_around_multiplicative_operators = true +ij_javascript_spaces_around_relational_operators = true +ij_javascript_spaces_around_shift_operators = true +ij_javascript_spaces_around_unary_operator = false +ij_javascript_spaces_within_array_initializer_brackets = false +ij_javascript_spaces_within_brackets = false +ij_javascript_spaces_within_catch_parentheses = false +ij_javascript_spaces_within_for_parentheses = false +ij_javascript_spaces_within_if_parentheses = false +ij_javascript_spaces_within_imports = false +ij_javascript_spaces_within_interpolation_expressions = false +ij_javascript_spaces_within_method_call_parentheses = false +ij_javascript_spaces_within_method_parentheses = false +ij_javascript_spaces_within_object_literal_braces = false +ij_javascript_spaces_within_object_type_braces = true +ij_javascript_spaces_within_parentheses = false +ij_javascript_spaces_within_switch_parentheses = false +ij_javascript_spaces_within_type_assertion = false +ij_javascript_spaces_within_union_types = true +ij_javascript_spaces_within_while_parentheses = false +ij_javascript_special_else_if_treatment = true +ij_javascript_ternary_operation_signs_on_next_line = false +ij_javascript_ternary_operation_wrap = off +ij_javascript_union_types_wrap = on_every_item +ij_javascript_use_chained_calls_group_indents = false +ij_javascript_use_double_quotes = true +ij_javascript_use_explicit_js_extension = auto +ij_javascript_use_path_mapping = always +ij_javascript_use_public_modifier = false +ij_javascript_use_semicolon_after_statement = true +ij_javascript_var_declaration_wrap = normal +ij_javascript_while_brace_force = never +ij_javascript_while_on_new_line = false +ij_javascript_wrap_comments = false + +[{*.cjsx,*.coffee}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 2 +ij_coffeescript_align_function_body = false +ij_coffeescript_align_imports = false +ij_coffeescript_align_multiline_array_initializer_expression = true +ij_coffeescript_align_multiline_parameters = true +ij_coffeescript_align_multiline_parameters_in_calls = false +ij_coffeescript_align_object_properties = 0 +ij_coffeescript_align_union_types = false +ij_coffeescript_align_var_statements = 0 +ij_coffeescript_array_initializer_new_line_after_left_brace = false +ij_coffeescript_array_initializer_right_brace_on_new_line = false +ij_coffeescript_array_initializer_wrap = normal +ij_coffeescript_blacklist_imports = rxjs/Rx, node_modules/**, **/node_modules/**, @angular/material, @angular/material/typings/** +ij_coffeescript_blank_lines_around_function = 1 +ij_coffeescript_call_parameters_new_line_after_left_paren = false +ij_coffeescript_call_parameters_right_paren_on_new_line = false +ij_coffeescript_call_parameters_wrap = normal +ij_coffeescript_chained_call_dot_on_new_line = true +ij_coffeescript_comma_on_new_line = false +ij_coffeescript_enforce_trailing_comma = keep +ij_coffeescript_field_prefix = _ +ij_coffeescript_file_name_style = relaxed +ij_coffeescript_force_quote_style = false +ij_coffeescript_force_semicolon_style = false +ij_coffeescript_function_expression_brace_style = end_of_line +ij_coffeescript_import_merge_members = global +ij_coffeescript_import_prefer_absolute_path = global +ij_coffeescript_import_sort_members = true +ij_coffeescript_import_sort_module_name = false +ij_coffeescript_import_use_node_resolution = true +ij_coffeescript_imports_wrap = on_every_item +ij_coffeescript_indent_chained_calls = true +ij_coffeescript_indent_package_children = 0 +ij_coffeescript_jsx_attribute_value = braces +ij_coffeescript_keep_blank_lines_in_code = 2 +ij_coffeescript_keep_first_column_comment = true +ij_coffeescript_keep_indents_on_empty_lines = false +ij_coffeescript_keep_line_breaks = true +ij_coffeescript_keep_simple_methods_in_one_line = false +ij_coffeescript_method_parameters_new_line_after_left_paren = false +ij_coffeescript_method_parameters_right_paren_on_new_line = false +ij_coffeescript_method_parameters_wrap = off +ij_coffeescript_object_literal_wrap = on_every_item +ij_coffeescript_prefer_as_type_cast = false +ij_coffeescript_prefer_explicit_types_function_expression_returns = false +ij_coffeescript_prefer_explicit_types_function_returns = false +ij_coffeescript_prefer_explicit_types_vars_fields = false +ij_coffeescript_reformat_c_style_comments = false +ij_coffeescript_space_after_comma = true +ij_coffeescript_space_after_dots_in_rest_parameter = false +ij_coffeescript_space_after_generator_mult = true +ij_coffeescript_space_after_property_colon = true +ij_coffeescript_space_after_type_colon = true +ij_coffeescript_space_after_unary_not = false +ij_coffeescript_space_before_async_arrow_lparen = true +ij_coffeescript_space_before_class_lbrace = true +ij_coffeescript_space_before_comma = false +ij_coffeescript_space_before_function_left_parenth = true +ij_coffeescript_space_before_generator_mult = false +ij_coffeescript_space_before_property_colon = false +ij_coffeescript_space_before_type_colon = false +ij_coffeescript_space_before_unary_not = false +ij_coffeescript_spaces_around_additive_operators = true +ij_coffeescript_spaces_around_arrow_function_operator = true +ij_coffeescript_spaces_around_assignment_operators = true +ij_coffeescript_spaces_around_bitwise_operators = true +ij_coffeescript_spaces_around_equality_operators = true +ij_coffeescript_spaces_around_logical_operators = true +ij_coffeescript_spaces_around_multiplicative_operators = true +ij_coffeescript_spaces_around_relational_operators = true +ij_coffeescript_spaces_around_shift_operators = true +ij_coffeescript_spaces_around_unary_operator = false +ij_coffeescript_spaces_within_array_initializer_braces = false +ij_coffeescript_spaces_within_array_initializer_brackets = false +ij_coffeescript_spaces_within_imports = false +ij_coffeescript_spaces_within_index_brackets = false +ij_coffeescript_spaces_within_interpolation_expressions = false +ij_coffeescript_spaces_within_method_call_parentheses = false +ij_coffeescript_spaces_within_method_parentheses = false +ij_coffeescript_spaces_within_object_braces = false +ij_coffeescript_spaces_within_object_literal_braces = false +ij_coffeescript_spaces_within_object_type_braces = true +ij_coffeescript_spaces_within_range_brackets = false +ij_coffeescript_spaces_within_type_assertion = false +ij_coffeescript_spaces_within_union_types = true +ij_coffeescript_union_types_wrap = on_every_item +ij_coffeescript_use_chained_calls_group_indents = false +ij_coffeescript_use_double_quotes = true +ij_coffeescript_use_explicit_js_extension = auto +ij_coffeescript_use_path_mapping = always +ij_coffeescript_use_public_modifier = false +ij_coffeescript_use_semicolon_after_statement = false +ij_coffeescript_var_declaration_wrap = normal + +[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body, div, p, form, h1, h2, h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title, h1, h2, h3, h4, h5, h6, p +ij_html_do_not_indent_children_of_tags = html, body, thead, tbody, tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a, abbr, acronym, b, basefont, bdo, big, br, cite, cite, code, dfn, em, font, i, img, input, kbd, label, q, s, samp, select, small, span, strike, strong, sub, sup, textarea, tt, u, var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span, pre, textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.py,*.pyw}] +ij_continuation_indent_size = 4 +ij_python_align_collections_and_comprehensions = true +ij_python_align_multiline_imports = true +ij_python_align_multiline_parameters = true +ij_python_align_multiline_parameters_in_calls = true +ij_python_blank_line_at_file_end = true +ij_python_blank_lines_after_imports = 1 +ij_python_blank_lines_after_local_imports = 0 +ij_python_blank_lines_around_class = 1 +ij_python_blank_lines_around_method = 1 +ij_python_blank_lines_around_top_level_classes_functions = 2 +ij_python_blank_lines_before_first_method = 0 +ij_python_call_parameters_new_line_after_left_paren = false +ij_python_call_parameters_right_paren_on_new_line = false +ij_python_call_parameters_wrap = normal +ij_python_dict_alignment = 0 +ij_python_dict_new_line_after_left_brace = false +ij_python_dict_new_line_before_right_brace = false +ij_python_dict_wrapping = 1 +ij_python_from_import_new_line_after_left_parenthesis = false +ij_python_from_import_new_line_before_right_parenthesis = false +ij_python_from_import_parentheses_force_if_multiline = false +ij_python_from_import_trailing_comma_if_multiline = false +ij_python_from_import_wrapping = 1 +ij_python_hang_closing_brackets = false +ij_python_keep_blank_lines_in_code = 1 +ij_python_keep_blank_lines_in_declarations = 1 +ij_python_keep_indents_on_empty_lines = false +ij_python_keep_line_breaks = true +ij_python_method_parameters_new_line_after_left_paren = false +ij_python_method_parameters_right_paren_on_new_line = false +ij_python_method_parameters_wrap = normal +ij_python_new_line_after_colon = false +ij_python_new_line_after_colon_multi_clause = true +ij_python_optimize_imports_always_split_from_imports = false +ij_python_optimize_imports_case_insensitive_order = false +ij_python_optimize_imports_join_from_imports_with_same_source = false +ij_python_optimize_imports_sort_by_type_first = true +ij_python_optimize_imports_sort_imports = true +ij_python_optimize_imports_sort_names_in_from_imports = false +ij_python_space_after_comma = true +ij_python_space_after_number_sign = true +ij_python_space_after_py_colon = true +ij_python_space_before_backslash = true +ij_python_space_before_comma = false +ij_python_space_before_for_semicolon = false +ij_python_space_before_lbracket = false +ij_python_space_before_method_call_parentheses = false +ij_python_space_before_method_parentheses = false +ij_python_space_before_number_sign = true +ij_python_space_before_py_colon = false +ij_python_space_within_empty_method_call_parentheses = false +ij_python_space_within_empty_method_parentheses = false +ij_python_spaces_around_additive_operators = true +ij_python_spaces_around_assignment_operators = true +ij_python_spaces_around_bitwise_operators = true +ij_python_spaces_around_eq_in_keyword_argument = false +ij_python_spaces_around_eq_in_named_parameter = false +ij_python_spaces_around_equality_operators = true +ij_python_spaces_around_multiplicative_operators = true +ij_python_spaces_around_power_operator = true +ij_python_spaces_around_relational_operators = true +ij_python_spaces_around_shift_operators = true +ij_python_spaces_within_braces = false +ij_python_spaces_within_brackets = false +ij_python_spaces_within_method_call_parentheses = false +ij_python_spaces_within_method_parentheses = false +ij_python_use_continuation_indent_for_arguments = false +ij_python_use_continuation_indent_for_collection_and_comprehensions = false +ij_python_use_continuation_indent_for_parameters = true +ij_python_wrap_long_lines = false + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +indent_size = 2 +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/server/__init__.py b/server/__init__.py index 4d8af204a..ccd566db1 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -117,6 +117,7 @@ from .rating_service.rating_service import RatingService from .servercontext import ServerContext from .stats.game_stats_service import GameStatsService +from .tournament_service import TournamentService __author__ = "Askaholic, Chris Kitching, Dragonfire, Gael Honorez, Jeroen De Dauw, Crotalus, Michael Søndergaard, Michel Jung" __contact__ = "admin@faforever.com" @@ -134,6 +135,7 @@ "MessageQueueService", "OAuthService", "PartyService", + "TournamentService", "PlayerService", "RatingService", "RatingService", @@ -145,6 +147,7 @@ "run_control_server", ) + logger = logging.getLogger("server") if config.ENABLE_METRICS: @@ -197,6 +200,7 @@ def __init__( party_service=self.services["party_service"], rating_service=self.services["rating_service"], oauth_service=self.services["oauth_service"], + tournament_service=self.services["tournament_service"], ) def write_broadcast( diff --git a/server/game_service.py b/server/game_service.py index 6f5c6e741..30fb1ea84 100644 --- a/server/game_service.py +++ b/server/game_service.py @@ -1,7 +1,7 @@ """ Manages the lifecycle of active games """ - +import asyncio from collections import Counter from typing import Optional, Union, ValuesView @@ -9,7 +9,6 @@ from sqlalchemy import select from server.config import config - from . import metrics from .core import Service from .db import FAFDatabase @@ -145,7 +144,7 @@ def create_game( visibility=VisibilityState.PUBLIC, host: Optional[Player] = None, name: Optional[str] = None, - mapname: Optional[str] = None, + map_name: Optional[str] = None, password: Optional[str] = None, matchmaker_queue_id: Optional[int] = None, **kwargs @@ -159,7 +158,7 @@ def create_game( "id_": game_id, "host": host, "name": name, - "map_": mapname, + "map_name": map_name, "game_mode": game_mode, "game_service": self, "game_stats_service": self.game_stats_service, @@ -262,3 +261,8 @@ async def publish_game_results(self, game_results: EndedGameInfo): metrics.rated_games.labels(game_results.rating_type).inc() # TODO: Remove when rating service starts listening to message queue await self._rating_service.enqueue(result_dict) + + +class NotConnectedError(asyncio.TimeoutError): + def __init__(self, players: list[Player]): + self.players = players diff --git a/server/gameconnection.py b/server/gameconnection.py index 71052f81c..514668e19 100644 --- a/server/gameconnection.py +++ b/server/gameconnection.py @@ -236,9 +236,8 @@ async def handle_game_option(self, key: str, value: Any): raw = repr(value) self.game.map_scenario_path = \ raw.replace("\\", "/").replace("//", "/").replace("'", "") - self.game.map_file_path = "maps/{}.zip".format( + self.game.map_name = \ self.game.map_scenario_path.split("/")[2].lower() - ) elif key == "Title": with contextlib.suppress(ValueError): self.game.name = value diff --git a/server/games/__init__.py b/server/games/__init__.py index 87c7603f5..7d885496f 100644 --- a/server/games/__init__.py +++ b/server/games/__init__.py @@ -8,6 +8,7 @@ from .custom_game import CustomGame from .game import Game, GameError from .ladder_game import LadderGame +from .tournament_game import TournamentGame from .typedefs import ( FeaturedModType, GameConnectionState, @@ -41,6 +42,7 @@ class FeaturedMod(NamedTuple): "GameType", "InitMode", "LadderGame", + "TournamentGame", "ValidityState", "Victory", "VisibilityState", diff --git a/server/games/game.py b/server/games/game.py index 7cfdea5fd..3ab763aae 100644 --- a/server/games/game.py +++ b/server/games/game.py @@ -1,6 +1,7 @@ import asyncio import json import logging +import re import time from collections import defaultdict from typing import Any, Iterable, Optional @@ -47,6 +48,9 @@ class GameError(Exception): pass +MAP_FILE_PATH_PATTERN = re.compile(r"maps/(.+)\.zip") + + class Game: """ Object that lasts for the lifetime of a game on FAF. @@ -62,7 +66,7 @@ def __init__( game_stats_service: "GameStatsService", host: Optional[Player] = None, name: str = "None", - map_: str = "SCMP_007", + map_name: str = "SCMP_007", game_mode: str = FeaturedModType.FAF, matchmaker_queue_id: Optional[int] = None, rating_type: Optional[str] = None, @@ -89,7 +93,7 @@ def __init__( self.host = host self.name = name self.map_id = None - self.map_file_path = f"maps/{map_}.zip" + self.map_name = map_name self.map_scenario_path = None self.password = None self._players_at_launch: list[Player] = [] @@ -153,6 +157,30 @@ def set_name_unchecked(self, value: str): max_len = game_stats.c.gameName.type.length self._name = value[:max_len] + @property + def map_name(self): + return self._map_name + + @map_name.setter + def map_name(self, name: str): + self._map_name = name + self._map_file_path = f"maps/{name}.zip" + + @property + def map_file_path(self): + return self._map_file_path + + @map_file_path.setter + def map_file_path(self, path: str): + m = re.match(MAP_FILE_PATH_PATTERN, path) + if m is None: + raise ValueError( + "Map path must start with 'maps/' and end with '.zip'" + ) + + self._map_name = m.group(1) + self._map_file_path = path + @property def armies(self) -> frozenset[int]: return frozenset( @@ -253,7 +281,7 @@ def get_team_sets(self) -> list[set[Player]]: raise GameError( "Missing team for at least one player. (player, team): {}" .format([(player, self.get_player_option(player.id, "Team")) - for player in self.players]) + for player in self.players]) ) teams = defaultdict(set) @@ -439,7 +467,7 @@ async def on_game_finish(self): await self.process_game_results() self._process_pending_army_stats() - except Exception: # pragma: no cover + except Exception: # pragma: no cover self._logger.exception("Error during game end") finally: self.state = GameState.ENDED @@ -565,6 +593,7 @@ async def persist_results(self): def get_basic_info(self) -> BasicGameInfo: return BasicGameInfo( self.id, + self.game_type, self.rating_type, self.map_id, self.game_mode, @@ -936,10 +965,7 @@ def map_folder_name(self) -> str: try: return str(self.map_scenario_path.split("/")[2]).lower() except (IndexError, AttributeError): - if self.map_file_path: - return self.map_file_path[5:-4].lower() - else: - return "scmp_009" + return self.map_name def __eq__(self, other): if not isinstance(other, Game): @@ -955,3 +981,9 @@ def __str__(self) -> str: f"Game({self.id}, {self.host.login if self.host else ''}, " f"{self.map_file_path})" ) + + def wait_launched(self, param): + pass + + def wait_hosted(self, param): + pass diff --git a/server/games/tournament_game.py b/server/games/tournament_game.py new file mode 100644 index 000000000..b8bb345dc --- /dev/null +++ b/server/games/tournament_game.py @@ -0,0 +1,12 @@ +import logging + +from . import LadderGame +from .typedefs import GameType + +logger = logging.getLogger(__name__) + + +class TournamentGame(LadderGame): + """Class for tournament games""" + + game_type = GameType.TOURNAMENT diff --git a/server/games/typedefs.py b/server/games/typedefs.py index 8d8142f7d..cc72b910a 100644 --- a/server/games/typedefs.py +++ b/server/games/typedefs.py @@ -33,6 +33,7 @@ class GameType(Enum): COOP = "coop" CUSTOM = "custom" MATCHMAKER = "matchmaker" + TOURNAMENT = "tournament" @unique @@ -90,12 +91,14 @@ class BasicGameInfo(NamedTuple): Holds basic information about a game that does not change after launch. Fields: - game_id: id of the game + - game_type: type of the game - rating_type: str (e.g. "ladder1v1") - map_id: id of the map used - game_mode: name of the featured mod """ game_id: int + game_type: GameType rating_type: Optional[str] map_id: int game_mode: str diff --git a/server/ladder_service/ladder_service.py b/server/ladder_service/ladder_service.py index 53dfe2f6b..5f23a0520 100644 --- a/server/ladder_service/ladder_service.py +++ b/server/ladder_service/ladder_service.py @@ -34,8 +34,8 @@ matchmaker_queue_map_pool ) from server.decorators import with_logger -from server.game_service import GameService -from server.games import InitMode, LadderGame +from server.game_service import GameService, NotConnectedError +from server.games import Game, InitMode, LadderGame from server.games.ladder_game import GameClosedError from server.ladder_service.game_name import game_name from server.ladder_service.violation_service import ViolationService @@ -563,7 +563,7 @@ def get_player_mean(player: Player) -> float: if game_options: game.gameOptions.update(game_options) - mapname = re.match("maps/(.+).zip", map_path).group(1) + map_name = re.match("maps/(.+).zip", map_path).group(1) # FIXME: Database filenames contain the maps/ prefix and .zip suffix. # Really in the future, just send a better description @@ -571,7 +571,7 @@ def get_player_mean(player: Player) -> float: def make_game_options(player: Player) -> GameLaunchOptions: return GameLaunchOptions( - mapname=mapname, + mapname=map_name, expected_players=len(all_players), game_options=game_options, team=game.get_player_option(player.id, "Team"), @@ -579,7 +579,7 @@ def make_game_options(player: Player) -> GameLaunchOptions: map_position=game.get_player_option(player.id, "StartSpot") ) - await self.launch_match(game, host, all_guests, make_game_options) + await self.launch_server_made_game(game, host, all_guests, make_game_options) self._logger.debug("Ladder game launched successfully %s", game) metrics.matches.labels(queue.name, MatchLaunch.SUCCESSFUL).inc() except Exception as e: @@ -623,9 +623,9 @@ def make_game_options(player: Player) -> GameLaunchOptions: ) self.violation_service.register_violations(abandoning_players) - async def launch_match( + async def launch_server_made_game( self, - game: LadderGame, + game: Game, host: Player, guests: list[Player], make_game_options: Callable[[Player], GameLaunchOptions] @@ -725,8 +725,3 @@ def on_connection_lost(self, conn: "LobbyConnection") -> None: async def shutdown(self): for queue in self.queues.values(): queue.shutdown() - - -class NotConnectedError(asyncio.TimeoutError): - def __init__(self, players: list[Player]): - self.players = players diff --git a/server/lobbyconnection.py b/server/lobbyconnection.py index 9478876e2..9f8df6a2d 100644 --- a/server/lobbyconnection.py +++ b/server/lobbyconnection.py @@ -58,6 +58,7 @@ from .protocol import DisconnectedError, Protocol from .rating import InclusiveRange, RatingType from .rating_service import RatingService +from .tournament_service import TournamentService from .types import Address, GameLaunchOptions @@ -75,6 +76,7 @@ def __init__( party_service: PartyService, rating_service: RatingService, oauth_service: OAuthService, + tournament_service: TournamentService, ): self._db = database self.geoip_service = geoip @@ -86,6 +88,7 @@ def __init__( self.party_service = party_service self.rating_service = rating_service self.oauth_service = oauth_service + self.tournament_service = tournament_service self._authenticated = False self.player: Optional[Player] = None self.game_connection: Optional[GameConnection] = None @@ -946,7 +949,7 @@ async def command_game_host(self, message): raise ClientError("Title must contain only ascii characters.") mod = message.get("mod") or FeaturedModType.FAF - mapname = message.get("mapname") or "scmp_007" + map_name = message.get("mapname") or "scmp_007" password = message.get("password") game_mode = mod.lower() rating_min = message.get("rating_min") @@ -965,7 +968,7 @@ async def command_game_host(self, message): game_class=game_class, host=self.player, name=title, - mapname=mapname, + map_name=map_name, password=password, rating_type=RatingType.GLOBAL, displayed_rating_range=InclusiveRange(rating_min, rating_max), @@ -973,6 +976,11 @@ async def command_game_host(self, message): ) await self.launch_game(game, is_host=True) + @player_idle("ready up for a tournament game") + async def command_is_ready_response(self, message): + assert isinstance(self.player, Player) + await self.tournament_service.on_is_ready_response(message, self.player) + async def command_match_ready(self, message): """ Replace with full implementation when implemented in client, see: diff --git a/server/message_queue_service.py b/server/message_queue_service.py index ca36ccc55..30cd0531f 100644 --- a/server/message_queue_service.py +++ b/server/message_queue_service.py @@ -4,9 +4,11 @@ import asyncio import json +from typing import Any, Callable import aio_pika from aio_pika import DeliveryMode, ExchangeType +from aio_pika.abc import AbstractIncomingMessage from aio_pika.exceptions import ProbableAuthenticationError from .asyncio_extensions import synchronizedmethod @@ -29,6 +31,8 @@ class MessageQueueService(Service): def __init__(self) -> None: self._connection = None self._channel = None + self._listening_info = [] + self.default_exchange = None self._exchanges = {} self._exchange_types = {} self._is_ready = False @@ -48,9 +52,27 @@ async def initialize(self) -> None: await self._connect() except ConnectionAttemptFailed: return - self._is_ready = True await self._declare_exchange(config.MQ_EXCHANGE_NAME, ExchangeType.TOPIC) + await self._connect_listening_queues() + self._is_ready = True + + async def _connect_listening_queues(self): + for listening_info in self._listening_info: + await self._declare_listening_queue(**listening_info) + + async def listen_to_message(self, queue_name: str, routing_key: str, + callback: Callable[[AbstractIncomingMessage], Any]): + listening_info = { + "queue_name": queue_name, + "routing_key": routing_key, + "callback": callback + } + self._listening_info.append( + listening_info + ) + if self._is_ready: + await self._declare_listening_queue(listening_info) async def _connect(self) -> None: try: @@ -64,6 +86,7 @@ async def _connect(self) -> None: ), loop=asyncio.get_running_loop(), ) + except ConnectionError as e: self._logger.warning( "Unable to connect to RabbitMQ. Is it running?", exc_info=True @@ -128,6 +151,7 @@ async def publish( payload: dict, mandatory: bool = False, delivery_mode: DeliveryMode = DeliveryMode.PERSISTENT, + correlation_id=None ) -> None: if not self._is_ready: self._logger.warning( @@ -140,7 +164,7 @@ async def publish( raise KeyError(f"Unknown exchange {exchange_name}.") message = aio_pika.Message( - json.dumps(payload).encode(), delivery_mode=delivery_mode + json.dumps(payload).encode(), delivery_mode=delivery_mode, correlation_id=correlation_id ) async with self._channel.transaction(): @@ -167,4 +191,12 @@ async def reconnect(self) -> None: await self._declare_exchange( exchange_name, self._exchange_types[exchange_name] ) + + await self._connect_listening_queues() self._is_ready = True + + async def _declare_listening_queue(self, listening_info): + queue = await self._channel.declare_queue(listening_info["queue_name"], auto_delete=False, durable=True) + # Binding queue + await queue.bind(self._exchanges[config.MQ_EXCHANGE_NAME], listening_info["routing_key"]) + await queue.consume(callback=listening_info["callback"], no_ack=True) diff --git a/server/metrics.py b/server/metrics.py index 52b725c1f..ed6d7aef2 100644 --- a/server/metrics.py +++ b/server/metrics.py @@ -14,6 +14,15 @@ class MatchLaunch: info = Info("build", "Information collected on server start") +# ========== +# Tournament +# ========== +matches_tournament = Counter( + "server_tournament_matches_total", + "Number of tournament matches made", + ["status"] +) + # ========== # Matchmaker # ========== diff --git a/server/players.py b/server/players.py index 13529005e..9800d01d9 100644 --- a/server/players.py +++ b/server/players.py @@ -21,6 +21,7 @@ class PlayerState(Enum): JOINING = 4 SEARCHING_LADDER = 5 STARTING_AUTOMATCH = 6 + STARTING_TOURNAMENT = 7 class Player: diff --git a/server/rating_service/rating_service.py b/server/rating_service/rating_service.py index 6b81bd559..611517483 100644 --- a/server/rating_service/rating_service.py +++ b/server/rating_service/rating_service.py @@ -20,7 +20,6 @@ from server.metrics import rating_service_backlog from server.player_service import PlayerService from server.rating import Leaderboard, PlayerRatings, Rating, RatingType - from .game_rater import AdjustmentGameRater, GameRater, GameRatingError from .typedefs import ( GameRatingResult, @@ -141,6 +140,10 @@ async def _handle_rating_queue(self) -> None: async def _rate(self, summary: GameRatingSummary) -> None: assert self._rating_type_ids is not None + if summary.rating_type is None: + self._logger.debug(f"Not processing game {summary.game_id} since it is not rated.") + return + if summary.rating_type not in self._rating_type_ids: raise GameRatingError(f"Unknown rating type {summary.rating_type}.") diff --git a/server/tournament_service.py b/server/tournament_service.py new file mode 100644 index 000000000..16c115cd3 --- /dev/null +++ b/server/tournament_service.py @@ -0,0 +1,279 @@ +import asyncio +import datetime +import json + +import aio_pika + +from server import games, metrics +from server.config import config + +from .core import Service +from .decorators import with_logger +from .exceptions import ClientError +from .game_service import GameService +from .games import InitMode +from .games.ladder_game import GameClosedError +from .ladder_service.ladder_service import LadderService, NotConnectedError +from .message_queue_service import MessageQueueService +from .player_service import PlayerService +from .players import Player, PlayerState +from .timing import at_interval, datetime_now +from .tournaments.tournament_game import TournamentGameInfo, TournamentGameState +from .types import GameLaunchOptions + + +def _notify_players(game: TournamentGameInfo): + for player in game: + player.state = PlayerState.STARTING_TOURNAMENT + player.write_message( + { + "command": "is_ready", + "featured_mod": game.featured_mod, + "request_id": game.request_id, + "response_time_seconds": game.response_time_seconds, + "game_name": game.name + } + ) + game.state = TournamentGameState.CONFIRMATION_PENDING + + +@with_logger +class TournamentService(Service): + """ + Service responsible for managing the tournament or galactic war games. + """ + + def __init__(self, game_service: GameService, message_queue_service: MessageQueueService, + player_service: PlayerService, ladder_service: LadderService): + self._update_task = None + self.game_service = game_service + self.ladder_service = ladder_service + self.player_service = player_service + self.message_queue_service = message_queue_service + self._games: set[TournamentGameInfo] = set() + + async def initialize(self): + self._update_task = at_interval(5, self.update_dirties) + await self.message_queue_service.listen_to_message("faf-lobby.tourneylauncher.createGame", + "request.match.create", self._create_game) + + async def shutdown(self): + self._update_task.stop() + + async def update_dirties(self): + await self._check_for_timed_out_games() + + async def _check_for_timed_out_games(self): + for game in self._games.copy(): + if game.state == TournamentGameState.CONFIRMATION_PENDING and game.is_confirmation_overdue(): + self._logger.info("Ready responses from players missing, canceling tournament game %s", game.request_id) + game.players_causing_cancel = set(player.id for player in game.players) - game.players_ready_ids + game.state = TournamentGameState.PLAYER_NOT_CONFIRMING + await self._remove_and_cancel_game(game) + return + if game.state.is_done(): + self._games.remove(game) + return + if (datetime_now() - game.created_time).hours > 10: + self._games.remove(game) + self._logger.warning("Deleting leaked game with id: %s", game.request_id) + + async def _remove_and_cancel_game(self, game: TournamentGameInfo, make_idle=True): + for player in game.players: + player.write_message({ + "command": "match_cancelled", + }) + self._games.remove(game) + if make_idle: + for player in game.players: + player.state = PlayerState.IDLE + await self.message_queue_service.publish( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.failed", + { + "error_code": game.get_error_code(), + "players_causing_cancel": game.players_causing_cancel + }, + correlation_id=game.request_id + ) + metrics.matches_tournament.labels("failed").inc() + + async def _game_created(self, game: games.TournamentGame, tournament_game: TournamentGameInfo): + tournament_game.state = TournamentGameState.RUNNING + self._games.remove(tournament_game) + await self.message_queue_service.publish( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.success", + { + "game_id": game.id + }, + correlation_id=tournament_game.request_id + ) + metrics.matches_tournament.labels("success").inc() + + async def _create_game(self, message: aio_pika.abc.AbstractIncomingMessage): + metrics.matches_tournament.labels("requested").inc() + try: + self._logger.info("Received Tournament game message") + await self._process_create_game(message) + except Exception: + self._logger.exception("Unknown failure creating tournament game") + await self.message_queue_service.publish( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.failed", + { + "request_id": message.correlation_id, + "error_code": "OTHER" + }, + ) + + async def _process_create_game(self, message): + body = message.body + body = json.loads(body) + game = TournamentGameInfo(**body) + assert game.request_id == message.correlation_id + if not await self._fetch_players(game): + await self._remove_and_cancel_game(game, make_idle=False) + return + _notify_players(game) + + async def _fetch_players(self, game): + if not game.participants: + self._logger.warning("Tournament game requested with empty player list") + return False + await self.add_tournament_game(game) + for participant in game.participants: + player_id = participant["player_id"] + player = self.player_service[player_id] + if player is None: + self._logger.warning("Tournament game requested with player id(%s) that could not be found", player_id) + game.state = TournamentGameState.PLAYER_NOT_ONLINE + game.players_causing_cancel.add(player_id) + continue + if not player.state == PlayerState.IDLE: + self._logger.warning("Tournament game requested with player id(%s), player not idle", player_id) + game.state = TournamentGameState.PLAYER_NOT_IDLE + game.players_causing_cancel.add(player_id) + game.players.append(player) + return game.state == TournamentGameState.SCHEDULED + + async def add_tournament_game(self, game): + self._games.add(game) + + async def on_is_ready_response(self, message, player): + game = await self._get_game_for_request_id(message["request_id"]) + if game is None: + raise ClientError("You try to ready up for a game that does not exist") + if game.state != TournamentGameState.CONFIRMATION_PENDING: + raise ClientError("You try to ready up for a game that is not waiting for confirmation") + if player not in game: + raise ClientError("You try to ready up for a game that you are not in") + if player.id in game.players_ready_ids: + return + await self.add_player_to_ready_list(game, player) + + async def add_player_to_ready_list(self, game, player): + game.players_ready_ids.add(player.id) + if game.is_ready(): + await self._launch(game) + + async def _get_game_for_request_id(self, request_id) -> TournamentGameInfo: + for game in self._games.copy(): + if game.request_id == request_id: + return game + + async def _launch(self, tournament_game: TournamentGameInfo): + tournament_game.state = TournamentGameState.STARTING + self._logger.debug( + "Starting %s game with", + tournament_game.name + ) + game = None + try: + host = tournament_game.players[0] + all_players = tournament_game.players + all_guests = all_players[1:] + + game = self.game_service.create_game( + game_class=games.tournament_game.TournamentGame, + game_mode=tournament_game.featured_mod, + host=host, + name="Matchmaker Game", + max_players=len(all_players), + map_name=tournament_game.map_name + ) + game.init_mode = InitMode.AUTO_LOBBY + game.set_name_unchecked(tournament_game.name) + + for player in all_players: + player.state = PlayerState.STARTING_AUTOMATCH + for player in all_players: + # FA uses lua and lua arrays are 1-indexed + slot = tournament_game.get_slot_of_player(player) + # 2 if even, 3 if odd + team = tournament_game.get_team_of_player(player) + player.game = game + + # Set player options without triggering the logic for + # determining that players have actually connected to the game. + game._player_options[player.id]["Faction"] = tournament_game.get_faction_of_player(player) + game._player_options[player.id]["Team"] = team + game._player_options[player.id]["StartSpot"] = slot + game._player_options[player.id]["Army"] = slot + game._player_options[player.id]["Color"] = slot + + game_options = tournament_game.game_options + if game_options: + game.gameOptions.update(game_options) + + self._logger.debug("Starting tournament game: %s", game) + + def make_game_options(player: Player) -> GameLaunchOptions: + return GameLaunchOptions( + mapname=tournament_game.map_name, + expected_players=len(all_players), + game_options=game_options, + team=game.get_player_option(player.id, "Team"), + faction=game.get_player_option(player.id, "Faction"), + map_position=game.get_player_option(player.id, "StartSpot") + ) + + await self.ladder_service.launch_server_made_game(game, host, all_guests, make_game_options) + self._logger.debug("Tournament game launched successfully %s", game) + await self._game_created(game, tournament_game) + except Exception as e: + abandoning_players = [] + if isinstance(e, NotConnectedError): + self._logger.info( + "Tournament game failed to start! %s setup timed out", + game + ) + # TODO: metrics.matches.labels(queue.name, MatchLaunch.TIMED_OUT).inc() + abandoning_players = e.players + tournament_game.state = TournamentGameState.PLAYER_NOT_ONLINE + elif isinstance(e, GameClosedError): + self._logger.info( + "Tournament game %s failed to start! " + "Player %s closed their game instance", + game, e.player + ) + # TODO: metrics.matches.labels(queue.name, MatchLaunch.ABORTED_BY_PLAYER).inc() + abandoning_players = [e.player] + tournament_game.state = TournamentGameState.PLAYER_NOT_STARTING + else: + # All timeout errors should be transformed by the match starter. + assert not isinstance(e, asyncio.TimeoutError) + + self._logger.exception("Tournament game failed to start %s", game) + # TODO: metrics.matches.labels(queue.name, MatchLaunch.ERRORED).inc() + tournament_game.state = TournamentGameState.PLAYER_NOT_CONNECTING + if game: + await game.on_game_finish() + + if abandoning_players: + self._logger.info( + "Players failed to connect: %s", + abandoning_players + ) + tournament_game.players_causing_cancel = set([player.id for player in abandoning_players]) + await self._remove_and_cancel_game(tournament_game) diff --git a/server/tournaments/__init__.py b/server/tournaments/__init__.py new file mode 100644 index 000000000..eb897648d --- /dev/null +++ b/server/tournaments/__init__.py @@ -0,0 +1,8 @@ +""" +The tournament related classes +""" +from server.tournaments.tournament_game import TournamentGameInfo + +__all__ = [ + "TournamentGameInfo" +] diff --git a/server/tournaments/tournament_game.py b/server/tournaments/tournament_game.py new file mode 100644 index 000000000..159ac6071 --- /dev/null +++ b/server/tournaments/tournament_game.py @@ -0,0 +1,77 @@ +from datetime import datetime +from enum import Enum + +from server.players import Player +from server.timing import datetime_now + + +class TournamentGameState(Enum): + SCHEDULED = 1 + CONFIRMATION_PENDING = 2 + STARTING = 3 + RUNNING = 4 + PLAYER_NOT_ONLINE = 5 + PLAYER_NOT_IDLE = 6 + PLAYER_NOT_CONFIRMING = 7 + PLAYER_NOT_STARTING = 8 + PLAYER_NOT_CONNECTING = 9 + + def is_done(self): + return self not in { + TournamentGameState.SCHEDULED, + TournamentGameState.CONFIRMATION_PENDING, + TournamentGameState.STARTING + } + + +class TournamentGameInfo: + def __init__( + self, + request_id, + game_name, + participants, + featured_mod, + map_name, + game_options, + ): + self.created_time = datetime_now() + self.players = [] + self.players_ready_ids = set() + self.state = TournamentGameState.SCHEDULED + self.request_id = request_id + self.name = game_name + self.participants = participants + self.featured_mod = featured_mod + self.map_name = map_name + self.game_options = game_options + self.players_causing_cancel = set() + self.response_time_seconds = 30 + self.participants_by_id = {participant.id: participant for participant in participants} + + def __contains__(self, player: Player) -> bool: + return player in self.players + + def __iter__(self): + return iter(self.players) + + def is_ready(self) -> bool: + return set(player.id for player in self.players) == self.players_ready_ids + + def is_confirmation_overdue(self): + time_passed = datetime.utcnow() - self.created_time + return time_passed.seconds > self.response_time_seconds + 5 + + def get_team_of_player(self, player) -> int: + return self.get_participant_of_player(player)["team"] + + def get_slot_of_player(self, player) -> int: + return self.get_participant_of_player(player)["slot"] + + def get_faction_of_player(self, player) -> int: + return self.get_participant_of_player(player)["faction"] + + def get_participant_of_player(self, player): + return self.participants_by_id[player.id] + + def get_error_code(self): + return self.state.name diff --git a/tests/conftest.py b/tests/conftest.py index a837f94f7..d6b6c66df 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ import hypothesis import pytest +from server import LadderService, TournamentService, ViolationService from server.api.api_accessor import ApiAccessor from server.api.oauth_session import OAuth2Session from server.config import TRACE, config @@ -118,6 +119,7 @@ def caplog_context(): be reset between examples. Use this fixture instead to ensure that cleanup happens every time the test function is called. """ + @contextmanager def make_caplog_context(request): result = pytest.LogCaptureFixture(request.node, _ispytest=True) @@ -148,6 +150,7 @@ async def _test_data(): async def global_database(request) -> FAFDatabase: def opt(val): return request.config.getoption(val) + host, user, pw, name, port = ( opt("--mysql_host"), opt("--mysql_username"), @@ -321,6 +324,29 @@ async def rating_service(database, player_service, message_queue_service): await service.shutdown() +@pytest.fixture +async def tournament_service(game_service: GameService, player_service: PlayerService, + message_queue_service: MessageQueueService, + ladder_service: LadderService) -> TournamentService: + service = TournamentService(game_service, message_queue_service, player_service, ladder_service) + await service.initialize() + + yield service + + await service.shutdown() + + +@pytest.fixture +async def ladder_service(database, game_service: GameService, player_service: PlayerService, + violation_service: ViolationService) -> LadderService: + service = LadderService(database, game_service, violation_service) + await service.initialize() + + yield service + + await service.shutdown() + + @pytest.fixture async def message_queue_service(): service = MessageQueueService() @@ -381,6 +407,7 @@ def make( team_size=team_size, **kwargs ) + return make diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index bea00ca98..ef3cb02d9 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -18,6 +18,7 @@ OAuthService, PartyService, ServerInstance, + TournamentService, ViolationService ) from server.config import config @@ -60,10 +61,20 @@ async def party_service(game_service): await service.shutdown() +@pytest.fixture +async def tournament_service(game_service, message_queue_service, player_service, ladder_service): + service = TournamentService(game_service, message_queue_service, player_service, ladder_service) + await service.initialize() + + yield service + + await service.shutdown() + + @pytest.fixture async def broadcast_service( - message_queue_service, - game_service, + message_queue_service, + game_service, player_service, ): # The reference to the ServerInstance needs to be established later @@ -119,17 +130,18 @@ async def lobby_server( event_loop, database, broadcast_service, - player_service, - game_service, - geoip_service, - ladder_service, - rating_service, - message_queue_service, - party_service, - oauth_service, - violation_service, - policy_server, - jwks_server + player_service, + game_service, + geoip_service, + ladder_service, + rating_service, + message_queue_service, + party_service, + oauth_service, + violation_service, + policy_server, + jwks_server, + tournament_service ): mock_policy = mock.patch( "server.lobbyconnection.config.FAF_POLICY_SERVER_BASE_URL", @@ -153,6 +165,7 @@ async def lobby_server( "party_service": party_service, "oauth_service": oauth_service, "violation_service": violation_service, + "tournament_service": tournament_service, }) # Set up the back reference broadcast_service.server = instance diff --git a/tests/integration_tests/test_message_queue_service.py b/tests/integration_tests/test_message_queue_service.py index d14a5b98b..7f64ebaee 100644 --- a/tests/integration_tests/test_message_queue_service.py +++ b/tests/integration_tests/test_message_queue_service.py @@ -1,8 +1,10 @@ import asyncio +import json from unittest import mock import aio_pika import pytest +from aio_pika.abc import AbstractIncomingMessage from server.config import config from server.decorators import with_logger @@ -175,3 +177,21 @@ async def test_declaring_exchange_without_initialization(mq_uninit_service): assert service._is_ready assert service._connection is not None assert service._exchanges.get(exchange_name) is not None + + +async def test_listening_to_incoming_messages(mq_service: MessageQueueService): + callback = Callback() + await mq_service.listen_to_message("test", "routing.test", callback.callback_listening) + await mq_service.declare_exchange(config.MQ_EXCHANGE_NAME) + await mq_service.publish(config.MQ_EXCHANGE_NAME, "routing.test", {"test": "test"}) + await callback.called_future + + +class Callback: + + def __init__(self) -> None: + self.called_future = asyncio.Future() + + def callback_listening(self, message: AbstractIncomingMessage): + assert json.loads(message.body)["test"] == "test" + self.called_future.set_result(None) diff --git a/tests/integration_tests/test_server_instance.py b/tests/integration_tests/test_server_instance.py index 52e8e6d7e..17d27124b 100644 --- a/tests/integration_tests/test_server_instance.py +++ b/tests/integration_tests/test_server_instance.py @@ -31,6 +31,7 @@ async def test_multiple_contexts( party_service, rating_service, oauth_service, + tournament_service, event_loop ): config.USE_POLICY_SERVER = False @@ -49,7 +50,8 @@ async def test_multiple_contexts( "ladder_service": ladder_service, "rating_service": rating_service, "party_service": party_service, - "oauth_service": oauth_service + "oauth_service": oauth_service, + "tournament_service": tournament_service, } ) broadcast_service.server = instance diff --git a/tests/integration_tests/test_servercontext.py b/tests/integration_tests/test_servercontext.py index 832e4f625..a8fb349a5 100644 --- a/tests/integration_tests/test_servercontext.py +++ b/tests/integration_tests/test_servercontext.py @@ -61,6 +61,7 @@ def make_connection() -> LobbyConnection: party_service=mock.Mock(), rating_service=mock.Mock(), oauth_service=mock.Mock(), + tournament_service=mock.Mock(), ) ctx = ServerContext("TestServer", make_connection, [mock_service]) diff --git a/tests/integration_tests/test_tournament_service.py b/tests/integration_tests/test_tournament_service.py new file mode 100644 index 000000000..82e13a172 --- /dev/null +++ b/tests/integration_tests/test_tournament_service.py @@ -0,0 +1,46 @@ +import pytest + +from server import TournamentService +from server.config import config +from server.message_queue_service import MessageQueueService +from tests.integration_tests.conftest import ( + connect_and_sign_in, + read_until_command +) + +pytestmark = pytest.mark.rabbitmq + + +async def test_create_game_by_message(message_queue_service: MessageQueueService, + tournament_service: TournamentService, lobby_server): + _, _, proto1 = await connect_and_sign_in( + ("test", "test_password"), lobby_server + ) + await message_queue_service.declare_exchange(config.MQ_EXCHANGE_NAME) + await message_queue_service.publish(config.MQ_EXCHANGE_NAME, "request.match.create", + { + "request_id": "9124e8c9-c62f-43c3-bb64-94f3093f2997", + "game_name": "My game name", + "participants": [ + { + "team": 1, + "slot": 1, + "faction": 1, + "player_id": 1 + } + ], + "featured_mod": "faf", + "map_name": "SCMP_001", + "game_options": { + "test": "test" + } + }, correlation_id="9124e8c9-c62f-43c3-bb64-94f3093f2997" + ) + msg = await read_until_command(proto1, "is_ready") + assert msg == { + 'command': 'is_ready', + 'featured_mod': 'faf', + 'game_name': 'My game name', + 'request_id': '9124e8c9-c62f-43c3-bb64-94f3093f2997', + 'response_time_seconds': 30 + } diff --git a/tests/unit_tests/test_coop_game.py b/tests/unit_tests/test_coop_game.py index ce16aaf06..78753ebda 100644 --- a/tests/unit_tests/test_coop_game.py +++ b/tests/unit_tests/test_coop_game.py @@ -9,7 +9,7 @@ async def test_create_coop_game(database): database=database, host=mock.Mock(), name="Some game", - map_="some_map", + map_name="some_map", game_mode="coop", game_service=mock.Mock(), game_stats_service=mock.Mock() diff --git a/tests/unit_tests/test_gameconnection.py b/tests/unit_tests/test_gameconnection.py index 7d17c82f9..837ec4a38 100644 --- a/tests/unit_tests/test_gameconnection.py +++ b/tests/unit_tests/test_gameconnection.py @@ -415,7 +415,8 @@ async def test_handle_action_GameOption( assert game.max_players == 7 # I don't know what these paths actually look like await game_connection.handle_action("GameOption", ["ScenarioFile", "C:\\Maps\\Some_Map"]) - assert game.map_file_path == "maps/some_map.zip" + assert game.map_name == "some_map" + assert game.map_scenario_path == "C:/Maps/Some_Map" await game_connection.handle_action("GameOption", ["Title", "All welcome"]) assert game.name == "All welcome" await game_connection.handle_action("GameOption", ["ArbitraryKey", "ArbitraryValue"]) diff --git a/tests/unit_tests/test_games_service.py b/tests/unit_tests/test_games_service.py index 6d3525362..51a7b340a 100644 --- a/tests/unit_tests/test_games_service.py +++ b/tests/unit_tests/test_games_service.py @@ -25,7 +25,7 @@ async def test_create_game(players, game_service): game_mode="faf", host=players.hosting, name="Test", - mapname="SCMP_007", + map_name="SCMP_007", password=None ) assert game is not None @@ -42,7 +42,7 @@ async def test_all_games(players, game_service): game_mode="faf", host=players.hosting, name="Test", - mapname="SCMP_007", + map_name="SCMP_007", password=None ) assert game in game_service.pending_games @@ -68,7 +68,7 @@ async def test_create_game_other_gamemode(players, game_service): game_mode="labwars", host=players.hosting, name="Test", - mapname="SCMP_007", + map_name="SCMP_007", password=None ) assert game is not None diff --git a/tests/unit_tests/test_lobbyconnection.py b/tests/unit_tests/test_lobbyconnection.py index fb89572ce..740fc3c6e 100644 --- a/tests/unit_tests/test_lobbyconnection.py +++ b/tests/unit_tests/test_lobbyconnection.py @@ -6,6 +6,7 @@ from aiohttp import web from sqlalchemy import and_, select +from server import TournamentService from server.config import config from server.db.models import ban, friends_and_foes from server.exceptions import BanError, ClientError @@ -104,7 +105,8 @@ def lobbyconnection( ladder_service=mock.create_autospec(LadderService), party_service=mock.create_autospec(PartyService), oauth_service=mock.create_autospec(OAuthService), - rating_service=rating_service + rating_service=rating_service, + tournament_service=mock.create_autospec(TournamentService) ) lc.player = mock_player @@ -250,7 +252,7 @@ async def test_command_game_host_creates_game( "host": players.hosting, "visibility": VisibilityState.PUBLIC, "password": test_game_info["password"], - "mapname": test_game_info["mapname"], + "map_name": test_game_info["mapname"], "rating_type": RatingType.GLOBAL, "displayed_rating_range": InclusiveRange(None, None), "enforce_rating_range": False diff --git a/tests/unit_tests/test_tournament_service.py b/tests/unit_tests/test_tournament_service.py new file mode 100644 index 000000000..e26089674 --- /dev/null +++ b/tests/unit_tests/test_tournament_service.py @@ -0,0 +1,195 @@ +from unittest import mock + +import pytest +from aio_pika.abc import AbstractIncomingMessage + +from server import ( + GameService, + LadderService, + LobbyConnection, + MessageQueueService, + PlayerService, + TournamentService, + config +) +from server.players import PlayerState +from server.tournaments.tournament_game import ( + TournamentGameInfo, + TournamentGameState +) + + +async def test_create_tournament_game(game_service: GameService, message_queue_service: MessageQueueService, + player_service: PlayerService, ladder_service: LadderService, player_factory): + player = player_factory(player_id=1) + mock_lconn = mock.create_autospec(LobbyConnection) + player.lobby_connection = mock_lconn + player_service[player.id] = player + tournament_service: TournamentService = TournamentService(game_service, message_queue_service, player_service, + ladder_service) + + message = mock.Mock(AbstractIncomingMessage) + message.body = """ + { + "request_id": "9124e8c9-c62f-43c3-bb64-94f3093f2997", + "game_name": "My game name", + "participants": [ + { + "team": 1, + "slot": 1, + "faction": 1, + "player_id": 1 + } + ], + "featured_mod": "faf", + "map_name": "SCMP_001", + "game_options": { + "test": "test" + } + } + """ + message.correlation_id = "9124e8c9-c62f-43c3-bb64-94f3093f2997" + await tournament_service._create_game( + message + ) + assert next(iter(tournament_service._games)).name == "My game name" + assert next(iter(tournament_service._games)).state == TournamentGameState.CONFIRMATION_PENDING + mock_lconn.write.assert_called_with( + { + 'command': 'is_ready', + 'featured_mod': 'faf', + 'game_name': 'My game name', + 'request_id': '9124e8c9-c62f-43c3-bb64-94f3093f2997', + 'response_time_seconds': 30 + } + ) + assert not mock_lconn.send_warning.called + assert player.state == PlayerState.STARTING_TOURNAMENT + + +async def test_create_tournament_game_player_not_idle(game_service: GameService, + player_service: PlayerService, ladder_service: LadderService, + player_factory): + player = player_factory(player_id=1) + player.state = PlayerState.PLAYING + mock_lconn = mock.create_autospec(LobbyConnection) + player.lobby_connection = mock_lconn + player_service[player.id] = player + message_queue_mock = mock.create_autospec(MessageQueueService) + tournament_service: TournamentService = TournamentService(game_service, message_queue_mock, player_service, + ladder_service) + + message = mock.Mock(AbstractIncomingMessage) + message.body = """ + { + "request_id": "9124e8c9-c62f-43c3-bb64-94f3093f2997", + "game_name": "My game name", + "participants": [ + { + "team": 1, + "slot": 1, + "faction": 1, + "player_id": 1 + } + ], + "featured_mod": "faf", + "map_name": "SCMP_001", + "game_options": { + "test": "test" + } + } + """ + message.correlation_id = "9124e8c9-c62f-43c3-bb64-94f3093f2997" + await tournament_service._create_game( + message + ) + message_queue_mock.publish.assert_called_with( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.failed", + { + "error_code": "PLAYER_NOT_IDLE", + "players_causing_cancel": {1} + }, + correlation_id="9124e8c9-c62f-43c3-bb64-94f3093f2997" + ) + assert player.state == PlayerState.PLAYING + + +@pytest.fixture +def tournament_game(): + tournament_game: TournamentGameInfo = mock.create_autospec(TournamentGameInfo) + tournament_game.request_id = "9124e8c9-c62f-43c3-bb64-94f3093f2997" + tournament_game.name = "My game name" + tournament_game.map_name = "SCMP_001" + tournament_game.players_causing_cancel = set() + tournament_game.featured_mod = "faf" + tournament_game.game_options = {} + tournament_game.state = TournamentGameState.CONFIRMATION_PENDING + tournament_game.players_ready_ids = set() + tournament_game.__contains__ = lambda x, y: True + return tournament_game + + +class Any: + def __eq__(self, other): + return True + + +async def test_tournament_on_is_ready_response(game_service: GameService, player_service: PlayerService, + player_factory, tournament_game: TournamentGameInfo): + player = player_factory(player_id=1) + mock_lconn = mock.create_autospec(LobbyConnection) + player.lobby_connection = mock_lconn + player_service[player.id] = player + message_queue_mock = mock.create_autospec(MessageQueueService) + ladder_service = mock.create_autospec(LadderService) + tournament_service: TournamentService = TournamentService(game_service, message_queue_mock, player_service, + ladder_service) + tournament_service._games.add(tournament_game) + tournament_game.players = [player] + await tournament_service.on_is_ready_response( + {"request_id": "9124e8c9-c62f-43c3-bb64-94f3093f2997"}, player + ) + assert not mock_lconn.send_warning.called + ladder_service.launch_server_made_game.assert_called_once() + message_queue_mock.publish.assert_called_with( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.success", + { + "game_id": Any() + }, + correlation_id="9124e8c9-c62f-43c3-bb64-94f3093f2997" + ) + assert len(tournament_service._games) == 0 + assert player.state == PlayerState.STARTING_AUTOMATCH + + +async def test_tournament_timeout_on_ready(game_service: GameService, player_service: PlayerService, + player_factory, tournament_game: TournamentGameInfo): + player = player_factory(player_id=1) + mock_lconn = mock.create_autospec(LobbyConnection) + player.lobby_connection = mock_lconn + player_service[player.id] = player + message_queue_mock = mock.create_autospec(MessageQueueService) + ladder_service = mock.create_autospec(LadderService) + tournament_service: TournamentService = TournamentService(game_service, message_queue_mock, player_service, + ladder_service) + tournament_game.players = [player] + tournament_game.is_confirmation_overdue.return_value = True + tournament_game.get_error_code.return_value = "PLAYER_NOT_CONFIRMING" + tournament_service._games.add(tournament_game) + await tournament_service.update_dirties() + assert not mock_lconn.send_warning.called + ladder_service.launch_server_made_game.assert_not_called() + message_queue_mock.publish.assert_called_with( + config.MQ_EXCHANGE_NAME, + "tourneylauncher.createGame.failed", + { + "error_code": "PLAYER_NOT_CONFIRMING", + "players_causing_cancel": {1} + }, + correlation_id="9124e8c9-c62f-43c3-bb64-94f3093f2997" + ) + assert len(tournament_service._games) == 0 + assert tournament_game.state == TournamentGameState.PLAYER_NOT_CONFIRMING + assert player.state == PlayerState.IDLE