diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..aabf240
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,795 @@
+# http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+#g-crlf
+end_of_line = lf
+#g-2
+indent_size = 4
+indent_style = space
+#g-false
+insert_final_newline = true
+max_line_length = 100
+#g-2
+tab_width = 4
+#g-4
+ij_continuation_indent_size = 8
+ij_formatter_off_tag = @formatter:off
+ij_formatter_on_tag = @formatter:on
+ij_formatter_tags_enabled = true
+ij_smart_tabs = false
+ij_visual_guides =
+ij_wrap_on_typing = false
+
+[*.dcl]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_declarative_keep_indents_on_empty_lines = false
+
+[*.java]
+ij_java_align_consecutive_assignments = false
+ij_java_align_consecutive_variable_declarations = false
+ij_java_align_group_field_declarations = false
+ij_java_align_multiline_annotation_parameters = false
+ij_java_align_multiline_array_initializer_expression = false
+ij_java_align_multiline_assignment = false
+ij_java_align_multiline_binary_operation = false
+ij_java_align_multiline_chained_methods = false
+ij_java_align_multiline_deconstruction_list_components = true
+ij_java_align_multiline_extends_list = false
+ij_java_align_multiline_for = false
+ij_java_align_multiline_method_parentheses = false
+ij_java_align_multiline_parameters = false
+ij_java_align_multiline_parameters_in_calls = false
+ij_java_align_multiline_parenthesized_expression = false
+ij_java_align_multiline_records = true
+ij_java_align_multiline_resources = false
+ij_java_align_multiline_ternary_operation = false
+ij_java_align_multiline_text_blocks = false
+ij_java_align_multiline_throws_list = false
+ij_java_align_subsequent_simple_methods = false
+ij_java_align_throws_keyword = false
+ij_java_align_types_in_multi_catch = true
+ij_java_annotation_new_line_in_record_component = false
+#g-off
+ij_java_annotation_parameter_wrap = split_into_lines
+#g-false
+ij_java_array_initializer_new_line_after_left_brace = true
+#g-false
+ij_java_array_initializer_right_brace_on_new_line = true
+#g-normal
+ij_java_array_initializer_wrap = on_every_item
+ij_java_assert_statement_colon_on_next_line = false
+#g-off
+ij_java_assert_statement_wrap = normal
+#g-off
+ij_java_assignment_wrap = normal
+ij_java_binary_operation_sign_on_next_line = true
+ij_java_binary_operation_wrap = normal
+ij_java_blank_lines_after_anonymous_class_header = 0
+ij_java_blank_lines_after_class_header = 1
+ij_java_blank_lines_after_imports = 1
+ij_java_blank_lines_after_package = 1
+ij_java_blank_lines_around_class = 1
+ij_java_blank_lines_around_field = 0
+ij_java_blank_lines_around_field_in_interface = 0
+ij_java_blank_lines_around_field_with_annotations = 0
+ij_java_blank_lines_around_initializer = 1
+ij_java_blank_lines_around_method = 1
+ij_java_blank_lines_around_method_in_interface = 1
+ij_java_blank_lines_before_class_end = 0
+ij_java_blank_lines_before_imports = 1
+ij_java_blank_lines_before_method_body = 0
+ij_java_blank_lines_before_package = 0
+ij_java_blank_lines_between_record_components = 0
+ij_java_block_brace_style = end_of_line
+ij_java_block_comment_add_space = false
+ij_java_block_comment_at_first_column = true
+ij_java_builder_methods =
+#g-false
+ij_java_call_parameters_new_line_after_left_paren = true
+#g-false
+ij_java_call_parameters_right_paren_on_new_line = true
+#g-normal
+ij_java_call_parameters_wrap = on_every_item
+ij_java_case_statement_on_separate_line = true
+ij_java_catch_on_new_line = false
+ij_java_class_annotation_wrap = split_into_lines
+ij_java_class_brace_style = end_of_line
+#g-999
+ij_java_class_count_to_use_import_on_demand = 5
+ij_java_class_names_in_javadoc = 1
+ij_java_deconstruction_list_wrap = normal
+ij_java_do_not_indent_top_level_class_members = false
+ij_java_do_not_wrap_after_single_annotation = false
+ij_java_do_not_wrap_after_single_annotation_in_parameter = false
+#g-always
+ij_java_do_while_brace_force = never
+ij_java_doc_add_blank_line_after_description = true
+ij_java_doc_add_blank_line_after_param_comments = false
+ij_java_doc_add_blank_line_after_return = false
+ij_java_doc_add_p_tag_on_empty_lines = true
+ij_java_doc_align_exception_comments = true
+ij_java_doc_align_param_comments = true
+ij_java_doc_do_not_wrap_if_one_line = false
+ij_java_doc_enable_formatting = true
+ij_java_doc_enable_leading_asterisks = true
+ij_java_doc_indent_on_continuation = false
+ij_java_doc_keep_empty_lines = true
+ij_java_doc_keep_empty_parameter_tag = true
+ij_java_doc_keep_empty_return_tag = true
+ij_java_doc_keep_empty_throws_tag = true
+ij_java_doc_keep_invalid_tags = true
+ij_java_doc_param_description_on_new_line = false
+ij_java_doc_preserve_line_breaks = false
+ij_java_doc_use_throws_not_exception_tag = true
+ij_java_else_on_new_line = false
+#g-off
+ij_java_enum_constants_wrap = split_into_lines
+#g-off
+ij_java_enum_field_annotation_wrap = split_into_lines
+#g-off
+ij_java_extends_keyword_wrap = normal
+ij_java_extends_list_wrap = normal
+ij_java_field_annotation_wrap = split_into_lines
+ij_java_field_name_prefix =
+ij_java_field_name_suffix =
+ij_java_finally_on_new_line = false
+#g-always
+ij_java_for_brace_force = never
+#g-false
+ij_java_for_statement_new_line_after_left_paren = true
+#g-false
+ij_java_for_statement_right_paren_on_new_line = true
+#g-normal
+ij_java_for_statement_wrap = on_every_item
+ij_java_generate_final_locals = false
+ij_java_generate_final_parameters = false
+ij_java_generate_use_type_annotation_before_type = true
+#g-always
+ij_java_if_brace_force = never
+#g-$*,|,*
+ij_java_imports_layout = *, |, javax.**, java.**, |, $*
+ij_java_indent_case_from_switch = true
+ij_java_insert_inner_class_imports = true
+ij_java_insert_override_annotation = true
+ij_java_keep_blank_lines_before_right_brace = 2
+ij_java_keep_blank_lines_between_package_declaration_and_header = 2
+ij_java_keep_blank_lines_in_code = 1
+ij_java_keep_blank_lines_in_declarations = 2
+ij_java_keep_builder_methods_indents = false
+ij_java_keep_control_statement_in_one_line = false
+ij_java_keep_first_column_comment = true
+ij_java_keep_indents_on_empty_lines = false
+ij_java_keep_line_breaks = true
+ij_java_keep_multiple_expressions_in_one_line = false
+ij_java_keep_simple_blocks_in_one_line = false
+ij_java_keep_simple_classes_in_one_line = false
+ij_java_keep_simple_lambdas_in_one_line = false
+ij_java_keep_simple_methods_in_one_line = false
+ij_java_label_indent_absolute = false
+ij_java_label_indent_size = 0
+ij_java_lambda_brace_style = end_of_line
+ij_java_layout_on_demand_import_from_same_package_first = true
+ij_java_layout_static_imports_separately = true
+ij_java_line_comment_add_space = false
+ij_java_line_comment_add_space_on_reformat = false
+ij_java_line_comment_at_first_column = true
+ij_java_local_variable_name_prefix =
+ij_java_local_variable_name_suffix =
+ij_java_method_annotation_wrap = split_into_lines
+ij_java_method_brace_style = end_of_line
+#g-normal
+ij_java_method_call_chain_wrap = on_every_item
+#g-false
+ij_java_method_parameters_new_line_after_left_paren = true
+#g-false
+ij_java_method_parameters_right_paren_on_new_line = true
+#g-normal
+ij_java_method_parameters_wrap = on_every_item
+ij_java_modifier_list_wrap = false
+ij_java_multi_catch_types_wrap = normal
+#g-999
+ij_java_names_count_to_use_import_on_demand = 3
+#g-false
+ij_java_new_line_after_lparen_in_annotation = true
+ij_java_new_line_after_lparen_in_deconstruction_pattern = true
+#g-false
+ij_java_new_line_after_lparen_in_record_header = true
+ij_java_new_line_when_body_is_presented = false
+#g-
+ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.*
+ij_java_parameter_annotation_wrap = off
+ij_java_parameter_name_prefix =
+ij_java_parameter_name_suffix =
+ij_java_parentheses_expression_new_line_after_left_paren = false
+ij_java_parentheses_expression_right_paren_on_new_line = false
+ij_java_place_assignment_sign_on_next_line = false
+ij_java_prefer_longer_names = true
+ij_java_prefer_parameters_wrap = false
+ij_java_preserve_module_imports = true
+#g-normal
+ij_java_record_components_wrap = on_every_item
+ij_java_repeat_synchronized = true
+ij_java_replace_instanceof_and_cast = false
+ij_java_replace_null_check = true
+ij_java_replace_sum_lambda_with_method_ref = true
+#g-false
+ij_java_resource_list_new_line_after_left_paren = true
+#g-false
+ij_java_resource_list_right_paren_on_new_line = true
+#g-off
+ij_java_resource_list_wrap = on_every_item
+#g-false
+ij_java_rparen_on_new_line_in_annotation = true
+ij_java_rparen_on_new_line_in_deconstruction_pattern = true
+#g-false
+ij_java_rparen_on_new_line_in_record_header = true
+ij_java_space_after_closing_angle_bracket_in_type_argument = false
+ij_java_space_after_colon = true
+ij_java_space_after_comma = true
+ij_java_space_after_comma_in_type_arguments = true
+ij_java_space_after_for_semicolon = true
+ij_java_space_after_quest = true
+ij_java_space_after_type_cast = true
+ij_java_space_before_annotation_array_initializer_left_brace = false
+ij_java_space_before_annotation_parameter_list = false
+ij_java_space_before_array_initializer_left_brace = false
+ij_java_space_before_catch_keyword = true
+ij_java_space_before_catch_left_brace = true
+ij_java_space_before_catch_parentheses = true
+ij_java_space_before_class_left_brace = true
+ij_java_space_before_colon = true
+ij_java_space_before_colon_in_foreach = true
+ij_java_space_before_comma = false
+ij_java_space_before_deconstruction_list = false
+ij_java_space_before_do_left_brace = true
+ij_java_space_before_else_keyword = true
+ij_java_space_before_else_left_brace = true
+ij_java_space_before_finally_keyword = true
+ij_java_space_before_finally_left_brace = true
+ij_java_space_before_for_left_brace = true
+ij_java_space_before_for_parentheses = true
+ij_java_space_before_for_semicolon = false
+ij_java_space_before_if_left_brace = true
+ij_java_space_before_if_parentheses = true
+ij_java_space_before_method_call_parentheses = false
+ij_java_space_before_method_left_brace = true
+ij_java_space_before_method_parentheses = false
+ij_java_space_before_opening_angle_bracket_in_type_parameter = false
+ij_java_space_before_quest = true
+ij_java_space_before_switch_left_brace = true
+ij_java_space_before_switch_parentheses = true
+ij_java_space_before_synchronized_left_brace = true
+ij_java_space_before_synchronized_parentheses = true
+ij_java_space_before_try_left_brace = true
+ij_java_space_before_try_parentheses = true
+ij_java_space_before_type_parameter_list = false
+ij_java_space_before_while_keyword = true
+ij_java_space_before_while_left_brace = true
+ij_java_space_before_while_parentheses = true
+ij_java_space_inside_one_line_enum_braces = false
+ij_java_space_within_empty_array_initializer_braces = false
+ij_java_space_within_empty_method_call_parentheses = false
+ij_java_space_within_empty_method_parentheses = false
+ij_java_spaces_around_additive_operators = true
+ij_java_spaces_around_annotation_eq = true
+ij_java_spaces_around_assignment_operators = true
+ij_java_spaces_around_bitwise_operators = true
+ij_java_spaces_around_equality_operators = true
+ij_java_spaces_around_lambda_arrow = true
+ij_java_spaces_around_logical_operators = true
+ij_java_spaces_around_method_ref_dbl_colon = false
+ij_java_spaces_around_multiplicative_operators = true
+ij_java_spaces_around_relational_operators = true
+ij_java_spaces_around_shift_operators = true
+ij_java_spaces_around_type_bounds_in_type_parameters = true
+ij_java_spaces_around_unary_operator = false
+ij_java_spaces_inside_block_braces_when_body_is_present = false
+ij_java_spaces_within_angle_brackets = false
+ij_java_spaces_within_annotation_parentheses = false
+ij_java_spaces_within_array_initializer_braces = false
+ij_java_spaces_within_braces = false
+ij_java_spaces_within_brackets = false
+ij_java_spaces_within_cast_parentheses = false
+ij_java_spaces_within_catch_parentheses = false
+ij_java_spaces_within_deconstruction_list = false
+ij_java_spaces_within_for_parentheses = false
+ij_java_spaces_within_if_parentheses = false
+ij_java_spaces_within_method_call_parentheses = false
+ij_java_spaces_within_method_parentheses = false
+ij_java_spaces_within_parentheses = false
+ij_java_spaces_within_record_header = false
+ij_java_spaces_within_switch_parentheses = false
+ij_java_spaces_within_synchronized_parentheses = false
+ij_java_spaces_within_try_parentheses = false
+ij_java_spaces_within_while_parentheses = false
+ij_java_special_else_if_treatment = true
+ij_java_static_field_name_prefix =
+ij_java_static_field_name_suffix =
+ij_java_subclass_name_prefix =
+ij_java_subclass_name_suffix = Impl
+ij_java_switch_expressions_wrap = normal
+ij_java_ternary_operation_signs_on_next_line = true
+#g-normal
+ij_java_ternary_operation_wrap = on_every_item
+ij_java_test_name_prefix =
+ij_java_test_name_suffix = Test
+ij_java_throws_keyword_wrap = normal
+ij_java_throws_list_wrap = off
+ij_java_use_external_annotations = false
+ij_java_use_fq_class_names = false
+ij_java_use_relative_indents = false
+ij_java_use_single_class_imports = true
+ij_java_variable_annotation_wrap = off
+ij_java_visibility = public
+ij_java_while_brace_force = always
+ij_java_while_on_new_line = false
+ij_java_wrap_comments = true
+ij_java_wrap_first_method_in_call_chain = false
+ij_java_wrap_long_lines = false
+ij_java_wrap_semicolon_after_call_chain = false
+
+[*.nbtt]
+indent_size = 4
+indent_style = tab
+max_line_length = 150
+tab_width = 4
+#g-unset
+ij_continuation_indent_size = 4
+ij_nbtt_keep_indents_on_empty_lines = false
+ij_nbtt_space_after_colon = true
+ij_nbtt_space_after_comma = true
+ij_nbtt_space_before_colon = true
+ij_nbtt_space_before_comma = false
+ij_nbtt_spaces_within_brackets = false
+ij_nbtt_spaces_within_parentheses = false
+
+[*.properties]
+ij_properties_align_group_field_declarations = false
+#g-false
+ij_properties_keep_blank_lines = true
+ij_properties_key_value_delimiter = equals
+ij_properties_spaces_around_key_value_delimiter = false
+
+[.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,*.jspx,*.pom,*.rng,*.tagx,*.tld,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
+ij_continuation_indent_size = 2
+ij_xml_align_attributes = false
+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
+
+[{*.bash,*.sh,*.zsh}]
+#g-unset
+indent_size = 2
+#g-unset
+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
+
+[{*.gant,*.gdsl,*.gradle,*.groovy,*.gy}]
+ij_groovy_align_group_field_declarations = false
+ij_groovy_align_multiline_array_initializer_expression = false
+ij_groovy_align_multiline_assignment = false
+ij_groovy_align_multiline_binary_operation = false
+ij_groovy_align_multiline_chained_methods = false
+ij_groovy_align_multiline_extends_list = false
+ij_groovy_align_multiline_for = true
+ij_groovy_align_multiline_list_or_map = true
+ij_groovy_align_multiline_method_parentheses = false
+ij_groovy_align_multiline_parameters = true
+ij_groovy_align_multiline_parameters_in_calls = false
+ij_groovy_align_multiline_resources = true
+ij_groovy_align_multiline_ternary_operation = false
+ij_groovy_align_multiline_throws_list = false
+ij_groovy_align_named_args_in_map = true
+ij_groovy_align_throws_keyword = false
+ij_groovy_array_initializer_new_line_after_left_brace = false
+ij_groovy_array_initializer_right_brace_on_new_line = false
+ij_groovy_array_initializer_wrap = off
+ij_groovy_assert_statement_wrap = off
+ij_groovy_assignment_wrap = off
+ij_groovy_binary_operation_wrap = off
+ij_groovy_blank_lines_after_class_header = 0
+ij_groovy_blank_lines_after_imports = 1
+ij_groovy_blank_lines_after_package = 1
+ij_groovy_blank_lines_around_class = 1
+ij_groovy_blank_lines_around_field = 0
+ij_groovy_blank_lines_around_field_in_interface = 0
+ij_groovy_blank_lines_around_method = 1
+ij_groovy_blank_lines_around_method_in_interface = 1
+ij_groovy_blank_lines_before_imports = 1
+ij_groovy_blank_lines_before_method_body = 0
+ij_groovy_blank_lines_before_package = 0
+ij_groovy_block_brace_style = end_of_line
+ij_groovy_block_comment_add_space = false
+ij_groovy_block_comment_at_first_column = true
+ij_groovy_call_parameters_new_line_after_left_paren = false
+ij_groovy_call_parameters_right_paren_on_new_line = false
+ij_groovy_call_parameters_wrap = off
+ij_groovy_catch_on_new_line = false
+ij_groovy_class_annotation_wrap = split_into_lines
+ij_groovy_class_brace_style = end_of_line
+ij_groovy_class_count_to_use_import_on_demand = 5
+ij_groovy_do_while_brace_force = never
+ij_groovy_else_on_new_line = false
+ij_groovy_enable_groovydoc_formatting = true
+ij_groovy_enum_constants_wrap = off
+ij_groovy_extends_keyword_wrap = off
+ij_groovy_extends_list_wrap = off
+ij_groovy_field_annotation_wrap = split_into_lines
+ij_groovy_finally_on_new_line = false
+ij_groovy_for_brace_force = never
+ij_groovy_for_statement_new_line_after_left_paren = false
+ij_groovy_for_statement_right_paren_on_new_line = false
+ij_groovy_for_statement_wrap = off
+ij_groovy_ginq_general_clause_wrap_policy = 2
+ij_groovy_ginq_having_wrap_policy = 1
+ij_groovy_ginq_indent_having_clause = true
+ij_groovy_ginq_indent_on_clause = true
+ij_groovy_ginq_on_wrap_policy = 1
+ij_groovy_ginq_space_after_keyword = true
+ij_groovy_if_brace_force = never
+ij_groovy_import_annotation_wrap = 2
+ij_groovy_imports_layout = *, |, javax.**, java.**, |, $*
+ij_groovy_indent_case_from_switch = true
+ij_groovy_indent_label_blocks = true
+ij_groovy_insert_inner_class_imports = false
+ij_groovy_keep_blank_lines_before_right_brace = 2
+ij_groovy_keep_blank_lines_in_code = 2
+ij_groovy_keep_blank_lines_in_declarations = 2
+ij_groovy_keep_control_statement_in_one_line = true
+ij_groovy_keep_first_column_comment = true
+ij_groovy_keep_indents_on_empty_lines = false
+ij_groovy_keep_line_breaks = true
+ij_groovy_keep_multiple_expressions_in_one_line = false
+ij_groovy_keep_simple_blocks_in_one_line = false
+ij_groovy_keep_simple_classes_in_one_line = true
+ij_groovy_keep_simple_lambdas_in_one_line = true
+ij_groovy_keep_simple_methods_in_one_line = true
+ij_groovy_label_indent_absolute = false
+ij_groovy_label_indent_size = 0
+ij_groovy_lambda_brace_style = end_of_line
+ij_groovy_layout_static_imports_separately = true
+ij_groovy_line_comment_add_space = false
+ij_groovy_line_comment_add_space_on_reformat = false
+ij_groovy_line_comment_at_first_column = true
+ij_groovy_method_annotation_wrap = split_into_lines
+ij_groovy_method_brace_style = end_of_line
+ij_groovy_method_call_chain_wrap = off
+ij_groovy_method_parameters_new_line_after_left_paren = false
+ij_groovy_method_parameters_right_paren_on_new_line = false
+ij_groovy_method_parameters_wrap = off
+ij_groovy_modifier_list_wrap = false
+ij_groovy_names_count_to_use_import_on_demand = 3
+ij_groovy_packages_to_use_import_on_demand = java.awt.*, javax.swing.*
+ij_groovy_parameter_annotation_wrap = off
+ij_groovy_parentheses_expression_new_line_after_left_paren = false
+ij_groovy_parentheses_expression_right_paren_on_new_line = false
+ij_groovy_prefer_parameters_wrap = false
+ij_groovy_resource_list_new_line_after_left_paren = false
+ij_groovy_resource_list_right_paren_on_new_line = false
+ij_groovy_resource_list_wrap = off
+ij_groovy_space_after_assert_separator = true
+ij_groovy_space_after_colon = true
+ij_groovy_space_after_comma = true
+ij_groovy_space_after_comma_in_type_arguments = true
+ij_groovy_space_after_for_semicolon = true
+ij_groovy_space_after_quest = true
+ij_groovy_space_after_type_cast = true
+ij_groovy_space_before_annotation_parameter_list = false
+ij_groovy_space_before_array_initializer_left_brace = false
+ij_groovy_space_before_assert_separator = false
+ij_groovy_space_before_catch_keyword = true
+ij_groovy_space_before_catch_left_brace = true
+ij_groovy_space_before_catch_parentheses = true
+ij_groovy_space_before_class_left_brace = true
+ij_groovy_space_before_closure_left_brace = true
+ij_groovy_space_before_colon = true
+ij_groovy_space_before_comma = false
+ij_groovy_space_before_do_left_brace = true
+ij_groovy_space_before_else_keyword = true
+ij_groovy_space_before_else_left_brace = true
+ij_groovy_space_before_finally_keyword = true
+ij_groovy_space_before_finally_left_brace = true
+ij_groovy_space_before_for_left_brace = true
+ij_groovy_space_before_for_parentheses = true
+ij_groovy_space_before_for_semicolon = false
+ij_groovy_space_before_if_left_brace = true
+ij_groovy_space_before_if_parentheses = true
+ij_groovy_space_before_method_call_parentheses = false
+ij_groovy_space_before_method_left_brace = true
+ij_groovy_space_before_method_parentheses = false
+ij_groovy_space_before_quest = true
+ij_groovy_space_before_record_parentheses = false
+ij_groovy_space_before_switch_left_brace = true
+ij_groovy_space_before_switch_parentheses = true
+ij_groovy_space_before_synchronized_left_brace = true
+ij_groovy_space_before_synchronized_parentheses = true
+ij_groovy_space_before_try_left_brace = true
+ij_groovy_space_before_try_parentheses = true
+ij_groovy_space_before_while_keyword = true
+ij_groovy_space_before_while_left_brace = true
+ij_groovy_space_before_while_parentheses = true
+ij_groovy_space_in_named_argument = true
+ij_groovy_space_in_named_argument_before_colon = false
+ij_groovy_space_within_empty_array_initializer_braces = false
+ij_groovy_space_within_empty_method_call_parentheses = false
+ij_groovy_spaces_around_additive_operators = true
+ij_groovy_spaces_around_assignment_operators = true
+ij_groovy_spaces_around_bitwise_operators = true
+ij_groovy_spaces_around_equality_operators = true
+ij_groovy_spaces_around_lambda_arrow = true
+ij_groovy_spaces_around_logical_operators = true
+ij_groovy_spaces_around_multiplicative_operators = true
+ij_groovy_spaces_around_regex_operators = true
+ij_groovy_spaces_around_relational_operators = true
+ij_groovy_spaces_around_shift_operators = true
+ij_groovy_spaces_within_annotation_parentheses = false
+ij_groovy_spaces_within_array_initializer_braces = false
+ij_groovy_spaces_within_braces = true
+ij_groovy_spaces_within_brackets = false
+ij_groovy_spaces_within_cast_parentheses = false
+ij_groovy_spaces_within_catch_parentheses = false
+ij_groovy_spaces_within_for_parentheses = false
+ij_groovy_spaces_within_gstring_injection_braces = false
+ij_groovy_spaces_within_if_parentheses = false
+ij_groovy_spaces_within_list_or_map = false
+ij_groovy_spaces_within_method_call_parentheses = false
+ij_groovy_spaces_within_method_parentheses = false
+ij_groovy_spaces_within_parentheses = false
+ij_groovy_spaces_within_switch_parentheses = false
+ij_groovy_spaces_within_synchronized_parentheses = false
+ij_groovy_spaces_within_try_parentheses = false
+ij_groovy_spaces_within_tuple_expression = false
+ij_groovy_spaces_within_while_parentheses = false
+ij_groovy_special_else_if_treatment = true
+ij_groovy_ternary_operation_wrap = off
+ij_groovy_throws_keyword_wrap = off
+ij_groovy_throws_list_wrap = off
+ij_groovy_use_flying_geese_braces = false
+ij_groovy_use_fq_class_names = false
+ij_groovy_use_fq_class_names_in_javadoc = true
+ij_groovy_use_relative_indents = false
+ij_groovy_use_single_class_imports = true
+ij_groovy_variable_annotation_wrap = off
+ij_groovy_while_brace_force = never
+ij_groovy_while_on_new_line = false
+ij_groovy_wrap_chain_calls_after_dot = false
+ij_groovy_wrap_long_lines = false
+
+[{*.har,*.inputactions,*.json,*.jsonc,*.png.mcmeta,mcmod.info,pack.mcmeta}]
+#g-unset
+indent_size = 2
+#g-unset
+tab_width = 2
+ij_json_array_wrapping = split_into_lines
+#g-0
+ij_json_keep_blank_lines_in_code = 1
+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,*.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
+
+[{*.kt,*.kts}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_align_multiline_binary_operation = false
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_align_multiline_method_parentheses = false
+ij_kotlin_align_multiline_parameters = true
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_allow_trailing_comma = false
+ij_kotlin_allow_trailing_comma_on_call_site = false
+#s-off
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_blank_lines_after_class_header = 0
+ij_kotlin_blank_lines_around_block_when_branches = 0
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+ij_kotlin_block_comment_add_space = false
+ij_kotlin_block_comment_at_first_column = true
+#s-false
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+#s-false
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+#s-off
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_class_annotation_wrap = split_into_lines
+#s-true
+ij_kotlin_continuation_indent_for_chained_calls = false
+#s-true
+ij_kotlin_continuation_indent_for_expression_bodies = false
+#s-true
+ij_kotlin_continuation_indent_in_argument_lists = false
+#s-true
+ij_kotlin_continuation_indent_in_elvis = false
+#s-true
+ij_kotlin_continuation_indent_in_if_conditions = false
+#s-true
+ij_kotlin_continuation_indent_in_parameter_lists = false
+#s-true
+ij_kotlin_continuation_indent_in_supertype_lists = false
+ij_kotlin_else_on_new_line = false
+ij_kotlin_enum_constants_wrap = off
+#s-off
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_finally_on_new_line = false
+#s-false
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_import_nested_classes = false
+ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
+ij_kotlin_indent_before_arrow_on_new_line = true
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_keep_blank_lines_before_right_brace = 2
+ij_kotlin_keep_blank_lines_in_code = 2
+ij_kotlin_keep_blank_lines_in_declarations = 2
+ij_kotlin_keep_first_column_comment = true
+ij_kotlin_keep_indents_on_empty_lines = false
+ij_kotlin_keep_line_breaks = true
+ij_kotlin_lbrace_on_next_line = false
+ij_kotlin_line_break_after_multiline_when_entry = true
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_add_space_on_reformat = false
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_method_annotation_wrap = split_into_lines
+#s-off
+ij_kotlin_method_call_chain_wrap = normal
+#s-false
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+#s-false
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+#s-off
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_name_count_to_use_star_import = 5
+ij_kotlin_name_count_to_use_star_import_for_members = 3
+ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**, io.ktor.**
+ij_kotlin_parameter_annotation_wrap = off
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_before_extend_colon = true
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_lambda_arrow = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_before_when_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_elvis = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_range = false
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_variable_annotation_wrap = off
+ij_kotlin_while_on_new_line = false
+ij_kotlin_wrap_elvis_expressions = 1
+#s-0
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_wrap_first_method_in_call_chain = false
+
+[{*.markdown,*.md}]
+#g-4
+indent_size = 2
+#g-4
+tab_width = 2
+#g-8
+ij_continuation_indent_size = 4
+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_format_tables = true
+ij_markdown_insert_quote_arrows_on_wrap = true
+ij_markdown_keep_indents_on_empty_lines = false
+#g-true
+ij_markdown_keep_line_breaks_inside_text_blocks = false
+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
+
+[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock,uv.lock}]
+indent_size = 4
+tab_width = 4
+ij_continuation_indent_size = 8
+ij_toml_keep_indents_on_empty_lines = false
+
+[{*.yaml,*.yml}]
+#g-unset
+indent_size = 2
+#g-unset
+tab_width = 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
+#g-false
+ij_yaml_line_comment_add_space = true
+#g-false
+ij_yaml_line_comment_add_space_on_reformat = true
+#g-true
+ij_yaml_line_comment_at_first_column = false
+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/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..cc9ea95
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,15 @@
+* text eol=lf
+*.bat text eol=crlf
+*.patch text eol=lf
+*.java text eol=lf
+*.gradle text eol=crlf
+*.png binary
+*.gif binary
+*.exe binary
+*.dll binary
+*.jar binary
+*.lzma binary
+*.zip binary
+*.pyd binary
+*.cfg text eol=lf
+*.jks binary
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..586efe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,126 @@
+name: Mod bug report
+description: "For reporting bugs and other defects"
+labels:
+ - S/needs-triage
+body:
+ - type: markdown
+ attributes:
+ value: >-
+ ### ⚠️ Bug Report
+
+ **Thank you for taking the time to make a report!** This form is designed to help you
+ provide the information necessary for us to understand and fix the issue. Before you start,
+ please complete the following checks:
+
+
+ 1. Are you using the latest version of the mod? If there is a more recent version of the mod
+ available for your version of Minecraft, please check whether the issue still occurs on that
+ version.
+
+
+ 2. Has the issue already been reported? If it has been, please do not open a new issue, but
+ if you have additional information please comment on the existing report.
+
+
+ 3. Have you determined the minimum set of instructions to reproduce the issue? If the issue
+ only occurs in specific situations or with other mods installed, please attempt to narrow
+ down exactly what conditions or mods are required to reproduce it.
+
+ - type: checkboxes
+ id: preliminary-checks
+ attributes:
+ label: Checklist
+ options:
+ - label: This issue exists on the latest release for my Minecraft version.
+ required: true
+ - label: This issue has not already been reported.
+ required: true
+ - label: I have determined the minimal reproduction requirements.
+ required: true
+
+ - type: input
+ id: mod-loader
+ attributes:
+ label: Mod Loader
+ description: >-
+ Which mod loader are you using?
+
+ **Examples:** `Fabric`, `NeoForge`
+ validations:
+ required: true
+
+ - type: input
+ id: mc-version
+ attributes:
+ label: Minecraft Version
+ description: >-
+ Which version of Minecraft are you using?
+
+ **Examples:** `1.21.4`, `1.20.1`
+ validations:
+ required: true
+
+ - type: input
+ id: mod-version
+ attributes:
+ label: Mod Version
+ description: >-
+ Which version of this mod are you using?
+
+ **Example:** `2.2.0+1.21.4`
+ validations:
+ required: true
+
+ - type: input
+ id: log-link
+ attributes:
+ label: Log File
+ description: >-
+ Please upload your log file to [mclo.gs](https://mclo.gs) and paste the link here. Even if
+ there are no crashes, logs help identify version details and potential error messages.
+
+
+ **Hint:** Log files are located in the `logs` folder of your Minecraft instance, typically
+ `.minecraft/logs`. You will usually want the `latest.log` file, since that file belongs to
+ the most recent session of the game. Alternatively, some launchers such as Prism support
+ uploading logs to mclo.gs directly.
+ placeholder: https://mclo.gs/your-log-link
+ validations:
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Issue Description
+ description: >-
+ Please describe in detail the issue you are experiencing. If you have any screenshots,
+ videos, or other resources that help illustrate the problem, you can attach them here.
+ placeholder: Describe the issue...
+ validations:
+ required: true
+
+ - type: textarea
+ id: reproduction-steps
+ attributes:
+ label: Steps to Reproduce
+ description: >-
+ Please list the steps that we should follow to encounter the issue. If you are unsure, or
+ unable to consistently reproduce it, please provide a detailed description of what you
+ were doing when the issue occurred.
+
+ 1. Do X
+
+ 2. Do Y
+
+ 3. Issue occurs
+ placeholder: List the steps to reproduce the issue...
+ validations:
+ required: true
+
+ - type: input
+ id: expected-behavior
+ attributes:
+ label: Expected Behavior (optional)
+ description: >-
+ Describe briefly what you believe should happen if the issue was fixed. If the answer is
+ obvious (such as if the game is crashing), you can skip this.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..3fa69ad
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Support enquiry, question or discussion
+ url: https://discord.terminalmc.dev
+ about: For any issue that isn't specifically a bug report or feature request, please use Discord
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..d1480f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,75 @@
+name: Mod feature request
+description: "For requesting new features or improvements"
+labels:
+ - S/needs-triage
+body:
+ - type: markdown
+ attributes:
+ value: >-
+ ### 🛠️ Feature Request
+
+ **Thank you for taking the time to suggest a new feature!** This form is designed to help
+ you provide the information necessary for us to understand and implement the feature. Before
+ you start, please complete the following checks:
+
+
+ 1. Are you requesting either a new feature or an improvement of existing functionality? For
+ general support, or to request an update or backport, please use
+ [Discord](https://discord.terminalmc.dev).
+
+
+ 2. Have you checked whether the feature already exists on the latest version? If you want to
+ request that an existing feature be backported, please use
+ [Discord](https://discord.terminalmc.dev).
+
+
+ 3. Has the feature already been requested? If it has been, please comment on the existing
+ request instead.
+
+ - type: checkboxes
+ id: preliminary-checks
+ attributes:
+ label: Checklist
+ options:
+ - label: I am requesting a new feature or a functional improvement.
+ required: true
+ - label: This feature does not exist on the latest version.
+ required: true
+ - label: This feature has not already been requested.
+ required: true
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Feature Description
+ description: >-
+ Please describe in detail what you would like added or changed, with reasoning. If you have
+ considered alternative options, include them as well.
+
+
+ **Hint:** Requests that lack detail or do not include a clear reason are less likely to be
+ considered.
+ placeholder: Describe the feature...
+ validations:
+ required: true
+
+ - type: textarea
+ id: use-case
+ attributes:
+ label: Use Case
+ description: >-
+ Please explain how this change would improve the experience of using the mod, with reference
+ to specific scenarios or examples as applicable.
+ placeholder: Describe your use-case...
+ validations:
+ required: true
+
+ - type: input
+ id: mc-version
+ attributes:
+ label: Minecraft Version (optional)
+ description: >-
+ Feature updates are generally only released for the latest Minecraft version. If you need
+ the new feature on an older version, list the version(s) here.
+
+ **Examples:** `1.21.4`, `1.20.1`
diff --git a/.github/labels.yml b/.github/labels.yml
new file mode 100644
index 0000000..ece9bc4
--- /dev/null
+++ b/.github/labels.yml
@@ -0,0 +1,120 @@
+- name: E/duplicate
+ color: 'BFD4F2'
+ description: 'Closed: Same as another'
+ aliases: []
+
+- name: E/external
+ color: 'BFD4F2'
+ description: 'Closed: External issue'
+ aliases: []
+
+- name: E/invalid
+ color: 'BFD4F2'
+ description: 'Closed: Not an issue'
+ aliases: []
+
+- name: E/no-action
+ color: 'BFD4F2'
+ description: 'Closed: Not actionable or inadequate information'
+ aliases: []
+
+- name: E/scope
+ color: 'BFD4F2'
+ description: 'Closed: Out of scope'
+ aliases: []
+
+- name: E/wontfix
+ color: 'BFD4F2'
+ description: 'Closed: Will not be worked on'
+ aliases: []
+
+
+
+- name: P/high
+ color: 'B60205'
+ description: 'Priority: High, for immediate attention'
+ aliases: []
+
+- name: P/low
+ color: '006B75'
+ description: 'Priority: Low, not time-sensitive'
+ aliases: []
+
+- name: P/medium
+ color: '9B5003'
+ description: 'Priority: Medium, time-sensitive but not urgent'
+ aliases: []
+
+
+
+- name: S/accepted
+ color: '172B72'
+ description: 'Status: Accepted but not assigned'
+ aliases: []
+
+- name: S/blocked
+ color: '97988B'
+ description: 'Status: Blocked by another event'
+ aliases: []
+
+- name: S/finished
+ color: '0E8A16'
+ description: 'Status: Complete, awaiting release'
+ aliases: [fixed,complete]
+
+- name: S/in-progress
+ color: '36210B'
+ description: 'Status: Being worked on'
+ aliases: []
+
+- name: S/info-needed
+ color: '4A4800'
+ description: 'Status: Awaiting further information'
+ aliases: []
+
+- name: S/needs-triage
+ color: 'D4C31A'
+ description: 'Status: Needs triage'
+ aliases: []
+
+
+
+- name: T/addition
+ color: '139399'
+ description: 'Type: New feature'
+ aliases: []
+
+- name: T/bug
+ color: 'A83400'
+ description: 'Type: Bug'
+ aliases: []
+
+- name: T/compat
+ color: '490839'
+ description: 'Type: Compatibility'
+ aliases: []
+
+- name: T/enhancement
+ color: '0052CC'
+ description: 'Type: Enhancement or optimization'
+ aliases: []
+
+- name: T/fix
+ color: '0E8A16'
+ description: 'Type: Issue fix'
+ aliases: []
+
+- name: T/port
+ color: '5319E7'
+ description: 'Type: Upgrade or downgrade game version'
+ aliases: []
+
+- name: T/security
+ color: 'B60205'
+ description: 'Type: Security issue'
+ aliases: []
+
+- name: T/translation
+ color: '8C02DA'
+ description: 'Type: Translation'
+ aliases: []
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
deleted file mode 100644
index 028f069..0000000
--- a/.github/workflows/build.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-on:
- push:
- tags-ignore:
- - 'v*'
- branches:
- - '*'
- pull_request:
-
-name: Build Mod
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-java@v1
- with:
- java-version: 17
- - name: Build
- uses: gradle/gradle-build-action@v2
- with:
- arguments: build --stacktrace
- - name: Upload Fabric Artifacts
- uses: actions/upload-artifact@v2
- with:
- name: Fabric
- path: |
- fabric/build/libs/*.jar
- release/now-playing-fabric*.jar
- - name: Upload Forge Artifacts
- uses: actions/upload-artifact@v2
- with:
- name: Forge
- path: |
- forge/build/libs/*.jar
- release/now-playing-forge*.jar
diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml
new file mode 100644
index 0000000..aab99b4
--- /dev/null
+++ b/.github/workflows/check-build.yml
@@ -0,0 +1,51 @@
+# Builds the project, as a first line of defense against bad commits.
+name: Check Build
+
+on:
+ push:
+ paths: [
+ '**src/**',
+ '**/*gradle*'
+ ]
+ pull_request:
+ paths: [
+ '**src/**',
+ '**/*gradle*'
+ ]
+ workflow_dispatch:
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build --stacktrace
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release-platform-curseforge.yml b/.github/workflows/release-platform-curseforge.yml
new file mode 100644
index 0000000..2dd4ae8
--- /dev/null
+++ b/.github/workflows/release-platform-curseforge.yml
@@ -0,0 +1,46 @@
+# A CurseForge-only version of the normal release workflow.
+name: Release-Platform-CurseForge
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build publishCurseforge --stacktrace
+ env:
+ CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release-platform-github.yml b/.github/workflows/release-platform-github.yml
new file mode 100644
index 0000000..8e3f1f3
--- /dev/null
+++ b/.github/workflows/release-platform-github.yml
@@ -0,0 +1,46 @@
+# A GitHub-only version of the normal release workflow.
+name: Release-Platform-GitHub
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build publishGithub --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release-platform-modrinth.yml b/.github/workflows/release-platform-modrinth.yml
new file mode 100644
index 0000000..1918e43
--- /dev/null
+++ b/.github/workflows/release-platform-modrinth.yml
@@ -0,0 +1,46 @@
+# A Modrinth-only version of the normal release workflow.
+name: Release-Platform-Modrinth
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build publishModrinth --stacktrace
+ env:
+ MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release-subproject-fabric.yml b/.github/workflows/release-subproject-fabric.yml
new file mode 100644
index 0000000..fc5fb0c
--- /dev/null
+++ b/.github/workflows/release-subproject-fabric.yml
@@ -0,0 +1,48 @@
+# A Fabric-only version of the normal release workflow.
+name: Release-Subproject-Fabric
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build fabric:publishGithub fabric:publishCurseforge fabric:publishModrinth --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
+ CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release-subproject-neoforge.yml b/.github/workflows/release-subproject-neoforge.yml
new file mode 100644
index 0000000..f7d09bb
--- /dev/null
+++ b/.github/workflows/release-subproject-neoforge.yml
@@ -0,0 +1,48 @@
+# A NeoForge-only version of the normal release workflow.
+name: Release-Subproject-NeoForge
+
+on:
+ workflow_dispatch:
+
+permissions:
+ contents: write
+
+jobs:
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
+ with:
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build neoforge:publishGithub neoforge:publishCurseforge neoforge:publishModrinth --stacktrace
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
+ CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
+ with:
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 143fbea..1efbad4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,35 +1,53 @@
+# Builds the project and publishes the artifacts to GitHub, Modrinth and CurseForge.
+# Modrinth publishing requires a Modrinth PAT `MODRINTH_TOKEN`
+# Will skip without error if not present.
+# CurseForge publishing requires a CurseForge API token `CURSEFORGE_TOKEN`
+# Will skip without error if not present.
+# Preference using the root gradle.properties file to configure releases.
+name: Release
+
on:
- push:
- tags:
- - 'v*'
+ workflow_dispatch:
-name: Release Mod
+permissions:
+ contents: write
jobs:
- build:
- runs-on: ubuntu-latest
+ release:
+ strategy:
+ matrix:
+ java: [
+ 25
+ ]
+ os: [
+ ubuntu-latest
+ ]
+ runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v1
- - uses: actions/setup-java@v1
- with:
- java-version: 17
- - name: Build
- uses: eskatos/gradle-command-action@v1.3.2
+ - name: Checkout repository
+ uses: actions/checkout@v5
with:
- gradle-version: wrapper
- arguments: build --stacktrace
- - name: Release to Github
- uses: softprops/action-gh-release@v1
- if: ${{ success() }}
+ fetch-depth: 0
+ - name: Validate Gradle wrapper
+ uses: gradle/actions/wrapper-validation@v5
+ - name: Setup JDK ${{ matrix.java }}
+ uses: actions/setup-java@v5
with:
- files: release/*.jar
- fail_on_unmatched_files: true
+ distribution: zulu
+ java-version: ${{ matrix.java }}
+ - name: Make Gradle wrapper executable
+ if: ${{ runner.os != 'Windows' }}
+ run: chmod +x ./gradlew
+ - name: Build
+ run: ./gradlew build neoforge:publishGithub neoforge:publishCurseforge neoforge:publishModrinth fabric:publishGithub fabric:publishCurseforge fabric:publishModrinth --stacktrace
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Release to CurseForge
- uses: gradle/gradle-build-action@v2
- if: ${{ success() }}
+ MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
+ CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
+ - name: Capture build artifacts
+ if: ${{ runner.os == 'Linux' && matrix.java == '25' }}
+ uses: actions/upload-artifact@v5
with:
- arguments: :fabric:curseforge :forge:curseforge --stacktrace
- env:
- CURSE_API_KEY: ${{ secrets.CURSE_API_KEY }}
+ name: artifacts
+ path: |
+ **/build/libs/
diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml
new file mode 100644
index 0000000..979f38b
--- /dev/null
+++ b/.github/workflows/sync-labels.yml
@@ -0,0 +1,22 @@
+# Synchronizes the repo's labels with a centralized `labels.yml` file.
+# Requires `GITHUB_TOKEN` to have write permissions; if not, replace it with a custom token.
+name: Sync Labels
+
+on:
+ push:
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Setup NodeJS
+ uses: actions/setup-node@v6
+ with:
+ node-version: 22
+ - run: curl https://raw.githubusercontent.com/TerminalMC/.github/HEAD/.github/labels.yml -o ./labels.yml
+ - run: npx github-label-sync -a '${{ secrets.GITHUB_TOKEN }}' -l 'labels.yml' ${{ github.repository }}
diff --git a/.gitignore b/.gitignore
index 5f81675..f1ecb28 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,26 @@
-.gradle
-.idea
-.vscode
-out
-build
-run
-.architectury-transformer
-*.iml
-release
+# Gradle
+.gradle/
+
+# Artifacts
bin/
+build/
+run/
+runs/
+out/
+classes/
+.eclipse/
+
+# IDEA
+.idea/
+*.iml
+*.ipr
+*.iws
+
+# VSCode
+.settings/
+.vscode/
+.classpath
+.project
+
+# Other
+.env
diff --git a/LICENSE b/LICENSE.txt
similarity index 96%
rename from LICENSE
rename to LICENSE.txt
index 1d032b9..ee50ab8 100644
--- a/LICENSE
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2020 AppleTheGolden
+Copyright (c) 2020-2024 AppleTheGolden
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2b907d0
--- /dev/null
+++ b/README.md
@@ -0,0 +1,140 @@
+
+
+
+
+## Now Playing
+
+Shows a popup whenever the active music track changes.
+
+[]()
+[](https://modrinth.com/project/eNF4Bfla/versions)
+
+[](https://fabricmc.net/)
+[](https://quiltmc.org/)
+[](https://neoforged.net/)
+
+[](https://modrinth.com/project/eNF4Bfla)
+[](https://github.com/Scotsguy/now-playing)
+
+
+
+### About
+
+Have you ever wondered what all the songs in Minecraft are actually called? Sure, you could check
+the soundtrack to find out, but then you have to listen to every song, hoping it's the one you're
+thinking of.
+
+With this mod, wonder no more! A toast will pop up in the top right corner of your screen or just
+above your hotbar (configurable), telling you what song you're about to listen to.
+
+### Setup
+
+
+General Options
+
+- Only use key or command
+ - Whether to only show pop-up when the keybind is pressed or the `/nowplaying` command is used.
+- Music pop-up style
+ - How to display pop-up for background music. Choice of toast, hotbar (status bar) message, or
+ nothing.
+- Jukebox pop-up style
+ - How to display pop-up for jukebox music. Choice of toast, hotbar (status bar) message, or
+ nothing.
+- Fallback to toast
+ - Whether to display a toast for music set to hotbar, if not possible to show a hotbar message.
+- Silent toast
+ - Whether the toast should make a whoosh noise.
+- Toast scale
+ - The size of the toast.
+- Simple toast
+ - Whether to show the "Now Playing" text as well as the track title in the toast.
+- Dark toast
+ - Whether to use a dark background for the toast.
+- Toast display time
+ - How long the toast will be displayed for.
+- Hotbar display time
+ - How long the hotbar message will be displayed for.
+- Narrate pop-up
+ - Whether pop-ups should be narrated, if the narrator is enabled.
+
+
+
+
+Custom Sprites
+
+Now Playing supports changing which disc sprites are displayed for each background music track
+via a resource pack. First, create a `nowplaying` folder in the `assets` folder of your pack,
+and place a `sprites.json` file in that folder, as shown below.
+
+```
+assets
+├── minecraft
+└── nowplaying
+ └── sprites.json
+```
+
+Next, populate the `sprites.json` file with key-value pairs, where the key is the music resource
+location (or part thereof), and the value is the sprite location, as shown below.
+
+```json
+{
+ "minecraft:music/game": "minecraft:textures/item/music_disc_cat",
+ "minecraft:music/game/creative": "minecraft:textures/item/music_disc_blocks",
+ "minecraft:music/game/creative/taswell": "minecraft:textures/item/music_disc_chirp",
+ "minecraft:music/game/nether": "minecraft:textures/item/music_disc_pigstep"
+}
+```
+
+If you only specify part of a music resource location, the corresponding disc will be shown for
+all music tracks in that location, except those that have a more specific definition. In the
+example above:
+
+- All tracks in the `game` folder will use the `cat` sprite, except;
+ - tracks in the `creative` folder, which will use the `blocks` sprite, except;
+ - the `taswell` track , which will use the `chirp` sprite
+ - tracks in the `nether` folder, which will use the `pigstep` sprite.
+
+You can use any existing music disc sprite, or any sprite provided by a resourcepack.
+
+
+
+
+Custom Music Track Titles
+
+If you have a resource pack that adds custom background music, you can specify titles using a
+translation file (e.g. `en_us.json`). First, create a `nowplaying` folder in the `assets` folder
+of your pack, create a `lang` folder inside, and place a translation file in that folder, as shown
+below.
+
+```
+assets
+├── minecraft
+└── nowplaying
+ └── lang
+ └── en_us.json
+```
+
+Next, populate the translation file with key-value pairs, where the key is the music resource
+location, prefixed with `nowplaying`, and the value is the translated title of the track, as
+shown below.
+
+```json
+{
+ "nowplaying.minecraft:music/game/a_familiar_room": "Aaron Cherof - A Familiar Room",
+ "nowplaying.minecraft:music/game/an_ordinary_day": "Kumi Tanioka - An Ordinary Day",
+ "nowplaying.minecraft:music/game/ancestry": "Lena Raine - Ancestry",
+ "nowplaying.minecraft:music/game/clark": "C418 - Clark",
+ "nowplaying.minecraft:music/game/creative/aria_math": "C418 - Aria Math"
+}
+```
+
+Old translation files which use the `music.nowplaying.[name]` format are still supported, but
+have a lower priority than the new format.
+
+
+
+### Contact
+
+[](https://github.com/Scotsguy/now-playing/issues)
+
+[](https://github.com/scotsguy/now-playing/blob/HEAD/LICENSE.txt)
diff --git a/build.gradle b/build.gradle
index 012875e..a7a06e6 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,58 +1,228 @@
-plugins {
- alias(libs.plugins.architectury)
- alias(libs.plugins.loom) apply false
- alias(libs.plugins.vineflower) apply false
- alias(libs.plugins.cursegradle) apply false
-}
+import me.modmuss50.mpp.ReleaseType
+import util.EnvUtil
+import util.PropUtil
+import util.StaticUtil
+
+import java.time.LocalDate
-architectury {
- minecraft = libs.versions.minecraft.get()
+plugins {
+ id("net.fabricmc.fabric-loom") version("${loom_version}") apply(false)
+ id("net.neoforged.moddev") version("${moddev_version}") apply(false)
+ id("net.neoforged.licenser") version("${licenser_version}") apply(false)
+ id("me.modmuss50.mod-publish-plugin") version("${mpp_version}")
+ id("org.ajoberstar.grgit.service") version("${grgitservice_version}")
}
subprojects {
- apply plugin: "dev.architectury.loom"
- apply plugin: 'io.github.juuxel.loom-vineflower'
-
- dependencies {
- minecraft libs.minecraft
- if (libs.versions.useParchment.get() == "1") {
- mappings loom.layered() {
- officialMojangMappings()
- parchment("org.parchmentmc.data:parchment-${libs.versions.minecraft.get()}:${libs.versions.parchment.get()}@zip")
+ final env = new EnvUtil(providers)
+ final prop = new PropUtil(project)
+ final cLog = StaticUtil.versionChangelog(rootProject.file("changelog.md"), mod_version)
+
+ // Append Minecraft version extension
+ mod_version = "${mod_version}+${minecraft_version}"
+ // Append dependency version extension, if configured
+ if (prop.has("dep_ext_${name}")) {
+ final data = prop.get("dep_ext_${name}").split(":")
+ final dep_abbrev = data[0]
+ // Strip meta from dependency version
+ final dep_ver = prop.get(data[1]).split("\\+")[0]
+ mod_version = "${mod_version}+${dep_abbrev}${dep_ver}"
+ }
+ version = mod_version
+ group = mod_group
+
+ // Configure license headers
+ apply(plugin: "net.neoforged.licenser")
+ final licenseDir = "src/main/resources/assets/${mod_id}/license/"
+ license {
+ include("**/*.java") // Java files only
+ // Apply main license header
+ header = rootProject.project("common").file("${licenseDir}HEADER.txt")
+ properties {
+ project_name = mod_name
+ owner_name = mod_owner
+ year = LocalDate.now().getYear().toString()
+ }
+ // Apply attribution license headers, if configured
+ if (att_license_mods != "") {
+ att_license_mods.split(",").each { String modId ->
+ //noinspection GroovyAssignabilityCheck
+ matching({ include(prop.list("att_license_files_${modId}")) }) {
+ header = rootProject.project("common").file("${licenseDir}${modId}/HEADER.txt")
+ it
+ }
}
- } else {
- mappings loom.officialMojangMappings()
}
}
- loom {
- silentMojangMappingsLicense()
+ // Configure multi-site publishing
+ if (name != "common") {
+ apply(plugin: "me.modmuss50.mod-publish-plugin")
+ apply(plugin: "org.ajoberstar.grgit.service")
+
+ afterEvaluate { sp ->
+ publishMods {
+ // Common configuration
+ file = jar.archiveFile
+ version = mod_version
+ type = ReleaseType.of(mod_version_type)
+ displayName = "v${mod_version}-${StaticUtil.capsLoader(sp.name)}"
+ modLoaders.addAll(prop.safeList("mod_loaders_${sp.name}"))
+ maxRetries = 5
+ dryRun = !env.has("GITHUB_TOKEN")
+ && !env.has("MODRINTH_TOKEN")
+ && !env.has("CURSEFORGE_TOKEN")
+ // GitHub configuration
+ github {
+ parent project(":").tasks.named("publishGithub")
+ accessToken = env.safe("GITHUB_TOKEN")
+ if (build_sources_jar == "true") additionalFiles.from(sourcesJar.archiveFile)
+ if (build_javadoc_jar == "true") additionalFiles.from(javadocJar.archiveFile)
+ return void
+ }
+ // Modrinth configuration
+ modrinth {
+ accessToken = env.safe("MODRINTH_TOKEN")
+ projectId = prop.safe("mod_modrinth_id")
+ minecraftVersions.addAll(prop.safeList("mc_versions_${sp.name}"))
+ changelog = cLog
+ prop.safeList("${sp.name}_deps").each { dep ->
+ try {
+ final depData = prop.list("d_${sp.name}_${dep}")
+ if (depData.length > 2 && depData[2] != "-") {
+ final mrDepData = depData[2].split(":")
+ final type = mrDepData[0]
+ if (type == "req") {
+ requires { id = mrDepData[1] }
+ } else if (type == "opt") {
+ optional { id = mrDepData[1] }
+ } else if (type == "inc") {
+ incompatible { id = mrDepData[1] }
+ } else if (type == "emb") {
+ embeds { id = mrDepData[1] }
+ } else {
+ throw new IllegalArgumentException(
+ "Unrecognized dependency type: ${mrDepData[0]}"
+ )
+ }
+ }
+ } catch (Exception ex) {
+ logger.error("Error processing Modrinth dependency '${dep}' for "
+ + "subproject '${sp.name}'. Check dependency property format.")
+ throw ex
+ }
+ }
+ // Sync Modrinth description with README
+ if (prop.has("mod_github_repo")) {
+ projectDescription = rootProject.file("README.md").text.replaceAll(
+ "src=\"\\./",
+ "src=\"https://raw.githubusercontent.com/${prop.get("mod_github_repo")}/HEAD/"
+ )
+ return
+ }
+ }
+ // CurseForge configuration
+ curseforge {
+ accessToken = env.safe("CURSEFORGE_TOKEN")
+ projectId = prop.safe("mod_curseforge_id")
+ projectSlug = prop.safe("mod_curseforge_slug")
+ minecraftVersions.addAll(prop.safeList("mc_versions_${sp.name}").collect { ver ->
+ final hyphenIdx = ver.indexOf("-")
+ if (hyphenIdx != -1) {
+ return ver.substring(0, hyphenIdx) + "-snapshot"
+ } else {
+ return ver
+ }
+ })
+ changelog = cLog
+ prop.safeList("${sp.name}_deps").each { dep ->
+ try {
+ final depData = prop.list("d_${sp.name}_${dep}")
+ if (depData.length > 3 && depData[3] != "-") {
+ final cfDepData = depData[3].split(":")
+ final type = cfDepData[0]
+ if (type == "req") {
+ requires(cfDepData[1])
+ } else if (type == "opt") {
+ optional(cfDepData[1])
+ } else if (type == "inc") {
+ incompatible(cfDepData[1])
+ } else if (type == "emb") {
+ embeds(cfDepData[1])
+ } else {
+ throw new IllegalArgumentException(
+ "Unrecognized dependency type: ${cfDepData[0]}"
+ )
+ }
+ }
+ } catch (Exception ex) {
+ logger.error("Error processing CurseForge dependency '${dep}' for "
+ + "subproject '${sp.name}'. Check dependency property format.")
+ throw ex
+ }
+ }
+ return void
+ }
+ }
+ final hasSpConf = prop.has("mod_loaders_${name}") && prop.has("mc_versions_${name}")
+ tasks.named("publishGithub") {
+ onlyIf {
+ return hasSpConf
+ && prop.has("mod_github_repo")
+ && (it["dryRun"].get() || env.has("GITHUB_TOKEN"))
+ }
+ }
+ tasks.named("publishModrinth") {
+ onlyIf {
+ return hasSpConf
+ && prop.has("mod_modrinth_id")
+ && (it["dryRun"].get() || env.has("MODRINTH_TOKEN"))
+ }
+ }
+ tasks.named("publishCurseforge") {
+ onlyIf {
+ return hasSpConf
+ && prop.has("mod_curseforge_id")
+ && prop.has("mod_curseforge_slug")
+ && (it["dryRun"].get() || env.has("CURSEFORGE_TOKEN"))
+ }
+ }
+ }
}
}
-allprojects {
- apply plugin: "java"
- apply plugin: "architectury-plugin"
- apply plugin: "maven-publish"
- apply plugin: "com.matthewprenger.cursegradle"
-
- archivesBaseName = rootProject.archives_base_name
- version = rootProject.mod_version
- group = rootProject.maven_group
-
- repositories {
- maven { url "https://maven.shedaniel.me/" }
- maven { url "https://maven.terraformersmc.com" }
- maven { url "https://maven.parchmentmc.org" }
- maven { url "https://maven.quiltmc.org/repository/release/" }
+/*
+ This configuration, combined with the subproject configuration above, allows
+ multiple files to be uploaded to a single GitHub release.
+ */
+publishMods {
+ final env = new EnvUtil(providers)
+ final prop = new PropUtil(project)
+ version = "${mod_version}+${minecraft_version}"
+ displayName = "v${mod_version}+${minecraft_version}"
+ type = ReleaseType.of(mod_version_type)
+ dryRun = !env.has("GITHUB_TOKEN")
+ github {
+ accessToken = env.safe("GITHUB_TOKEN")
+ repository = prop.safe("mod_github_repo")
+ commitish = grgitService.service.get().grgit.branch.current().name
+ tagName = "v${mod_version}+${minecraft_version}"
+ allowEmptyFiles = true
+ // Link to header in changelog file
+ changelog = "[Changelog](./changelog.md#${mod_version.replaceAll("\\.", "")})"
}
-
- tasks.withType(JavaCompile).configureEach {
- options.encoding = "UTF-8"
- options.release = 17
+ tasks.named("publishGithub") {
+ onlyIf {
+ return prop.has("mod_github_repo")
+ && (it["dryRun"].get() || env.has("GITHUB_TOKEN"))
+ }
}
+}
- java {
- withSourcesJar()
+file(".").eachFile {
+ if (it.isFile() && it.name.endsWith(".gradle")
+ && it.name != "build.gradle"
+ && it.name != "settings.gradle") {
+ apply(from: it.name)
}
}
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 0000000..fbd5c69
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,3 @@
+plugins {
+ id("groovy-gradle-plugin")
+}
diff --git a/buildSrc/src/main/groovy/multiloader-common.gradle b/buildSrc/src/main/groovy/multiloader-common.gradle
new file mode 100644
index 0000000..db6cb7e
--- /dev/null
+++ b/buildSrc/src/main/groovy/multiloader-common.gradle
@@ -0,0 +1,265 @@
+plugins {
+ id("java-library")
+ id("maven-publish")
+}
+
+// Configure project dependency repos, except for mod loader repos provided by
+// their respective plugins
+repositories {
+ mavenCentral()
+ mavenLocal()
+ exclusiveContent {
+ forRepository {
+ //noinspection ForeignDelegate
+ maven {
+ name = "ParchmentMC"
+ url = uri("https://maven.parchmentmc.org")
+ }
+ }
+ filter {
+ includeGroup("org.parchmentmc.data")
+ }
+ }
+ exclusiveContent {
+ forRepository {
+ //noinspection ForeignDelegate
+ maven {
+ name = "Fabric"
+ url = uri("https://maven.fabricmc.net")
+ }
+ }
+ filter {
+ includeGroupAndSubgroups("net.fabricmc")
+ }
+ }
+ maven {
+ name = "Modrinth"
+ url = uri("https://api.modrinth.com/maven")
+ }
+ maven {
+ name = "Shedaniel"
+ url = uri("https://maven.shedaniel.me")
+ }
+ maven {
+ name = "isXander"
+ url = uri("https://maven.isxander.dev/releases")
+ }
+}
+
+// Set output file name
+base {
+ archivesName.set("${mod_id}-${project.name}")
+}
+
+// Configure Java build
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(java_version))
+ if (build_sources_jar == "true") withSourcesJar()
+ if (build_javadoc_jar == "true") withJavadocJar()
+}
+
+// Declare capabilities on the outgoing configurations
+final capabilityVariants = ["apiElements", "runtimeElements"]
+if (build_sources_jar == "true") capabilityVariants.add("sourcesElements")
+if (build_javadoc_jar == "true") capabilityVariants.add("javadocElements")
+capabilityVariants.each { variant ->
+ configurations.named(variant).configure {
+ outgoing {
+ capability("$group:${base.archivesName.get()}:$version")
+ capability("$group:$mod_id:$version")
+ }
+ }
+ publishing.publications.configureEach {
+ suppressPomMetadataWarningsFor(variant)
+ }
+}
+
+// Configure main file
+jar {
+ from(rootProject.file("LICENSE.txt")) {
+ rename { "LICENSE_${mod_id}.txt" }
+ }
+
+ manifest {
+ attributes([
+ "Specification-Title" : mod_name,
+ "Specification-Vendor" : mod_owner,
+ "Specification-Version" : project.jar.archiveVersion,
+ "Implementation-Title" : project.name,
+ "Implementation-Version" : project.jar.archiveVersion,
+ "Implementation-Vendor" : mod_owner,
+ "Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ"),
+ "Timestamp" : System.currentTimeMillis(),
+ "Built-On-Java" : "${System.getProperty('java.vm.version')} (${System.getProperty('java.vm.vendor')})",
+ "Built-On-Minecraft" : minecraft_version
+ ])
+ }
+}
+
+// Configure sources file
+if (build_sources_jar == "true") {
+ sourcesJar {
+ from(rootProject.file("LICENSE.txt")) {
+ rename { "LICENSE_${mod_id}.txt" }
+ }
+ }
+}
+
+// Process Gradle property expansion for metadata files
+processResources {
+ final expandProps = [
+ // Mod
+ "mod_version" : mod_version,
+ "mod_group" : mod_group,
+ "mod_id" : mod_id,
+ "mod_name" : mod_name,
+ "mod_description_fabric" : mod_description.replace("\n", "\\n"),
+ "mod_description_neoforge" : mod_description,
+ "mod_icon" : "assets/${mod_id}/icon.png",
+ "mod_owner" : mod_owner,
+ "mod_authors_list" : asJsonList(mod_authors),
+ "mod_authors_string" : mod_authors.split(",").join(", "),
+ "mod_contributors_list" : asJsonList("${mod_contributors},${mod_translators},${mod_credits}"),
+ "mod_contributors_string" : asTomlList("${mod_contributors},${mod_translators},${mod_credits}"),
+ "mod_license" : mod_license,
+ // Links
+ "homepage_url" : homepage_url,
+ "sources_url" : sources_url,
+ "issues_url" : issues_url,
+ "contact_url" : contact_url,
+ // Java
+ "java_version" : java_version,
+ "java_versions_fabric_list" : asJsonList(java_versions_fabric),
+ "java_versions_neoforge" : java_versions_neoforge,
+ // Minecraft
+ "minecraft_versions_fabric_list": asJsonList(minecraft_versions_fabric),
+ "minecraft_versions_neoforge": minecraft_versions_neoforge,
+ // Mixin
+ "mixin_version_min" : mixin_version_min,
+ "mixinextras_version_min" : mixinextras_version_min,
+ // Fabric
+ "fabric_loader_versions_list": asJsonList(fabric_loader_versions),
+ "fabric_api_versions_list" : asJsonList(fabric_api_versions),
+ "fabric_entrypoints_main" : asJsonListPrefixed(fabric_entrypoints_main, "${mod_group}.${mod_id}."),
+ "fabric_entrypoints_client" : asJsonListPrefixed(fabric_entrypoints_client, "${mod_group}.${mod_id}."),
+ "fabric_entrypoints_server" : asJsonListPrefixed(fabric_entrypoints_server, "${mod_group}.${mod_id}."),
+ "fabric_entrypoints_modmenu" : asJsonListPrefixed(fabric_entrypoints_modmenu, "${mod_group}.${mod_id}."),
+ // NeoForge
+ "neoforge_versions" : neoforge_versions,
+ // Dependencies:
+ "fabric_depends" : "",
+ "fabric_recommends" : "",
+ "fabric_suggests" : "",
+ "fabric_conflicts" : "",
+ "fabric_breaks" : "",
+ "neoforge_all_deps" : ""
+ ]
+
+ // Apply property-defined dependencies
+ safePropList("fabric_deps").each { dep ->
+ try {
+ final depData = propList("d_fabric_${dep}")
+ if (depData.length > 1 && depData[1] != "-") {
+ final loaderData = depData[1].split(":")
+ final key = "fabric_${loaderData[0]}".toString()
+ final prefix = expandProps[key] == "" ? "" : ",\n "
+ final depList = "[${asJsonList(project.property("vr_fabric_${dep}").toString())}]"
+ final value = "${expandProps[key]}${prefix}\"${loaderData[1]}\": ${depList}"
+ expandProps[key] = value.toString()
+// logger.info("Fabric metadata '${key}': ${expandProps[key]}")
+ }
+ } catch (Exception ex) {
+ logger.error("Error processing Fabric dependency metadata for '${dep}'. "
+ + "Check dependency property format.")
+ throw ex
+ }
+ }
+ if (!expandProps["fabric_depends"].isBlank()) {
+ expandProps["fabric_depends"] = "${expandProps["fabric_depends"]},".toString()
+ }
+ safePropList("neoforge_deps").each { dep ->
+ try {
+ final depData = propList("d_neoforge_${dep}")
+ if (depData.length > 1 && depData[1] != "-") {
+ final loaderData = depData[1].split(":")
+ expandProps["neoforge_all_deps"] = expandProps["neoforge_all_deps"] +
+ "[[dependencies.${mod_id}]]\n" +
+ "modId=\"${loaderData[1]}\"\n" +
+ "type=\"${loaderData[0]}\"\n" +
+ "versionRange=\"${project.property("vr_neoforge_${dep}")}\"\n" +
+ "side=\"CLIENT\"\n\n"
+ }
+ } catch (Exception ex) {
+ logger.error("Error processing NeoForge dependency metadata for '${dep}'. "
+ + "Check dependency property format.")
+ throw ex
+ }
+ }
+// logger.info("NeoForge metadata 'neoforge_all_deps': ${expandProps["neoforge_all_deps"]}")
+
+ filesMatching(["pack.mcmeta", "*.mod.json", "*.mixins.json", "META-INF/*.toml"]) {
+ expand(expandProps)
+ }
+ inputs.properties(expandProps)
+}
+
+// Configure publication
+publishing {
+ publications {
+ mavenJava(MavenPublication) {
+ artifactId = base.archivesName.get()
+ from(components.java)
+ }
+ }
+}
+
+/**
+ * Converts a CSV list into an FMJ-usable format.
+ */
+static asJsonList(String csvString) {
+ return csvString.split(",")
+ .findAll { !it.isBlank() }
+ .collect { "\"${it.strip()}\"" }
+ .join(",")
+}
+
+/**
+ * Converts a CSV list into an FMJ-usable list format, prefixing each element with the source
+ * package (${mod_group}.${mod_id}).
+ */
+static asJsonListPrefixed(String csvString, String prefix) {
+ return csvString.split(",")
+ .findAll { !it.isBlank() }
+ .collect { "\"${prefix}${it.strip()}\"" }
+ .join(",")
+}
+
+/**
+ * Converts a CSV list into an NMT-usable format.
+ */
+static asTomlList(String csvString) {
+ return csvString.split(",")
+ .findAll { !it.isBlank() }
+ .collect { it.strip() }
+ .join(", ")
+}
+
+/**
+ @return the value of the property, CSV-split.
+ */
+String[] propList(String propertyName) {
+ final list = project.property(propertyName).toString().split(",")
+ for (int i = 0; i < list.length; i++) {
+ list[i] = list[i].strip()
+ }
+ return list
+}
+
+/**
+ @return the value of the property CSV-split if it is set, an empty array otherwise.
+ */
+String[] safePropList(String propertyName) {
+ return project.hasProperty(propertyName) && !project.property(propertyName).toString().isBlank()
+ ? propList(propertyName)
+ : []
+}
diff --git a/buildSrc/src/main/groovy/multiloader-loader.gradle b/buildSrc/src/main/groovy/multiloader-loader.gradle
new file mode 100644
index 0000000..44e0770
--- /dev/null
+++ b/buildSrc/src/main/groovy/multiloader-loader.gradle
@@ -0,0 +1,48 @@
+plugins {
+ id("multiloader-common")
+}
+
+// Access common subproject tasks
+configurations {
+ commonJava {
+ canBeResolved = true
+ }
+ commonResources {
+ canBeResolved = true
+ }
+}
+
+// Add common subproject dependency
+dependencies {
+ compileOnly(project(":common")) {
+ capabilities {
+ requireCapability("$group:$mod_id")
+ }
+ }
+ commonJava project(path: ":common", configuration: "commonJava")
+ commonResources project(path: ":common", configuration: "commonResources")
+}
+
+// Add common subproject task dependencies
+compileJava {
+ dependsOn(configurations.commonJava)
+ source(configurations.commonJava)
+}
+processResources {
+ dependsOn(configurations.commonResources)
+ from(configurations.commonResources)
+}
+if (build_sources_jar == "true") {
+ tasks.named("sourcesJar", Jar) {
+ dependsOn(configurations.commonJava)
+ from(configurations.commonJava)
+ dependsOn(configurations.commonResources)
+ from(configurations.commonResources)
+ }
+}
+if (build_javadoc_jar == "true") {
+ tasks.named("javadoc", Javadoc) {
+ dependsOn(configurations.commonJava)
+ source(configurations.commonJava)
+ }
+}
diff --git a/buildSrc/src/main/groovy/util/EnvUtil.groovy b/buildSrc/src/main/groovy/util/EnvUtil.groovy
new file mode 100644
index 0000000..b7ecef2
--- /dev/null
+++ b/buildSrc/src/main/groovy/util/EnvUtil.groovy
@@ -0,0 +1,25 @@
+package util
+
+import org.gradle.api.provider.ProviderFactory
+
+class EnvUtil {
+ final ProviderFactory providers
+
+ EnvUtil(ProviderFactory providers) {
+ this.providers = providers
+ }
+
+ /**
+ @return {@code true} if the environment variable is set.
+ */
+ boolean has(String variableName) {
+ return !safe(variableName).isBlank()
+ }
+
+ /**
+ @return the value of the environment variable if it is set, an empty string otherwise.
+ */
+ String safe(String variableName) {
+ return providers.environmentVariable(variableName).getOrElse("")
+ }
+}
diff --git a/buildSrc/src/main/groovy/util/PropUtil.groovy b/buildSrc/src/main/groovy/util/PropUtil.groovy
new file mode 100644
index 0000000..ed4d640
--- /dev/null
+++ b/buildSrc/src/main/groovy/util/PropUtil.groovy
@@ -0,0 +1,84 @@
+package util
+
+import org.gradle.api.Project
+import org.gradle.api.artifacts.ExternalModuleDependency
+
+import java.util.function.BiFunction
+
+class PropUtil {
+ final Project project
+
+ PropUtil(Project project) {
+ this.project = project
+ }
+
+ /**
+ @return {@code true} if the property is set.
+ */
+ boolean has(String propertyName) {
+ return project.hasProperty(propertyName) && !project.property(propertyName).toString().isBlank()
+ }
+
+ /**
+ @return the value of the property.
+ */
+ String get(String propertyName) {
+ return project.property(propertyName)
+ }
+
+ /**
+ @return the value of the property if it exists, an empty string otherwise.
+ */
+ String safe(String propertyName) {
+ return has(propertyName) ? project.property(propertyName) : ""
+ }
+
+ /**
+ @return the value of the property, CSV-split.
+ */
+ String[] list(String propertyName) {
+ return project.property(propertyName).toString().split(",")
+ .findAll { !it.isBlank() }
+ .collect { it.strip() }
+ }
+
+ /**
+ @return the value of the property CSV-split if it is set, an empty array otherwise.
+ */
+ String[] safeList(String propertyName) {
+ return has(propertyName) ? list(propertyName) : []
+ }
+
+ /**
+ * Applies configured dependencies for a subproject.
+ * @param pName the subproject name.
+ * @param selector the dependency configuration selector.
+ */
+ void applyDependencies(
+ String pName,
+ BiFunction> selector
+ ) {
+ safeList("${pName}_deps").each { dep ->
+ try {
+ final depData = list("d_${pName}_${dep}")
+ if (depData[0] != "-") {
+ final mavenData = depData[0].split(":")
+ final mavenGroup = mavenData[3]
+ final mavenProject = mavenData[4]
+ final subVersion = ((mavenData.length > 6 && mavenData[6] != "-")
+ ? project.property(mavenData[6])
+ : project.property("v_${dep}")).toString()
+ final mavenVersion = "${mavenData[5]}".replace("\$v", subVersion)
+ def gradleDep = "${mavenGroup}:${mavenProject}:${mavenVersion}"
+ for (int i = 2; i >= 0; i--) {
+ gradleDep = selector.apply(mavenData[i], gradleDep)
+ }
+ }
+ } catch (Exception ex) {
+ logger.error("Error processing Gradle dependency '${dep}' for subproject "
+ + "'${pName}'. Check dependency property format.")
+ throw ex
+ }
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/util/StaticUtil.groovy b/buildSrc/src/main/groovy/util/StaticUtil.groovy
new file mode 100644
index 0000000..d3e040c
--- /dev/null
+++ b/buildSrc/src/main/groovy/util/StaticUtil.groovy
@@ -0,0 +1,46 @@
+package util
+
+class StaticUtil {
+ /**
+ * Converts a lowercase mod loader name into its formal version.
+ */
+ static String capsLoader(String loader) {
+ switch (loader) {
+ case "fabric": return "Fabric"
+ case "quilt": return "Quilt"
+ case "forge": return "Forge"
+ case "neoforge": return "NeoForge"
+ default: return loader
+ }
+ }
+
+ /**
+ @returns the latest changelog from the file, verified to match the version.
+ */
+ static String versionChangelog(File file, String version) {
+ final lines = file.readLines()
+ final builder = new StringBuilder()
+ // Changelog version header is on the third line of the file; check it
+ if (version != lines.get(2).substring(3)) {
+ throw new IllegalArgumentException(String.format(
+ "Mod version '%s' does not match changelog version '%s'",
+ version,
+ lines.get(2).substring(3)
+ ))
+ } else {
+ // Iterate over content lines
+ for (int i = 4; i < lines.size(); i++) {
+ final line = lines.get(i)
+ if (line.startsWith("## ")) {
+ // Encountered next changelog header; finish
+ break
+ } else {
+ // Append the content line, respecting blank lines
+ if (!builder.isEmpty()) builder.append("\n")
+ if (!line.isBlank()) builder.append(line)
+ }
+ }
+ }
+ return builder.toString()
+ }
+}
diff --git a/changelog.md b/changelog.md
new file mode 100644
index 0000000..c12712e
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,51 @@
+# Changelog
+
+## 2.1.0
+
+- Updated to mc26.1.1
+
+## 2.0.0
+
+- Updated to mc26.1
+- Mod versioning scheme is now `major.mc.minor`:
+ - `major` is incremented on 'significant' feature changes, or breaking API changes (if
+ applicable).
+ - `mc` is never reset, and is incremented on every MC release, irrespective of whether a mod
+ update was required.
+ - `minor` is reset when `major` is changed, and is incremented on every update that does not
+ change either of the previous two numbers.
+
+## 1.6.0
+
+- Changed mod ID to `nowplaying` on all platforms
+
+## 1.5.16
+
+- Fixed toast queueing when using the Dynamic FPS mod
+- Fixed Modrinth links
+
+## 1.5.15
+
+- Fixed 'Silent Toast' option not working on 1.21.5
+
+## 1.5.14
+
+- Fixed partial paths in sprites.json being overwritten by defaults
+
+## 1.5.13
+
+- Updated Russian translation (rfin0)
+
+## 1.5.13-beta.1
+
+- Added support for defining custom disc sprite locations
+- Added option for dark mode toast
+- Added a keybind to play the next background music track
+
+## 1.5.12
+
+- Added Spanish translations (warbacon)
+
+## 1.5.11
+
+- Added Russian translation (rfin0)
diff --git a/common/build.gradle b/common/build.gradle
index 970015e..bb6ab6d 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -1,25 +1,86 @@
-dependencies {
- // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies
- // Do NOT use other classes from fabric loader
- modImplementation libs.fabric.loader
+import util.PropUtil
- modImplementation libs.cloth.common
+plugins {
+ id("multiloader-common")
+ id("net.fabricmc.fabric-loom")
}
-architectury {
- common(rootProject.enabled_platforms.split(","))
+// Configure common dependencies
+dependencies {
+ // Minecraft
+ minecraft("com.mojang:minecraft:${minecraft_version}")
+
+ // ASM
+ compileOnly("org.ow2.asm:asm:${asm_version}")
+ compileOnly("org.ow2.asm:asm-analysis:${asm_version}")
+ compileOnly("org.ow2.asm:asm-commons:${asm_version}")
+ compileOnly("org.ow2.asm:asm-tree:${asm_version}")
+ compileOnly("org.ow2.asm:asm-util:${asm_version}")
+
+ // Mixin
+ compileOnly("net.fabricmc:sponge-mixin:${mixin_version}")
+
+ // MixinExtras
+ compileOnly(annotationProcessor("io.github.llamalad7:mixinextras-common:${mixinextras_version}"))
+
+ // Apply property-defined dependencies
+ final selector = (String type, dep) -> {
+ switch (type) {
+ case "anp": //noinspection DependencyNotationArgument
+ return annotationProcessor(dep)
+ break
+ case "api": //noinspection DependencyNotationArgument
+ return api(dep)
+ break
+ case "con": //noinspection DependencyNotationArgument
+ return compileOnly(dep)
+ break
+ case "imp": //noinspection DependencyNotationArgument
+ return implementation(dep)
+ break
+ case "-":
+ return dep
+ break
+ default:
+ throw new IllegalArgumentException("Unrecognized dependency type: ${type}")
+ }
+ }
+ new PropUtil(project).applyDependencies(project.name, selector)
}
-publishing {
- publications {
- mavenCommon(MavenPublication) {
- artifactId = rootProject.archives_base_name
- from components.java
+// Configure Loom
+loom {
+ // Apply common classTweaker if it exists
+ def aw = file("src/main/resources/${mod_id}.classtweaker")
+ if (aw.exists()) accessWidenerPath.set(aw)
+ if (aw.exists()) {
+ validateAccessWidener { accessWidener = aw }
+ afterEvaluate {
+ validateAccessWidener.run()
}
}
+}
- // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing.
- repositories {
- // Add repositories to publish to here.
+// Set up access to common files
+configurations {
+ commonJava {
+ canBeResolved = false
+ canBeConsumed = true
+ }
+ commonResources {
+ canBeResolved = false
+ canBeConsumed = true
}
+ configureEach {
+ resolutionStrategy.eachDependency { details ->
+ if (details.requested.group == "org.ow2.asm") {
+ details.useVersion(asm_version)
+ details.because("Mixin config plugin requires new ASM")
+ }
+ }
+ }
+}
+artifacts {
+ commonJava sourceSets.main.java.sourceDirectories.singleFile
+ commonResources sourceSets.main.resources.sourceDirectories.singleFile
}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/LevelRendererMixinImpl.java b/common/src/main/java/com/github/scotsguy/nowplaying/LevelRendererMixinImpl.java
deleted file mode 100644
index 0707e91..0000000
--- a/common/src/main/java/com/github/scotsguy/nowplaying/LevelRendererMixinImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.scotsguy.nowplaying;
-
-import com.github.scotsguy.nowplaying.NowPlayingConfig;
-import me.shedaniel.autoconfig.AutoConfig;
-import net.minecraft.client.gui.Gui;
-import net.minecraft.client.renderer.LevelRenderer;
-import net.minecraft.network.chat.Component;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Redirect;
-
-public class LevelRendererMixinImpl {
- public static void modifyRecordPlayingOverlay(Gui gui, Component text) {
- NowPlayingConfig config = AutoConfig.getConfigHolder(NowPlayingConfig.class).getConfig();
- if (config.jukeboxStyle == NowPlayingConfig.Style.Hotbar) {
- gui.setNowPlaying(text);
- }
- }
-}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlaying.java b/common/src/main/java/com/github/scotsguy/nowplaying/NowPlaying.java
index 893f7d4..146349f 100644
--- a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlaying.java
+++ b/common/src/main/java/com/github/scotsguy/nowplaying/NowPlaying.java
@@ -1,13 +1,150 @@
+/*
+ * Copyright (c) 2022-2026 AppleTheGolden
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+ * OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
package com.github.scotsguy.nowplaying;
-import me.shedaniel.autoconfig.AutoConfig;
-import me.shedaniel.autoconfig.serializer.JanksonConfigSerializer;
-import net.fabricmc.api.ClientModInitializer;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.Environment;
+import com.github.scotsguy.nowplaying.config.Config;
+import com.github.scotsguy.nowplaying.gui.toast.NowPlayingToast;
+import com.github.scotsguy.nowplaying.mixin.accessor.GuiAccessor;
+import com.github.scotsguy.nowplaying.mixin.accessor.MinecraftAccessor;
+import com.github.scotsguy.nowplaying.mixin.accessor.ToastManagerAccessor;
+import com.github.scotsguy.nowplaying.util.Localization;
+import com.github.scotsguy.nowplaying.util.ModLogger;
+import com.github.scotsguy.nowplaying.util.SpriteProvider;
+import com.mojang.blaze3d.platform.InputConstants;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.KeyMapping;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.ChatScreen;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.client.resources.language.I18n;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.Identifier;
+
+import java.util.function.Supplier;
+
+import static com.github.scotsguy.nowplaying.config.Config.options;
+import static com.github.scotsguy.nowplaying.util.Localization.localized;
+import static com.github.scotsguy.nowplaying.util.Localization.translationKey;
public class NowPlaying {
+ public static final String MOD_ID = "nowplaying";
+ public static final String MOD_NAME = "Now Playing";
+ public static final ModLogger LOG = new ModLogger(MOD_NAME);
+ public static final KeyMapping.Category KEY_CATEGORY = KeyMapping.Category.register(
+ Identifier.fromNamespaceAndPath(MOD_ID, "group")
+ );
+ public static final KeyMapping DISPLAY_KEY = new KeyMapping(
+ translationKey("key", "group.display"), InputConstants.Type.KEYSYM,
+ InputConstants.UNKNOWN.getValue(), KEY_CATEGORY);
+ public static final KeyMapping NEXT_KEY = new KeyMapping(
+ translationKey("key", "group.next"), InputConstants.Type.KEYSYM,
+ InputConstants.UNKNOWN.getValue(), KEY_CATEGORY);
+
+ public static Identifier lastMusic;
+
public static void init() {
- AutoConfig.register(NowPlayingConfig.class, JanksonConfigSerializer::new);
+ Config.getAndSave();
+ }
+
+ public static void onEndTick(Minecraft mc) {
+ while (DISPLAY_KEY.consumeClick()) {
+ displayLastMusic();
+ }
+ while (NEXT_KEY.consumeClick()) {
+ ((MinecraftAccessor)mc).nowplaying$getMusicManager().stopPlaying();
+ ((MinecraftAccessor)mc).nowplaying$getMusicManager().startPlaying(mc.getSituationalMusic());
+ }
+ }
+
+ public static void onResourceReload() {
+ SpriteProvider.onResourceReload();
+ }
+
+ public static void displayLastMusic() {
+ if (lastMusic != null) {
+ displayMusic(lastMusic);
+ } else {
+ Minecraft.getInstance().gui.setOverlayMessage(
+ localized("message", "notFound").withStyle(ChatFormatting.RED), true);
+ }
+ }
+
+ public static void displayMusic(Identifier location) {
+ Component title = getTranslatedTitle(location.toString());
+ display(title, () -> SpriteProvider.getMusicSprite(location, title.getString()),
+ options().musicStyle);
+ }
+
+ public static void displayDisc(Component text, Identifier location) {
+ display(text, () -> SpriteProvider.getDiscSprite(location), options().jukeboxStyle);
+ }
+
+ private static void display(Component name, Supplier spriteSupplier,
+ Config.Options.Style style) {
+ Minecraft mc = Minecraft.getInstance();
+ Component message = Component.translatable("record.nowPlaying", name);
+
+ switch(style) {
+ case Toast -> {
+ ((ToastManagerAccessor)mc.getToastManager()).nowplaying$getQueued()
+ .removeIf((toast) -> toast instanceof NowPlayingToast);
+ mc.getToastManager().addToast(new NowPlayingToast(name, spriteSupplier.get(),
+ options().toastTime * 1000L, options().toastScale, options().darkToast));
+ if (options().narrate) mc.getNarrator().saySystemNow(message);
+ }
+ case Hotbar -> {
+ if (isHotbarVisible(mc.screen)) {
+ mc.gui.setOverlayMessage(message, true);
+ ((GuiAccessor)mc.gui).nowplaying$setOverlayMessageTime(options().hotbarTime * 20);
+ } else if (options().fallbackToast) {
+ ((ToastManagerAccessor)mc.getToastManager()).nowplaying$getQueued()
+ .removeIf((toast) -> toast instanceof NowPlayingToast);
+ mc.getToastManager().addToast(new NowPlayingToast(name, spriteSupplier.get(),
+ options().toastTime * 1000L, options().toastScale, options().darkToast));
+ }
+ if (options().narrate) mc.getNarrator().saySystemNow(message);
+ }
+ }
+ }
+
+ public static boolean isHotbarVisible(Screen screen) {
+ return (screen == null || screen instanceof ChatScreen);
+ }
+
+ private static Component getTranslatedTitle(String location) {
+ String key = Localization.translationKey(location);
+ if (!I18n.exists(key)) {
+ String[] splitLocation = location.split("/");
+ if (splitLocation.length > 0) {
+ String name = splitLocation[splitLocation.length -1];
+ if (name != null && !name.isBlank()) {
+ String oldKey = Localization.translationKey("music", name);
+ if (I18n.exists(oldKey)) {
+ return Component.translatable(oldKey);
+ }
+ }
+ }
+ }
+ return Component.translatable(key);
}
}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingConfig.java b/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingConfig.java
deleted file mode 100644
index ecb0fc6..0000000
--- a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingConfig.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package com.github.scotsguy.nowplaying;
-
-import me.shedaniel.autoconfig.ConfigData;
-import me.shedaniel.autoconfig.annotation.Config;
-import me.shedaniel.autoconfig.annotation.ConfigEntry;
-import me.shedaniel.clothconfig2.gui.entries.SelectionListEntry;
-import org.jetbrains.annotations.NotNull;
-
-@Config(name = "now-playing")
-@SuppressWarnings("unused")
-public class NowPlayingConfig implements ConfigData {
- @ConfigEntry.Gui.Tooltip
- @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
- public Style musicStyle = Style.Toast;
-
- @ConfigEntry.Gui.Tooltip
- @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
- public Style jukeboxStyle = Style.Hotbar;
-
- @ConfigEntry.Gui.Tooltip
- public boolean silenceWoosh = false;
-
- public enum Style implements SelectionListEntry.Translatable {
- Hotbar {
- @Override
- public @NotNull String getKey() {
- return "now_playing.config.style.hotbar";
- }
- },
- Toast {
- @Override
- public @NotNull String getKey() {
- return "now_playing.config.style.toast";
- }
- },
- Disabled {
- @Override
- public @NotNull String getKey() {
- return "now_playing.config.style.disabled";
- }
- }
- }
-}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingListener.java b/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingListener.java
deleted file mode 100644
index d701681..0000000
--- a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingListener.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.github.scotsguy.nowplaying;
-
-import me.shedaniel.autoconfig.AutoConfig;
-import net.fabricmc.api.EnvType;
-import net.fabricmc.api.Environment;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.resources.sounds.SoundInstance;
-import net.minecraft.client.sounds.SoundEventListener;
-import net.minecraft.client.sounds.WeighedSoundEvents;
-import net.minecraft.network.chat.Component;
-import net.minecraft.sounds.SoundSource;
-import net.minecraft.world.item.ItemStack;
-import net.minecraft.world.item.RecordItem;
-
-@Environment(EnvType.CLIENT)
-public class NowPlayingListener implements SoundEventListener {
- @Override
- public void onPlaySound(SoundInstance sound, WeighedSoundEvents soundSet) {
- if (sound.getSource() == SoundSource.MUSIC) {
- Component name = Util.getSoundName(sound);
- if (name == null) return;
-
- NowPlayingConfig config = AutoConfig.getConfigHolder(NowPlayingConfig.class).getConfig();
-
- if (config.musicStyle == NowPlayingConfig.Style.Toast) {
- Minecraft.getInstance().getToasts().addToast(new NowPlayingToast(name));
- } else if (config.musicStyle == NowPlayingConfig.Style.Hotbar) {
- Minecraft.getInstance().gui.setOverlayMessage(Component.translatable("record.nowPlaying", name), true);
- }
- } else if (sound.getSource() == SoundSource.RECORDS) {
- NowPlayingConfig config = AutoConfig.getConfigHolder(NowPlayingConfig.class).getConfig();
- if (config.jukeboxStyle != NowPlayingConfig.Style.Toast) return;
-
- RecordItem disc = Util.getDiscFromSound(sound);
- if (disc == null) return;
-
- Minecraft.getInstance().getToasts().addToast(new NowPlayingToast(disc.getDisplayName(), new ItemStack(disc)));
-
- }
-
- }
-}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingToast.java b/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingToast.java
deleted file mode 100644
index f3c4c34..0000000
--- a/common/src/main/java/com/github/scotsguy/nowplaying/NowPlayingToast.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package com.github.scotsguy.nowplaying;
-
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.Font;
-import net.minecraft.client.gui.GuiGraphics;
-import net.minecraft.client.gui.components.toasts.Toast;
-import net.minecraft.client.gui.components.toasts.ToastComponent;
-import net.minecraft.network.chat.Component;
-import net.minecraft.resources.ResourceLocation;
-import net.minecraft.util.FormattedCharSequence;
-import net.minecraft.world.item.ItemStack;
-import net.minecraft.world.item.Items;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.List;
-
-public class NowPlayingToast implements Toast {
- private static final ResourceLocation BACKGROUND_SPRITE = new ResourceLocation("toast/recipe");
-
- private final Component description;
- private final ItemStack itemStack;
- private boolean justUpdated;
- private long startTime;
-
- private static final int TEXT_LEFT_MARGIN = 30;
- private static final int TEXT_RIGHT_MARGIN = 7;
-
- public NowPlayingToast(Component description) {
- this(description, new ItemStack(Items.MUSIC_DISC_CAT));
- }
-
- public NowPlayingToast(Component description, ItemStack itemStack) {
- this.description = description;
- this.itemStack = itemStack;
- }
-
- @Override
- public Visibility render(@NotNull GuiGraphics guiGraphics, @NotNull ToastComponent toastComponent, long startTime) {
- Minecraft game = Minecraft.getInstance();
-
- int width = this.width();
- int height = this.height();
- Font font = game.gui.getFont();
- List textLines = font.split(description, width - TEXT_LEFT_MARGIN - TEXT_RIGHT_MARGIN);
-
- if (width == 160 && textLines.size() <= 1) {
- // Draw the whole toast from the texture
- guiGraphics.blitSprite(BACKGROUND_SPRITE, 0, 0, width, height);
- } else {
- height = height + Math.max(0, textLines.size() - 1) * 12;
- int bottomHeight = Math.min(4, height - 28);
- // Draw the top border
- this.renderBackgroundRow(guiGraphics, width, 0, 0, 28);
-
- // Draw plain background
- for (int n = 28; n < height - bottomHeight; n += 10) {
- this.renderBackgroundRow(guiGraphics, width, 16 /* middle */, n, Math.min(16, height - n - bottomHeight));
- }
-
- // Draw the bottom border
- this.renderBackgroundRow(guiGraphics, width, 32 - bottomHeight, height - bottomHeight, bottomHeight);
- }
- // Draw "Now Playing"
- guiGraphics.drawString(game.font, Component.translatable("now_playing.toast.now_playing"), TEXT_LEFT_MARGIN, 7, -11534256, false);
-
- // Draw song title
- for (int i = 0; i < textLines.size(); ++i) {
- guiGraphics.drawString(game.font, textLines.get(i), TEXT_LEFT_MARGIN, (18 + i * 12), -16777216, false);
- }
-
- // Draw icon
- guiGraphics.renderFakeItem(itemStack, 9, (height / 2) - (16 / 2));
-
- return startTime - this.startTime >= 5000L ? Toast.Visibility.HIDE : Toast.Visibility.SHOW;
- }
-
- private void renderBackgroundRow(GuiGraphics guiGraphics, int i, int vOffset, int y, int vHeight) {
- int uWidth = vOffset == 0 ? 20 : 5;
- int n = Math.min(60, i - uWidth);
-
- guiGraphics.blitSprite(BACKGROUND_SPRITE, 160, 32, 0, vOffset, 0, y, uWidth, vHeight);
-
- for (int o = uWidth; o < i - n; o += 64) {
- guiGraphics.blitSprite(BACKGROUND_SPRITE, 160, 32, 32, vOffset, o, y, Math.min(64, i - o - n), vHeight);
- }
-
- guiGraphics.blitSprite(BACKGROUND_SPRITE, 160, 32, 160 - n, vOffset, i - n, y, n, vHeight);
- }
-}
-
-
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/Util.java b/common/src/main/java/com/github/scotsguy/nowplaying/Util.java
deleted file mode 100644
index 3919748..0000000
--- a/common/src/main/java/com/github/scotsguy/nowplaying/Util.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.github.scotsguy.nowplaying;
-
-import net.minecraft.client.resources.language.I18n;
-import net.minecraft.client.resources.sounds.SoundInstance;
-import net.minecraft.network.chat.Component;
-import net.minecraft.sounds.SoundEvent;
-import net.minecraft.world.item.RecordItem;
-import com.github.scotsguy.nowplaying.mixin.RecordItemAccessor;
-
-public class Util {
- public static Component getSoundName(SoundInstance instance) {
- String soundLocation = instance.getSound().getLocation().toString();
- if (soundLocation.startsWith("minecraft:music/") || I18n.exists("now_playing.sound." + soundLocation)) {
- return Component.translatable("now_playing.sound." + soundLocation);
- }
- return null;
- }
- public static RecordItem getDiscFromSound(SoundInstance instance) {
- for (SoundEvent event : RecordItemAccessor.getDiscs().keySet()) {
- if (event.getLocation().equals(instance.getLocation())) {
- return RecordItem.getBySound(event);
- }
- }
- return null;
- }
-}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/command/Commands.java b/common/src/main/java/com/github/scotsguy/nowplaying/command/Commands.java
new file mode 100644
index 0000000..2ac6f02
--- /dev/null
+++ b/common/src/main/java/com/github/scotsguy/nowplaying/command/Commands.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2022-2026 AppleTheGolden
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+ * OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.github.scotsguy.nowplaying.command;
+
+import com.github.scotsguy.nowplaying.NowPlaying;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import net.minecraft.client.Minecraft;
+import net.minecraft.commands.CommandBuildContext;
+
+import static net.minecraft.commands.Commands.literal;
+
+@SuppressWarnings("unchecked")
+public class Commands extends CommandDispatcher {
+ public void register(Minecraft mc, CommandDispatcher dispatcher, CommandBuildContext buildCtx) {
+ dispatcher.register((LiteralArgumentBuilder)literal("nowplaying")
+ .executes(ctx -> {
+ NowPlaying.displayLastMusic();
+ return Command.SINGLE_SUCCESS;
+ })
+ );
+ }
+}
diff --git a/common/src/main/java/com/github/scotsguy/nowplaying/config/Config.java b/common/src/main/java/com/github/scotsguy/nowplaying/config/Config.java
new file mode 100644
index 0000000..040aa8c
--- /dev/null
+++ b/common/src/main/java/com/github/scotsguy/nowplaying/config/Config.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2022-2026 AppleTheGolden
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+ * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+ * OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+package com.github.scotsguy.nowplaying.config;
+
+import com.github.scotsguy.nowplaying.NowPlaying;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+
+import static com.github.scotsguy.nowplaying.util.Localization.localized;
+
+public class Config {
+ private static final Path DIR_PATH = Path.of("config");
+ private static final String FILE_NAME = NowPlaying.MOD_ID + ".json";
+ private static final String BACKUP_FILE_NAME = NowPlaying.MOD_ID + ".unreadable.json";
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
+
+ // Options
+
+ public final Options options = new Options();
+
+ public static Options options() {
+ return Config.get().options;
+ }
+
+ public static class Options {
+ public static final boolean onlyKeybindDefault = false;
+ public boolean onlyKeybind = onlyKeybindDefault;
+
+ public static final Style musicStyleDefault = Style.Toast;
+ public Style musicStyle = musicStyleDefault;
+
+ public static final Style jukeboxStyleDefault = Style.Hotbar;
+ public Style jukeboxStyle = jukeboxStyleDefault;
+
+ public static final boolean fallbackToastDefault = true;
+ public boolean fallbackToast = fallbackToastDefault;
+
+ public static final boolean silenceWooshDefault = true;
+ public boolean silenceWoosh = silenceWooshDefault;
+
+ public static final float toastScaleDefault = 1.0F;
+ public float toastScale = toastScaleDefault;
+
+ public static final boolean simpleToastDefault = false;
+ public boolean simpleToast = simpleToastDefault;
+
+ public static final boolean darkToastDefault = false;
+ public boolean darkToast = darkToastDefault;
+
+ public static final int toastTimeDefault = 5;
+ public int toastTime = toastTimeDefault;
+
+ public static final int hotbarTimeDefault = 3;
+ public int hotbarTime = hotbarTimeDefault;
+
+ public static final boolean narrateDefault = true;
+ public boolean narrate = narrateDefault;
+
+ public enum Style {
+ Toast,
+ Hotbar,
+ Disabled;
+
+ public static Component name(Enum