diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..a38e6ef4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,491 @@ +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = false +max_line_length = 120 +tab_width = 2 +ij_continuation_indent_size = 4 +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 + +[*.java] +ij_smart_tabs = true +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 = true +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 = true +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_parameter_wrap = off +ij_java_array_initializer_new_line_after_left_brace = false +ij_java_array_initializer_right_brace_on_new_line = true +ij_java_array_initializer_wrap = off +ij_java_assert_statement_colon_on_next_line = false +ij_java_assert_statement_wrap = off +ij_java_assignment_wrap = off +ij_java_binary_operation_sign_on_next_line = false +ij_java_binary_operation_wrap = off +ij_java_blank_lines_after_anonymous_class_header = 0 +ij_java_blank_lines_after_class_header = 0 +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 = 1 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 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 = +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = true +ij_java_call_parameters_wrap = off +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 +ij_java_class_count_to_use_import_on_demand = 1000 +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 +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 = false +ij_java_doc_align_param_comments = false +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 +ij_java_enum_constants_wrap = off +ij_java_enum_field_annotation_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +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 +ij_java_for_brace_force = never +ij_java_for_statement_new_line_after_left_paren = false +ij_java_for_statement_right_paren_on_new_line = true +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_generate_use_type_annotation_before_type = true +ij_java_if_brace_force = never +ij_java_imports_layout = @*,*,|,java.**,|,$* +ij_java_indent_case_from_switch = true +ij_java_insert_inner_class_imports = false +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 = 2 +ij_java_keep_blank_lines_in_declarations = 2 +ij_java_keep_builder_methods_indents = false +ij_java_keep_control_statement_in_one_line = true +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 = true +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 +ij_java_method_call_chain_wrap = off +ij_java_method_parameters_new_line_after_left_paren = true +ij_java_method_parameters_right_paren_on_new_line = true +ij_java_method_parameters_wrap = on_every_item +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 10 +ij_java_new_line_after_lparen_in_annotation = false +ij_java_new_line_after_lparen_in_deconstruction_pattern = true +ij_java_new_line_after_lparen_in_record_header = false +ij_java_new_line_when_body_is_presented = false +ij_java_packages_to_use_import_on_demand = +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 = true +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 +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = true +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = true +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = true +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +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 = false +ij_java_ternary_operation_wrap = off +ij_java_test_name_prefix = +ij_java_test_name_suffix = Test +ij_java_throws_keyword_wrap = off +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 = never +ij_java_while_on_new_line = false +ij_java_wrap_comments = false +ij_java_wrap_first_method_in_call_chain = false +ij_java_wrap_long_lines = false +ij_java_wrap_semicolon_after_call_chain = false + +[*.properties] +ij_properties_align_group_field_declarations = false +ij_properties_keep_blank_lines = false +ij_properties_key_value_delimiter = equals +ij_properties_spaces_around_key_value_delimiter = false + +[.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_xml_align_attributes = true +ij_xml_align_text = false +ij_xml_attribute_wrap = normal +ij_xml_block_comment_add_space = false +ij_xml_block_comment_at_first_column = true +ij_xml_keep_blank_lines = 2 +ij_xml_keep_indents_on_empty_lines = false +ij_xml_keep_line_breaks = true +ij_xml_keep_line_breaks_in_text = true +ij_xml_keep_whitespaces = false +ij_xml_keep_whitespaces_around_cdata = preserve +ij_xml_keep_whitespaces_inside_cdata = false +ij_xml_line_comment_at_first_column = true +ij_xml_space_after_tag_name = false +ij_xml_space_around_equals_in_attribute = false +ij_xml_space_inside_empty_tag = false +ij_xml_text_wrap = normal + +[{*.bash,*.sh,*.zsh}] +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 + +[{*.kt,*.kts}] +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 +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 +ij_kotlin_call_parameters_new_line_after_left_paren = true +ij_kotlin_call_parameters_right_paren_on_new_line = true +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_catch_on_new_line = false +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ij_kotlin_continuation_indent_for_chained_calls = false +ij_kotlin_continuation_indent_for_expression_bodies = false +ij_kotlin_continuation_indent_in_argument_lists = false +ij_kotlin_continuation_indent_in_elvis = false +ij_kotlin_continuation_indent_in_if_conditions = false +ij_kotlin_continuation_indent_in_parameter_lists = false +ij_kotlin_continuation_indent_in_supertype_lists = false +ij_kotlin_else_on_new_line = false +ij_kotlin_enum_constants_wrap = off +ij_kotlin_extends_list_wrap = normal +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_finally_on_new_line = 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 +ij_kotlin_method_call_chain_wrap = normal +ij_kotlin_method_parameters_new_line_after_left_paren = true +ij_kotlin_method_parameters_right_paren_on_new_line = true +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 +ij_kotlin_wrap_expression_body_functions = 1 +ij_kotlin_wrap_first_method_in_call_chain = false + +[{*.har,*.json,*.jsonc,*.mcmeta,.prettierrc}] +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 0 +ij_json_keep_indents_on_empty_lines = false +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = false +ij_json_object_wrapping = split_into_lines +ij_json_property_alignment = do_not_align +ij_json_space_after_colon = true +ij_json_space_after_comma = true +ij_json_space_before_colon = false +ij_json_space_before_comma = false +ij_json_spaces_within_braces = false +ij_json_spaces_within_brackets = false +ij_json_wrap_long_lines = false + +[{*.htm,*.html,*.sht,*.shtm,*.shtml}] +ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3 +ij_html_align_attributes = true +ij_html_align_text = false +ij_html_attribute_wrap = normal +ij_html_block_comment_add_space = false +ij_html_block_comment_at_first_column = true +ij_html_do_not_align_children_of_min_lines = 0 +ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p +ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot +ij_html_enforce_quotes = false +ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var +ij_html_keep_blank_lines = 2 +ij_html_keep_indents_on_empty_lines = false +ij_html_keep_line_breaks = true +ij_html_keep_line_breaks_in_text = true +ij_html_keep_whitespaces = false +ij_html_keep_whitespaces_inside = span,pre,textarea +ij_html_line_comment_at_first_column = true +ij_html_new_line_after_last_attribute = never +ij_html_new_line_before_first_attribute = never +ij_html_quote_style = double +ij_html_remove_new_line_before_tags = br +ij_html_space_after_tag_name = false +ij_html_space_around_equality_in_attribute = false +ij_html_space_inside_empty_tag = false +ij_html_text_wrap = normal + +[{*.markdown,*.md}] +ij_markdown_force_one_space_after_blockquote_symbol = true +ij_markdown_force_one_space_after_header_symbol = true +ij_markdown_force_one_space_after_list_bullet = true +ij_markdown_force_one_space_between_words = true +ij_markdown_format_tables = true +ij_markdown_insert_quote_arrows_on_wrap = true +ij_markdown_keep_indents_on_empty_lines = false +ij_markdown_keep_line_breaks_inside_text_blocks = true +ij_markdown_max_lines_around_block_elements = 1 +ij_markdown_max_lines_around_header = 1 +ij_markdown_max_lines_between_paragraphs = 1 +ij_markdown_min_lines_around_block_elements = 1 +ij_markdown_min_lines_around_header = 1 +ij_markdown_min_lines_between_paragraphs = 1 +ij_markdown_wrap_text_if_long = true +ij_markdown_wrap_text_inside_blockquotes = true + +[{*.toml,Cargo.lock,Cargo.toml.orig,Gopkg.lock,Pipfile,poetry.lock,uv.lock}] +ij_toml_keep_indents_on_empty_lines = false + +[{*.yaml,*.yml}] +ij_yaml_align_values_properties = do_not_align +ij_yaml_autoinsert_sequence_marker = true +ij_yaml_block_mapping_on_new_line = false +ij_yaml_indent_sequence_value = true +ij_yaml_keep_indents_on_empty_lines = false +ij_yaml_keep_line_breaks = true +ij_yaml_line_comment_add_space = false +ij_yaml_line_comment_add_space_on_reformat = false +ij_yaml_line_comment_at_first_column = true +ij_yaml_sequence_on_new_line = false +ij_yaml_space_before_colon = false +ij_yaml_spaces_within_braces = true +ij_yaml_spaces_within_brackets = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f017c46e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# Use lf endings by default. +* text=auto eol=lf + +# Declare text file types just in case +*.java text +*.yml text +*.xml text +*.md text + +# Exclude binary files +*.png binary diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..f24e4164 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/automerge_dependabot.yml b/.github/workflows/automerge_dependabot.yml new file mode 100644 index 00000000..6fe77258 --- /dev/null +++ b/.github/workflows/automerge_dependabot.yml @@ -0,0 +1,55 @@ +name: Auto-merge Dependabot PRs + +on: + workflow_run: + workflows: [ "Pull Request" ] + types: [ completed ] + +jobs: + merge-dependabot: + if: "github.actor == 'dependabot[bot]' + && github.event.workflow_run.event == 'pull_request' + && github.event.workflow_run.conclusion == 'success'" + runs-on: ubuntu-latest + steps: + # Note: this is directly from GitHub's example for using data from a triggering workflow: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow + - name: 'Download artifact' + uses: actions/github-script@v8 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "pr_number" + })[0]; + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data)); + + # This might be a useless use of cat, but I'm not sure what shell Actions is going to be running. + - name: Add Pull Number Variable + run: |- + unzip pr_number.zip + echo "PR_NUMBER=$(cat pr_number)" >> "$GITHUB_ENV" + + - name: Approve + uses: hmarr/auto-approve-action@v4.0.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + pull-request-number: "${{ env.PR_NUMBER }}" + - name: Merge + uses: pascalgn/automerge-action@v0.16.4 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + MERGE_LABELS: "dependencies,java" + MERGE_METHOD: "squash" + PULL_REQUEST: "${{ env.PR_NUMBER }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a73e107f..03d2e29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,86 +2,46 @@ name: OpenInv CI on: push: - create: - types: [tag] - pull_request_target: + branches: + - 'master' + tags-ignore: + - '**' + paths-ignore: + - resource-pack/openinv-legibility-pack/** + # Enable running CI via other Actions, i.e. for drafting releases and handling PRs. + workflow_call: jobs: build: runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - - name: Set Up Java - uses: actions/setup-java@v1 + - uses: actions/setup-java@v5 with: - java-version: 1.8 + distribution: 'temurin' + java-version: '21' - # Use cache to speed up build - - name: Cache Maven Repo - uses: actions/cache@v2 - id: cache + # We can't use 'maven' prebuilt cache setup because it requires that the project have a pom file. + # BuildTools installs to Maven local if available, so it's easier to just rely on that. + - name: Cache Spigot dependency + uses: actions/cache@v4 with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + path: | + ~/.m2/repository/org/spigotmc/ + key: ${{ runner.os }}-buildtools-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-buildtools- - # Install Spigot dependencies. - # This script uses Maven to check all required installations and ensure that they are present. - - name: Install Spigot Dependencies - run: . scripts/install_spigot_dependencies.sh + - uses: gradle/actions/setup-gradle@v5 - - name: Build With Maven - run: mvn -e clean package -am -P all + - name: Build with Gradle + run: ./gradlew clean build # Upload artifacts - name: Upload Distributable Jar id: upload-final - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v5 with: name: dist - path: ./target/OpenInv.jar - - name: Upload API Jar - id: upload-api - uses: actions/upload-artifact@v2 - with: - name: api - path: ./api/target/openinvapi*.jar - - release: - name: Create Github Release - needs: [ build ] - if: github.event_name == 'create' && github.event.ref_type == 'tag' - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v2 - - - name: Download Artifacts - uses: actions/download-artifact@v2 - - - name: Generate changelog - run: . scripts/generate_changelog.sh - - - name: Create Release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - body: ${{ env.GENERATED_CHANGELOG }} - draft: true - prerelease: false - - - name: Upload Release Asset - id: upload-release-asset - uses: actions/upload-release-asset@v1.0.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create-release.outputs.upload_url }} - asset_path: ./OpenInv.jar - asset_name: OpenInv.jar - asset_content_type: application/java-archive \ No newline at end of file + path: ./dist/* diff --git a/.github/workflows/draft_release.yml b/.github/workflows/draft_release.yml new file mode 100644 index 00000000..0655b2e0 --- /dev/null +++ b/.github/workflows/draft_release.yml @@ -0,0 +1,36 @@ +name: Draft Github Release + +on: + push: + tags: + - '**' + +jobs: + run-ci: + uses: Jikoo/OpenInv/.github/workflows/ci.yml@master + draft-release: + needs: [ run-ci ] + runs-on: ubuntu-latest + steps: + - name: Download build + uses: actions/download-artifact@v6 + with: + name: dist + path: dist + + - name: Create Release + id: create-release + uses: softprops/action-gh-release@v2.5.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + name: OpenInv ${{ env.VERSIONED_NAME }} + body: |- + ## Supported server versions + **Paper:** TODO VERSION, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.1 + **Spigot:** TODO VERSION + + TODO HELLO HUMAN, PRESS THE GENERATE CHANGELOG BUTTON PLEASE. + draft: true + prerelease: false + files: ./dist/** diff --git a/.github/workflows/external_release.yml b/.github/workflows/external_release.yml new file mode 100644 index 00000000..a4c4dc76 --- /dev/null +++ b/.github/workflows/external_release.yml @@ -0,0 +1,37 @@ +name: Release to CurseForge + +on: + release: + types: [ released ] + +jobs: + curseforge_release: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Fetch Github Release Asset + uses: dsaltares/fetch-gh-release-asset@1.1.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ github.event.release.id }} + file: OpenInv.jar + + - name: Set CurseForge Variables + run: . scripts/set_curseforge_env.sh "${{ github.event.release.body }}" + + - name: Create CurseForge Release + uses: itsmeow/curseforge-upload@v3 + with: + token: "${{ secrets.CURSEFORGE_TOKEN }}" + project_id: 31432 + game_endpoint: minecraft + file_path: ./OpenInv.jar + display_name: "${{ github.event.release.name }}" + game_versions: "${{ env.CURSEFORGE_MINECRAFT_VERSIONS }}" + release_type: release + changelog_type: markdown + changelog: "${{ github.event.release.body }}" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..5ad508c1 --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,22 @@ +name: Pull Request + +on: + pull_request: + +jobs: + run-ci: + uses: Jikoo/OpenInv/.github/workflows/ci.yml@master + store-dependabot-pr-data: + if: "github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'" + runs-on: ubuntu-latest + steps: + # Note: this is directly from GitHub's example for using data from a triggering workflow: + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow + - name: Store Pull Number + run: | + mkdir -p ./pr + echo ${{ github.event.number }} > ./pr/pr_number + - uses: actions/upload-artifact@v5 + with: + name: pr_number + path: pr/ diff --git a/.github/workflows/resource_pack_ci.yml b/.github/workflows/resource_pack_ci.yml new file mode 100644 index 00000000..b4651e16 --- /dev/null +++ b/.github/workflows/resource_pack_ci.yml @@ -0,0 +1,24 @@ +name: Resource Pack CI + +on: + push: + branches: + - 'master' + tags-ignore: + - '**' + paths: + - resource-pack/openinv-legibility-pack/** + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Build resource pack + id: upload-resource-pack + uses: actions/upload-artifact@v5 + with: + name: openinv-legibility-pack + path: ./resource-pack/openinv-legibility-pack/ + compression-level: 9 diff --git a/.gitignore b/.gitignore index b48a4778..27ac0af7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,12 @@ **/.project **/.classpath **/.idea/ +**/.gradle/ **.iml **/target/ **/bin/ **/out/ +**/build/ +**/dist/ **/dependency-reduced-pom.xml **/pom.xml.versionsBackup diff --git a/README.MD b/README.MD deleted file mode 100644 index 66c183ff..00000000 --- a/README.MD +++ /dev/null @@ -1,172 +0,0 @@ -## About -OpenInv is a [Bukkit plugin](https://dev.bukkit.org/bukkit-plugins/openinv/) which allows users to open and edit anyone's inventory or ender chest - online or not! - -## Features -- **OpenInv**: Open anyone's inventory, even if they're offline. - - Read-only mode! No edits allowed! Don't grant the permission `OpenInv.editinv` - - Cross-world support! Don't grant `OpenInv.crossworld` - - No self-opening! Don't grant `OpenInv.openself` - - Drop items as the player! Place items in the unused slots to the right of the armor to drop them -- **OpenEnder**: Open anyone's ender chest, even if they're offline. - - Read-only mode! No edits allowed! Don't grant `OpenInv.editender` - - Cross-world support! Don't grant `OpenInv.crossworld` - - No opening others! Don't grant `OpenInv.openenderall` -- **SilentContainer**: Open containers without displaying an animation or making sound. -- **AnyContainer**: Open containers, even if blocked by ocelots or blocks. - -## Commands - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CommandAliasesDescription
/openinv [player]oi, inv, openOpen a player's inventory. If unspecified, will select last player opened or own if none opened previously.
/openender [player]oeOpen a player's ender chest. If unspecified, will select last player opened or own if none opened previously.
/searchinv <item> [minAmount]siLists all online players that have a certain item in their inventory.
/searchender <item> [minAmount]seLists all online players that have a certain item in their ender chest.
/searchenchant <[enchantment] [MinLevel]>searchenchantsLists all online players with a specific enchantment.
/anycontainer [check]ac, anychestCheck or toggle the AnyContainer function, allowing opening blocked containers.
/silentcontainer [check]sc, silentchestCheck or toggle the SilentContainer function, allowing opening containers silently.
- -## Permissions - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NodeDescription
OpenInv.*Gives permission to use all of OpenInv.
OpenInv.openinvRequired to use /openinv.
OpenInv.openselfRequired to open own inventory.
OpenInv.editinvRequired to make changes to open inventories.
OpenInv.openonlineAllows users to open online players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.
OpenInv.openofflineAllows users to open offline players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.
OpenInv.openenderRequired to use /openender.
OpenInv.editenderRequired to make changes to open ender chests.
OpenInv.openenderallAllows users to open others' ender chests. Without it, users can only open their own.
OpenInv.exemptPrevents the player's inventory being opened by others.
OpenInv.overrideAllows bypassing of the exempt permission.
OpenInv.crossworldAllows cross-world usage of /openinv and /openender.
OpenInv.searchRequired to use /searchinv and /searchender.
OpenInv.searchenchantRequired to use /searchenchant.
OpenInv.anychestRequired to use /anychest.
OpenInv.any.defaultCause AnyContainer to be enabled by default.
OpenInv.silentRequired to use /silentcontainer.
OpenInv.silent.defaultCause SilentContainer to be enabled by default.
OpenInv.spectateAllows users in spectate gamemode to edit inventories.
- -## For Developers -To compile, the relevant Craftbukkit/Spigot jars must be installed in your local repository using the install plugin. -Ex: `mvn install:install-file -Dpackaging=jar -Dfile=spigot-1.8-R0.1-SNAPSHOT.jar -DgroupId=org.spigotmc -DartifactId=spigot -Dversion=1.8-R0.1-SNAPSHOT` - -To compile for a single version, specify the NMS revision you are targeting: `mvn -pl -am clean install` - -To compile for a set of versions, you'll need to use a profile. The only provided profile is `all`. Select a profile using the `-P` argument: `mvn clean package -am -P all` - -For more information, check out the [official Maven guide](http://maven.apache.org/guides/introduction/introduction-to-profiles.html). - -The final file is `target/OpenInv.jar` - -## License -``` -Copyright (C) 2011-2020 lishid. All rights reserved. - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, version 3. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -``` diff --git a/README.md b/README.md new file mode 100644 index 00000000..618e7860 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +## About + +OpenInv is a [Bukkit plugin](https://dev.bukkit.org/projects/openinv) which allows users to open and edit anyone's +inventory or ender chest - online or not! + +## Features + +- **OpenInv**: Open anyone's inventory, even if they're offline. + - Read-only mode! Don't grant edit permission. + - Cross-world support! Allow access only from the same world. + - No duplicate slots! Only armor is accessible when opening self (if allowed at all)! + - Drop items as the player! Place items in the dropper slot in the bottom right. Can be disabled via permission! + - Allow any item in armor slots! Configurable via permission. +- **OpenEnder**: Open anyone's ender chest, even if they're offline. + - Allow access only to own ender chest! Don't grant permission to open others. + - Read-only mode! Don't grant edit permission. + - Cross-world support! Allow access only from the same world. +- **SilentContainer**: Open containers without displaying an animation or making sound. +- **AnyContainer**: Open containers, even if blocked by ocelots or blocks. + +## Commands + +See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Commands). + +## Permissions + +See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Permissions) + +## For Developers + +### As a Dependency + +The OpenInv API is available via [JitPack](https://jitpack.io/). + +```xml + + + jitpack.io + https://jitpack.io + + +``` + +```xml + + + com.github.Jikoo + OpenInv + ${openinv.version} + + +``` + +Note that since JitPack only builds the API now, the "full" OpenInv jar on JitPack is actually the openinvapi artifact. +This is a change from previous dependency declaration that I hope to revert. + +### Compilation + +Execute the gradle wrapper: +`./gradlew build` + +If you encounter issues with building the Spigot module, try running BuildTools manually. diff --git a/addon/togglepersist/build.gradle.kts b/addon/togglepersist/build.gradle.kts new file mode 100644 index 00000000..78cb9095 --- /dev/null +++ b/addon/togglepersist/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `openinv-base` +} + +dependencies { + implementation(project(":openinvapi")) +} + +tasks.processResources { + expand("version" to version) +} + +tasks.register("distributeAddons") { + into(rootProject.layout.projectDirectory.dir("dist")) + from(tasks.jar) + rename("openinvtogglepersist.*\\.jar", "OITogglePersist.jar") +} + +tasks.assemble { + dependsOn(tasks.named("distributeAddons")) +} diff --git a/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java b/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java new file mode 100644 index 00000000..6a0cc298 --- /dev/null +++ b/addon/togglepersist/src/main/java/com/github/jikoo/openinv/togglepersist/TogglePersist.java @@ -0,0 +1,152 @@ +package com.github.jikoo.openinv.togglepersist; + +import com.google.errorprone.annotations.Keep; +import com.lishid.openinv.event.PlayerToggledEvent; +import com.lishid.openinv.util.setting.PlayerToggle; +import com.lishid.openinv.util.setting.PlayerToggles; +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.logging.Level; + +public class TogglePersist extends JavaPlugin implements Listener { + + private final Map> enabledToggles = new HashMap<>(); + + @Override + public void onEnable() { + getServer().getPluginManager().registerEvents(this, this); + + File file = new File(getDataFolder(), "toggles.yml"); + + // If there's no save file, there's nothing to load. + if (!file.exists()) { + return; + } + + Configuration loaded = YamlConfiguration.loadConfiguration(file); + + // For each toggle, enable loaded players. + for (String toggleName : loaded.getKeys(false)) { + PlayerToggle toggle = PlayerToggles.get(toggleName); + // Ensure toggle exists. + if (toggle == null) { + continue; + } + + for (String idString : loaded.getStringList(toggleName)) { + // Ensure valid UUID. + UUID uuid; + try { + uuid = UUID.fromString(idString); + } catch (IllegalArgumentException e) { + continue; + } + + // Track that toggle is enabled. + set(uuid, toggleName); + } + } + } + + private void set(UUID playerId, String toggleName) { + enabledToggles.compute( + playerId, + (uuid, toggles) -> { + if (toggles == null) { + toggles = new HashSet<>(); + } + toggles.add(toggleName); + return toggles; + } + ); + } + + @Override + public void onDisable() { + Map> converted = getSaveData(); + + YamlConfiguration data = new YamlConfiguration(); + for (Map.Entry> playerToggle : converted.entrySet()) { + data.set(playerToggle.getKey(), playerToggle.getValue()); + } + + File file = new File(getDataFolder(), "toggles.yml"); + try { + data.save(file); + } catch (IOException e) { + getLogger().log(Level.SEVERE, "Unable to save player toggle states", e); + } + } + + private @NotNull Map> getSaveData() { + Map> converted = new HashMap<>(); + + for (Map.Entry> playerToggles : enabledToggles.entrySet()) { + String idString = playerToggles.getKey().toString(); + for (String toggleName : playerToggles.getValue()) { + // Add player ID to listing for each enabled toggle. + converted.compute( + toggleName, + (name, ids) -> { + if (ids == null) { + ids = new ArrayList<>(); + } + ids.add(idString); + return ids; + } + ); + } + } + return converted; + } + + @Keep + @EventHandler + private void onPlayerJoin(@NotNull PlayerJoinEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + Set toggleNames = enabledToggles.get(playerId); + + if (toggleNames == null) { + return; + } + + for (String toggleName : toggleNames) { + PlayerToggle toggle = PlayerToggles.get(toggleName); + if (toggle != null) { + toggle.set(playerId, true); + } + } + } + + @Keep + @EventHandler + private void onToggleSet(@NotNull PlayerToggledEvent event) { + if (event.isEnabled()) { + set(event.getPlayerId(), event.getToggle().getName()); + } else { + enabledToggles.computeIfPresent( + event.getPlayerId(), + (uuid, toggles) -> { + toggles.remove(event.getToggle().getName()); + return toggles.isEmpty() ? null : toggles; + } + ); + } + } + +} diff --git a/addon/togglepersist/src/main/resources/plugin.yml b/addon/togglepersist/src/main/resources/plugin.yml new file mode 100644 index 00000000..3760e30f --- /dev/null +++ b/addon/togglepersist/src/main/resources/plugin.yml @@ -0,0 +1,7 @@ +name: OITogglePersist +main: com.github.jikoo.openinv.togglepersist.TogglePersist +version: ${version} +author: Jikoo +description: An OpenInv addon allowing /anycontainer and /silentcontainer to persist across sessions. +api-version: "1.20" +depend: [ OpenInv ] diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 00000000..9b2665ad --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `openinv-base` + `maven-publish` +} + +publishing { + publications { + create("jitpack") { + groupId = "com.github.Jikoo.OpenInv" + artifactId = "openinvapi" + from(components["java"]) + } + } +} diff --git a/api/pom.xml b/api/pom.xml deleted file mode 100644 index cc34012f..00000000 --- a/api/pom.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - 4.0.0 - - - com.lishid - openinvparent - 4.1.6-SNAPSHOT - - - openinvapi - OpenInvAPI - - - - org.jetbrains - annotations - 17.0.0 - - - org.spigotmc - spigot-api - 1.16.5-R0.1-SNAPSHOT - provided - - - - - - - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - - - - - - diff --git a/api/src/main/java/com/lishid/openinv/IOpenInv.java b/api/src/main/java/com/lishid/openinv/IOpenInv.java index b4f4e8f5..9634f7af 100644 --- a/api/src/main/java/com/lishid/openinv/IOpenInv.java +++ b/api/src/main/java/com/lishid/openinv/IOpenInv.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2023 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,293 +17,182 @@ package com.lishid.openinv; import com.lishid.openinv.internal.IAnySilentContainer; -import com.lishid.openinv.internal.IInventoryAccess; import com.lishid.openinv.internal.ISpecialEnderChest; import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; -import com.lishid.openinv.util.InventoryAccess; -import com.lishid.openinv.util.StringMetric; -import java.util.UUID; -import java.util.logging.Logger; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; import org.bukkit.inventory.InventoryView; -import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.UUID; +import java.util.logging.Logger; + /** * Interface defining behavior for the OpenInv plugin. - * - * @author Jikoo */ public interface IOpenInv { - /** - * Check the configuration value for whether or not OpenInv saves player data when unloading - * players. This is exclusively for users who do not allow editing of inventories, only viewing, - * and wish to prevent any possibility of bugs such as lishid#40. If true, OpenInv will not ever - * save any edits made to players. - * - * @return false unless configured otherwise - */ - boolean disableSaving(); - - /** - * Gets the active ISilentContainer implementation. - * - * @return the ISilentContainer - * @throws IllegalStateException if the server version is unsupported - */ - @NotNull IAnySilentContainer getAnySilentContainer(); - - /** - * Gets the active IInventoryAccess implementation. - * - * @return the IInventoryAccess - * @throws IllegalStateException if the server version is unsupported - */ - @Deprecated - default @NotNull IInventoryAccess getInventoryAccess() { - return new InventoryAccess(); - } - - /** - * Gets the provided player's AnyChest setting. - * - * @param player the OfflinePlayer - * @return true if AnyChest is enabled - * @throws IllegalStateException if the server version is unsupported - */ - boolean getPlayerAnyChestStatus(@NotNull OfflinePlayer player); - - /** - * Gets a unique identifier by which the OfflinePlayer can be referenced. Using the value - * returned to look up a Player will generally be much faster for later implementations. - * - * @param offline the OfflinePlayer - * @return the identifier - * @throws IllegalStateException if the server version is unsupported - */ - default @NotNull String getPlayerID(@NotNull OfflinePlayer offline) { - return offline.getUniqueId().toString(); - } - - /** - * Gets a player's SilentChest setting. - * - * @param offline the OfflinePlayer - * @return true if SilentChest is enabled - * @throws IllegalStateException if the server version is unsupported - */ - boolean getPlayerSilentChestStatus(@NotNull OfflinePlayer offline); - - /** - * Gets an ISpecialEnderChest for the given Player. - * - * @param player the Player - * @param online true if the Player is currently online - * @return the ISpecialEnderChest - * @throws IllegalStateException if the server version is unsupported - * @throws InstantiationException if the ISpecialEnderChest could not be instantiated - */ - @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull Player player, boolean online) throws InstantiationException; - - /** - * Gets an ISpecialPlayerInventory for the given Player. - * - * @param player the Player - * @param online true if the Player is currently online - * @return the ISpecialPlayerInventory - * @throws IllegalStateException if the server version is unsupported - * @throws InstantiationException if the ISpecialPlayerInventory could not be instantiated - */ - @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull Player player, boolean online) throws InstantiationException; - - /** - * Checks if the server version is supported by OpenInv. - * - * @return true if the server version is supported - */ - boolean isSupportedVersion(); - - /** - * Load a Player from an OfflinePlayer. May return null under some circumstances. - * - * @param offline the OfflinePlayer to load a Player for - * @return the Player, or null - * @throws IllegalStateException if the server version is unsupported - */ - @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline); - - /** - * Get an OfflinePlayer by name. - *

- * Note: This method is potentially very heavily blocking. It should not ever be called on the - * main thread, and if it is, a stack trace will be displayed alerting server owners to the - * call. - * - * @param name the name of the Player - * @return the OfflinePlayer with the closest matching name or null if no players have ever logged in - */ - default @Nullable OfflinePlayer matchPlayer(@NotNull String name) { - - // Warn if called on the main thread - if we resort to searching offline players, this may take several seconds. - if (Bukkit.getServer().isPrimaryThread()) { - this.getLogger().warning("Call to OpenInv#matchPlayer made on the main thread!"); - this.getLogger().warning("This can cause the server to hang, potentially severely."); - this.getLogger().warning("Trace:"); - for (StackTraceElement element : new Throwable().fillInStackTrace().getStackTrace()) { - this.getLogger().warning(element.toString()); - } - } - - OfflinePlayer player; - - try { - UUID uuid = UUID.fromString(name); - player = Bukkit.getOfflinePlayer(uuid); - // Ensure player is a real player, otherwise return null - if (player.hasPlayedBefore() || player.isOnline()) { - return player; - } - } catch (IllegalArgumentException ignored) { - // Not a UUID - } - - // Ensure name is valid if server is in online mode to avoid unnecessary searching - if (Bukkit.getServer().getOnlineMode() && !name.matches("[a-zA-Z0-9_]{3,16}")) { - return null; - } - - player = Bukkit.getServer().getPlayerExact(name); - - if (player != null) { - return player; - } - - player = Bukkit.getServer().getOfflinePlayer(name); - - if (player.hasPlayedBefore()) { - return player; - } - - player = Bukkit.getServer().getPlayer(name); - - if (player != null) { - return player; - } - - float bestMatch = 0; - for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) { - if (offline.getName() == null) { - // Loaded by UUID only, name has never been looked up. - continue; - } - - float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName()); - - if (currentMatch == 1.0F) { - return offline; - } - - if (currentMatch > bestMatch) { - bestMatch = currentMatch; - player = offline; - } - } - - // Only null if no players have played ever, otherwise even the worst match will do. - return player; - } - - /** - * Open an ISpecialInventory for a Player. - * - * @param player the Player - * @param inventory the ISpecialInventory - * @return the InventoryView for the opened ISpecialInventory - */ - @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory); - - /** - * Check the configuration value for whether or not OpenInv displays a notification to the user - * when a container is activated with AnyChest. - * - * @return true unless configured otherwise - */ - boolean notifyAnyChest(); - - /** - * Check the configuration value for whether or not OpenInv displays a notification to the user - * when a container is activated with SilentChest. - * - * @return true unless configured otherwise - */ - boolean notifySilentChest(); - - /** - * Mark a Player as no longer in use by a Plugin to allow OpenInv to remove it from the cache - * when eligible. - * - * @param player the Player - * @param plugin the Plugin no longer holding a reference to the Player - * @throws IllegalStateException if the server version is unsupported - */ - void releasePlayer(@NotNull Player player, @NotNull Plugin plugin); - - /** - * Mark a Player as in use by a Plugin to prevent it from being removed from the cache. Used to - * prevent issues with multiple copies of the same Player being loaded such as lishid#49. - * Changes made to loaded copies overwrite changes to the others when saved, leading to - * duplication bugs and more. - *

- * When finished with the Player object, be sure to call {@link #releasePlayer(Player, Plugin)} - * to prevent the cache from keeping it stored until the plugin is disabled. - *

- * When using a Player object from OpenInv, you must handle the Player coming online, replacing - * your Player reference with the Player from the PlayerJoinEvent. In addition, you must change - * any values in the Player to reflect any unsaved alterations to the existing Player which do - * not affect the inventory or ender chest contents. - *

- * OpenInv only saves player data when unloading a Player from the cache, and then only if - * {@link #disableSaving()} returns false. If you are making changes that OpenInv does not cause - * to persist when a Player logs in as noted above, it is suggested that you manually call - * {@link Player#saveData()} when releasing your reference to ensure your changes persist. - * - * @param player the Player - * @param plugin the Plugin holding the reference to the Player - * @throws IllegalStateException if the server version is unsupported - */ - void retainPlayer(@NotNull Player player, @NotNull Plugin plugin); - - /** - * Sets a player's AnyChest setting. - * - * @param offline the OfflinePlayer - * @param status the status - * @throws IllegalStateException if the server version is unsupported - */ - void setPlayerAnyChestStatus(@NotNull OfflinePlayer offline, boolean status); - - /** - * Sets a player's SilentChest setting. - * - * @param offline the OfflinePlayer - * @param status the status - * @throws IllegalStateException if the server version is unsupported - */ - void setPlayerSilentChestStatus(@NotNull OfflinePlayer offline, boolean status); - - /** - * Forcibly unload a cached Player's data. - * - * @param offline the OfflinePlayer to unload - * @throws IllegalStateException if the server version is unsupported - */ - void unload(@NotNull OfflinePlayer offline); - - Logger getLogger(); + /** + * Check if the server version is supported by OpenInv. + * + * @return true if the server version is supported + */ + boolean isSupportedVersion(); + + /** + * Check the configuration value for whether OpenInv saves player data when unloading players. This is exclusively + * for users who do not allow editing of inventories, only viewing, and wish to prevent any possibility of bugs such + * as lishid#40. If true, OpenInv will not ever save any edits made to players. + * + * @return false unless configured otherwise + */ + boolean disableSaving(); + + /** + * Check the configuration value for whether OpenInv allows offline access. If true, OpenInv will not load or allow + * modification of players while they are not online. This does not prevent other plugins from using existing loaded + * players who have gone offline. + * + * @return false unless configured otherwise + * @since 4.2.0 + */ + boolean disableOfflineAccess(); + + /** + * Check the configuration value for whether OpenInv uses history for opening commands. If false, OpenInv will use + * the previous parameterized search when no parameters are provided. + * + * @return false unless configured otherwise + * @since 4.3.0 + */ + boolean noArgsOpensSelf(); + + /** + * Get the active {@link IAnySilentContainer} implementation. + * + * @return the active implementation for the server version + * @throws IllegalStateException if the server version is unsupported + */ + @NotNull IAnySilentContainer getAnySilentContainer(); + + /** + * Get whether a user has AnyContainer mode enabled. + * + * @param offline the user to obtain the state of + * @return true if AnyContainer mode is enabled + */ + boolean getAnyContainerStatus(@NotNull OfflinePlayer offline); + + /** + * Set whether a user has AnyContainer mode enabled. + * + * @param offline the user to set the state of + * @param status the state of the mode + */ + void setAnyContainerStatus(@NotNull OfflinePlayer offline, boolean status); + + /** + * Get whether a user has SilentContainer mode enabled. + * + * @param offline the user to obtain the state of + * @return true if SilentContainer mode is enabled + */ + boolean getSilentContainerStatus(@NotNull OfflinePlayer offline); + + /** + * Set whether a user has SilentContainer mode enabled. + * + * @param offline the user to set the state of + * @param status the state of the mode + */ + void setSilentContainerStatus(@NotNull OfflinePlayer offline, boolean status); + + /** + * Get an {@link ISpecialEnderChest} for a user. + * + * @param player the {@link Player} owning the inventory + * @param online whether the owner is currently online + * @return the created inventory + * @throws IllegalStateException if the server version is unsupported + * @throws InstantiationException if there was an issue creating the inventory + */ + @NotNull ISpecialEnderChest getSpecialEnderChest( + @NotNull Player player, + boolean online + ) throws InstantiationException; + + /** + * Get an {@link ISpecialPlayerInventory} for a user. + * + * @param player the {@link Player} owning the inventory + * @param online whether the owner is currently online + * @return the created inventory + * @throws IllegalStateException if the server version is unsupported + * @throws InstantiationException if there was an issue creating the inventory + */ + @NotNull ISpecialPlayerInventory getSpecialInventory( + @NotNull Player player, + boolean online + ) throws InstantiationException; + + /** + * @deprecated Use {@link #openInventory(Player, ISpecialInventory, boolean)} + */ + @Deprecated(forRemoval = true, since = "5.2.0") + @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory); + + /** + * Open an {@link ISpecialInventory} for a {@link Player}. + * + * @param player the viewer + * @param inventory the inventory to open + * @param viewOnly whether the inventory should be view-only + * @return the resulting {@link InventoryView} + */ + @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly); + + /** + * Check if a {@link Player} is currently loaded by OpenInv. + * + * @param playerUuid the {@link UUID} of the {@code Player} + * @return whether the {@code Player} is loaded + * @since 4.2.0 + */ + boolean isPlayerLoaded(@NotNull UUID playerUuid); + + /** + * Load a {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for + * the server is not loaded, this will return {@code null}. + * + * @param offline the {@code OfflinePlayer} to load a {@code Player} for + * @return the loaded {@code Player} + * @throws IllegalStateException if the server version is unsupported + */ + @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline); + + /** + * Match an existing {@link OfflinePlayer}. If the name is a {@link UUID#toString() UUID string}, this will only + * return the user if they have actually played on the server before, unlike {@link Bukkit#getOfflinePlayer(UUID)}. + * + *

This method is potentially very heavily blocking. It should not ever be called on the + * main thread, and if it is, a stack trace will be displayed alerting server owners to the + * call. + * + * @param name the string to match + * @return the user with the closest matching name + */ + @Nullable OfflinePlayer matchPlayer(@NotNull String name); + + /** + * Forcibly close inventories of and unload any cached data for a user. + * + * @param offline the {@link OfflinePlayer} to unload + */ + void unload(@NotNull OfflinePlayer offline); + + Logger getLogger(); } diff --git a/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java b/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java new file mode 100644 index 00000000..7f0e16c6 --- /dev/null +++ b/api/src/main/java/com/lishid/openinv/event/OpenPlayerSaveEvent.java @@ -0,0 +1,56 @@ +package com.lishid.openinv.event; + +import com.google.errorprone.annotations.RestrictedApi; +import com.lishid.openinv.internal.ISpecialInventory; +import org.bukkit.entity.Player; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Event fired before OpenInv saves a player's data when closing an {@link ISpecialInventory}. + */ +public class OpenPlayerSaveEvent extends PlayerSaveEvent { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final ISpecialInventory inventory; + + /** + * Construct a new {@code OpenPlayerSaveEvent}. + * + *

The constructor is not considered part of the API, and may be subject to change.

+ * + * @param player the player to be saved + * @param inventory the {@link ISpecialInventory} being closed + */ + @RestrictedApi( + explanation = "Constructor is not considered part of the API and may be subject to change.", + allowedOnPath = ".*/com/lishid/openinv/event/OpenEvents.java" + ) + @ApiStatus.Internal + OpenPlayerSaveEvent(@NotNull Player player, @NotNull ISpecialInventory inventory) { + super(player); + this.inventory = inventory; + } + + /** + * Get the {@link ISpecialInventory} that triggered the save by being closed. + * + * @return the special inventory + */ + public @NotNull ISpecialInventory getInventory() { + return inventory; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + +} diff --git a/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java b/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java new file mode 100644 index 00000000..3fd98039 --- /dev/null +++ b/api/src/main/java/com/lishid/openinv/event/PlayerSaveEvent.java @@ -0,0 +1,67 @@ +package com.lishid.openinv.event; + + +import com.google.errorprone.annotations.RestrictedApi; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Event fired before a {@link Player} loaded via OpenInv is saved. + */ +public class PlayerSaveEvent extends PlayerEvent implements Cancellable { + + private static final HandlerList HANDLERS = new HandlerList(); + + private boolean cancelled = false; + + /** + * Construct a new {@code PlayerSaveEvent}. + * + *

The constructor is not considered part of the API, and may be subject to change.

+ * + * @param player the player to be saved + */ + @RestrictedApi( + explanation = "Constructor is not considered part of the API and may be subject to change.", + allowedOnPath = ".*/com/lishid/openinv/event/(OpenPlayerSaveEvent|OpenEvents).java" + ) + @ApiStatus.Internal + PlayerSaveEvent(@NotNull Player player) { + super(player); + } + + /** + * Get whether the event is cancelled. + * + * @return true if the event is cancelled + */ + @Override + public boolean isCancelled() { + return cancelled; + } + + /** + * Set whether the event is cancelled. + * + * @param cancel whether the event is cancelled + */ + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + +} diff --git a/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java b/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java new file mode 100644 index 00000000..ed00a5ee --- /dev/null +++ b/api/src/main/java/com/lishid/openinv/event/PlayerToggledEvent.java @@ -0,0 +1,71 @@ +package com.lishid.openinv.event; + +import com.google.errorprone.annotations.RestrictedApi; +import com.lishid.openinv.util.setting.PlayerToggle; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Event fired after OpenInv modifies a toggleable setting for a player. + */ +public class PlayerToggledEvent extends Event { + + private static final HandlerList HANDLERS = new HandlerList(); + + private final @NotNull PlayerToggle toggle; + private final @NotNull UUID uuid; + private final boolean enabled; + + @RestrictedApi( + explanation = "Constructor is not considered part of the API and may be subject to change.", + allowedOnPath = ".*/com/lishid/openinv/event/OpenEvents.java" + ) + @ApiStatus.Internal + PlayerToggledEvent(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean enabled) { + this.toggle = toggle; + this.uuid = uuid; + this.enabled = enabled; + } + + /** + * Get the {@link PlayerToggle} affected. + * + * @return the toggle + */ + public @NotNull PlayerToggle getToggle() { + return toggle; + } + + /** + * Get the {@link UUID} of the player whose setting was changed. + * + * @return the player ID + */ + public @NotNull UUID getPlayerId() { + return uuid; + } + + /** + * Get whether the toggle is enabled. + * + * @return true if the toggle is enabled + */ + public boolean isEnabled() { + return enabled; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLERS; + } + + public static HandlerList getHandlerList() { + return HANDLERS; + } + +} diff --git a/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java b/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java index 02cab779..3f83147a 100644 --- a/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java +++ b/api/src/main/java/com/lishid/openinv/internal/IAnySilentContainer.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2023 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,45 +17,106 @@ package com.lishid.openinv.internal; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.EnderChest; +import org.bukkit.block.data.Directional; +import org.bukkit.entity.Cat; import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.util.BoundingBox; import org.jetbrains.annotations.NotNull; public interface IAnySilentContainer { - /** - * Opens the container at the given coordinates for the Player. If you do not want blocked - * containers to open, be sure to check {@link #isAnyContainerNeeded(Player, Block)} - * first. - * - * @param player the Player opening the container - * @param silent whether the container's noise is to be silenced - * @param block the Block - * @return true if the container can be opened - */ - boolean activateContainer(@NotNull Player player, boolean silent, @NotNull Block block); - - /** - * Closes the Player's currently open container silently, if necessary. - * - * @param player the Player closing a container - */ - void deactivateContainer(@NotNull Player player); - - /** - * Checks if the container at the given coordinates is blocked. - * - * @param player the Player opening the container - * @param block the Block - * @return true if the container is blocked - */ - boolean isAnyContainerNeeded(@NotNull Player player, @NotNull Block block); - - /** - * Checks if the given block is a container which can be unblocked or silenced. - * - * @param block the BlockState - * @return true if the Block is a supported container - */ - boolean isAnySilentContainer(@NotNull Block block); + /** + * Forcibly open the container at the given coordinates for the Player. This will open blocked containers! Be sure + * to check {@link #isAnyContainerNeeded(Block)} first if that is not desirable. + * + * @param player the {@link Player} opening the container + * @param silent whether the container's noise is to be silenced + * @param block the {@link Block} of the container + * @return true if the container can be opened + */ + boolean activateContainer(@NotNull Player player, boolean silent, @NotNull Block block); + + /** + * Perform operations required to close the current container silently. + * + * @param player the {@link Player} closing a container + */ + void deactivateContainer(@NotNull Player player); + + /** + * Check if the container at the given coordinates is blocked. + * + * @param block the {@link Block} of the container + * @return true if the container is blocked + */ + boolean isAnyContainerNeeded(@NotNull Block block); + + /** + * Check if a shulker box block cannot be opened under ordinary circumstances. + * + * @param shulkerBox the shulker box block + * @return whether the container is blocked + */ + default boolean isShulkerBlocked(@NotNull Block shulkerBox) { + Directional directional = (Directional) shulkerBox.getBlockData(); + BlockFace facing = directional.getFacing(); + // Construct a new 1-block bounding box at the origin. + BoundingBox box = new BoundingBox(0, 0, 0, 1, 1, 1); + // Expand the box in the direction the shulker will open. + box.expand(facing, 0.5); + // Move the box away from the origin by a block so only the expansion intersects with a box around the origin. + box.shift(facing.getOppositeFace().getDirection()); + // Check if the relative block's collision shape (which will be at the origin) intersects with the expanded box. + return shulkerBox.getRelative(facing).getCollisionShape().overlaps(box); + } + + /** + * Check if a chest cannot be opened under ordinary circumstances. + * + * @param chest the chest block + * @return whether the container is blocked + */ + default boolean isChestBlocked(@NotNull Block chest) { + org.bukkit.block.Block relative = chest.getRelative(0, 1, 0); + return relative.getType().isOccluding() + || !chest.getWorld().getNearbyEntities(BoundingBox.of(relative), Cat.class::isInstance).isEmpty(); + } + + /** + * Check if the given {@link Block} is a container which can be unblocked or silenced. + * + * @param block the potential container + * @return true if the type is a supported container + */ + boolean isAnySilentContainer(@NotNull Block block); + + /** + * Check if the given {@link BlockState} is a container which can be unblocked or silenced. + * + * @param blockState the potential container + * @return true if the type is a supported container + */ + default boolean isAnySilentContainer(@NotNull BlockState blockState) { + return (blockState instanceof InventoryHolder holder && isAnySilentContainer(holder)) + || blockState instanceof EnderChest; + } + + /** + * Check if the given {@link InventoryHolder} is a container which can be unblocked or silenced. + * + * @param holder the potential container + * @return true if the type is a supported container + */ + default boolean isAnySilentContainer(@NotNull InventoryHolder holder) { + return holder instanceof org.bukkit.block.EnderChest + || holder instanceof org.bukkit.block.Chest + || holder instanceof org.bukkit.block.DoubleChest + || holder instanceof org.bukkit.block.ShulkerBox + || holder instanceof org.bukkit.block.Barrel; + } } diff --git a/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java b/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java deleted file mode 100644 index 37677047..00000000 --- a/api/src/main/java/com/lishid/openinv/internal/IInventoryAccess.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal; - -import org.bukkit.inventory.Inventory; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -@Deprecated -public interface IInventoryAccess { - - /** - * Gets an ISpecialEnderChest from an Inventory or null if the Inventory is not backed by an - * ISpecialEnderChest. - * - * @param inventory the Inventory - * @return the ISpecialEnderChest or null - */ - @Deprecated - @Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory); - - /** - * Gets an ISpecialPlayerInventory from an Inventory or null if the Inventory is not backed by - * an ISpecialPlayerInventory. - * - * @param inventory the Inventory - * @return the ISpecialPlayerInventory or null - */ - @Deprecated - @Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory); - - /** - * Check if an Inventory is an ISpecialEnderChest implementation. - * - * @param inventory the Inventory - * @return true if the Inventory is backed by an ISpecialEnderChest - */ - @Deprecated - boolean isSpecialEnderChest(@NotNull Inventory inventory); - - /** - * Check if an Inventory is an ISpecialPlayerInventory implementation. - * - * @param inventory the Inventory - * @return true if the Inventory is backed by an ISpecialPlayerInventory - */ - @Deprecated - boolean isSpecialPlayerInventory(@NotNull Inventory inventory); - -} diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java index 86657b05..4b35c4c8 100644 --- a/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java +++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialEnderChest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,6 +16,17 @@ package com.lishid.openinv.internal; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * An {@link ISpecialInventory} representing an ender chest. + */ public interface ISpecialEnderChest extends ISpecialInventory { + @Override + default @NotNull InventoryType getBukkitType() { + return InventoryType.ENDER_CHEST; + } + } diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java index e6929c96..2793526d 100644 --- a/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java +++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialInventory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,36 +16,60 @@ package com.lishid.openinv.internal; +import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.jetbrains.annotations.NotNull; +/** + * Interface defining behavior for special inventories backed by other inventories' content listings. + */ public interface ISpecialInventory { - /** - * Gets the Inventory associated with this ISpecialInventory. - * - * @return the Inventory - */ - @NotNull Inventory getBukkitInventory(); - - /** - * Sets the Player associated with this ISpecialInventory online. - * - * @param player the Player coming online - */ - void setPlayerOnline(@NotNull Player player); - - /** - * Sets the Player associated with this ISpecialInventory offline. - */ - void setPlayerOffline(); - - /** - * Gets whether or not this ISpecialInventory is in use. - * - * @return true if the ISpecialInventory is in use - */ - boolean isInUse(); + /** + * Get the {@link Inventory} associated with this {@code ISpecialInventory}. + * + * @return the Bukkit inventory + */ + @NotNull Inventory getBukkitInventory(); + + /** + * Get the {@link InventoryType} corresponding to this {@code ISpecialInventory}. + * + * @return the type of Bukkit inventory + */ + @NotNull InventoryType getBukkitType(); + + /** + * Set the owning {@link Player} instance to a newly-joined user. + * + * @param player the user coming online + */ + void setPlayerOnline(@NotNull Player player); + + /** + * Mark the owner of the inventory offline. + * + * @deprecated No longer used by implementations. + */ + @Deprecated(forRemoval = true, since = "5.1.11") + default void setPlayerOffline() {} + + /** + * Get whether the inventory is being viewed by any users. + * + * @return true if the inventory is being viewed + */ + default boolean isInUse() { + return !getBukkitInventory().getViewers().isEmpty(); + } + + /** + * Get the {@link Player} who owns the inventory. + * + * @return the {@link HumanEntity} who owns the inventory + */ + @NotNull HumanEntity getPlayer(); } diff --git a/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java b/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java index f06f724f..91affaf8 100644 --- a/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java +++ b/api/src/main/java/com/lishid/openinv/internal/ISpecialPlayerInventory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,6 +16,17 @@ package com.lishid.openinv.internal; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * An {@link ISpecialInventory} representing a player inventory. + */ public interface ISpecialPlayerInventory extends ISpecialInventory { + @Override + default @NotNull InventoryType getBukkitType() { + return InventoryType.PLAYER; + } + } diff --git a/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java b/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java index 9c2ffd72..6b3aa005 100644 --- a/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java +++ b/api/src/main/java/com/lishid/openinv/util/InventoryAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,119 +16,91 @@ package com.lishid.openinv.util; -import com.lishid.openinv.internal.IInventoryAccess; +import com.google.errorprone.annotations.RestrictedApi; import com.lishid.openinv.internal.ISpecialEnderChest; import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import org.bukkit.Bukkit; import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class InventoryAccess implements IInventoryAccess { - - private static Class craftInventory = null; - private static Method getInventory = null; - - static { - String packageName = Bukkit.getServer().getClass().getPackage().getName(); - try { - craftInventory = Class.forName(packageName + ".inventory.CraftInventory"); - } catch (ClassNotFoundException ignored) {} - try { - getInventory = craftInventory.getDeclaredMethod("getInventory"); - } catch (NoSuchMethodException ignored) {} - } - - /** - * @deprecated use {@link #isUsable()} - */ - @Deprecated - public static boolean isUseable() { - return isUsable(); - } - - public static boolean isUsable() { - return craftInventory != null && getInventory != null; - } - - public static boolean isPlayerInventory(@NotNull Inventory inventory) { - return getPlayerInventory(inventory) != null; - } - - public static @Nullable ISpecialPlayerInventory getPlayerInventory(@NotNull Inventory inventory) { - return getSpecialInventory(ISpecialPlayerInventory.class, inventory); - } - - public static boolean isEnderChest(@NotNull Inventory inventory) { - return getEnderChest(inventory) != null; - } - - public static @Nullable ISpecialEnderChest getEnderChest(@NotNull Inventory inventory) { - return getSpecialInventory(ISpecialEnderChest.class, inventory); - } - - private static @Nullable T getSpecialInventory(@NotNull Class expected, @NotNull Inventory inventory) { - Object inv; - if (craftInventory != null && getInventory != null && craftInventory.isAssignableFrom(inventory.getClass())) { - try { - inv = getInventory.invoke(inventory); - if (expected.isInstance(inv)) { - return expected.cast(inv); - } - } catch (ReflectiveOperationException ignored) {} - } - - inv = grabFieldOfTypeFromObject(expected, inventory); - - if (expected.isInstance(inv)) { - return expected.cast(inv); - } - - return null; - } - - private static @Nullable T grabFieldOfTypeFromObject(final Class type, final Object object) { - // Use reflection to find the IInventory - Class clazz = object.getClass(); - T result = null; - for (Field f : clazz.getDeclaredFields()) { - f.setAccessible(true); - if (type.isAssignableFrom(f.getDeclaringClass())) { - try { - result = type.cast(f.get(object)); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - return result; - } - - @Deprecated - @Override - public @Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory) { - return getEnderChest(inventory); - } - - @Deprecated - @Override - public @Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory) { - return getPlayerInventory(inventory); - } - - @Deprecated - @Override - public boolean isSpecialEnderChest(@NotNull Inventory inventory) { - return isEnderChest(inventory); - } - - @Deprecated - @Override - public boolean isSpecialPlayerInventory(@NotNull Inventory inventory) { - return isPlayerInventory(inventory); - } +import java.util.function.BiFunction; + +public final class InventoryAccess { + + private static @Nullable BiFunction, ISpecialInventory> provider; + + public static boolean isUsable() { + return provider != null; + } + + /** + * Check if an {@link Inventory} is an {@link ISpecialPlayerInventory} implementation. + * + * @param inventory the Bukkit inventory + * @return true if backed by the correct implementation + */ + public static boolean isPlayerInventory(@NotNull Inventory inventory) { + return getPlayerInventory(inventory) != null; + } + + /** + * Get the {@link ISpecialPlayerInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is + * not backed by the correct class. + * + * @param inventory the Bukkit inventory + * @return the backing implementation if available + */ + public static @Nullable ISpecialPlayerInventory getPlayerInventory(@NotNull Inventory inventory) { + return provider == null ? null : (ISpecialPlayerInventory) provider.apply(inventory, ISpecialPlayerInventory.class); + } + + /** + * Check if an {@link Inventory} is an {@link ISpecialEnderChest} implementation. + * + * @param inventory the Bukkit inventory + * @return true if backed by the correct implementation + */ + public static boolean isEnderChest(@NotNull Inventory inventory) { + return getEnderChest(inventory) != null; + } + + /** + * Get the {@link ISpecialEnderChest} backing an {@link Inventory}. Returns {@code null} if the inventory is + * not backed by the correct class. + * + * @param inventory the Bukkit inventory + * @return the backing implementation if available + */ + public static @Nullable ISpecialEnderChest getEnderChest(@NotNull Inventory inventory) { + return provider == null ? null : (ISpecialEnderChest) provider.apply(inventory, ISpecialEnderChest.class); + } + + /** + * Get a {@link ISpecialInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is not backed + * by the correct class. + * + * @param inventory the Bukkit inventory + * @return the backing implementation if available + */ + public static @Nullable ISpecialInventory getInventory(@NotNull Inventory inventory) { + return provider == null ? null : provider.apply(inventory, ISpecialInventory.class); + } + + @RestrictedApi( + explanation = "Not part of the API.", + allowedOnPath = ".*/com/lishid/openinv/util/InternalAccessor.java" + ) + @ApiStatus.Internal + static void setProvider( + @Nullable BiFunction, ISpecialInventory> provider + ) { + InventoryAccess.provider = provider; + } + + private InventoryAccess() { + throw new IllegalStateException("Cannot create instance of utility class."); + } } diff --git a/api/src/main/java/com/lishid/openinv/util/StringMetric.java b/api/src/main/java/com/lishid/openinv/util/StringMetric.java deleted file mode 100644 index fe2e4a88..00000000 --- a/api/src/main/java/com/lishid/openinv/util/StringMetric.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -public class StringMetric { - - public static float compareJaroWinkler(String a, String b) { - final float jaroScore = compareJaro(a, b); - - if (jaroScore < (float) 0.7) { - return jaroScore; - } - - String prefix = commonPrefix(a, b); - int prefixLength = Math.min(prefix.codePointCount(0, prefix.length()), 4); - - return jaroScore + (prefixLength * (float) 0.1 * (1.0f - jaroScore)); - - } - - private static float compareJaro(String a, String b) { - if (a.isEmpty() && b.isEmpty()) { - return 1.0f; - } - - if (a.isEmpty() || b.isEmpty()) { - return 0.0f; - } - - final int[] charsA = a.codePoints().toArray(); - final int[] charsB = b.codePoints().toArray(); - - // Intentional integer division to round down. - final int halfLength = Math.max(0, Math.max(charsA.length, charsB.length) / 2 - 1); - - final int[] commonA = getCommonCodePoints(charsA, charsB, halfLength); - final int[] commonB = getCommonCodePoints(charsB, charsA, halfLength); - - // commonA and commonB will always contain the same multi-set of - // characters. Because getCommonCharacters has been optimized, commonA - // and commonB are -1-padded. So in this loop we count transposition - // and use commonCharacters to determine the length of the multi-set. - float transpositions = 0; - int commonCharacters = 0; - for (int length = commonA.length; commonCharacters < length - && commonA[commonCharacters] > -1; commonCharacters++) { - if (commonA[commonCharacters] != commonB[commonCharacters]) { - transpositions++; - } - } - - if (commonCharacters == 0) { - return 0.0f; - } - - float aCommonRatio = commonCharacters / (float) charsA.length; - float bCommonRatio = commonCharacters / (float) charsB.length; - float transpositionRatio = (commonCharacters - transpositions / 2.0f) / commonCharacters; - - return (aCommonRatio + bCommonRatio + transpositionRatio) / 3.0f; - } - - /* - * Returns an array of code points from a within b. A character in b is - * counted as common when it is within separation distance from the position - * in a. - */ - private static int[] getCommonCodePoints(final int[] charsA, final int[] charsB, final int separation) { - final int[] common = new int[Math.min(charsA.length, charsB.length)]; - final boolean[] matched = new boolean[charsB.length]; - - // Iterate of string a and find all characters that occur in b within - // the separation distance. Mark any matches found to avoid - // duplicate matchings. - int commonIndex = 0; - for (int i = 0, length = charsA.length; i < length; i++) { - final int character = charsA[i]; - final int index = indexOf(character, charsB, i - separation, i - + separation + 1, matched); - if (index > -1) { - common[commonIndex++] = character; - matched[index] = true; - } - } - - if (commonIndex < common.length) { - common[commonIndex] = -1; - } - - // Both invocations will yield the same multi-set terminated by -1, so - // they can be compared for transposition without making a copy. - return common; - } - - /* - * Search for code point in buffer starting at fromIndex to toIndex - 1. - * - * Returns -1 when not found. - */ - private static int indexOf(int character, int[] buffer, int fromIndex, int toIndex, boolean[] matched) { - - // compare char with range of characters to either side - for (int j = Math.max(0, fromIndex), length = Math.min(toIndex, buffer.length); j < length; j++) { - // check if found - if (buffer[j] == character && !matched[j]) { - return j; - } - } - - return -1; - } - - private static String commonPrefix(CharSequence a, CharSequence b) { - int maxPrefixLength = Math.min(a.length(), b.length()); - - int p; - - p = 0; - while (p < maxPrefixLength && a.charAt(p) == b.charAt(p)) { - ++p; - } - - if (validSurrogatePairAt(a, p - 1) || validSurrogatePairAt(b, p - 1)) { - --p; - } - - return a.subSequence(0, p).toString(); - } - - private static boolean validSurrogatePairAt(CharSequence string, int index) { - return index >= 0 && index <= string.length() - 2 && Character.isHighSurrogate(string.charAt(index)) && Character.isLowSurrogate(string.charAt(index + 1)); - } - - private StringMetric(){} - -} diff --git a/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java new file mode 100644 index 00000000..6e8dc880 --- /dev/null +++ b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggle.java @@ -0,0 +1,36 @@ +package com.lishid.openinv.util.setting; + +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * A per-player setting that may be enabled or disabled. + */ +public interface PlayerToggle { + + /** + * Get the name of the setting. + * + * @return the setting name + */ + @NotNull String getName(); + + /** + * Get the state of the toggle for a particular player ID. + * + * @param uuid the player ID + * @return true if the setting is enabled + */ + boolean is(@NotNull UUID uuid); + + /** + * Set the state of the toggle for a particular player ID. + * + * @param uuid the player ID + * @param enabled whether the setting is enabled + * @return true if the setting changed as a result of being set + */ + boolean set(@NotNull UUID uuid, boolean enabled); + +} diff --git a/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java new file mode 100644 index 00000000..d61cd115 --- /dev/null +++ b/api/src/main/java/com/lishid/openinv/util/setting/PlayerToggles.java @@ -0,0 +1,106 @@ +package com.lishid.openinv.util.setting; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Utility class containing all of OpenInv's {@link PlayerToggle PlayerToggles}. + */ +public final class PlayerToggles { + + private static final Map TOGGLES = new HashMap<>(); + private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer")); + private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer")); + + /** + * Get the AnyContainer toggle. + * + * @return the AnyContainer toggle + */ + public static @NotNull PlayerToggle any() { + return ANY; + } + + /** + * Get the SilentContainer toggle. + * + * @return the SilentContainer toggle + */ + public static @NotNull PlayerToggle silent() { + return SILENT; + } + + /** + * Get a toggle by name. + * + * @param toggleName the name of the toggle + * @return the toggle, or null if no such toggle exists. + */ + public static @Nullable PlayerToggle get(@NotNull String toggleName) { + PlayerToggle toggle = TOGGLES.get(toggleName); + if (toggle == null) { + toggle = TOGGLES.get(toggleName.toLowerCase(Locale.ENGLISH)); + } + return toggle; + } + + /** + * Get an unmodifable view of all toggles available. + * + * @return a view of all toggles available + */ + public static @UnmodifiableView @NotNull Collection get() { + return Collections.unmodifiableCollection(TOGGLES.values()); + } + + private static @NotNull PlayerToggle add(@NotNull PlayerToggle toggle) { + TOGGLES.put(toggle.getName().toLowerCase(Locale.ENGLISH), toggle); + return toggle; + } + + private PlayerToggles() { + throw new IllegalStateException("Cannot create instance of utility class."); + } + + private static class MemoryToggle implements PlayerToggle { + + private final @NotNull Set enabled; + private final @NotNull String name; + + private MemoryToggle(@NotNull String name) { + enabled = new HashSet<>(); + this.name = name; + } + + @Override + public @NotNull String getName() { + return this.name; + } + + @Override + public boolean is(@NotNull UUID uuid) { + return enabled.contains(uuid); + } + + @Override + public boolean set(@NotNull UUID uuid, boolean enabled) { + if (enabled) { + return this.enabled.add(uuid); + } else { + return this.enabled.remove(uuid); + } + } + + } + +} diff --git a/assembly/pom.xml b/assembly/pom.xml deleted file mode 100644 index 421f77ec..00000000 --- a/assembly/pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - 4.0.0 - - - com.lishid - openinvparent - 4.1.6-SNAPSHOT - - - openinvassembly - OpenInvAssembly - - - ../target - OpenInv - - - - maven-assembly-plugin - 3.2.0 - - - reactor-uberjar - package - - single - - - false - - src/assembly/reactor-uberjar.xml - - - - - - - - - - diff --git a/assembly/src/assembly/reactor-uberjar.xml b/assembly/src/assembly/reactor-uberjar.xml deleted file mode 100644 index 36beb33e..00000000 --- a/assembly/src/assembly/reactor-uberjar.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - reactor-uberjar - - - jar - - - false - - - - - true - - - / - true - - - - - - - - diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..2aa1d9f4 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `java-library` + alias(libs.plugins.paperweight) apply false + alias(libs.plugins.shadow) apply false + id(libs.plugins.errorprone.gradle.get().pluginId) apply false +} + +// Set by Spigot module, used by Paper module to convert to Spigot's version of Mojang mappings. +project.ext.set("craftbukkitPackage", "UNKNOWN") + +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + +// Allow submodules to target higher Java release versions. +// Not currently necessary (as lowest supported version is in the 1.21 range) +// but may become relevant in the future. +java.disableAutoTargetJvm() + +// Task to delete ./dist where final files are output. +tasks.register("cleanDist") { + delete("dist") +} + +tasks.clean { + // Also delete distribution folder when cleaning. + dependsOn(tasks.named("cleanDist")) +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..aef4e143 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + mavenCentral() +} + +dependencies { + val libs = project.extensions.getByType(VersionCatalogsExtension::class.java).named("libs") + implementation(libs.findLibrary("specialsource").orElseThrow()) + implementation(libs.findLibrary("errorprone-gradle").orElseThrow()) +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..215a5d58 --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt new file mode 100644 index 00000000..2bedf878 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/BuildToolsValueSource.kt @@ -0,0 +1,100 @@ +package com.github.jikoo.openinv + +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.ValueSource +import org.gradle.api.provider.ValueSourceParameters +import org.gradle.process.ExecOperations +import java.io.File +import java.net.URI +import java.nio.file.Files +import javax.inject.Inject + +abstract class BuildToolsValueSource : ValueSource { + + @get:Inject + abstract val exec: ExecOperations + + interface Parameters : ValueSourceParameters { + val mavenLocal: Property + val workingDir: DirectoryProperty + + val spigotVersion: Property + val spigotRevision: Property + + val ignoreCached: Property + + val javaHome: DirectoryProperty + val javaExecutable: Property + } + + override fun obtain(): File { + val version = parameters.spigotVersion.get() + val revision = parameters.spigotRevision.get() + val installLocation = getInstallLocation(version) + // If Spigot is already installed, don't reinstall. + if (!parameters.ignoreCached.get() && installLocation.exists()) { + println("Skipping Spigot installation, $version is present") + return installLocation + } + + val buildTools = installBuildTools(parameters.workingDir.get().asFile) + + println("Installing Spigot $version (rev $revision)") + + exec.javaexec { + environment["JAVA_HOME"] = parameters.javaHome.get() + executable = parameters.javaExecutable.get() + workingDir = buildTools.parentFile + classpath(buildTools) + args = listOf("--nogui", "--rev", revision, "--remapped") + }.rethrowFailure() + + // Mark work for delete. + cleanUp(buildTools.parentFile) + + if (!installLocation.exists()) { + throw IllegalStateException( + "Failed to install Spigot $version from $revision. Does the revision point to a different version?" + ) + } + return installLocation + } + + private fun getInstallLocation(version: String): File { + return parameters.mavenLocal.get().resolve("org/spigotmc/spigot/$version/spigot-$version-remapped-mojang.jar") + } + + private fun installBuildTools(workingDir: File): File { + val buildTools = workingDir.resolve("BuildTools.jar") + if (buildTools.exists()) { + return buildTools + } + + workingDir.mkdirs() + + val buildToolsUrl = + "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar" + println("Downloading $buildToolsUrl") + val stream = URI.create(buildToolsUrl).toURL().openStream() + Files.copy(stream, buildTools.toPath()) + stream.close() + + return buildTools + } + + private fun cleanUp(dir: File) { + dir.deleteOnExit() + if (!dir.isDirectory) { + return + } + + dir.listFiles()?.forEach { + if (it.isDirectory) { + cleanUp(it) + } else { + it.deleteOnExit() + } + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt new file mode 100644 index 00000000..94e82f47 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotDependencyExtension.kt @@ -0,0 +1,21 @@ +package com.github.jikoo.openinv + +import org.gradle.api.model.ObjectFactory +import org.gradle.jvm.toolchain.JavaToolchainSpec + +abstract class SpigotDependencyExtension( + objects: ObjectFactory +) { + + val version = objects.property(String::class.java) + val revision = objects.property(String::class.java) + .convention(version.map { + it.replace("-R\\d+\\.\\d+-SNAPSHOT".toRegex(), "") + }) + val configuration = objects.property(String::class.java) + val classifier = objects.property(String::class.java).convention("remapped-mojang") + val ext = objects.property(String::class.java) + val java = objects.property(JavaToolchainSpec::class.java) + val ignoreCached = objects.property(Boolean::class.java).convention(false) + +} diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt new file mode 100644 index 00000000..ca12b330 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobf.kt @@ -0,0 +1,56 @@ +package com.github.jikoo.openinv + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.LibraryElements +import org.gradle.api.attributes.Usage +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register +import java.nio.file.Paths + +class SpigotReobf : Plugin { + + companion object { + const val ARTIFACT_CONFIG = "reobf" + } + + override fun apply(target: Project) { + // Re-use extension from Spigot dependency declaration if available to reduce configuration requirements. + val spigotExt = target.dependencies.extensions.findByType(SpigotDependencyExtension::class.java) + ?: target.dependencies.extensions.create( + "spigot", + SpigotDependencyExtension::class.java, + target.objects + ) + + val mvnLocal = target.repositories.mavenLocal() + + val reobfTask = target.tasks.register("reobfTask") { + dependsOn(target.tasks.named("shadowJar")) + // ShadowJar extends Jar, so this should be a safe way to get the result without having + // to jump through hoops and shift around shadow declarations in the rest of the project. + inputFile.convention(target.tasks.named("shadowJar").get().archiveFile) + spigotVersion.convention(spigotExt.version) + getMavenLocal().set(Paths.get(mvnLocal.url).toFile()) + } + + // Set up configuration for producing reobf jar. + target.configurations.consumable(ARTIFACT_CONFIG) { + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, target.objects.named(Category.LIBRARY)) + attribute(Usage.USAGE_ATTRIBUTE, target.objects.named(Usage.JAVA_RUNTIME)) + attribute(Bundling.BUNDLING_ATTRIBUTE, target.objects.named(Bundling.EXTERNAL)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, target.objects.named(LibraryElements.JAR)) + } + } + + // Add artifact from reobf task. + target.artifacts { + add(ARTIFACT_CONFIG, reobfTask) + } + } + +} diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt new file mode 100644 index 00000000..e9e12ede --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotReobfTask.kt @@ -0,0 +1,85 @@ +package com.github.jikoo.openinv + +import net.md_5.specialsource.Jar +import net.md_5.specialsource.JarMapping +import net.md_5.specialsource.JarRemapper +import net.md_5.specialsource.RemapperProcessor +import net.md_5.specialsource.provider.JarProvider +import net.md_5.specialsource.provider.JointProvider +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.TaskAction +import java.io.File + +abstract class SpigotReobfTask : org.gradle.api.tasks.bundling.Jar() { + + @get:Input + val spigotVersion: Property = objectFactory.property(String::class.java) + + @get:InputFile + val inputFile: RegularFileProperty = objectFactory.fileProperty() + + @get:Input + val intermediaryClassifier: Property = objectFactory.property(String::class.java).convention("mojang-mapped") + + private val mavenLocal: Property = objectFactory.property(File::class.java) + + init { + archiveClassifier.convention(SpigotReobf.ARTIFACT_CONFIG) + } + + @TaskAction + override fun copy() { + val spigotVer = spigotVersion.get() + val inFile = inputFile.get().asFile + val obfPath = inFile.resolveSibling(inFile.name.replace(".jar", "-${intermediaryClassifier.get()}.jar")) + + // https://www.spigotmc.org/threads/510208/#post-4184317 + val repo = mavenLocal.get() + val spigotDir = repo.resolve("org/spigotmc/spigot/$spigotVer/") + val mappingDir = repo.resolve("org/spigotmc/minecraft-server/$spigotVer/") + + // Remap original Mojang-mapped jar to obfuscated intermediary + val mojangServer = spigotDir.resolve("spigot-$spigotVer-remapped-mojang.jar") + val mojangMappings = mappingDir.resolve("minecraft-server-$spigotVer-maps-mojang.txt") + remapPartial(mojangServer, mojangMappings, inFile, obfPath, true) + + // Remap obfuscated intermediary jar to Spigot and replace original + val obfServer = spigotDir.resolve("spigot-$spigotVer-remapped-obf.jar") + val spigotMappings = mappingDir.resolve("minecraft-server-$spigotVer-maps-spigot.csrg") + remapPartial(obfServer, spigotMappings, obfPath, archiveFile.get().asFile, false) + } + + private fun remapPartial(server: File, mapping: File, input: File, output: File, reverse: Boolean) { + val jarMapping = JarMapping() + jarMapping.loadMappings(mapping.path, reverse, false, null, null) + + val inheritance = JointProvider() + jarMapping.setFallbackInheritanceProvider(inheritance) + + // Equivalent of --live with server jar on classpath. + val serverJar = Jar.init(server) + inheritance.add(JarProvider(serverJar)) + + val inputJar = Jar.init(input) + inheritance.add(JarProvider(inputJar)) + + // Remap reflective access. + val preprocessor = RemapperProcessor(null, jarMapping, null) + + val remapper = JarRemapper(preprocessor, jarMapping, null) + remapper.remapJar(inputJar, output, emptySet()) + + serverJar.close() + inputJar.close() + } + + @Internal + internal fun getMavenLocal(): Property { + return mavenLocal + } + +} diff --git a/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt new file mode 100644 index 00000000..f9304be4 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/jikoo/openinv/SpigotSetup.kt @@ -0,0 +1,60 @@ +package com.github.jikoo.openinv + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.jvm.toolchain.JavaToolchainService +import org.gradle.kotlin.dsl.create +import java.nio.file.Paths +import javax.inject.Inject + +abstract class SpigotSetup : Plugin { + + @get:Inject + abstract val javaToolchainService: JavaToolchainService + + override fun apply(target: Project) { + target.plugins.apply("java") + + // Set up extension for configuring Spigot dependency. + val spigotExt = target.dependencies.extensions.findByType(SpigotDependencyExtension::class.java) + ?: target.dependencies.extensions.create( + "spigot", + SpigotDependencyExtension::class.java, + target.objects + ) + + val mvnLocal = target.repositories.mavenLocal() + + target.afterEvaluate { + // Get Java requirements, defaulting to version used for compilation. + spigotExt.java.convention(target.extensions.getByType(JavaPluginExtension::class.java).toolchain) + val launcher = javaToolchainService.launcherFor(spigotExt.java.get()).get() + + // Install Spigot with BuildTools. + target.providers.of(BuildToolsValueSource::class.java) { + parameters { + mavenLocal.set(Paths.get(mvnLocal.url).toFile()) + workingDir.set(target.layout.buildDirectory.dir("tmp/buildtools")) + spigotVersion.set(spigotExt.version) + spigotRevision.set(spigotExt.revision) + ignoreCached.set(spigotExt.ignoreCached) + javaHome.set(launcher.metadata.installationPath) + javaExecutable.set(launcher.executablePath.asFile.path) + } + }.get() + + // Add Spigot dependency. + val dependency = target.dependencies.create( + "org.spigotmc", + "spigot", + spigotExt.version.get(), + spigotExt.configuration.orNull, + spigotExt.classifier.orNull, + spigotExt.ext.orNull + ) + target.dependencies.add("compileOnly", dependency) + } + } + +} diff --git a/buildSrc/src/main/kotlin/openinv-base.gradle.kts b/buildSrc/src/main/kotlin/openinv-base.gradle.kts new file mode 100644 index 00000000..be1979a3 --- /dev/null +++ b/buildSrc/src/main/kotlin/openinv-base.gradle.kts @@ -0,0 +1,31 @@ +plugins { + `java-library` + id("net.ltgt.errorprone") +} + +java { + toolchain.languageVersion = JavaLanguageVersion.of(21) +} + +repositories { + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://hub.spigotmc.org/nexus/content/groups/public/") +} + +dependencies { + val libs = versionCatalogs.named("libs") + compileOnly(libs.findLibrary("annotations").orElseThrow()) + compileOnly(libs.findLibrary("spigotapi").orElseThrow()) + errorprone(libs.findLibrary("errorprone-core").orElseThrow()) +} + +tasks { + withType().configureEach { + options.release = 21 + options.encoding = Charsets.UTF_8.name() + } + withType().configureEach { + options.encoding = Charsets.UTF_8.name() + } +} diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 00000000..3321bb0e --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,8 @@ +plugins { + `openinv-base` +} + +dependencies { + implementation(project(":openinvapi")) + compileOnly(libs.slf4j.api) +} diff --git a/common/src/main/java/com/lishid/openinv/event/OpenEvents.java b/common/src/main/java/com/lishid/openinv/event/OpenEvents.java new file mode 100644 index 00000000..626372c2 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/event/OpenEvents.java @@ -0,0 +1,39 @@ +package com.lishid.openinv.event; + +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.util.setting.PlayerToggle; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * Construct and call events. + */ +public final class OpenEvents { + + public static boolean saveCancelled(@NotNull Player player) { + return call(new PlayerSaveEvent(player)); + } + + public static boolean saveCancelled(@NotNull ISpecialInventory inventory) { + return call(new OpenPlayerSaveEvent((Player) inventory.getPlayer(), inventory)); + } + + public static void notifyPlayerToggle(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean state) { + Bukkit.getPluginManager().callEvent(new PlayerToggledEvent(toggle, uuid, state)); + } + + private static boolean call(T event) { + Bukkit.getPluginManager().callEvent(event); + return event.isCancelled(); + } + + private OpenEvents() { + throw new IllegalStateException("Cannot create instance of utility class."); + } + +} diff --git a/common/src/main/java/com/lishid/openinv/internal/Accessor.java b/common/src/main/java/com/lishid/openinv/internal/Accessor.java new file mode 100644 index 00000000..e57a4afb --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/internal/Accessor.java @@ -0,0 +1,24 @@ +package com.lishid.openinv.internal; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface Accessor { + + @NotNull + PlayerManager getPlayerManager(); + + @NotNull IAnySilentContainer getAnySilentContainer(); + + @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player); + + @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player); + + @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz); + + void reload(@NotNull ConfigurationSection config); + +} diff --git a/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java b/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java new file mode 100644 index 00000000..df477629 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/internal/AnySilentContainerBase.java @@ -0,0 +1,106 @@ +package com.lishid.openinv.internal; + +import org.bukkit.block.Barrel; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.EnderChest; +import org.bukkit.block.ShulkerBox; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Chest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Method; + +public abstract class AnySilentContainerBase implements IAnySilentContainer { + + private static final @Nullable Method BLOCK_GET_STATE_BOOLEAN; + + static { + @Nullable Method getState; + try { + //noinspection JavaReflectionMemberAccess + getState = Block.class.getMethod("getState", boolean.class); + } catch (NoSuchMethodException e) { + getState = null; + } + BLOCK_GET_STATE_BOOLEAN = getState; + } + + private static BlockState getBlockState(Block block) { + // Paper: Get state without snapshotting. + if (BLOCK_GET_STATE_BOOLEAN != null) { + try { + return (BlockState) BLOCK_GET_STATE_BOOLEAN.invoke(block, false); + } catch (ReflectiveOperationException ignored) { + // If we encounter an issue, fall through to regular snapshotting method. + } + } + return block.getState(); + } + + @Override + public boolean isAnyContainerNeeded(@NotNull Block block) { + BlockState blockState = getBlockState(block); + + // Barrels do not require AnyContainer. + if (blockState instanceof Barrel) { + return false; + } + + // Enderchests require a non-occluding block on top to open. + if (blockState instanceof EnderChest) { + return block.getRelative(0, 1, 0).getType().isOccluding(); + } + + // Shulker boxes require half a block clear in the direction they open. + if (blockState instanceof ShulkerBox) { + return isShulkerBlocked(block); + } + + if (!(blockState instanceof org.bukkit.block.Chest)) { + return false; + } + + if (isChestBlocked(block)) { + return true; + } + + BlockData blockData = block.getBlockData(); + if (!(blockData instanceof Chest chest) || chest.getType() == Chest.Type.SINGLE) { + return false; + } + + BlockFace relativeFace = switch (chest.getFacing()) { + case NORTH -> chest.getType() == Chest.Type.RIGHT ? BlockFace.WEST : BlockFace.EAST; + case EAST -> chest.getType() == Chest.Type.RIGHT ? BlockFace.NORTH : BlockFace.SOUTH; + case SOUTH -> chest.getType() == Chest.Type.RIGHT ? BlockFace.EAST : BlockFace.WEST; + case WEST -> chest.getType() == Chest.Type.RIGHT ? BlockFace.SOUTH : BlockFace.NORTH; + default -> BlockFace.SELF; + }; + Block relative = block.getRelative(relativeFace); + + if (relative.getType() != block.getType()) { + return false; + } + + BlockData relativeData = relative.getBlockData(); + if (!(relativeData instanceof Chest relativeChest)) { + return false; + } + + if (relativeChest.getFacing() != chest.getFacing() + || relativeChest.getType() != (chest.getType() == Chest.Type.RIGHT ? Chest.Type.LEFT : Chest.Type.RIGHT)) { + return false; + } + + return isChestBlocked(relative); + } + + @Override + public boolean isAnySilentContainer(@NotNull Block block) { + return isAnySilentContainer(getBlockState(block)); + } + +} diff --git a/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java b/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java new file mode 100644 index 00000000..24f826d0 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/internal/InternalOwned.java @@ -0,0 +1,7 @@ +package com.lishid.openinv.internal; + +public interface InternalOwned { + + T getOwnerHandle(); + +} diff --git a/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java b/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java new file mode 100644 index 00000000..c60c1557 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/internal/PlayerManager.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.internal; + +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public interface PlayerManager { + + /** + * Loads a Player for an OfflinePlayer. + *

+ * This method is potentially blocking, and should not be called on the main thread. + * + * @param offline the OfflinePlayer + * @return the Player loaded + */ + @Nullable Player loadPlayer(@NotNull OfflinePlayer offline); + + /** + * Creates a new Player from an existing one that will function slightly better offline. + * + * @return the Player + */ + @NotNull Player inject(@NotNull Player player); + + /** + * Opens an ISpecialInventory for a Player. + * + * @param player the Player opening the ISpecialInventory + * @param inventory the Inventory + * + * @return the InventoryView opened + */ + @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly); + +} diff --git a/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java b/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java new file mode 100644 index 00000000..b8c5bfa6 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/internal/ViewOnly.java @@ -0,0 +1,4 @@ +package com.lishid.openinv.internal; + +public interface ViewOnly { +} diff --git a/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java b/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java new file mode 100644 index 00000000..f53a3374 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/util/JulLoggerAdapter.java @@ -0,0 +1,134 @@ +package com.lishid.openinv.util; + +import org.slf4j.Marker; +import org.slf4j.helpers.LegacyAbstractLogger; +import org.slf4j.helpers.MessageFormatter; +import org.slf4j.helpers.NormalizedParameters; +import org.slf4j.spi.LocationAwareLogger; + +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * An adapter for wrapping a {@link java.util.logging.Logger} as a {@link org.slf4j.Logger}. + *
Largely based on {@code JDK14LoggerAdapter}, which is not present at runtime. + */ +public class JulLoggerAdapter extends LegacyAbstractLogger implements LocationAwareLogger { + + private final Logger wrapped; + + public JulLoggerAdapter(Logger wrapped) { + this.wrapped = wrapped; + this.name = wrapped.getName(); + } + + @Override + protected String getFullyQualifiedCallerName() { + return JulLoggerAdapter.class.getName(); + } + + @Override + protected void handleNormalizedLoggingCall( + org.slf4j.event.Level slf4jLevel, + Marker marker, + String msg, + Object[] args, + Throwable thrown + ) { + Level level = fromSlf4jLevel(slf4jLevel); + + if (wrapped.isLoggable(level)) { + normalizedLog(getFullyQualifiedCallerName(), level, msg, args, thrown); + } + } + + private void normalizedLog(String fqcn, Level level, String msg, Object[] args, Throwable thrown) { + String formatted = MessageFormatter.basicArrayFormat(msg, args); + LogRecord logRecord = new LogRecord(level, formatted); + logRecord.setLoggerName(getName()); + logRecord.setThrown(thrown); + + addSource(fqcn, logRecord); + + wrapped.log(logRecord); + } + + private void addSource(String fqcn, LogRecord logRecord) { + // TODO stackwalker? + StackTraceElement[] trace = new Throwable().getStackTrace(); + int maxElements = 12; + int lastIgnored = maxElements; + // Start from 2; 0 is above and 1 is caller of internal method. + for (int i = 2; i < maxElements; ++i) { + if (isIgnored(trace[i].getClassName(), fqcn)) { + lastIgnored = i; + } + } + + if (lastIgnored < maxElements - 1) { + StackTraceElement caller = trace[lastIgnored + 1]; + logRecord.setSourceClassName(caller.getClassName()); + logRecord.setSourceMethodName(caller.getMethodName()); + } + } + + private boolean isIgnored(String className, String fqcn) { + if (className.equals(fqcn)) { + return true; + } + // Ignore slf4j classes - they shouldn't be the source. + if (className.startsWith("org.slf4j.")) { + return true; + } + return className.equals(getFullyQualifiedCallerName()); + } + + @Override + public void log(Marker marker, String callerFqn, int levelInt, String msg, Object[] args, Throwable thrown) { + Level level = fromSlf4jLevel(org.slf4j.event.Level.intToLevel(levelInt)); + + if (!wrapped.isLoggable(level)) { + return; + } + + NormalizedParameters params = NormalizedParameters.normalize(msg, args, thrown); + normalizedLog(callerFqn, level, params.getMessage(), params.getArguments(), params.getThrowable()); + } + + private Level fromSlf4jLevel(org.slf4j.event.Level level) { + return switch (level) { + case TRACE -> Level.FINEST; + case DEBUG -> Level.FINE; + case INFO -> Level.INFO; + case WARN -> Level.WARNING; + case ERROR -> Level.SEVERE; + }; + } + + @Override + public boolean isTraceEnabled() { + return wrapped.isLoggable(Level.FINEST); + } + + @Override + public boolean isDebugEnabled() { + return wrapped.isLoggable(Level.FINE); + } + + @Override + public boolean isInfoEnabled() { + return wrapped.isLoggable(Level.INFO); + } + + @Override + public boolean isWarnEnabled() { + return wrapped.isLoggable(Level.WARNING); + } + + @Override + public boolean isErrorEnabled() { + return wrapped.isLoggable(Level.SEVERE); + } + +} diff --git a/common/src/main/java/com/lishid/openinv/util/Permissions.java b/common/src/main/java/com/lishid/openinv/util/Permissions.java new file mode 100644 index 00000000..0f2fcd65 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/util/Permissions.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.util; + +import org.bukkit.permissions.Permissible; +import org.jetbrains.annotations.NotNull; + +public enum Permissions { + + INVENTORY_OPEN_SELF("inventory.open.self"), + INVENTORY_OPEN_OTHER("inventory.open.other"), + INVENTORY_EDIT_SELF("inventory.edit.self"), + INVENTORY_EDIT_OTHER("inventory.edit.other"), + INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"), + INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"), + INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"), + INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"), + INVENTORY_SLOT_DROP("inventory.slot.drop"), + + ENDERCHEST_OPEN_SELF("enderchest.open.self"), + ENDERCHEST_OPEN_OTHER("enderchest.open.other"), + ENDERCHEST_EDIT_SELF("enderchest.edit.self"), + ENDERCHEST_EDIT_OTHER("enderchest.edit.other"), + + CLEAR_SELF("clear.self"), + CLEAR_OTHER("clear.other"), + + ACCESS_CROSSWORLD("access.crossworld"), + ACCESS_OFFLINE("access.offline"), + ACCESS_ONLINE("access.online"), + ACCESS_EQUAL_EDIT("access.equal.edit"), + ACCESS_EQUAL_VIEW("access.equal.view"), + ACCESS_EQUAL_DENY("access.equal.deny"), + + SPECTATE_CLICK("spectate.click"), + + CONTAINER_ANY("container.any"), + CONTAINER_SILENT("container.silent"), + SEARCH_INVENTORY("search.inventory"), + SEARCH_CONTAINER("search.container"); + + private final String permission; + + Permissions(String permission) { + this.permission = "openinv." + permission; + } + + public boolean hasPermission(@NotNull Permissible permissible) { + return permissible.hasPermission(permission); + } + +} diff --git a/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java b/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java new file mode 100644 index 00000000..d22e6fcf --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/util/ReflectionHelper.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011-2021 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.util; + +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; + +/** + * A utility for making reflection easier. + */ +public final class ReflectionHelper { + + /** + * Grab an {@link Object} stored in a {@link Field} of another {@code Object}. + * + *

This casts the field to the correct class. Any issues will result in a {@code null} return value. + * + * @param fieldType the {@link Class} of {@code Object} stored in the {@code Field} + * @param holder the containing {@code Object} + * @param the type of stored {@code Object} + * @return the first matching {@code Object} or {@code null} if none match + */ + public static @Nullable T grabObjectByType(final Object holder, final Class fieldType) { + Field field = grabFieldByType(holder.getClass(), fieldType); + + if (field != null) { + try { + return fieldType.cast(field.get(holder)); + } catch (IllegalAccessException ignored) { + // Ignore issues obtaining field + } + } + + return null; + } + + /** + * Grab a {@link Field} of an {@link Object} + * + * @param fieldType the {@link Class} of the object + * @param holderType the containing {@code Class} + * @return the first matching object or {@code null} if none match + */ + public static @Nullable Field grabFieldByType(Class holderType, Class fieldType) { + for (Field field : holderType.getDeclaredFields()) { + if (fieldType.isAssignableFrom(field.getType())) { + field.setAccessible(true); + return field; + } + } + + if (holderType.getSuperclass() != null) { + return grabFieldByType(fieldType, holderType.getSuperclass()); + } + + return null; + } + + private ReflectionHelper() { + throw new IllegalStateException("Cannot create instance of utility class."); + } + +} diff --git a/common/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java b/common/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java new file mode 100644 index 00000000..01b22025 --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/util/lang/LanguageManager.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.util.lang; + +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; +import java.util.logging.Level; + +/** + * A simple language manager supporting both custom and bundled languages. + * + * @author Jikoo + */ +public class LanguageManager { + + private final Plugin plugin; + private final File folder; + private final String defaultLocale; + private final Map locales; + + public LanguageManager(@NotNull Plugin plugin, @NotNull String defaultLocale) { + this.plugin = plugin; + this.defaultLocale = defaultLocale; + this.locales = new HashMap<>(); + this.folder = new File(plugin.getDataFolder(), "locale"); + + if (!folder.exists() && !folder.mkdirs()) { + plugin.getLogger().warning(() -> "Unable to create " + folder.getPath() + "! Languages may not be editable."); + } + + reload(); + } + + public void reload() { + this.locales.clear(); + getOrLoadLocale(defaultLocale); + } + + private @NotNull YamlConfiguration getOrLoadLocale(@NotNull String locale) { + YamlConfiguration loaded = locales.get(locale); + if (loaded != null) { + return loaded; + } + + LangLocation lang = bestMatch(locale, null); + + // If a parent was a better match, check if it is already loaded. + if (!locale.equals(lang.locale)) { + loaded = locales.get(lang.locale); + if (loaded != null) { + locales.put(locale, loaded); + return loaded; + } + } + + // Load locale config from disk and bundled locale defaults. + YamlConfiguration localeConfig = loadLocale(lang); + + // If the locale is not the default locale, also handle any missing translations from the default locale. + if (!locale.equals(defaultLocale)) { + addTranslationFallthrough(lang, localeConfig); + + if (plugin.getConfig().getBoolean("settings.secret.warn-about-guess-section", true) + && localeConfig.isConfigurationSection("guess")) { + // Warn that guess section exists. This should run once per language per server restart + // when accessed by a user to hint to server owners that they can make UX improvements. + plugin.getLogger().info(() -> "[LanguageManager] Missing translations from " + lang.locale + ".yml! Check the guess section!"); + } + } + + locales.put(locale, localeConfig); + locales.put(lang.locale, localeConfig); + + return localeConfig; + } + + private @NotNull LangLocation bestMatch(@NotNull String locale, @Nullable LangLocation initial) { + File file = new File(folder, locale + ".yml"); + InputStream bundled = plugin.getResource("locale/" + locale + ".yml"); + + if (file.exists() || bundled != null) { + return new LangLocation(locale, file, bundled); + } + + if (initial == null) { + initial = new LangLocation(locale, file, null); + } + + int lastSeparator = locale.lastIndexOf('_'); + + // Must be at least some content before separator. + if (lastSeparator < 1) { + return initial; + } + + return bestMatch(locale.substring(0, lastSeparator), initial); + } + + private @NotNull YamlConfiguration loadLocale(@NotNull LangLocation lang) { + YamlConfiguration localeConfigDefaults; + if (lang.bundled == null) { + localeConfigDefaults = new YamlConfiguration(); + } else { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(lang.bundled, StandardCharsets.UTF_8))) { + localeConfigDefaults = YamlConfiguration.loadConfiguration(reader); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to load resource " + lang.locale + ".yml"); + localeConfigDefaults = new YamlConfiguration(); + } + } + + if (!lang.file.exists()) { + // If the file does not exist on disk, save bundled defaults. + try { + localeConfigDefaults.save(lang.file); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + lang.locale + ".yml"); + } + // Return loaded bundled locale. + return localeConfigDefaults; + } + + // If the file does exist on disk, load it. + YamlConfiguration localeConfig = YamlConfiguration.loadConfiguration(lang.file); + // Check for missing translations from the bundled file. + List newKeys = getMissingKeys(localeConfigDefaults, localeConfig::isSet); + + if (newKeys.isEmpty()) { + return localeConfig; + } + + // Get guess section for missing keys. + ConfigurationSection guess = localeConfig.getConfigurationSection("guess"); + + for (String newKey : newKeys) { + // Set all missing keys to defaults. + localeConfig.set(newKey, localeConfigDefaults.get(newKey)); + + // Delete relevant guess keys in case this is a new translation. + if (guess != null) { + guess.set(newKey, null); + } + } + + // If guess section is empty, delete it. + if (guess != null && guess.getKeys(false).isEmpty()) { + localeConfig.set("guess", null); + } + + plugin.getLogger().info(() -> "[LanguageManager] Added new translation keys to " + lang.locale + ".yml: " + String.join(", ", newKeys)); + + // Write new keys to disk. + try { + localeConfig.save(lang.file); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + lang.locale + ".yml"); + } + + return localeConfig; + } + + private void addTranslationFallthrough(@NotNull LangLocation location, @NotNull YamlConfiguration localeConfig) { + YamlConfiguration defaultLocaleConfig = locales.get(defaultLocale); + + // Get missing keys. Keys that already have a guess value are not new and don't need to trigger another write. + List missingKeys = getMissingKeys( + defaultLocaleConfig, + key -> localeConfig.isSet(key) || localeConfig.isSet("guess." + key) + ); + + if (!missingKeys.isEmpty()) { + // Set up guess section for missing keys. + for (String key : missingKeys) { + localeConfig.set("guess." + key, defaultLocaleConfig.get(key)); + } + + // Write modified guess section to disk. + try { + localeConfig.save(location.file); + } catch (IOException e) { + plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + location.locale + ".yml"); + } + } + + // Fall through to default locale. + localeConfig.setDefaults(defaultLocaleConfig); + } + + private @NotNull List getMissingKeys( + @NotNull Configuration configurationDefault, + @NotNull Predicate nodeSetPredicate + ) { + List missingKeys = new ArrayList<>(); + for (String key : configurationDefault.getKeys(true)) { + if (!configurationDefault.isConfigurationSection(key) && !nodeSetPredicate.test(key)) { + // Missing keys are non-section keys that fail the predicate. + missingKeys.add(key); + } + } + return missingKeys; + } + + public @Nullable String getValue(@NotNull String key, @Nullable String locale) { + String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase(Locale.ENGLISH)).getString(key); + if (value == null || value.isBlank()) { + return null; + } + + value = ChatColor.translateAlternateColorCodes('&', value); + + return value; + } + + public @Nullable String getValue( + @NotNull String key, + @Nullable String locale, + Replacement @NotNull ... replacements + ) { + String value = getValue(key, locale); + + if (value == null) { + return null; + } + + for (Replacement replacement : replacements) { + value = value.replace(replacement.placeholder(), replacement.value()); + } + + return value; + } + + public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key) { + return getValue(key, getLocale(sender)); + } + + public @Nullable String getLocalizedMessage( + @NotNull CommandSender sender, + @NotNull String key, + Replacement @NotNull ... replacements + ) { + return getValue(key, getLocale(sender), replacements); + } + + private @NotNull String getLocale(@NotNull CommandSender sender) { + if (sender instanceof Player player) { + return player.getLocale(); + } else { + return plugin.getConfig().getString("settings.locale", "en"); + } + } + + public void sendMessage(@NotNull CommandSender sender, @NotNull String key) { + String message = getLocalizedMessage(sender, key); + + if (message != null && !message.isEmpty()) { + sender.sendMessage(message); + } + } + + public void sendMessage(@NotNull CommandSender sender, @NotNull String key, Replacement @NotNull ... replacements) { + String message = getLocalizedMessage(sender, key, replacements); + + if (message != null && !message.isEmpty()) { + sender.sendMessage(message); + } + } + + public void sendSystemMessage(@NotNull Player player, @NotNull String key) { + String message = getLocalizedMessage(player, key); + + if (message == null) { + return; + } + + int newline = message.indexOf('\n'); + if (newline != -1) { + // No newlines in action bar chat. + message = message.substring(0, newline); + } + + if (message.isEmpty()) { + return; + } + + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacy(message)); + } + + private record LangLocation(@NotNull String locale, @NotNull File file, @Nullable InputStream bundled) {} + +} diff --git a/common/src/main/java/com/lishid/openinv/util/lang/Replacement.java b/common/src/main/java/com/lishid/openinv/util/lang/Replacement.java new file mode 100644 index 00000000..6753044c --- /dev/null +++ b/common/src/main/java/com/lishid/openinv/util/lang/Replacement.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.util.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * A data holder for string replacement in translations. + * + * @param placeholder the placeholder to be replaced + * @param value the value to insert + */ +public record Replacement(@NotNull String placeholder, @NotNull String value) { + +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..79c27754 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,9 @@ +# Project meta +group=com.lishid.openinv +version=5.2.1-SNAPSHOT +description=A Bukkit plugin for opening normally-inaccessible inventories. + +# Gradle configuration +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..ddfe1660 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,26 @@ +[versions] +spigotapi = "1.21.5-R0.1-SNAPSHOT" +specialsource = "1.11.5" +planarwrappers = "3.3.0" +annotations = "26.0.2-1" +paperweight = "2.0.0-beta.17" +shadow = "9.2.2" +folia-scheduler-wrapper = "v0.0.3" +errorprone-core = "2.45.0" +errorprone-gradle = "4.2.0" +slf4j = "2.0.17" + +[libraries] +spigotapi = { module = "org.spigotmc:spigot-api", version.ref = "spigotapi" } +specialsource = { module = "net.md-5:SpecialSource", version.ref = "specialsource" } +planarwrappers = { module = "com.github.jikoo:planarwrappers", version.ref = "planarwrappers" } +annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" } +folia-scheduler-wrapper = { module = "com.github.NahuLD.folia-scheduler-wrapper:folia-scheduler-wrapper", version.ref = "folia-scheduler-wrapper" } +errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone-core" } +errorprone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorprone-gradle" } +slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } + +[plugins] +paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } +errorprone-gradle = { id = "net.ltgt.errorprone", version.ref = "errorprone-gradle" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..a4b76b95 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ca025c83 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..f3b75f3b --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..9d21a218 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/internal/common/build.gradle.kts b/internal/common/build.gradle.kts new file mode 100644 index 00000000..71ca46d2 --- /dev/null +++ b/internal/common/build.gradle.kts @@ -0,0 +1,60 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +//tasks { +// withType { +// // OpenPlayer unchecked warning is due to superclass' messy inheritance and legacy methods. +// options.compilerArgs.add("-Xlint:unchecked") +// // PlayerManager uses "deprecated" method matching vanilla to support legacy save data. +// // While vanilla still feels that it is appropriate to use in the load process, we will too. +// options.compilerArgs.add("-Xlint:deprecation") +// } +//} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + + paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT") +} + +val spigot = tasks.register("spigotRelocations") { + dependsOn(tasks.jar) + from(sourceSets.main.get().output) + relocate("com.lishid.openinv.internal.common", "com.lishid.openinv.internal.reobf") + relocate("org.bukkit.craftbukkit", "org.bukkit.craftbukkit.${rootProject.extra["craftbukkitPackage"]}") + archiveClassifier = "spigot" +} + +configurations { + consumable("spigotRelocated") { + attributes { + attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY)) + attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME)) + attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.EXTERNAL)) + attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR)) + } + } +} + +artifacts { + add("spigotRelocated", spigot) +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/InternalAccessor.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/InternalAccessor.java new file mode 100644 index 00000000..16218663 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/InternalAccessor.java @@ -0,0 +1,80 @@ +package com.lishid.openinv.internal.common; + +import com.lishid.openinv.internal.Accessor; +import com.lishid.openinv.internal.IAnySilentContainer; +import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.common.container.AnySilentContainer; +import com.lishid.openinv.internal.common.container.OpenEnderChest; +import com.lishid.openinv.internal.common.container.OpenInventory; +import com.lishid.openinv.internal.common.container.slot.placeholder.PlaceholderLoader; +import com.lishid.openinv.internal.common.player.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import net.minecraft.world.Container; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalAccessor implements Accessor { + + protected final @NotNull Logger logger; + private final @NotNull PlayerManager manager; + private final @NotNull AnySilentContainer anySilentContainer; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + this.logger = logger; + manager = new PlayerManager(logger); + anySilentContainer = new AnySilentContainer(logger, lang); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public @NotNull IAnySilentContainer getAnySilentContainer() { + return anySilentContainer; + } + + @Override + public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) { + return new OpenInventory(player); + } + + @Override + public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) { + return new OpenEnderChest(player); + } + + @Override + public @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz) { + if (!(bukkitInventory instanceof CraftInventory craftInventory)) { + return null; + } + Container container = craftInventory.getInventory(); + if (clazz.isInstance(container)) { + return clazz.cast(container); + } + return null; + } + + @Override + public void reload(@NotNull ConfigurationSection config) { + ConfigurationSection placeholders = config.getConfigurationSection("placeholders"); + try { + // Reset placeholders to defaults and try to load configuration. + new PlaceholderLoader().load(placeholders); + } catch (Exception e) { + logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e); + } + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/AnySilentContainer.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/AnySilentContainer.java new file mode 100644 index 00000000..4ca16ad4 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/AnySilentContainer.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2011-2023 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.internal.common.container; + +import com.lishid.openinv.internal.AnySilentContainerBase; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.player.PlayerManager; +import com.lishid.openinv.util.ReflectionHelper; +import com.lishid.openinv.util.lang.LanguageManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.BarrelBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import net.minecraft.world.level.block.TrappedChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.logging.Logger; + +public class AnySilentContainer extends AnySilentContainerBase { + + private final @NotNull Logger logger; + private final @NotNull LanguageManager lang; + private @Nullable Field serverPlayerGameModeGameType; + + public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) { + this.logger = logger; + this.lang = lang; + try { + try { + this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("gameModeForPlayer"); + this.serverPlayerGameModeGameType.setAccessible(true); + } catch (NoSuchFieldException e) { + logger.warning("The field ServerPlayerGameMode#gameModeForPlayer is no longer present!"); + logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues"); + logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail."); + // N.B. gameModeForPlayer is (for now) declared before previousGameModeForPlayer so silent shouldn't break. + this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class); + } + } catch (SecurityException e) { + logger.warning("Unable to directly write player game mode! SilentContainer will fail."); + logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e); + } + } + + @Override + public boolean activateContainer( + @NotNull final Player bukkitPlayer, + final boolean silentchest, + @NotNull final org.bukkit.block.Block bukkitBlock + ) { + + // Silent ender chest is API-only + if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) { + bukkitPlayer.openInventory(bukkitPlayer.getEnderChest()); + bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); + return true; + } + + ServerPlayer player = PlayerManager.getHandle(bukkitPlayer); + + final net.minecraft.world.level.Level level = ((Entity) player).level(); + final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ()); + final BlockEntity blockEntity = level.getBlockEntity(blockPos); + + if (blockEntity == null) { + return false; + } + + if (blockEntity instanceof EnderChestBlockEntity enderChestTile) { + // Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock + PlayerEnderChestContainer enderChest = player.getEnderChestInventory(); + enderChest.setActiveChest(enderChestTile); + player.openMenu( + new SimpleMenuProvider( + (containerCounter, playerInventory, ignored) -> { + MenuType containers = OpenChestMenu.getChestMenuType(enderChest.getContainerSize()); + int rows = enderChest.getContainerSize() / 9; + return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows); + }, Component.translatable("container.enderchest") + ) + ); + bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); + return true; + } + + if (!(blockEntity instanceof MenuProvider menuProvider)) { + return false; + } + + BlockState blockState = level.getBlockState(blockPos); + Block block = blockState.getBlock(); + + if (block instanceof ChestBlock chestBlock) { + + // boolean flag: do not check if chest is blocked + menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true); + + if (menuProvider == null) { + lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); + return false; + } + + if (block instanceof TrappedChestBlock) { + bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED); + } else { + bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED); + } + } + + if (block instanceof ShulkerBoxBlock) { + bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED); + } + + if (block instanceof BarrelBlock) { + bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL); + } + + // AnyChest only - SilentChest not active, container unsupported, or unnecessary. + if (!silentchest || player.gameMode.getGameModeForPlayer() == GameType.SPECTATOR) { + player.openMenu(menuProvider); + return true; + } + + // SilentChest requires access to setting players' game mode directly. + if (this.serverPlayerGameModeGameType == null) { + return false; + } + + if (blockEntity instanceof RandomizableContainerBlockEntity lootable) { + if (lootable.lootTable != null) { + lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); + return false; + } + } + + GameType gameType = player.gameMode.getGameModeForPlayer(); + this.forceGameType(player, GameType.SPECTATOR); + player.openMenu(menuProvider); + this.forceGameType(player, gameType); + return true; + } + + @Override + public void deactivateContainer(@NotNull final Player bukkitPlayer) { + if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) { + return; + } + + ServerPlayer player = PlayerManager.getHandle(bukkitPlayer); + + // Force game mode change without informing plugins or players. + // Regular game mode set calls GameModeChangeEvent and is cancellable. + GameType gameType = player.gameMode.getGameModeForPlayer(); + this.forceGameType(player, GameType.SPECTATOR); + + // ServerPlayer#closeContainer cannot be called without entering an + // infinite loop because this method is called during inventory close. + // From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent + player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity()); + // From ServerPlayer#closeContainer + player.doCloseContainer(); + // Regular inventory close will handle the rest - packet sending, etc. + + // Revert forced game mode. + this.forceGameType(player, gameType); + } + + private void forceGameType(final ServerPlayer player, final GameType gameMode) { + if (this.serverPlayerGameModeGameType == null) { + // No need to warn repeatedly, error on startup and lack of function should be enough. + return; + } + try { + this.serverPlayerGameModeGameType.setAccessible(true); + this.serverPlayerGameModeGameType.set(player.gameMode, gameMode); + } catch (IllegalArgumentException | IllegalAccessException e) { + logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e); + } + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/BaseOpenInventory.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/BaseOpenInventory.java new file mode 100644 index 00000000..4987afd3 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/BaseOpenInventory.java @@ -0,0 +1,335 @@ +package com.lishid.openinv.internal.common.container; + +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.InternalOwned; +import com.lishid.openinv.internal.common.container.bukkit.OpenPlayerInventory; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.container.menu.OpenInventoryMenu; +import com.lishid.openinv.internal.common.container.slot.Content; +import com.lishid.openinv.internal.common.container.slot.ContentCrafting; +import com.lishid.openinv.internal.common.container.slot.ContentCraftingResult; +import com.lishid.openinv.internal.common.container.slot.ContentCursor; +import com.lishid.openinv.internal.common.container.slot.ContentDrop; +import com.lishid.openinv.internal.common.container.slot.ContentEquipment; +import com.lishid.openinv.internal.common.container.slot.ContentList; +import com.lishid.openinv.internal.common.container.slot.ContentOffHand; +import com.lishid.openinv.internal.common.container.slot.ContentViewOnly; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import com.lishid.openinv.internal.common.player.PlayerManager; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.Location; +import org.bukkit.craftbukkit.entity.CraftHumanEntity; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public abstract class BaseOpenInventory implements Container, InternalOwned, ISpecialPlayerInventory { + + protected final List slots; + private final int size; + protected ServerPlayer owner; + private int maxStackSize = 99; + protected CraftInventory bukkitEntity; + public List transaction = new ArrayList<>(); + + public BaseOpenInventory(@NotNull org.bukkit.entity.Player bukkitPlayer) { + owner = PlayerManager.getHandle(bukkitPlayer); + + // Get total size, rounding up to nearest 9 for client compatibility. + int rawSize = owner.getInventory().getContainerSize() + owner.inventoryMenu.getCraftSlots().getContainerSize() + 1; + size = ((int) Math.ceil(rawSize / 9.0)) * 9; + + slots = NonNullList.withSize(size, new ContentViewOnly(owner)); + setupSlots(); + } + + protected void setupSlots() { + // Top of inventory: Regular contents. + int nextIndex = addMainInventory(); + + // If inventory is expected size, we can arrange slots to be pretty. + Inventory ownerInv = owner.getInventory(); + if (ownerInv.getNonEquipmentItems().size() == 36 + && owner.inventoryMenu.getCraftSlots().getContainerSize() == 4 + && (Inventory.EQUIPMENT_SLOT_MAPPING.size() == 5 || Inventory.EQUIPMENT_SLOT_MAPPING.size() == 7)) { + // Armor slots: Bottom left. + addArmor(36); + // Off-hand: Below chestplate. + addOffHand(46); + // Drop slot: Bottom right. + slots.set(53, new ContentDrop(owner)); + // Cursor slot: Above drop. + slots.set(44, new ContentCursor(owner)); + + // Crafting is displayed in the bottom right corner. + // As we're using the pretty view, this is a 3x2. + addCrafting(41, true); + return; + } + + // Otherwise we'll just add elements linearly. + nextIndex = addArmor(nextIndex); + nextIndex = addOffHand(nextIndex); + nextIndex = addCrafting(nextIndex, false); + slots.set(nextIndex, new ContentCursor(owner)); + // Drop slot last. + slots.set(slots.size() - 1, new ContentDrop(owner)); + } + + private int addMainInventory() { + int listSize = owner.getInventory().getNonEquipmentItems().size(); + // Hotbar slots are 0-8. We want those to appear on the bottom of the inventory like a normal player inventory, + // so everything else needs to move up a row. + int hotbarDiff = listSize - 9; + for (int localIndex = 0; localIndex < listSize; ++localIndex) { + InventoryType.SlotType type; + int invIndex; + if (localIndex < hotbarDiff) { + invIndex = localIndex + 9; + type = InventoryType.SlotType.CONTAINER; + } else { + type = InventoryType.SlotType.QUICKBAR; + invIndex = localIndex - hotbarDiff; + } + + slots.set( + localIndex, + new ContentList(owner, invIndex, type) { + @Override + public void setHolder(@NotNull ServerPlayer holder) { + items = holder.getInventory().getNonEquipmentItems(); + } + } + ); + } + return listSize; + } + + private int addArmor(int startIndex) { + // Armor slots go bottom to top; boots are first and helmet is last. + // Since we have to display horizontally due to space restrictions, + // making the left side the "top" is more user-friendly. + EquipmentSlot[] sorted = Inventory.EQUIPMENT_SLOT_MAPPING.int2ObjectEntrySet() + .stream() + .sorted(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey)) + .map(Map.Entry::getValue) + .toArray(EquipmentSlot[]::new); + int localIndex = 0; + for (int i = sorted.length - 1; i >= 0; --i) { + // Skip off-hand, handled separately. Also skip non-player slots. + if (sorted[i].getType() != EquipmentSlot.Type.HUMANOID_ARMOR) { + continue; + } + + slots.set(startIndex + localIndex, new ContentEquipment(owner, sorted[i])); + ++localIndex; + } + + return startIndex + localIndex; + } + + private int addOffHand(int startIndex) { + // No off-hand? + if (!Inventory.EQUIPMENT_SLOT_MAPPING.containsValue(EquipmentSlot.OFFHAND)) { + return startIndex; + } + + slots.set(startIndex, new ContentOffHand(owner)); + return startIndex + 1; + } + + private int addCrafting(int startIndex, boolean pretty) { + int listSize = owner.inventoryMenu.getCraftSlots().getContents().size(); + pretty &= listSize == 4; + + for (int localIndex = 0; localIndex < listSize; ++localIndex) { + // Pretty display is a 2x2 rather than linear. + // If index is in top row, grid is not 2x2, or pretty is disabled, just use current index. + // Otherwise, subtract 2 and add 9 to start in the same position on the next row. + int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7); + + slots.set(modIndex, new ContentCrafting(owner, localIndex)); + } + + if (pretty) { + slots.set(startIndex + 2, new ContentViewOnly(owner) { + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotViewOnly(container, slot, x, y) { + @Override + public ItemStack getOrDefault() { + return Placeholders.craftingOutput; + } + }; + } + } + ); + slots.set(startIndex + 11, getCraftingResult(owner)); + } + + return startIndex + listSize; + } + + protected Content getCraftingResult(@NotNull ServerPlayer serverPlayer) { + return new ContentCraftingResult(serverPlayer); + } + + public Slot getMenuSlot(int index, int x, int y) { + return slots.get(index).asSlot(this, index, x, y); + } + + public InventoryType.SlotType getSlotType(int index) { + return slots.get(index).getSlotType(); + } + + public abstract Component getTitle(ServerPlayer player, @Nullable OpenChestMenu menu); + + @Override + public ServerPlayer getOwnerHandle() { + return owner; + } + + @Override + public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() { + if (bukkitEntity == null) { + bukkitEntity = new OpenPlayerInventory(this); + } + return bukkitEntity; + } + + @Override + public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) { + ServerPlayer newOwner = PlayerManager.getHandle(player); + // Only transfer regular inventory - crafting and cursor slots are transient. + newOwner.getInventory().replaceWith(owner.getInventory()); + owner = newOwner; + // Update slots to point to new inventory. + slots.forEach(slot -> slot.setHolder(newOwner)); + } + + @Override + public boolean isInUse() { + return !transaction.isEmpty(); + } + + @Override + public @NotNull org.bukkit.entity.Player getPlayer() { + return getOwner(); + } + + @Override + public int getContainerSize() { + return size; + } + + @Override + public boolean isEmpty() { + return slots.stream().map(Content::get).allMatch(ItemStack::isEmpty); + } + + @Override + public @NotNull ItemStack getItem(int index) { + return slots.get(index).get(); + } + + @Override + public @NotNull ItemStack removeItem(int index, int amount) { + return slots.get(index).removePartial(amount); + } + + @Override + public @NotNull ItemStack removeItemNoUpdate(int index) { + return slots.get(index).remove(); + } + + @Override + public void setItem(int index, @NotNull ItemStack itemStack) { + slots.get(index).set(itemStack); + } + + @Override + public int getMaxStackSize() { + return maxStackSize; + } + + @Override + public void setMaxStackSize(int maxStackSize) { + this.maxStackSize = maxStackSize; + } + + @Override + public void setChanged() { + } + + @Override + public boolean stillValid(@NotNull Player player) { + return true; + } + + @Override + public @NotNull List getContents() { + NonNullList contents = NonNullList.withSize(getContainerSize(), ItemStack.EMPTY); + for (int i = 0; i < getContainerSize(); ++i) { + contents.set(i, getItem(i)); + } + return contents; + } + + @Override + public void onOpen(@NotNull CraftHumanEntity viewer) { + transaction.add(viewer); + } + + @Override + public void onClose(@NotNull CraftHumanEntity viewer) { + transaction.remove(viewer); + } + + @Override + public @NotNull List getViewers() { + return transaction; + } + + @Override + public @NotNull org.bukkit.entity.Player getOwner() { + return owner.getBukkitEntity(); + } + + @Override + public Location getLocation() { + return owner.getBukkitEntity().getLocation(); + } + + @Override + public void clearContent() { + owner.getInventory().clearContent(); + owner.inventoryMenu.getCraftSlots().clearContent(); + owner.inventoryMenu.slotsChanged(owner.inventoryMenu.getCraftSlots()); + owner.containerMenu.setCarried(ItemStack.EMPTY); + } + + public @Nullable OpenChestMenu createMenu(Player player, int i, boolean viewOnly) { + if (player instanceof ServerPlayer serverPlayer) { + return new OpenInventoryMenu(this, serverPlayer, i, viewOnly); + } + return null; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenEnderChest.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenEnderChest.java new file mode 100644 index 00000000..284bf7ab --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenEnderChest.java @@ -0,0 +1,201 @@ +package com.lishid.openinv.internal.common.container; + +import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.InternalOwned; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.container.menu.OpenEnderChestMenu; +import com.lishid.openinv.internal.common.player.PlayerManager; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.StackedItemContents; +import net.minecraft.world.inventory.StackedContentsCompatible; +import net.minecraft.world.item.ItemStack; +import org.bukkit.Location; +import org.bukkit.craftbukkit.entity.CraftHumanEntity; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class OpenEnderChest implements Container, StackedContentsCompatible, InternalOwned, + ISpecialEnderChest { + + private CraftInventory inventory; + private @NotNull ServerPlayer owner; + private NonNullList items; + private int maxStack = 64; + private final List transaction = new ArrayList<>(); + + public OpenEnderChest(@NotNull org.bukkit.entity.Player player) { + this.owner = PlayerManager.getHandle(player); + this.items = owner.getEnderChestInventory().items; + } + + @Override + public @NotNull ServerPlayer getOwnerHandle() { + return owner; + } + + @Override + public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() { + if (inventory == null) { + inventory = new CraftInventory(this) { + @Override + public @NotNull InventoryType getType() { + return InventoryType.ENDER_CHEST; + } + }; + } + return inventory; + } + + @Override + public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) { + owner = PlayerManager.getHandle(player); + NonNullList activeItems = owner.getEnderChestInventory().items; + + // Guard against size changing. Theoretically on Purpur all row variations still have 6 rows internally. + int max = Math.min(items.size(), activeItems.size()); + for (int index = 0; index < max; ++index) { + activeItems.set(index, items.get(index)); + } + + items = activeItems; + } + + @Override + public @NotNull org.bukkit.entity.Player getPlayer() { + return owner.getBukkitEntity(); + } + + @Override + public int getContainerSize() { + return items.size(); + } + + @Override + public boolean isEmpty() { + return items.stream().allMatch(ItemStack::isEmpty); + } + + @Override + public @NotNull ItemStack getItem(int index) { + return index >= 0 && index < items.size() ? items.get(index) : ItemStack.EMPTY; + } + + @Override + public @NotNull ItemStack removeItem(int index, int amount) { + ItemStack itemstack = ContainerHelper.removeItem(items, index, amount); + + if (!itemstack.isEmpty()) { + setChanged(); + } + + return itemstack; + } + + @Override + public @NotNull ItemStack removeItemNoUpdate(int index) { + return index >= 0 && index < items.size() ? items.set(index, ItemStack.EMPTY) : ItemStack.EMPTY; + } + + @Override + public void setItem(int index, @NotNull ItemStack itemStack) { + if (index >= 0 && index < items.size()) { + items.set(index, itemStack); + } + } + + @Override + public int getMaxStackSize() { + return maxStack; + } + + @Override + public void setChanged() { + this.owner.getEnderChestInventory().setChanged(); + } + + @Override + public boolean stillValid(@NotNull Player player) { + return true; + } + + @Override + public @NotNull List getContents() { + return items; + } + + @Override + public void onOpen(@NotNull CraftHumanEntity craftHumanEntity) { + transaction.add(craftHumanEntity); + } + + @Override + public void onClose(@NotNull CraftHumanEntity craftHumanEntity) { + transaction.remove(craftHumanEntity); + } + + @Override + public @NotNull List getViewers() { + return transaction; + } + + @Override + public org.bukkit.entity.Player getOwner() { + return getPlayer(); + } + + @Override + public void setMaxStackSize(int size) { + maxStack = size; + } + + @Override + public @Nullable Location getLocation() { + return null; + } + + @Override + public void clearContent() { + items.clear(); + setChanged(); + } + + @Override + public void fillStackedContents(@NotNull StackedItemContents stackedContents) { + for (ItemStack itemstack : items) { + stackedContents.accountStack(itemstack); + } + } + + public Component getTitle(@Nullable OpenChestMenu menu) { + MutableComponent component; + if (menu != null && menu.isViewOnly()) { + component = Component.translatableWithFallback("openinv.container.enderchest.viewonly", "[RO] "); + } else { + component = Component.translatableWithFallback("openinv.container.enderchest.editable", ""); + } + return component + .append(Component.translatableWithFallback("openinv.container.enderchest.prefix", "", owner.getName())) + .append(Component.translatable("container.enderchest")) + .append(Component.translatableWithFallback("openinv.container.enderchest.suffix", " - %s", owner.getName())); + } + + public @Nullable OpenChestMenu createMenu(Player player, int i, boolean viewOnly) { + if (player instanceof ServerPlayer serverPlayer) { + return new OpenEnderChestMenu(this, serverPlayer, i, viewOnly); + } + return null; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenInventory.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenInventory.java new file mode 100644 index 00000000..641b4d54 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/OpenInventory.java @@ -0,0 +1,49 @@ +package com.lishid.openinv.internal.common.container; + +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FontDescription; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventory extends BaseOpenInventory { + + public OpenInventory(@NotNull Player bukkitPlayer) { + super(bukkitPlayer); + } + + @Override + public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu menu) { + MutableComponent component = Component.empty(); + // Prefix for use with custom bitmap image fonts. + if (owner.equals(viewer)) { + component.append( + Component.translatableWithFallback("openinv.container.inventory.self", "") + .withStyle(style -> style + .withFont(new FontDescription.Resource(Identifier.parse("openinv:font/inventory"))) + .withColor(ChatFormatting.WHITE))); + } else { + component.append( + Component.translatableWithFallback("openinv.container.inventory.other", "") + .withStyle(style -> style + .withFont(new FontDescription.Resource(Identifier.parse("openinv:font/inventory"))) + .withColor(ChatFormatting.WHITE))); + } + if (menu != null && menu.isViewOnly()) { + component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] ")); + } else { + component.append(Component.translatableWithFallback("openinv.container.inventory.editable", "")); + } + // Normal title: "Inventory - OwnerName" + component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName())) + .append(Component.translatable("container.inventory")) + .append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName())); + return component; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyInventory.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyInventory.java new file mode 100644 index 00000000..e505c8c1 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyInventory.java @@ -0,0 +1,173 @@ +package com.lishid.openinv.internal.common.container.bukkit; + +import com.lishid.openinv.internal.ViewOnly; +import net.minecraft.world.Container; +import org.bukkit.Material; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.ListIterator; + +/** + * A locked down "empty" inventory that rejects plugin interaction. + */ +public class OpenDummyInventory extends CraftInventory implements ViewOnly { + + private final InventoryType type; + + public OpenDummyInventory(Container inventory, InventoryType type) { + super(inventory); + this.type = type; + } + + @Override + public @NotNull InventoryType getType() { + return type; + } + + @Override + public @Nullable ItemStack getItem(int index) { + return null; + } + + @Override + public void setItem(int index, @Nullable ItemStack item) { + + } + + @SuppressWarnings("NonApiType") + @Override + public @NotNull HashMap addItem(@NotNull ItemStack... items) throws IllegalArgumentException { + return arrayToHashMap(items); + } + + @SuppressWarnings("NonApiType") + @Override + public @NotNull HashMap removeItem(@NotNull ItemStack... items) throws IllegalArgumentException { + return arrayToHashMap(items); + } + + @SuppressWarnings("NonApiType") + private static @NotNull HashMap arrayToHashMap(@NotNull ItemStack[] items) { + HashMap ignored = new HashMap<>(); + for (int index = 0; index < items.length; ++index) { + ignored.put(index, items[index]); + } + return ignored; + } + + @Override + public ItemStack @NotNull [] getContents() { + return new ItemStack[getSize()]; + } + + @Override + public void setContents(@NotNull ItemStack[] items) throws IllegalArgumentException { + + } + + @Override + public @NotNull ItemStack @NotNull [] getStorageContents() { + return new ItemStack[getSize()]; + } + + @Override + public void setStorageContents(@NotNull ItemStack[] items) throws IllegalArgumentException { + + } + + @Override + public boolean contains(@NotNull Material material) throws IllegalArgumentException { + return false; + } + + @Override + public boolean contains(@Nullable ItemStack item) { + return false; + } + + @Override + public boolean contains(@NotNull Material material, int amount) throws IllegalArgumentException { + return false; + } + + @Override + public boolean contains(@Nullable ItemStack item, int amount) { + return false; + } + + @Override + public boolean containsAtLeast(@Nullable ItemStack item, int amount) { + return false; + } + + @SuppressWarnings("NonApiType") + @Override + public @NotNull HashMap all( + @NotNull Material material + ) throws IllegalArgumentException { + return new HashMap<>(); + } + + @SuppressWarnings("NonApiType") + @Override + public @NotNull HashMap all(@Nullable ItemStack item) { + return new HashMap<>(); + } + + @Override + public int first(@NotNull Material material) throws IllegalArgumentException { + return -1; + } + + @Override + public int first(@NotNull ItemStack item) { + return -1; + } + + @Override + public int firstEmpty() { + return -1; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void remove(@NotNull Material material) throws IllegalArgumentException { + + } + + @Override + public void remove(@NotNull ItemStack item) { + + } + + @Override + public void clear(int index) { + + } + + @Override + public void clear() { + + } + + @Override + public @NotNull ListIterator iterator() { + return Collections.emptyListIterator(); + } + + @Override + public @NotNull ListIterator iterator(int index) { + return Collections.emptyListIterator(); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyPlayerInventory.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyPlayerInventory.java new file mode 100644 index 00000000..2c482710 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenDummyPlayerInventory.java @@ -0,0 +1,137 @@ +package com.lishid.openinv.internal.common.container.bukkit; + +import net.minecraft.world.Container; +import org.bukkit.Material; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenDummyPlayerInventory extends OpenDummyInventory implements PlayerInventory { + + public OpenDummyPlayerInventory(Container inventory) { + super(inventory, InventoryType.PLAYER); + } + + @Override + public HumanEntity getHolder() { + return (HumanEntity) super.getHolder(); + } + + @Override + public @NotNull ItemStack @NotNull [] getArmorContents() { + return new ItemStack[4]; + } + + @Override + public @NotNull ItemStack @NotNull [] getExtraContents() { + return new ItemStack[4]; + } + + @Override + public @Nullable ItemStack getHelmet() { + return null; + } + + @Override + public @Nullable ItemStack getChestplate() { + return null; + } + + @Override + public @Nullable ItemStack getLeggings() { + return null; + } + + @Override + public @Nullable ItemStack getBoots() { + return null; + } + + @Override + public void setItem(@NotNull EquipmentSlot slot, @Nullable ItemStack item) { + + } + + @Override + public @NotNull ItemStack getItem(@NotNull EquipmentSlot slot) { + return new ItemStack(Material.AIR); + } + + @Override + public void setArmorContents(ItemStack @NotNull [] items) { + + } + + @Override + public void setExtraContents(ItemStack @NotNull [] items) { + + } + + @Override + public void setHelmet(@Nullable ItemStack helmet) { + + } + + @Override + public void setChestplate(@Nullable ItemStack chestplate) { + + } + + @Override + public void setLeggings(@Nullable ItemStack leggings) { + + } + + @Override + public void setBoots(@Nullable ItemStack boots) { + + } + + @Override + public @NotNull ItemStack getItemInMainHand() { + return new ItemStack(Material.AIR); + } + + @Override + public void setItemInMainHand(@Nullable ItemStack item) { + + } + + @Override + public @NotNull ItemStack getItemInOffHand() { + return new ItemStack(Material.AIR); + } + + @Override + public void setItemInOffHand(@Nullable ItemStack item) { + + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public @NotNull ItemStack getItemInHand() { + return new ItemStack(Material.AIR); + } + + @Deprecated + @Override + public void setItemInHand(@Nullable ItemStack stack) { + + } + + @Override + public int getHeldItemSlot() { + return 0; + } + + @Override + public void setHeldItemSlot(int slot) { + + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventory.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventory.java new file mode 100644 index 00000000..9ef9a468 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventory.java @@ -0,0 +1,229 @@ +package com.lishid.openinv.internal.common.container.bukkit; + +import com.google.common.base.Preconditions; +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class OpenPlayerInventory extends CraftInventory implements PlayerInventory { + + public OpenPlayerInventory(@NotNull BaseOpenInventory inventory) { + super(inventory); + } + + @Override + public @NotNull BaseOpenInventory getInventory() { + return (BaseOpenInventory) super.getInventory(); + } + + @Override + public ItemStack @NotNull [] getContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents()); + } + + @Override + public void setContents(ItemStack[] items) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + int size = internal.getContainerSize(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + + for (int index = 0; index < size; ++index) { + if (index < items.length) { + internal.setItem(index, CraftItemStack.asNMSCopy(items[index])); + } else { + internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + + @Override + public ItemStack @NotNull [] getStorageContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getNonEquipmentItems()); + } + + @Override + public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { + NonNullList list = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems(); + int size = list.size(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + list.set(index, CraftItemStack.asNMSCopy(items[index])); + } + } + + @Override + public @NotNull InventoryType getType() { + return InventoryType.PLAYER; + } + + @Override + public @NotNull Player getHolder() { + return getInventory().getOwner(); + } + + @Override + public @NotNull ItemStack @NotNull [] getArmorContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getArmorContents()); + } + + @Override + public void setArmorContents(ItemStack @NotNull [] items) { + int size = Inventory.EQUIPMENT_SLOTS_SORTED_BY_INDEX.length; + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + getInventory().getOwnerHandle().getInventory().equipment.set( + Inventory.EQUIPMENT_SLOTS_SORTED_BY_INDEX[index], + CraftItemStack.asNMSCopy(items[index]) + ); + } + } + + @Override + public @NotNull ItemStack @NotNull [] getExtraContents() { + return asCraftMirror(List.of(getInventory().getOwnerHandle().getInventory().equipment.get(EquipmentSlot.OFFHAND))); + } + + @Override + public void setExtraContents(ItemStack @NotNull [] items) { + Preconditions.checkArgument(items.length <= 1, "items.length must be <= 1"); + for (ItemStack item : items) { + getInventory().getOwnerHandle().getInventory().equipment.set(EquipmentSlot.OFFHAND, CraftItemStack.asNMSCopy(item)); + } + } + + @Override + public @NotNull ItemStack getHelmet() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().equipment + .get(EquipmentSlot.HEAD)); + } + + @Override + public void setHelmet(@Nullable ItemStack helmet) { + getInventory().getOwnerHandle().getInventory().equipment + .set(EquipmentSlot.HEAD, CraftItemStack.asNMSCopy(helmet)); + } + + @Override + public @NotNull ItemStack getChestplate() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().equipment + .get(EquipmentSlot.CHEST)); + } + + @Override + public void setChestplate(@Nullable ItemStack chestplate) { + getInventory().getOwnerHandle().getInventory().equipment + .set(EquipmentSlot.CHEST, CraftItemStack.asNMSCopy(chestplate)); + } + + @Override + public @NotNull ItemStack getLeggings() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().equipment + .get(EquipmentSlot.LEGS)); + } + + @Override + public void setLeggings(@Nullable ItemStack leggings) { + getInventory().getOwnerHandle().getInventory().equipment + .set(EquipmentSlot.LEGS, CraftItemStack.asNMSCopy(leggings)); + } + + @Override + public @NotNull ItemStack getBoots() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().equipment + .get(EquipmentSlot.FEET)); + } + + @Override + public void setBoots(@Nullable ItemStack boots) { + getInventory().getOwnerHandle().getInventory().equipment + .set(EquipmentSlot.FEET, CraftItemStack.asNMSCopy(boots)); + } + + @Override + public @NotNull ItemStack getItemInMainHand() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return CraftItemStack.asCraftMirror(internal.getSelectedItem()); + } + + @Override + public void setItemInMainHand(@Nullable ItemStack item) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + internal.setSelectedItem(CraftItemStack.asNMSCopy(item)); + } + + @Override + public @NotNull ItemStack getItemInOffHand() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().equipment + .get(EquipmentSlot.OFFHAND)); + } + + @Override + public void setItemInOffHand(@Nullable ItemStack item) { + getInventory().getOwnerHandle().getInventory().equipment + .set(EquipmentSlot.OFFHAND, CraftItemStack.asNMSCopy(item)); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public @NotNull ItemStack getItemInHand() { + return getItemInMainHand(); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public void setItemInHand(@Nullable ItemStack stack) { + setItemInMainHand(stack); + } + + @Override + public int getHeldItemSlot() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return internal.getNonEquipmentItems().size() - 9 + internal.getSelectedSlot(); + } + + @Override + public void setHeldItemSlot(int slot) { + slot %= 9; + getInventory().getOwnerHandle().getInventory().setSelectedSlot(slot); + } + + @Override + public @NotNull ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) { + return switch (slot) { + case HAND -> getItemInMainHand(); + case OFF_HAND -> getItemInOffHand(); + case FEET -> getBoots(); + case LEGS -> getLeggings(); + case CHEST -> getChestplate(); + case HEAD -> getHelmet(); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + }; + } + + @Override + public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) { + switch (slot) { + case HAND -> setItemInMainHand(item); + case OFF_HAND -> setItemInOffHand(item); + case FEET -> setBoots(item); + case LEGS -> setLeggings(item); + case CHEST -> setChestplate(item); + case HEAD -> setHelmet(item); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + } + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventorySelf.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventorySelf.java new file mode 100644 index 00000000..326a6f51 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/bukkit/OpenPlayerInventorySelf.java @@ -0,0 +1,26 @@ +package com.lishid.openinv.internal.common.container.bukkit; + +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class OpenPlayerInventorySelf extends OpenPlayerInventory { + + private final int offset; + + public OpenPlayerInventorySelf(@NotNull BaseOpenInventory inventory, int offset) { + super(inventory); + this.offset = offset; + } + + @Override + public ItemStack getItem(int index) { + return super.getItem(offset + index); + } + + @Override + public void setItem(int index, ItemStack item) { + super.setItem(offset + index, item); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenChestMenu.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenChestMenu.java new file mode 100644 index 00000000..939ecd1b --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenChestMenu.java @@ -0,0 +1,288 @@ +package com.lishid.openinv.internal.common.container.menu; + +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.InternalOwned; +import com.lishid.openinv.internal.common.container.bukkit.OpenDummyInventory; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.craftbukkit.inventory.CraftInventoryView; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An extension of {@link AbstractContainerMenu} storing and managing data common to all special inventories. + */ +public abstract class OpenChestMenu> + extends AbstractContainerMenu { + + protected static final int BOTTOM_INVENTORY_SIZE = 36; + + protected final T container; + protected final ServerPlayer viewer; + protected final boolean viewOnly; + protected final boolean ownContainer; + protected final int topSize; + private CraftInventoryView, Inventory> bukkitEntity; + + protected OpenChestMenu( + @NotNull MenuType type, + int containerCounter, + @NotNull T container, + @NotNull ServerPlayer viewer, + boolean viewOnly + ) { + super(type, containerCounter); + this.container = container; + this.viewer = viewer; + this.viewOnly = viewOnly; + ownContainer = container.getOwnerHandle().equals(viewer); + topSize = getTopSize(viewer); + + preSlotSetup(); + + int upperRows = topSize / 9; + // View's upper inventory - our container + for (int row = 0; row < upperRows; ++row) { + for (int col = 0; col < 9; ++col) { + // x and y for client purposes, but hey, we're thorough here. + // Adapted from net.minecraft.world.inventory.ChestMenu + int x = 8 + col * 18; + int y = 18 + row * 18; + int index = row * 9 + col; + + // Guard against weird inventory sizes. + if (index >= container.getContainerSize()) { + addSlot(new SlotViewOnly(container, index, x, y)); + continue; + } + + Slot slot = getUpperSlot(index, x, y); + + addSlot(slot); + } + } + + // View's lower inventory - viewer inventory + int playerInvPad = (upperRows - 4) * 18; + for (int row = 0; row < 3; ++row) { + for (int col = 0; col < 9; ++col) { + int x = 8 + col * 18; + int y = playerInvPad + row * 18 + 103; + addSlot(new Slot(viewer.getInventory(), row * 9 + col + 9, x, y)); + } + } + // Hotbar + for (int col = 0; col < 9; ++col) { + int x = 8 + col * 18; + int y = playerInvPad + 161; + addSlot(new Slot(viewer.getInventory(), col, x, y)); + } + } + + public static @NotNull MenuType getChestMenuType(int inventorySize) { + inventorySize = ((int) Math.ceil(inventorySize / 9.0)) * 9; + return switch (inventorySize) { + case 9 -> MenuType.GENERIC_9x1; + case 18 -> MenuType.GENERIC_9x2; + case 27 -> MenuType.GENERIC_9x3; + case 36 -> MenuType.GENERIC_9x4; + case 45 -> MenuType.GENERIC_9x5; + case 54 -> MenuType.GENERIC_9x6; + default -> throw new IllegalArgumentException("Inventory size unsupported: " + inventorySize); + }; + } + + + protected void preSlotSetup() { + } + + protected @NotNull Slot getUpperSlot(int index, int x, int y) { + Slot slot = new Slot(container, index, x, y); + if (viewOnly) { + return SlotViewOnly.wrap(slot); + } + return slot; + } + + public boolean isViewOnly() { + return viewOnly; + } + + @Override + public final @NotNull CraftInventoryView, Inventory> getBukkitView() { + if (bukkitEntity == null) { + bukkitEntity = createBukkitEntity(); + } + + return bukkitEntity; + } + + protected @NotNull CraftInventoryView, Inventory> createBukkitEntity() { + Inventory top; + if (viewOnly) { + top = new OpenDummyInventory(container, container.getBukkitType()); + } else { + top = container.getBukkitInventory(); + } + return new CraftInventoryView<>(viewer.getBukkitEntity(), top, this) { + @Override + public @Nullable Inventory getInventory(int rawSlot) { + if (viewOnly) { + return null; + } + return super.getInventory(rawSlot); + } + + @Override + public int convertSlot(int rawSlot) { + if (viewOnly) { + return InventoryView.OUTSIDE; + } + return super.convertSlot(rawSlot); + } + + @Override + public @NotNull InventoryType.SlotType getSlotType(int slot) { + if (viewOnly) { + return InventoryType.SlotType.OUTSIDE; + } + return super.getSlotType(slot); + } + }; + } + + private int getTopSize(ServerPlayer viewer) { + MenuType menuType = getType(); + if (menuType == MenuType.GENERIC_9x1) { + return 9; + } else if (menuType == MenuType.GENERIC_9x2) { + return 18; + } else if (menuType == MenuType.GENERIC_9x3) { + return 27; + } else if (menuType == MenuType.GENERIC_9x4) { + return 36; + } else if (menuType == MenuType.GENERIC_9x5) { + return 45; + } else if (menuType == MenuType.GENERIC_9x6) { + return 54; + } + // This is a bit gross, but allows us a safe fallthrough. + return menuType.create(-1, viewer.getInventory()).slots.size() - BOTTOM_INVENTORY_SIZE; + } + + /** + * Reimplementation of {@link AbstractContainerMenu#moveItemStackTo(ItemStack, int, int, boolean)} that ignores fake + * slots and respects {@link Slot#hasItem()}. + * + * @param itemStack the stack to quick-move + * @param rangeLow the start of the range of slots that can be moved to, inclusive + * @param rangeHigh the end of the range of slots that can be moved to, exclusive + * @param topDown whether to start at the top of the range or bottom + * @return whether the stack was modified as a result of being quick-moved + */ + @Override + protected boolean moveItemStackTo(ItemStack itemStack, int rangeLow, int rangeHigh, boolean topDown) { + boolean modified = false; + boolean stackable = itemStack.isStackable(); + Slot firstEmpty = null; + + for (int index = topDown ? rangeHigh - 1 : rangeLow; + !itemStack.isEmpty() && (topDown ? index >= rangeLow : index < rangeHigh); + index += topDown ? -1 : 1 + ) { + Slot slot = slots.get(index); + // If the slot cannot be added to, check the next slot. + if (slot.isFake() || !slot.mayPlace(itemStack)) { + continue; + } + + if (slot.hasItem()) { + // If the item isn't stackable, check the next slot. + if (!stackable) { + continue; + } + // Otherwise, add as many as we can from our stack to the slot. + modified |= addToExistingStack(itemStack, slot); + } else { + // If this is the first empty slot, keep track of it for later use. + if (firstEmpty == null) { + firstEmpty = slot; + } + // If the item isn't stackable, we've located the slot we're adding it to, so we're done. + if (!stackable) { + break; + } + } + } + + // If the item hasn't been fully added yet, add as many as we can to the first open slot. + if (!itemStack.isEmpty() && firstEmpty != null) { + firstEmpty.setByPlayer(itemStack.split(Math.min(itemStack.getCount(), firstEmpty.getMaxStackSize(itemStack)))); + firstEmpty.setChanged(); + modified = true; + } + + return modified; + } + + private static boolean addToExistingStack(ItemStack itemStack, Slot slot) { + ItemStack existing = slot.getItem(); + + // If the items aren't the same, we can't add our item. + if (!ItemStack.isSameItemSameComponents(itemStack, existing)) { + return false; + } + + int max = slot.getMaxStackSize(existing); + int existingCount = existing.getCount(); + + // If the stack is already full, we can't add more. + if (existingCount >= max) { + return false; + } + + int total = existingCount + itemStack.getCount(); + + // If the existing item can accept the entirety of our item, we're done! + if (total <= max) { + itemStack.setCount(0); + existing.setCount(total); + slot.setChanged(); + return true; + } + + // Otherwise, add as many as we can. + itemStack.shrink(max - existingCount); + existing.setCount(max); + slot.setChanged(); + return true; + } + + @Override + public void clicked(int i, int j, @NotNull ClickType clickType, @NotNull Player player) { + if (viewOnly) { + if (clickType == ClickType.QUICK_CRAFT) { + sendAllDataToRemote(); + } + return; + } + super.clicked(i, j, clickType, player); + } + + @Override + public boolean stillValid(@NotNull Player player) { + return true; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenEnderChestMenu.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenEnderChestMenu.java new file mode 100644 index 00000000..f0e70d33 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenEnderChestMenu.java @@ -0,0 +1,54 @@ +package com.lishid.openinv.internal.common.container.menu; + +import com.lishid.openinv.internal.common.container.OpenEnderChest; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class OpenEnderChestMenu extends OpenSyncMenu { + + public OpenEnderChestMenu( + @NotNull OpenEnderChest enderChest, + @NotNull ServerPlayer viewer, + int containerId, + boolean viewOnly + ) { + super(getChestMenuType(enderChest.getContainerSize()), containerId, enderChest, viewer, viewOnly); + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + if (viewOnly) { + return ItemStack.EMPTY; + } + + // See ChestMenu + Slot slot = this.slots.get(index); + + if (slot.isFake() || !slot.hasItem()) { + return ItemStack.EMPTY; + } + + ItemStack itemStack = slot.getItem(); + ItemStack original = itemStack.copy(); + + if (index < topSize) { + if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (!this.moveItemStackTo(itemStack, 0, topSize, false)) { + return ItemStack.EMPTY; + } + + if (itemStack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + return original; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenInventoryMenu.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenInventoryMenu.java new file mode 100644 index 00000000..52ca724e --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenInventoryMenu.java @@ -0,0 +1,265 @@ +package com.lishid.openinv.internal.common.container.menu; + +import com.google.common.base.Preconditions; +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import com.lishid.openinv.internal.common.container.bukkit.OpenDummyPlayerInventory; +import com.lishid.openinv.internal.common.container.bukkit.OpenPlayerInventorySelf; +import com.lishid.openinv.internal.common.container.slot.ContentDrop; +import com.lishid.openinv.internal.common.container.slot.ContentEquipment; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import com.lishid.openinv.util.Permissions; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.craftbukkit.inventory.CraftInventoryView; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventoryMenu extends OpenSyncMenu { + + private int offset; + + public OpenInventoryMenu(BaseOpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) { + super(getMenuType(inventory, viewer), i, inventory, viewer, viewOnly); + } + + private static MenuType getMenuType(BaseOpenInventory inventory, ServerPlayer viewer) { + int size = inventory.getContainerSize(); + // Disallow duplicate access to own main inventory contents. + if (inventory.getOwnerHandle().equals(viewer)) { + size -= viewer.getInventory().getNonEquipmentItems().size(); + size = ((int) Math.ceil(size / 9.0)) * 9; + } + + return getChestMenuType(size); + } + + @Override + protected void preSlotSetup() { + offset = ownContainer ? viewer.getInventory().getNonEquipmentItems().size() : 0; + } + + @Override + protected @NotNull Slot getUpperSlot(int index, int x, int y) { + index += offset; + Slot slot = container.getMenuSlot(index, x, y); + + // If the slot cannot be interacted with there's nothing to configure. + if (slot.getClass().equals(SlotViewOnly.class)) { + return slot; + } + + // Remove drop slot if viewer is not allowed to use it. + if (slot instanceof ContentDrop.SlotDrop + && (viewOnly || !Permissions.INVENTORY_SLOT_DROP.hasPermission(viewer.getBukkitEntity()))) { + return new SlotViewOnly(container, index, x, y); + } + + if (slot instanceof ContentEquipment.SlotEquipment equipment) { + if (viewOnly) { + return SlotViewOnly.wrap(slot); + } + + Permissions perm = switch (equipment.getEquipmentSlot()) { + case HEAD -> Permissions.INVENTORY_SLOT_HEAD_ANY; + case CHEST -> Permissions.INVENTORY_SLOT_CHEST_ANY; + case LEGS -> Permissions.INVENTORY_SLOT_LEGS_ANY; + case FEET -> Permissions.INVENTORY_SLOT_FEET_ANY; + // Off-hand can hold anything, not just equipment. + default -> null; + }; + + // If the viewer doesn't have permission, only allow equipment the viewee can equip in the slot. + if (perm != null && !perm.hasPermission(viewer.getBukkitEntity())) { + equipment.onlyEquipmentFor(container.getOwnerHandle()); + } + + // Equipment slots are a core part of the inventory, so they will always be shown. + return slot; + } + + // When viewing own inventory, only allow access to equipment and drop slots (equipment allowed above). + if (ownContainer && !(slot instanceof ContentDrop.SlotDrop)) { + return new SlotViewOnly(container, index, x, y); + } + + if (viewOnly) { + return SlotViewOnly.wrap(slot); + } + + return slot; + } + + @Override + protected @NotNull CraftInventoryView, Inventory> createBukkitEntity() { + org.bukkit.inventory.Inventory bukkitInventory; + if (viewOnly) { + bukkitInventory = new OpenDummyPlayerInventory(container); + } else if (ownContainer) { + bukkitInventory = new OpenPlayerInventorySelf(container, offset); + } else { + bukkitInventory = container.getBukkitInventory(); + } + + return new CraftInventoryView<>(viewer.getBukkitEntity(), bukkitInventory, this) { + @Override + public org.bukkit.inventory.ItemStack getItem(int index) { + if (viewOnly || index < 0) { + return null; + } + + Slot slot = slots.get(index); + return CraftItemStack.asCraftMirror(slot.hasItem() ? slot.getItem() : ItemStack.EMPTY); + } + + @Override + public boolean isInTop(int rawSlot) { + return rawSlot < topSize; + } + + @Override + public @Nullable Inventory getInventory(int rawSlot) { + if (viewOnly) { + return null; + } + if (rawSlot == InventoryView.OUTSIDE || rawSlot == -1) { + return null; + } + Preconditions.checkArgument( + rawSlot >= 0 && rawSlot < topSize + offset + BOTTOM_INVENTORY_SIZE, + "Slot %s outside of inventory", + rawSlot + ); + if (rawSlot > topSize) { + return getBottomInventory(); + } + Slot slot = slots.get(rawSlot); + if (slot.isFake()) { + return null; + } + return getTopInventory(); + } + + @Override + public int convertSlot(int rawSlot) { + if (viewOnly) { + return InventoryView.OUTSIDE; + } + if (rawSlot < 0) { + return rawSlot; + } + if (rawSlot < topSize) { + Slot slot = slots.get(rawSlot); + if (slot.isFake()) { + return InventoryView.OUTSIDE; + } + return rawSlot; + } + + int slot = rawSlot - topSize; + + if (slot >= 27) { + slot -= 27; + } else { + slot += 9; + } + + return slot; + } + + @Override + public @NotNull InventoryType.SlotType getSlotType(int slot) { + if (viewOnly || slot < 0) { + return InventoryType.SlotType.OUTSIDE; + } + if (slot >= topSize) { + slot -= topSize; + if (slot >= 27) { + return InventoryType.SlotType.QUICKBAR; + } + return InventoryType.SlotType.CONTAINER; + } + return OpenInventoryMenu.this.container.getSlotType(offset + slot); + } + + @Override + public int countSlots() { + return topSize + BOTTOM_INVENTORY_SIZE; + } + }; + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + if (viewOnly) { + return ItemStack.EMPTY; + } + + // See ChestMenu and InventoryMenu + Slot slot = this.slots.get(index); + + if (!slot.hasItem() || slot.isFake()) { + return ItemStack.EMPTY; + } + + ItemStack itemStack = slot.getItem(); + ItemStack originalStack = itemStack.copy(); + + if (index < topSize) { + // If we're moving top to bottom, do a normal transfer. + if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) { + return ItemStack.EMPTY; + } + } else { + EquipmentSlot equipmentSlot = player.getEquipmentSlotForItem(itemStack); + boolean movedGear = switch (equipmentSlot) { + // If this is gear, try to move it to the correct slot first. + case OFFHAND, FEET, LEGS, CHEST, HEAD -> { + // Locate the correct slot in the contents following the main inventory. + for (int extra = container.getOwnerHandle().getInventory().getNonEquipmentItems().size() - offset; extra < topSize; ++extra) { + Slot extraSlot = getSlot(extra); + if (extraSlot instanceof ContentEquipment.SlotEquipment equipSlot + && equipSlot.getEquipmentSlot() == equipmentSlot) { + // If we've found a matching slot, try to move to it. + // If this succeeds, even partially, we will not attempt to move to other slots. + // Otherwise, armor is already occupied, so we'll fall through to main inventory. + yield this.moveItemStackTo(itemStack, extra, extra + 1, false); + } + } + yield false; + } + // Non-gear gets no special treatment. + default -> false; + }; + + // If main inventory is not available, there's nowhere else to move. + if (offset != 0) { + if (!movedGear) { + return ItemStack.EMPTY; + } + } else { + // If we didn't move to a gear slot, try to move to a main inventory slot. + if (!movedGear && !this.moveItemStackTo(itemStack, 0, container.getOwnerHandle().getInventory().getNonEquipmentItems().size(), true)) { + return ItemStack.EMPTY; + } + } + } + + if (itemStack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + return originalStack; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenSyncMenu.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenSyncMenu.java new file mode 100644 index 00000000..612b2ec9 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/menu/OpenSyncMenu.java @@ -0,0 +1,251 @@ +package com.lishid.openinv.internal.common.container.menu; + +import com.google.common.base.Suppliers; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.InternalOwned; +import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.network.HashedStack; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.inventory.ContainerSynchronizer; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.RemoteSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * An extension of {@link OpenChestMenu} that supports {@link SlotPlaceholder placeholders}. + */ +@SuppressWarnings("HidingField") // Revisit when removing 1.21.4 support +public abstract class OpenSyncMenu> + extends OpenChestMenu { + + // Syncher fields + protected @Nullable ContainerSynchronizer synchronizer; + protected final List dataSlots = new ArrayList<>(); + protected final IntList remoteDataSlots = new IntArrayList(); + protected final List containerListeners = new ArrayList<>(); + private RemoteSlot remoteCarried = RemoteSlot.PLACEHOLDER; + protected boolean suppressRemoteUpdates; + + protected OpenSyncMenu( + @NotNull MenuType type, + int containerCounter, + @NotNull T container, + @NotNull ServerPlayer viewer, + boolean viewOnly + ) { + super(type, containerCounter, container, viewer, viewOnly); + } + + // Overrides from here on are purely to modify the sync process to send placeholder items. + @Override + protected @NotNull Slot addSlot(@NotNull Slot slot) { + slot.index = this.slots.size(); + this.slots.add(slot); + this.lastSlots.add(ItemStack.EMPTY); + this.remoteSlots.add(this.synchronizer != null ? this.synchronizer.createSlot() : RemoteSlot.PLACEHOLDER); + return slot; + } + + @Override + protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) { + this.dataSlots.add(dataSlot); + this.remoteDataSlots.add(0); + return dataSlot; + } + + @Override + protected void addDataSlots(ContainerData containerData) { + for (int i = 0; i < containerData.getCount(); i++) { + this.addDataSlot(DataSlot.forContainer(containerData, i)); + } + } + + @Override + public void addSlotListener(@NotNull ContainerListener containerListener) { + if (!this.containerListeners.contains(containerListener)) { + this.containerListeners.add(containerListener); + this.broadcastChanges(); + } + } + + @Override + public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) { + this.synchronizer = containerSynchronizer; + this.remoteCarried = synchronizer.createSlot(); + this.remoteSlots.replaceAll(slot -> synchronizer.createSlot()); + this.sendAllDataToRemote(); + } + + @Override + public void sendAllDataToRemote() { + List contentsCopy = new ArrayList<>(); + for (int index = 0; index < slots.size(); ++index) { + Slot slot = slots.get(index); + ItemStack itemStack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem(); + contentsCopy.add(itemStack); + this.remoteSlots.get(index).force(itemStack); + } + + remoteCarried.force(getCarried()); + + for (int index = 0; index < this.dataSlots.size(); ++index) { + this.remoteDataSlots.set(index, this.dataSlots.get(index).get()); + } + + if (this.synchronizer != null) { + this.synchronizer.sendInitialData(this, contentsCopy, this.getCarried().copy(), this.remoteDataSlots.toIntArray()); + } + } + + @Override + public void forceSlot(@NotNull Container container, int slot) { + int slotsIndex = this.findSlot(container, slot).orElse(-1); + if (slotsIndex != -1) { + ItemStack item = this.slots.get(slotsIndex).getItem(); + this.remoteSlots.get(slotsIndex).force(item); + if (this.synchronizer != null) { + this.synchronizer.sendSlotChange(this, slotsIndex, item.copy()); + } + } + } + + @Override + public void broadcastCarriedItem() { + ItemStack carried = this.getCarried(); + this.remoteCarried.force(carried); + if (this.synchronizer != null) { + this.synchronizer.sendCarriedChange(this, carried.copy()); + } + } + + @Override + public void removeSlotListener(@NotNull ContainerListener containerListener) { + this.containerListeners.remove(containerListener); + } + + @Override + public void broadcastChanges() { + for (int index = 0; index < this.slots.size(); ++index) { + Slot slot = this.slots.get(index); + ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem(); + Supplier supplier = Suppliers.memoize(itemstack::copy); + this.triggerSlotListeners(index, itemstack, supplier); + this.synchronizeSlotToRemote(index, itemstack, supplier); + } + + this.synchronizeCarriedToRemote(); + + for (int index = 0; index < this.dataSlots.size(); ++index) { + DataSlot dataSlot = this.dataSlots.get(index); + int j = dataSlot.get(); + if (dataSlot.checkAndClearUpdateFlag()) { + this.updateDataSlotListeners(index, j); + } + + this.synchronizeDataSlotToRemote(index, j); + } + } + + @Override + public void broadcastFullState() { + for (int index = 0; index < this.slots.size(); ++index) { + ItemStack itemstack = this.slots.get(index).getItem(); + this.triggerSlotListeners(index, itemstack, itemstack::copy); + } + + for (int index = 0; index < this.dataSlots.size(); ++index) { + DataSlot containerproperty = this.dataSlots.get(index); + if (containerproperty.checkAndClearUpdateFlag()) { + this.updateDataSlotListeners(index, containerproperty.get()); + } + } + + this.sendAllDataToRemote(); + } + + private void updateDataSlotListeners(int i, int j) { + for (ContainerListener containerListener : this.containerListeners) { + containerListener.dataChanged(this, i, j); + } + } + + @Override + public void triggerSlotListeners(int index, @NotNull ItemStack itemStack, @NotNull Supplier supplier) { + ItemStack itemStack1 = this.lastSlots.get(index); + if (!ItemStack.matches(itemStack1, itemStack)) { + ItemStack itemStack2 = supplier.get(); + this.lastSlots.set(index, itemStack2); + + for (ContainerListener containerListener : this.containerListeners) { + containerListener.slotChanged(this, index, itemStack2); + } + } + } + + @Override + public void synchronizeSlotToRemote(int i, @NotNull ItemStack itemStack, @NotNull Supplier supplier) { + if (!this.suppressRemoteUpdates) { + RemoteSlot slot = this.remoteSlots.get(i); + if (!slot.matches(itemStack)) { + slot.force(itemStack); + if (this.synchronizer != null) { + this.synchronizer.sendSlotChange(this, i, supplier.get()); + } + } + } + } + + private void synchronizeDataSlotToRemote(int index, int value) { + if (!this.suppressRemoteUpdates) { + int existing = this.remoteDataSlots.getInt(index); + if (existing != value) { + this.remoteDataSlots.set(index, value); + if (this.synchronizer != null) { + this.synchronizer.sendDataChange(this, index, value); + } + } + } + } + + private void synchronizeCarriedToRemote() { + if (!this.suppressRemoteUpdates) { + ItemStack carried = this.getCarried(); + if (!this.remoteCarried.matches(carried)) { + this.remoteCarried.force(carried); + if (this.synchronizer != null) { + this.synchronizer.sendCarriedChange(this, carried.copy()); + } + } + } + } + + @Override + public void setRemoteCarried(@NotNull HashedStack stack) { + this.remoteCarried.receive(stack); + } + + @Override + public void suppressRemoteUpdates() { + this.suppressRemoteUpdates = true; + } + + @Override + public void resumeRemoteUpdates() { + this.suppressRemoteUpdates = false; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/Content.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/Content.java new file mode 100644 index 00000000..9b63ed54 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/Content.java @@ -0,0 +1,69 @@ +package com.lishid.openinv.internal.common.container.slot; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +/** + * An interface defining behaviors for entries in a {@link Container}. Used to reduce duplicate content reordering. + */ +public interface Content { + + /** + * Update internal holder. + * + * @param holder the new holder + */ + void setHolder(@NotNull ServerPlayer holder); + + /** + * Get the current item. + * + * @return the current item + */ + ItemStack get(); + + /** + * Remove the current item. + * + * @return the current item + */ + ItemStack remove(); + + /** + * Remove some of the current item. + * + * @return the current item + */ + ItemStack removePartial(int amount); + + /** + * Set the current item. If slot is currently not usable, will drop item instead. + * + * @param itemStack the item to set + */ + void set(ItemStack itemStack); + + /** + * Get a {@link Slot} for use in a {@link net.minecraft.world.inventory.AbstractContainerMenu ContainerMenu}. Will + * impose any specific restrictions to insertion or removal. + * + * @param container the backing container + * @param slot the slot of the backing container represented + * @param x clientside x dimension from top left of inventory, not used + * @param y clientside y dimension from top left of inventory, not used + * @return a menu slot + */ + Slot asSlot(Container container, int slot, int x, int y); + + /** + * Get a loose Bukkit translation of what this slot stores. For example, any slot that drops items at the owner rather + * than insert them will report itself as being {@link org.bukkit.event.inventory.InventoryType.SlotType#OUTSIDE}. + * + * @return the closes Bukkit slot type + */ + org.bukkit.event.inventory.InventoryType.SlotType getSlotType(); + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCrafting.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCrafting.java new file mode 100644 index 00000000..ee8b370f --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCrafting.java @@ -0,0 +1,132 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * A slot in a survival crafting inventory. Unavailable when not online in a survival mode. + */ +public class ContentCrafting implements Content { + + private final int index; + private ServerPlayer holder; + private List items; + + public ContentCrafting(@NotNull ServerPlayer holder, int index) { + setHolder(holder); + this.index = index; + } + + private boolean isAvailable() { + return isAvailable(holder); + } + + public static boolean isAvailable(@NotNull ServerPlayer holder) { + // Player must be online and not in creative - since the creative client is (semi-)authoritative, + // it ignores changes without extra help, and will delete the item as a result. + // Spectator mode is technically possible but may cause the item to be dropped if the client opens an inventory. + return BaseOpenPlayer.isConnected(holder.connection) && holder.gameMode.isSurvival(); + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.holder = holder; + // Note: CraftingContainer#getItems is immutable! Be careful with updates. + this.items = holder.inventoryMenu.getCraftSlots().getContents(); + } + + @Override + public ItemStack get() { + return isAvailable() ? items.get(index) : ItemStack.EMPTY; + } + + @Override + public ItemStack remove() { + if (!this.isAvailable()) { + return ItemStack.EMPTY; + } + ItemStack removed = items.remove(index); + if (removed.isEmpty()) { + return ItemStack.EMPTY; + } + holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots()); + return removed; + } + + @Override + public ItemStack removePartial(int amount) { + if (!this.isAvailable()) { + return ItemStack.EMPTY; + } + ItemStack removed = ContainerHelper.removeItem(items, index, amount); + if (removed.isEmpty()) { + return ItemStack.EMPTY; + } + holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots()); + return removed; + } + + @Override + public void set(ItemStack itemStack) { + if (isAvailable()) { + items.set(index, itemStack); + holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots()); + } else { + holder.drop(itemStack, false); + } + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotCrafting(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + return isAvailable() ? InventoryType.SlotType.CRAFTING : InventoryType.SlotType.OUTSIDE; + } + + public class SlotCrafting extends SlotPlaceholder { + + private SlotCrafting(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + return isAvailable() ? items.get(ContentCrafting.this.index) : Placeholders.survivalOnly(holder); + } + + @Override + public boolean mayPickup(@NotNull Player player) { + return isAvailable(); + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + return isAvailable(); + } + + @Override + public boolean hasItem() { + return isAvailable() && super.hasItem(); + } + + @Override + public boolean isFake() { + return !isAvailable(); + } + + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCraftingResult.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCraftingResult.java new file mode 100644 index 00000000..e930c8e8 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCraftingResult.java @@ -0,0 +1,48 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot allowing viewing of the crafting result. + * + *

Unmodifiable because I said so. Use your own crafting grid.

+ */ +public class ContentCraftingResult extends ContentViewOnly { + + public ContentCraftingResult(@NotNull ServerPlayer holder) { + super(holder); + } + + @Override + public ItemStack get() { + InventoryMenu inventoryMenu = holder.inventoryMenu; + return inventoryMenu.getResultSlot().getItem(); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotViewOnly(container, slot, x, y) { + @Override + public ItemStack getOrDefault() { + if (!ContentCrafting.isAvailable(holder)) { + return Placeholders.survivalOnly(holder); + } + InventoryMenu inventoryMenu = holder.inventoryMenu; + return inventoryMenu.getResultSlot().getItem(); + } + }; + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.RESULT; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java new file mode 100644 index 00000000..2693698d --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentCursor.java @@ -0,0 +1,118 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot wrapping the active menu's cursor. Unavailable when not online in a survival mode. + */ +public class ContentCursor implements Content { + + private @NotNull ServerPlayer holder; + + public ContentCursor(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public ItemStack get() { + return isAvailable() ? holder.containerMenu.getCarried() : ItemStack.EMPTY; + } + + @Override + public ItemStack remove() { + ItemStack carried = holder.containerMenu.getCarried(); + holder.containerMenu.setCarried(ItemStack.EMPTY); + return carried; + } + + @Override + public ItemStack removePartial(int amount) { + ItemStack carried = holder.containerMenu.getCarried(); + if (!carried.isEmpty() && carried.getCount() >= amount) { + ItemStack value = carried.split(amount); + if (carried.isEmpty()) { + holder.containerMenu.setCarried(ItemStack.EMPTY); + } + return value; + } + return ItemStack.EMPTY; + } + + @Override + public void set(ItemStack itemStack) { + if (isAvailable()) { + holder.containerMenu.setCarried(itemStack); + } else { + holder.drop(itemStack, false); + } + } + + private boolean isAvailable() { + // Player must be online and not in creative - since the creative client is (semi-)authoritative, + // it ignores changes without extra help, and will delete the item as a result. + // Spectator mode is technically possible but may cause the item to be dropped if the client opens an inventory. + return BaseOpenPlayer.isConnected(holder.connection) && holder.gameMode.isSurvival(); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotCursor(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + // As close as possible to "not real" + return InventoryType.SlotType.OUTSIDE; + } + + public class SlotCursor extends SlotPlaceholder { + + private SlotCursor(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + if (!isAvailable()) { + return Placeholders.survivalOnly(holder); + } + ItemStack carried = holder.containerMenu.getCarried(); + return carried.isEmpty() ? Placeholders.cursor : carried; + } + + @Override + public boolean mayPickup(@NotNull Player player) { + return isAvailable(); + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + return isAvailable(); + } + + @Override + public boolean hasItem() { + return isAvailable() && super.hasItem(); + } + + @Override + public boolean isFake() { + return true; + } + + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java new file mode 100644 index 00000000..7ed6e700 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentDrop.java @@ -0,0 +1,89 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A fake slot used to drop items. Unavailable offline. + */ +public class ContentDrop implements Content { + + private ServerPlayer holder; + + public ContentDrop(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public ItemStack get() { + return ItemStack.EMPTY; + } + + @Override + public ItemStack remove() { + return ItemStack.EMPTY; + } + + @Override + public ItemStack removePartial(int amount) { + return ItemStack.EMPTY; + } + + @Override + public void set(ItemStack itemStack) { + holder.drop(itemStack, true); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotDrop(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + // Behaves like dropping an item outside the screen, just by the target player. + return InventoryType.SlotType.OUTSIDE; + } + + public class SlotDrop extends SlotPlaceholder { + + private SlotDrop(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + return BaseOpenPlayer.isConnected(holder.connection) + ? Placeholders.drop + : Placeholders.blockedOffline; + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + return BaseOpenPlayer.isConnected(holder.connection); + } + + @Override + public boolean hasItem() { + return false; + } + + @Override + public boolean isFake() { + return true; + } + + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java new file mode 100644 index 00000000..9087232d --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentEquipment.java @@ -0,0 +1,109 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EntityEquipment; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot for equipment that displays placeholders if empty. + */ +public class ContentEquipment implements Content { + + private EntityEquipment equipment; + private final ItemStack placeholder; + private final EquipmentSlot equipmentSlot; + + public ContentEquipment(ServerPlayer holder, EquipmentSlot equipmentSlot) { + setHolder(holder); + placeholder = switch (equipmentSlot) { + case HEAD -> Placeholders.emptyHelmet; + case CHEST -> Placeholders.emptyChestplate; + case LEGS -> Placeholders.emptyLeggings; + case FEET -> Placeholders.emptyBoots; + default -> Placeholders.emptyOffHand; + }; + this.equipmentSlot = equipmentSlot; + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.equipment = holder.getInventory().equipment; + } + + @Override + public ItemStack get() { + return equipment.get(equipmentSlot); + } + + @Override + public ItemStack remove() { + return equipment.set(equipmentSlot, ItemStack.EMPTY); + } + + @Override + public ItemStack removePartial(int amount) { + ItemStack current = get(); + if (!current.isEmpty() && amount > 0) { + return current.split(amount); + } + return ItemStack.EMPTY; + } + + @Override + public void set(ItemStack itemStack) { + equipment.set(equipmentSlot, itemStack); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.ARMOR; + } + + public class SlotEquipment extends SlotPlaceholder { + + private ServerPlayer viewer; + + SlotEquipment(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + ItemStack itemStack = getItem(); + if (!itemStack.isEmpty()) { + return itemStack; + } + return placeholder; + } + + public EquipmentSlot getEquipmentSlot() { + return equipmentSlot; + } + + public void onlyEquipmentFor(ServerPlayer viewer) { + this.viewer = viewer; + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + if (viewer == null) { + return true; + } + + return equipmentSlot == EquipmentSlot.OFFHAND || viewer.getEquipmentSlotForItem(itemStack) == equipmentSlot; + } + + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java new file mode 100644 index 00000000..5748c51a --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentList.java @@ -0,0 +1,58 @@ +package com.lishid.openinv.internal.common.container.slot; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; + +import java.util.List; + +/** + * A normal slot backed by an item list. + */ +public abstract class ContentList implements Content { + + private final int index; + private final InventoryType.SlotType slotType; + protected List items; + + public ContentList(ServerPlayer holder, int index, InventoryType.SlotType slotType) { + this.index = index; + this.slotType = slotType; + setHolder(holder); + } + + @Override + public ItemStack get() { + return items.get(index); + } + + @Override + public ItemStack remove() { + ItemStack removed = items.remove(index); + return removed == null || removed.isEmpty() ? ItemStack.EMPTY : removed; + } + + @Override + public ItemStack removePartial(int amount) { + return ContainerHelper.removeItem(items, index, amount); + } + + @Override + public void set(ItemStack itemStack) { + items.set(index, itemStack); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new Slot(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + return slotType; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java new file mode 100644 index 00000000..ba1db443 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentOffHand.java @@ -0,0 +1,53 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot for equipment that updates held items if necessary. + */ +public class ContentOffHand extends ContentEquipment { + + private ServerPlayer holder; + + public ContentOffHand(ServerPlayer holder) { + super(holder, EquipmentSlot.OFFHAND); + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + super.setHolder(holder); + this.holder = holder; + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.QUICKBAR; + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y) { + @Override + public void setChanged() { + if (BaseOpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) { + holder.connection.send( + new ClientboundContainerSetSlotPacket( + holder.inventoryMenu.containerId, + holder.inventoryMenu.incrementStateId(), + InventoryMenu.SHIELD_SLOT, + holder.getOffhandItem() + )); + } + } + }; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java new file mode 100644 index 00000000..319ef0e5 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/ContentViewOnly.java @@ -0,0 +1,56 @@ +package com.lishid.openinv.internal.common.container.slot; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A view-only slot that can't be interacted with. + */ +public class ContentViewOnly implements Content { + + protected @NotNull ServerPlayer holder; + + public ContentViewOnly(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.holder = holder; + } + + @Override + public ItemStack get() { + return ItemStack.EMPTY; + } + + @Override + public ItemStack remove() { + return ItemStack.EMPTY; + } + + @Override + public ItemStack removePartial(int amount) { + return ItemStack.EMPTY; + } + + @Override + public void set(ItemStack itemStack) { + this.holder.drop(itemStack, false); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotViewOnly(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.OUTSIDE; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java new file mode 100644 index 00000000..7e7a79ad --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotPlaceholder.java @@ -0,0 +1,20 @@ +package com.lishid.openinv.internal.common.container.slot; + +import net.minecraft.world.Container; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +/** + * An implementation of a slot as used by a menu that may have fake placeholder items. + * + *

Used to prevent plugins (particularly sorting plugins) from adding placeholders to inventories.

+ */ +public abstract class SlotPlaceholder extends Slot { + + public SlotPlaceholder(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + public abstract ItemStack getOrDefault(); + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java new file mode 100644 index 00000000..3fe82fd9 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/SlotViewOnly.java @@ -0,0 +1,151 @@ +package com.lishid.openinv.internal.common.container.slot; + +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +/** + * A view-only {@link Slot}. "Blank" by default, but can wrap another slot to display its content. + */ +public class SlotViewOnly extends SlotPlaceholder { + + public static @NotNull SlotViewOnly wrap(@NotNull Slot wrapped) { + SlotViewOnly wrapper; + if (wrapped instanceof SlotPlaceholder placeholder) { + wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) { + @Override + public ItemStack getOrDefault() { + return placeholder.getOrDefault(); + } + }; + } else { + wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) { + @Override + public ItemStack getOrDefault() { + return wrapped.getItem(); + } + }; + } + wrapper.index = wrapped.index; + return wrapper; + } + + public SlotViewOnly(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + return Placeholders.notSlot; + } + + @Override + public void onQuickCraft(@NotNull ItemStack itemStack1, @NotNull ItemStack itemStack2) { + } + + @Override + public void onTake(@NotNull Player player, @NotNull ItemStack itemStack) { + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + return false; + } + + @Override + public @NotNull ItemStack getItem() { + return ItemStack.EMPTY; + } + + @Override + public boolean hasItem() { + return false; + } + + @Override + public void setByPlayer(@NotNull ItemStack newStack) { + } + + @Override + public void setByPlayer(@NotNull ItemStack newStack, @NotNull ItemStack oldStack) { + } + + @Override + public void set(@NotNull ItemStack itemStack) { + } + + @Override + public void setChanged() { + } + + @Override + public int getMaxStackSize() { + return 0; + } + + @Override + public int getMaxStackSize(@NotNull ItemStack itemStack) { + return 0; + } + + @Override + public @NotNull ItemStack remove(int amount) { + return ItemStack.EMPTY; + } + + @Override + public boolean mayPickup(@NotNull Player player) { + return false; + } + + @Override + public boolean isActive() { + return false; + } + + @Override + public @NotNull Optional tryRemove(int var0, int var1, @NotNull Player player) { + return Optional.empty(); + } + + @Override + public @NotNull ItemStack safeTake(int var0, int var1, @NotNull Player player) { + return ItemStack.EMPTY; + } + + @Override + public @NotNull ItemStack safeInsert(@NotNull ItemStack itemStack) { + return itemStack; + } + + @Override + public @NotNull ItemStack safeInsert(@NotNull ItemStack itemStack, int amount) { + return itemStack; + } + + @Override + public boolean allowModification(@NotNull Player player) { + return false; + } + + @Override + public int getContainerSlot() { + return this.slot; + } + + @Override + public boolean isHighlightable() { + return false; + } + + @Override + public boolean isFake() { + return true; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java new file mode 100644 index 00000000..c859e7d6 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoader.java @@ -0,0 +1,40 @@ +package com.lishid.openinv.internal.common.container.slot.placeholder; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.item.component.DyedItemColor; +import net.minecraft.world.item.component.TooltipDisplay; +import org.jetbrains.annotations.NotNull; + +import java.util.LinkedHashSet; +import java.util.List; + +public class PlaceholderLoader extends PlaceholderLoaderBase { + + private static final CustomModelData DEFAULT_CUSTOM_MODEL_DATA = new CustomModelData(List.of(), List.of(), List.of("openinv:custom"), List.of()); + private static final TooltipDisplay HIDE_TOOLTIP = new TooltipDisplay(true, new LinkedHashSet<>()); + + @Override + protected @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception { + return TagParser.parseCompoundFully(itemText); + } + + @Override + protected void addModelData(@NotNull ItemStack itemStack) { + itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA); + } + + @Override + protected void hideTooltip(@NotNull ItemStack itemStack) { + itemStack.set(DataComponents.TOOLTIP_DISPLAY, HIDE_TOOLTIP); + } + + @Override + protected DyedItemColor getDye(int rgb) { + return new DyedItemColor(rgb); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java new file mode 100644 index 00000000..4de686d0 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/PlaceholderLoaderBase.java @@ -0,0 +1,180 @@ +package com.lishid.openinv.internal.common.container.slot.placeholder; + +import com.mojang.serialization.DataResult; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.DyedItemColor; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.block.entity.BannerPattern; +import net.minecraft.world.level.block.entity.BannerPatternLayers; +import net.minecraft.world.level.block.entity.BannerPatterns; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public abstract class PlaceholderLoaderBase { + + public void load(@Nullable ConfigurationSection section) throws Exception { + Placeholders.craftingOutput = parse(section, "crafting-output", defaultCraftingOutput()); + Placeholders.cursor = parse(section, "cursor", defaultCursor()); + Placeholders.drop = parse(section, "drop", defaultDrop()); + Placeholders.emptyHelmet = parse(section, "empty-helmet", getEmptyArmor(Items.LEATHER_HELMET)); + Placeholders.emptyChestplate = parse(section, "empty-chestplate", getEmptyArmor(Items.LEATHER_CHESTPLATE)); + Placeholders.emptyLeggings = parse(section, "empty-leggings", getEmptyArmor(Items.LEATHER_LEGGINGS)); + Placeholders.emptyBoots = parse(section, "empty-boots", getEmptyArmor(Items.LEATHER_BOOTS)); + Placeholders.emptyOffHand = parse(section, "empty-off-hand", defaultShield()); + Placeholders.notSlot = parse(section, "not-a-slot", defaultNotSlot()); + Placeholders.blockedOffline = parse(section, "blocked.offline", defaultBlockedOffline()); + + for (GameType type : GameType.values()) { + // Barrier: "Not available - Creative" etc. + ItemStack typeItem = new ItemStack(Items.BARRIER); + typeItem.set( + DataComponents.ITEM_NAME, + Component.translatable("options.narrator.notavailable").append(" - ").append(type.getShortDisplayName()) + ); + Placeholders.BLOCKED_GAME_TYPE.put(type, typeItem); + } + + Placeholders.BLOCKED_GAME_TYPE.put(GameType.CREATIVE, parse(section, "blocked.creative", Placeholders.BLOCKED_GAME_TYPE.get(GameType.CREATIVE))); + Placeholders.BLOCKED_GAME_TYPE.put(GameType.SPECTATOR, parse(section, "blocked.spectator", Placeholders.BLOCKED_GAME_TYPE.get(GameType.SPECTATOR))); + } + + protected @NotNull ItemStack parse( + @Nullable ConfigurationSection section, + @NotNull String path, + @NotNull ItemStack defaultStack + ) throws Exception { + if (section == null) { + return defaultStack; + } + + String itemText = section.getString(path); + + if (itemText == null) { + return defaultStack; + } + + CompoundTag compoundTag = parseTag(itemText); + DataResult parsed = ItemStack.CODEC.parse(CraftRegistry.getMinecraftRegistry().createSerializationContext(NbtOps.INSTANCE), compoundTag); + ItemStack itemStack; + try { + itemStack = parsed.getOrThrow(); + } catch (Exception e) { + itemStack = null; + } + return itemStack == null ? defaultStack : itemStack; + } + + protected abstract @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception; + + protected abstract void addModelData(@NotNull ItemStack itemStack); + + protected abstract void hideTooltip(@NotNull ItemStack itemStack); + + protected abstract DyedItemColor getDye(int rgb); + + protected @NotNull ItemStack defaultCraftingOutput() { + // Crafting table: "Crafting" + ItemStack itemStack = new ItemStack(Items.CRAFTING_TABLE); + itemStack.set(DataComponents.ITEM_NAME, Component.translatable("container.crafting")); + addModelData(itemStack); + return itemStack; + } + + protected @NotNull ItemStack defaultCursor() { + // Cursor-like banner with no tooltip + ItemStack itemStack = new ItemStack(Items.WHITE_BANNER); + RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry(); + Registry bannerPatterns = minecraftRegistry.lookupOrThrow(Registries.BANNER_PATTERN); + BannerPattern halfDiagBottomRight = bannerPatterns.getOrThrow(BannerPatterns.DIAGONAL_RIGHT).value(); + BannerPattern downRight = bannerPatterns.getOrThrow(BannerPatterns.STRIPE_DOWNRIGHT).value(); + BannerPattern border = bannerPatterns.getOrThrow(BannerPatterns.BORDER).value(); + itemStack.set(DataComponents.BANNER_PATTERNS, + new BannerPatternLayers(List.of( + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfDiagBottomRight), DyeColor.GRAY), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(downRight), DyeColor.WHITE), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(border), DyeColor.GRAY) + )) + ); + addModelData(itemStack); + hideTooltip(itemStack); + return itemStack; + } + + protected @NotNull ItemStack defaultDrop() { + // Dropper: "Drop Selected Item" + ItemStack itemStack = new ItemStack(Items.DROPPER); + // Note: translatable component, not keybind component! We want the text identifying the keybind, not the key. + itemStack.set(DataComponents.ITEM_NAME, Component.translatable("key.drop")); + addModelData(itemStack); + return itemStack; + } + + protected @NotNull ItemStack getEmptyArmor(@NotNull ItemLike item) { + // Inventory-background-grey-ish leather armor with no tooltip + ItemStack itemStack = new ItemStack(item); + DyedItemColor color = getDye(0xC8C8C8); + itemStack.set(DataComponents.DYED_COLOR, color); + hideTooltip(itemStack); + addModelData(itemStack); + return itemStack; + } + + protected @NotNull ItemStack defaultShield() { + // Shield with "missing texture" pattern, magenta and black squares. + ItemStack itemStack = new ItemStack(Items.SHIELD); + itemStack.set(DataComponents.BASE_COLOR, DyeColor.MAGENTA); + RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry(); + Registry bannerPatterns = minecraftRegistry.lookupOrThrow(Registries.BANNER_PATTERN); + BannerPattern halfLeft = bannerPatterns.getOrThrow(BannerPatterns.HALF_VERTICAL).value(); + BannerPattern topLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_LEFT).value(); + BannerPattern topRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_RIGHT).value(); + BannerPattern bottomLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_LEFT).value(); + BannerPattern bottomRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_RIGHT).value(); + itemStack.set(DataComponents.BANNER_PATTERNS, + new BannerPatternLayers(List.of( + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfLeft), DyeColor.BLACK), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topLeft), DyeColor.MAGENTA), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomLeft), DyeColor.MAGENTA), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topRight), DyeColor.BLACK), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomRight), DyeColor.BLACK) + )) + ); + hideTooltip(itemStack); + addModelData(itemStack); + return itemStack; + } + + protected @NotNull ItemStack defaultNotSlot() { + // White pane with no tooltip + ItemStack itemStack = new ItemStack(Items.WHITE_STAINED_GLASS_PANE); + hideTooltip(itemStack); + addModelData(itemStack); + return itemStack; + } + + protected @NotNull ItemStack defaultBlockedOffline() { + // Barrier: "Not available - Offline" + ItemStack itemStack = new ItemStack(Items.BARRIER); + itemStack.set(DataComponents.ITEM_NAME, + Component.translatable("options.narrator.notavailable") + .append(Component.literal(" - ")) + .append(Component.translatable("gui.socialInteractions.status_offline")) + ); + return itemStack; + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java new file mode 100644 index 00000000..b9fcd7cb --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/container/slot/placeholder/Placeholders.java @@ -0,0 +1,37 @@ +package com.lishid.openinv.internal.common.container.slot.placeholder; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameType; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumMap; + +public final class Placeholders { + + static final @NotNull EnumMap BLOCKED_GAME_TYPE = new EnumMap<>(GameType.class); + public static @NotNull ItemStack craftingOutput = ItemStack.EMPTY; + public static @NotNull ItemStack cursor = ItemStack.EMPTY; + public static @NotNull ItemStack drop = ItemStack.EMPTY; + public static @NotNull ItemStack emptyHelmet = ItemStack.EMPTY; + public static @NotNull ItemStack emptyChestplate = ItemStack.EMPTY; + public static @NotNull ItemStack emptyLeggings = ItemStack.EMPTY; + public static @NotNull ItemStack emptyBoots = ItemStack.EMPTY; + public static @NotNull ItemStack emptyOffHand = ItemStack.EMPTY; + public static @NotNull ItemStack notSlot = ItemStack.EMPTY; + public static @NotNull ItemStack blockedOffline = ItemStack.EMPTY; + + public static ItemStack survivalOnly(@NotNull ServerPlayer serverPlayer) { + if (!BaseOpenPlayer.isConnected(serverPlayer.connection)) { + return blockedOffline; + } + + return BLOCKED_GAME_TYPE.getOrDefault(serverPlayer.gameMode.getGameModeForPlayer(), ItemStack.EMPTY); + } + + private Placeholders() { + throw new IllegalStateException("Cannot create instance of utility class."); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java new file mode 100644 index 00000000..77e9fb50 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/BaseOpenPlayer.java @@ -0,0 +1,212 @@ +package com.lishid.openinv.internal.common.player; + +import com.lishid.openinv.event.OpenEvents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.world.level.storage.PlayerDataStorage; +import net.minecraft.world.level.storage.ValueOutput; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +public abstract class BaseOpenPlayer extends CraftPlayer { + + /** + * List of tags to always reset when saving. These are items that do not get written + * if unset or empty, resulting in older values not being clobbered appropriately. + * + * @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput, boolean, boolean, boolean) + * @see net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(ValueOutput) + * @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(ValueOutput) + * @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(ValueOutput) + */ + @Unmodifiable + protected static final Set RESET_TAGS = Set.of( + // Entity#saveWithoutId(CompoundTag) + "CustomName", + "CustomNameVisible", + "Silent", + "NoGravity", + "Glowing", + "TicksFrozen", + "HasVisualFire", + "Tags", + "data", + "Passengers", + // ServerPlayer#addAdditionalSaveData(CompoundTag) + // Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle + "warden_spawn_tracker", // No longer needed as of 1.21.11 + "entered_nether_pos", // Replaces enteredNetherPosition as of 1.21.6 + "enteredNetherPosition", + "respawn", // Replaces SpawnXyz fields as of 1.21.6 + "SpawnX", + "SpawnY", + "SpawnZ", + "SpawnForced", + "SpawnAngle", + "SpawnDimension", + "raid_omen_position", + "ender_pearls", + "ShoulderEntityLeft", + "ShoulderEntityRight", + // Player#addAdditionalSaveData(CompoundTag) + "LastDeathLocation", + "current_explosion_impact_pos", + // LivingEntity#addAdditionalSaveData(CompoundTag) + "active_effects", + "sleeping_pos", // Replaces SleepingXyz fields as of 1.21.6 + "SleepingX", + "SleepingY", + "SleepingZ", + "Brain", + "last_hurt_by_player", + "last_hurt_by_player_memory_time", + "last_hurt_by_mob", + "ticks_since_last_hurt_by_mob", + "equipment", + "locator_bar_icon" + ); + + private final PlayerManager manager; + + protected BaseOpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) { + super(server, entity); + this.manager = manager; + } + + @Override + public void loadData() { + manager.loadData(server.getServer(), getHandle()); + } + + @Override + public void saveData() { + if (OpenEvents.saveCancelled(this)) { + return; + } + + trySave(this.getHandle()); + } + + protected abstract void trySave(ServerPlayer player); + + protected void saveSafe( + @NotNull ServerPlayer player, + @Nullable CompoundTag oldData, + @NotNull CompoundTag playerData, + @NotNull PlayerDataStorage worldNbtStorage + ) throws IOException { + // Revert certain special data values when offline. + revertSpecialValues(playerData, oldData); + + Path playerDataDir = worldNbtStorage.getPlayerDir().toPath(); + Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat"); + NbtIo.writeCompressed(playerData, tempFile); + Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat"); + Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old"); + safeReplaceFile(dataFile, tempFile, backupFile); + } + + protected void safeReplaceFile( + @NotNull Path dataFile, + @NotNull Path tempFile, + @NotNull Path backupFile + ) { + net.minecraft.util.Util.safeReplaceFile(dataFile, tempFile, backupFile); + } + + @Contract("null -> new") + protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) { + if (oldData == null) { + return new CompoundTag(); + } + + // Copy old data. This is a deep clone, so operating on it should be safe. + oldData = oldData.copy(); + + // Remove vanilla/server data that is not written every time. + oldData.keySet().removeIf( + key -> RESET_TAGS.contains(key) + || key.startsWith("Bukkit") + || (key.startsWith("Paper") && key.length() > 5) + ); + + return oldData; + } + + protected void revertSpecialValues(@NotNull CompoundTag newData, @Nullable CompoundTag oldData) { + if (oldData == null) { + return; + } + + // Revert automatic updates to play timestamps. + copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class); + copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class); + copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class); + } + + private void copyValue( + @NotNull CompoundTag source, + @NotNull CompoundTag target, + @NotNull String container, + @NotNull String key, + @SuppressWarnings("SameParameterValue") @NotNull Class tagType + ) { + CompoundTag oldContainer = getTag(source, container, CompoundTag.class); + CompoundTag newContainer = getTag(target, container, CompoundTag.class); + + // New container being null means the server implementation doesn't store this data. + if (newContainer == null) { + return; + } + + // If old tag exists, copy it to new location, removing otherwise. + setTag(newContainer, key, getTag(oldContainer, key, tagType)); + } + + private @Nullable T getTag( + @Nullable CompoundTag container, + @NotNull String key, + @NotNull Class dataType + ) { + if (container == null) { + return null; + } + Tag value = container.get(key); + if (value == null || !dataType.isAssignableFrom(value.getClass())) { + return null; + } + return dataType.cast(value); + } + + private void setTag( + @NotNull CompoundTag container, + @NotNull String key, + @Nullable T data + ) { + if (data == null) { + remove(container, key); + } else { + container.put(key, data); + } + } + + protected abstract void remove(@NotNull CompoundTag tag, @NotNull String key); + + public static boolean isConnected(@Nullable ServerGamePacketListenerImpl connection) { + return connection != null && !connection.isDisconnected(); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java new file mode 100644 index 00000000..80f87129 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/OpenPlayer.java @@ -0,0 +1,50 @@ +package com.lishid.openinv.internal.common.player; + +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.level.storage.PlayerDataStorage; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.level.storage.ValueOutput; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +public class OpenPlayer extends BaseOpenPlayer { + + protected OpenPlayer( + CraftServer server, + ServerPlayer entity, + PlayerManager manager + ) { + super(server, entity, manager); + } + + @Override + protected void trySave(ServerPlayer player) { + Logger logger = LogUtils.getLogger(); + // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman) + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) { + PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo; + + CompoundTag oldData = isOnline() + ? null + : worldNbtStorage.load(player.nameAndId()).orElse(null); + CompoundTag playerData = getWritableTag(oldData); + + ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData); + player.saveWithoutId(valueOutput); + + saveSafe(player, oldData, playerData, worldNbtStorage); + } catch (Exception e) { + LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e); + } + } + + @Override + protected void remove(@NotNull CompoundTag tag, @NotNull String key) { + tag.remove(key); + } + +} diff --git a/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java new file mode 100644 index 00000000..b43348a5 --- /dev/null +++ b/internal/common/src/main/java/com/lishid/openinv/internal/common/player/PlayerManager.java @@ -0,0 +1,287 @@ +package com.lishid.openinv.internal.common.player; + +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import com.lishid.openinv.internal.common.container.OpenEnderChest; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.util.JulLoggerAdapter; +import com.mojang.authlib.GameProfile; +import io.papermc.paper.adventure.PaperAdventure; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ParticleStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.ChatVisiblity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.LevelData; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.ValueInput; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.craftbukkit.event.CraftEventFactory; +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Logger; + +public class PlayerManager implements com.lishid.openinv.internal.PlayerManager { + + protected final @NotNull Logger logger; + protected @Nullable Field bukkitEntity; + + public PlayerManager(@NotNull Logger logger) { + this.logger = logger; + try { + bukkitEntity = Entity.class.getDeclaredField("bukkitEntity"); + } catch (NoSuchFieldException e) { + logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!"); + logger.log(java.util.logging.Level.WARNING, e.getMessage(), e); + bukkitEntity = null; + } + } + + public static @NotNull ServerPlayer getHandle(final Player player) { + if (player instanceof CraftPlayer craftPlayer) { + return craftPlayer.getHandle(); + } + + Server server = player.getServer(); + ServerPlayer nmsPlayer = null; + + if (server instanceof CraftServer craftServer) { + nmsPlayer = craftServer.getHandle().getPlayer(player.getUniqueId()); + } + + if (nmsPlayer == null) { + // Could use reflection to examine fields, but it's honestly not worth the bother. + throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName()); + } + + return nmsPlayer; + } + + @Override + public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { + if (!(Bukkit.getServer() instanceof CraftServer craftServer)) { + return null; + } + + MinecraftServer server = craftServer.getServer(); + ServerLevel worldServer = server.getLevel(Level.OVERWORLD); + + if (worldServer == null) { + return null; + } + + // Create a new ServerPlayer. + ServerPlayer entity = createNewPlayer(server, worldServer, offline); + + // Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak. + entity.getAdvancements().stopListening(); + + // Try to load the player's data. + if (loadData(server, entity)) { + // If data is loaded successfully, return the Bukkit entity. + return entity.getBukkitEntity(); + } + + return null; + } + + protected @NotNull ServerPlayer createNewPlayer( + @NotNull MinecraftServer server, + @NotNull ServerLevel worldServer, + @NotNull final OfflinePlayer offline + ) { + // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile) + // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket) + GameProfile profile = new GameProfile(offline.getUniqueId(), + offline.getName() != null ? offline.getName() : offline.getUniqueId().toString() + ); + + ClientInformation dummyInfo = new ClientInformation( + "en_us", + 1, // Reduce distance just in case. + ChatVisiblity.HIDDEN, // Don't accept chat. + false, + ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION, + ServerPlayer.DEFAULT_MAIN_HAND, + true, + false, // Don't list in player list (not that this player is in the list anyway). + ParticleStatus.MINIMAL + ); + + ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo); + + try { + injectPlayer(server, entity); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + } + + return entity; + } + + protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + // See CraftPlayer#loadData + + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) { + CompoundTag loadedData = server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null); + + if (loadedData == null) { + // Exceptions with loading are logged. + return false; + } + + ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData); + + // Read basic data into the player. + player.load(valueInput); + + // World is not loaded by ServerPlayer#load(CompoundTag) on Paper. + parseWorld(server, player, valueInput); + } + + return true; + } + + protected void parseWorld( + @NotNull MinecraftServer server, + @NotNull ServerPlayer player, + @NotNull ValueInput loadedData + ) { + // See PlayerList#placeNewPlayer + World bukkitWorld; + Optional msbs = loadedData.getLong("WorldUUIDMost"); + Optional lsbs = loadedData.getLong("WorldUUIDLeast"); + if (msbs.isPresent() && lsbs.isPresent()) { + // Modern Bukkit world. + bukkitWorld = Bukkit.getServer().getWorld(new UUID(msbs.get(), lsbs.get())); + } else { + bukkitWorld = loadedData.getString("world").map(Bukkit::getWorld).orElse(null); + } + if (bukkitWorld == null) { + spawnInDefaultWorld(server, player); + return; + } + player.setServerLevel(((CraftWorld) bukkitWorld).getHandle()); + } + + protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + ServerLevel level = server.getLevel(Level.OVERWORLD); + if (level != null) { + // Adjust player to default spawn (in keeping with Paper handling) when world not found. + LevelData.RespawnData respawnData = level.levelData.getRespawnData(); + player.snapTo(player.adjustSpawnLocation(level, respawnData.pos()).getBottomCenter(), respawnData.yaw(), 0.0F); + player.spawnIn(level); + } else { + logger.warning("Tried to load player with invalid world when no fallback was available!"); + } + } + + protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + + bukkitEntity.set(player, new OpenPlayer(server.server, player, this)); + } + + @Override + public @NotNull Player inject(@NotNull Player player) { + try { + ServerPlayer nmsPlayer = getHandle(player); + if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) { + return openPlayer; + } + MinecraftServer server = nmsPlayer.level().getServer(); + injectPlayer(server, nmsPlayer); + return nmsPlayer.getBukkitEntity(); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + return player; + } + } + + @Override + public @Nullable InventoryView openInventory( + @NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory, + boolean viewOnly + ) { + ServerPlayer player = getHandle(bukkitPlayer); + + if (!BaseOpenPlayer.isConnected(player.connection)) { + return null; + } + + // See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider) + OpenChestMenu menu; + Component title; + if (inventory instanceof BaseOpenInventory playerInv) { + menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly); + title = playerInv.getTitle(player, menu); + } else if (inventory instanceof OpenEnderChest enderChest) { + menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly); + title = enderChest.getTitle(menu); + } else { + return null; + } + + // Should never happen, player is a ServerPlayer with an active connection. + if (menu == null) { + return null; + } + + // Set up title. Title can only be set once for a menu, and is set during the open process. + // Further title changes are a hack where the client is sent a "new" inventory with the same ID, + // resulting in a title change but no other state modifications (like cursor position). + menu.setTitle(title); + + var pair = CraftEventFactory.callInventoryOpenEventWithTitle(player, menu); + AbstractContainerMenu opened = pair.getSecond(); + + // Menu is null if event is cancelled. + if (opened == null) { + return null; + } + + var newTitle = pair.getFirst(); + if (newTitle != null) { + title = PaperAdventure.asVanilla(newTitle); + } + + player.containerMenu = opened; + player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), title)); + player.initMenu(opened); + + return opened.getBukkitView(); + } + +} diff --git a/internal/paper1_21_1/build.gradle.kts b/internal/paper1_21_1/build.gradle.kts new file mode 100644 index 00000000..84ba9a0d --- /dev/null +++ b/internal/paper1_21_1/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + implementation(project(":openinvadapterpaper1_21_8")) + implementation(project(":openinvadapterpaper1_21_5")) + implementation(project(":openinvadapterpaper1_21_4")) + implementation(project(":openinvadapterpaper1_21_3")) + + paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java new file mode 100644 index 00000000..100bf31f --- /dev/null +++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/InternalAccessor.java @@ -0,0 +1,80 @@ +package com.lishid.openinv.internal.paper1_21_1; + +import com.lishid.openinv.internal.Accessor; +import com.lishid.openinv.internal.IAnySilentContainer; +import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.common.container.AnySilentContainer; +import com.lishid.openinv.internal.paper1_21_1.container.OpenInventory; +import com.lishid.openinv.internal.paper1_21_1.container.slot.placeholder.PlaceholderLoader; +import com.lishid.openinv.internal.paper1_21_1.player.PlayerManager; +import com.lishid.openinv.internal.paper1_21_4.container.OpenEnderChest; +import com.lishid.openinv.util.lang.LanguageManager; +import net.minecraft.world.Container; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalAccessor implements Accessor { + + private final @NotNull Logger logger; + private final @NotNull PlayerManager manager; + private final @NotNull AnySilentContainer anySilentContainer; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + this.logger = logger; + this.manager = new PlayerManager(logger); + this.anySilentContainer = new AnySilentContainer(logger, lang); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public @NotNull IAnySilentContainer getAnySilentContainer() { + return anySilentContainer; + } + + @Override + public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) { + return new OpenInventory(player); + } + + @Override + public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) { + return new OpenEnderChest(player); + } + + @Override + public @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz) { + if (!(bukkitInventory instanceof CraftInventory craftInventory)) { + return null; + } + Container container = craftInventory.getInventory(); + if (clazz.isInstance(container)) { + return clazz.cast(container); + } + return null; + } + + @Override + public void reload(@NotNull ConfigurationSection config) { + ConfigurationSection placeholders = config.getConfigurationSection("placeholders"); + try { + // Reset placeholders to defaults and try to load configuration. + new PlaceholderLoader().load(placeholders); + } catch (Exception e) { + logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e); + } + } + +} diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java new file mode 100644 index 00000000..1368d89a --- /dev/null +++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/OpenInventory.java @@ -0,0 +1,20 @@ +package com.lishid.openinv.internal.paper1_21_1.container; + +import com.lishid.openinv.internal.common.container.slot.Content; +import com.lishid.openinv.internal.paper1_21_1.container.slot.ContentCraftingResult; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class OpenInventory extends com.lishid.openinv.internal.paper1_21_4.container.OpenInventory { + + public OpenInventory(@NotNull Player bukkitPlayer) { + super(bukkitPlayer); + } + + @Override + protected Content getCraftingResult(@NotNull ServerPlayer serverPlayer) { + return new ContentCraftingResult(serverPlayer); + } + +} diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java new file mode 100644 index 00000000..d0d74f74 --- /dev/null +++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/ContentCraftingResult.java @@ -0,0 +1,46 @@ +package com.lishid.openinv.internal.paper1_21_1.container.slot; + +import com.lishid.openinv.internal.common.container.slot.ContentCrafting; +import com.lishid.openinv.internal.common.container.slot.ContentViewOnly; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +public class ContentCraftingResult extends ContentViewOnly { + + public ContentCraftingResult(@NotNull ServerPlayer holder) { + super(holder); + } + + @Override + public ItemStack get() { + InventoryMenu inventoryMenu = holder.inventoryMenu; + return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem(); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotViewOnly(container, slot, x, y) { + @Override + public ItemStack getOrDefault() { + if (!ContentCrafting.isAvailable(holder)) { + return Placeholders.survivalOnly(holder); + } + InventoryMenu inventoryMenu = holder.inventoryMenu; + return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem(); + } + }; + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.RESULT; + } + +} diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java new file mode 100644 index 00000000..0ac03f26 --- /dev/null +++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/container/slot/placeholder/PlaceholderLoader.java @@ -0,0 +1,74 @@ +package com.lishid.openinv.internal.paper1_21_1.container.slot.placeholder; + +import com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder.NumericDataPlaceholderLoader; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +import net.minecraft.util.Unit; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.entity.BannerPattern; +import net.minecraft.world.level.block.entity.BannerPatternLayers; +import net.minecraft.world.level.block.entity.BannerPatterns; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PlaceholderLoader extends NumericDataPlaceholderLoader { + + @Override + protected @NotNull ItemStack defaultCursor() { + // Cursor-like banner with no tooltip + ItemStack itemStack = new ItemStack(Items.WHITE_BANNER); + RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry(); + Registry bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN); + BannerPattern halfDiagBottomRight = bannerPatterns.getOrThrow(BannerPatterns.DIAGONAL_RIGHT); + BannerPattern downRight = bannerPatterns.getOrThrow(BannerPatterns.STRIPE_DOWNRIGHT); + BannerPattern border = bannerPatterns.getOrThrow(BannerPatterns.BORDER); + itemStack.set( + DataComponents.BANNER_PATTERNS, + new BannerPatternLayers( + List.of( + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfDiagBottomRight), DyeColor.GRAY), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(downRight), DyeColor.WHITE), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(border), DyeColor.GRAY) + ) + ) + ); + addModelData(itemStack); + itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE); + return itemStack; + } + + @Override + protected @NotNull ItemStack defaultShield() { + // Shield with "missing texture" pattern, magenta and black squares. + ItemStack itemStack = new ItemStack(Items.SHIELD); + itemStack.set(DataComponents.BASE_COLOR, DyeColor.MAGENTA); + RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry(); + Registry bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN); + BannerPattern halfLeft = bannerPatterns.getOrThrow(BannerPatterns.HALF_VERTICAL); + BannerPattern topLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_LEFT); + BannerPattern topRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_RIGHT); + BannerPattern bottomLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_LEFT); + BannerPattern bottomRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_RIGHT); + itemStack.set(DataComponents.BANNER_PATTERNS, + new BannerPatternLayers( + List.of( + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfLeft), DyeColor.BLACK), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topLeft), DyeColor.MAGENTA), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomLeft), DyeColor.MAGENTA), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topRight), DyeColor.BLACK), + new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomRight), DyeColor.BLACK) + ) + ) + ); + itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE); + addModelData(itemStack); + return itemStack; + } + +} diff --git a/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java new file mode 100644 index 00000000..61d78002 --- /dev/null +++ b/internal/paper1_21_1/src/main/java/com/lishid/openinv/internal/paper1_21_1/player/PlayerManager.java @@ -0,0 +1,58 @@ +package com.lishid.openinv.internal.paper1_21_1.player; + +import com.mojang.authlib.GameProfile; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.ChatVisiblity; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class PlayerManager extends com.lishid.openinv.internal.paper1_21_4.player.PlayerManager { + + public PlayerManager(@NotNull Logger logger) { + super(logger); + } + + @Override + protected @NotNull ServerPlayer createNewPlayer( + @NotNull MinecraftServer server, + @NotNull ServerLevel worldServer, + @NotNull final OfflinePlayer offline + ) { + // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile) + // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket) + GameProfile profile = new GameProfile(offline.getUniqueId(), + offline.getName() != null ? offline.getName() : offline.getUniqueId().toString() + ); + + ClientInformation dummyInfo = new ClientInformation( + "en_us", + 1, // Reduce distance just in case. + ChatVisiblity.HIDDEN, // Don't accept chat. + false, + ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION, + ServerPlayer.DEFAULT_MAIN_HAND, + true, + false // Don't list in player list (not that this player is in the list anyway). + ); + + ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo); + + try { + injectPlayer(server, entity); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + } + + return entity; + } + +} diff --git a/internal/paper1_21_10/build.gradle.kts b/internal/paper1_21_10/build.gradle.kts new file mode 100644 index 00000000..d727317a --- /dev/null +++ b/internal/paper1_21_10/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + + paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java new file mode 100644 index 00000000..b6333bf0 --- /dev/null +++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/InternalAccessor.java @@ -0,0 +1,31 @@ +package com.lishid.openinv.internal.paper1_21_10; + +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.paper1_21_10.container.OpenInventory; +import com.lishid.openinv.internal.paper1_21_10.player.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor { + + private final @NotNull PlayerManager manager; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + super(logger, lang); + manager = new PlayerManager(logger); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) { + return new OpenInventory(player); + } + +} diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java new file mode 100644 index 00000000..3210726c --- /dev/null +++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/container/OpenInventory.java @@ -0,0 +1,50 @@ +package com.lishid.openinv.internal.paper1_21_10.container; + +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FontDescription; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventory extends BaseOpenInventory { + + public OpenInventory(@NotNull Player bukkitPlayer) { + super(bukkitPlayer); + } + + @Override + public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu menu) { + MutableComponent component = Component.empty(); + // Prefix for use with custom bitmap image fonts. + if (owner.equals(viewer)) { + component.append( + Component.translatableWithFallback("openinv.container.inventory.self", "") + .withStyle(style -> style + .withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory"))) + .withColor(ChatFormatting.WHITE))); + } else { + component.append( + Component.translatableWithFallback("openinv.container.inventory.other", "") + .withStyle(style -> style + .withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory"))) + .withColor(ChatFormatting.WHITE))); + } + if (menu != null && menu.isViewOnly()) { + component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] ")); + } else { + component.append(Component.translatableWithFallback("openinv.container.inventory.editable", "")); + } + // Normal title: "Inventory - OwnerName" + component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName())) + .append(Component.translatable("container.inventory")) + .append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName())); + return component; + } + +} diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java new file mode 100644 index 00000000..0595e9b0 --- /dev/null +++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/OpenPlayer.java @@ -0,0 +1,35 @@ +package com.lishid.openinv.internal.paper1_21_10.player; + +import com.lishid.openinv.internal.common.player.PlayerManager; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +public class OpenPlayer extends com.lishid.openinv.internal.common.player.OpenPlayer { + + protected OpenPlayer( + CraftServer server, + ServerPlayer entity, + PlayerManager manager + ) { + super(server, entity, manager); + } + + @Override + protected void safeReplaceFile( + @NotNull Path dataFile, + @NotNull Path tempFile, + @NotNull Path backupFile + ) { + net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile); + } + + @Override + protected void remove(@NotNull CompoundTag tag, @NotNull String key) { + tag.remove(key); + } + +} diff --git a/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java new file mode 100644 index 00000000..3382bfaf --- /dev/null +++ b/internal/paper1_21_10/src/main/java/com/lishid/openinv/internal/paper1_21_10/player/PlayerManager.java @@ -0,0 +1,26 @@ +package com.lishid.openinv.internal.paper1_21_10.player; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager { + + public PlayerManager(@NotNull Logger logger) { + super(logger); + } + + @Override + protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + + bukkitEntity.set(player, new OpenPlayer(server.server, player, this)); + } + +} diff --git a/internal/paper1_21_3/build.gradle.kts b/internal/paper1_21_3/build.gradle.kts new file mode 100644 index 00000000..86fa2f13 --- /dev/null +++ b/internal/paper1_21_3/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + implementation(project(":openinvadapterpaper1_21_8")) + implementation(project(":openinvadapterpaper1_21_5")) + implementation(project(":openinvadapterpaper1_21_4")) + + paperweight.paperDevBundle("1.21.3-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java new file mode 100644 index 00000000..e72aeb0a --- /dev/null +++ b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/InternalAccessor.java @@ -0,0 +1,28 @@ +package com.lishid.openinv.internal.paper1_21_3; + +import com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder.NumericDataPlaceholderLoader; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalAccessor extends com.lishid.openinv.internal.paper1_21_4.InternalAccessor { + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + super(logger, lang); + } + + @Override + public void reload(@NotNull ConfigurationSection config) { + ConfigurationSection placeholders = config.getConfigurationSection("placeholders"); + try { + // Reset placeholders to defaults and try to load configuration. + new NumericDataPlaceholderLoader().load(placeholders); + } catch (Exception e) { + logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e); + } + } + +} diff --git a/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java new file mode 100644 index 00000000..d19552a4 --- /dev/null +++ b/internal/paper1_21_3/src/main/java/com/lishid/openinv/internal/paper1_21_3/container/slot/placeholder/NumericDataPlaceholderLoader.java @@ -0,0 +1,12 @@ +package com.lishid.openinv.internal.paper1_21_3.container.slot.placeholder; + +import com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder.CustomModelBase; +import net.minecraft.world.item.component.CustomModelData; + +public class NumericDataPlaceholderLoader extends CustomModelBase { + + public NumericDataPlaceholderLoader() { + super(new CustomModelData(9999)); + } + +} diff --git a/internal/paper1_21_4/build.gradle.kts b/internal/paper1_21_4/build.gradle.kts new file mode 100644 index 00000000..761ada30 --- /dev/null +++ b/internal/paper1_21_4/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + implementation(project(":openinvadapterpaper1_21_8")) + implementation(project(":openinvadapterpaper1_21_5")) + + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java new file mode 100644 index 00000000..98b09873 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/InternalAccessor.java @@ -0,0 +1,80 @@ +package com.lishid.openinv.internal.paper1_21_4; + +import com.lishid.openinv.internal.Accessor; +import com.lishid.openinv.internal.IAnySilentContainer; +import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.common.container.AnySilentContainer; +import com.lishid.openinv.internal.paper1_21_4.container.OpenEnderChest; +import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory; +import com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder.CustomModelPlaceholderLoader; +import com.lishid.openinv.internal.paper1_21_4.player.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import net.minecraft.world.Container; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalAccessor implements Accessor { + + protected final @NotNull Logger logger; + private final @NotNull PlayerManager manager; + private final @NotNull AnySilentContainer anySilentContainer; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + this.logger = logger; + manager = new PlayerManager(logger); + anySilentContainer = new AnySilentContainer(logger, lang); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public @NotNull IAnySilentContainer getAnySilentContainer() { + return anySilentContainer; + } + + @Override + public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) { + return new OpenInventory(player); + } + + @Override + public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) { + return new OpenEnderChest(player); + } + + @Override + public @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class clazz) { + if (!(bukkitInventory instanceof CraftInventory craftInventory)) { + return null; + } + Container container = craftInventory.getInventory(); + if (clazz.isInstance(container)) { + return clazz.cast(container); + } + return null; + } + + @Override + public void reload(@NotNull ConfigurationSection config) { + ConfigurationSection placeholders = config.getConfigurationSection("placeholders"); + try { + // Reset placeholders to defaults and try to load configuration. + new CustomModelPlaceholderLoader().load(placeholders); + } catch (Exception e) { + logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e); + } + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java new file mode 100644 index 00000000..5ffd2665 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenEnderChest.java @@ -0,0 +1,28 @@ +package com.lishid.openinv.internal.paper1_21_4.container; + +import com.lishid.openinv.internal.paper1_21_4.container.menu.OpenEnderChestMenu; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenEnderChest extends com.lishid.openinv.internal.common.container.OpenEnderChest { + + public OpenEnderChest(@NotNull Player player) { + super(player); + } + + @Override + public @Nullable OpenEnderChestMenu createMenu( + net.minecraft.world.entity.player.Player player, + int i, + boolean viewOnly + ) { + if (player instanceof ServerPlayer serverPlayer) { + return new OpenEnderChestMenu(this, serverPlayer, i, viewOnly); + } + return null; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java new file mode 100644 index 00000000..4eea3374 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/OpenInventory.java @@ -0,0 +1,191 @@ +package com.lishid.openinv.internal.paper1_21_4.container; + +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.container.slot.ContentCrafting; +import com.lishid.openinv.internal.common.container.slot.ContentCursor; +import com.lishid.openinv.internal.common.container.slot.ContentDrop; +import com.lishid.openinv.internal.common.container.slot.ContentList; +import com.lishid.openinv.internal.common.container.slot.ContentViewOnly; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import com.lishid.openinv.internal.paper1_21_4.container.bukkit.OpenPlayerInventory; +import com.lishid.openinv.internal.paper1_21_4.container.menu.OpenInventoryMenu; +import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentEquipment; +import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentOffHand; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventory extends com.lishid.openinv.internal.paper1_21_8.container.OpenInventory { + + public OpenInventory(@NotNull org.bukkit.entity.Player bukkitPlayer) { + super(bukkitPlayer); + } + + @Override + protected void setupSlots() { + // Top of inventory: Regular contents. + int nextIndex = addMainInventory(); + + // If inventory is expected size, we can arrange slots to be pretty. + Inventory ownerInv = owner.getInventory(); + if (ownerInv.items.size() == 36 + && ownerInv.armor.size() == 4 + && ownerInv.offhand.size() == 1 + && owner.inventoryMenu.getCraftSlots().getContainerSize() == 4) { + // Armor slots: Bottom left. + addArmor(36); + // Off-hand: Below chestplate. + addOffHand(46); + // Drop slot: Bottom right. + slots.set(53, new ContentDrop(owner)); + // Cursor slot: Above drop. + slots.set(44, new ContentCursor(owner)); + + // Crafting is displayed in the bottom right corner. + // As we're using the pretty view, this is a 3x2. + addCrafting(41, true); + return; + } + + // Otherwise we'll just add elements linearly. + nextIndex = addArmor(nextIndex); + nextIndex = addOffHand(nextIndex); + nextIndex = addCrafting(nextIndex, false); + slots.set(nextIndex, new ContentCursor(owner)); + // Drop slot last. + slots.set(slots.size() - 1, new ContentDrop(owner)); + } + + private int addMainInventory() { + int listSize = owner.getInventory().items.size(); + // Hotbar slots are 0-8. We want those to appear on the bottom of the inventory like a normal player inventory, + // so everything else needs to move up a row. + int hotbarDiff = listSize - 9; + for (int localIndex = 0; localIndex < listSize; ++localIndex) { + InventoryType.SlotType type; + int invIndex; + if (localIndex < hotbarDiff) { + invIndex = localIndex + 9; + type = InventoryType.SlotType.CONTAINER; + } else { + type = InventoryType.SlotType.QUICKBAR; + invIndex = localIndex - hotbarDiff; + } + + slots.set( + localIndex, + new ContentList(owner, invIndex, type) { + @Override + public void setHolder(@NotNull ServerPlayer holder) { + items = holder.getInventory().items; + } + } + ); + } + return listSize; + } + + private int addArmor(int startIndex) { + int listSize = owner.getInventory().armor.size(); + + for (int i = 0; i < listSize; ++i) { + // Armor slots go bottom to top; boots are slot 0, helmet is slot 3. + // Since we have to display horizontally due to space restrictions, + // making the left side the "top" is more user-friendly. + int armorIndex; + EquipmentSlot slot; + switch (i) { + case 3 -> { + armorIndex = 0; + slot = EquipmentSlot.FEET; + } + case 2 -> { + armorIndex = 1; + slot = EquipmentSlot.LEGS; + } + case 1 -> { + armorIndex = 2; + slot = EquipmentSlot.CHEST; + } + case 0 -> { + armorIndex = 3; + slot = EquipmentSlot.HEAD; + } + default -> { + // In the event that new armor slots are added, they can be placed at the end. + armorIndex = i; + slot = EquipmentSlot.MAINHAND; + } + } + + slots.set(startIndex + i, new ContentEquipment(owner, armorIndex, slot)); + } + + return startIndex + listSize; + } + + private int addOffHand(int startIndex) { + int listSize = owner.getInventory().offhand.size(); + for (int localIndex = 0; localIndex < listSize; ++localIndex) { + slots.set(startIndex + localIndex, new ContentOffHand(owner, localIndex)); + } + return startIndex + listSize; + } + + private int addCrafting(int startIndex, boolean pretty) { + int listSize = owner.inventoryMenu.getCraftSlots().getContents().size(); + pretty &= listSize == 4; + + for (int localIndex = 0; localIndex < listSize; ++localIndex) { + // Pretty display is a 2x2 rather than linear. + // If index is in top row, grid is not 2x2, or pretty is disabled, just use current index. + // Otherwise, subtract 2 and add 9 to start in the same position on the next row. + int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7); + + slots.set(modIndex, new ContentCrafting(owner, localIndex)); + } + + if (pretty) { + slots.set(startIndex + 2, new ContentViewOnly(owner) { + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotViewOnly(container, slot, x, y) { + @Override + public ItemStack getOrDefault() { + return Placeholders.craftingOutput; + } + }; + } + } + ); + slots.set(startIndex + 11, getCraftingResult(owner)); + } + + return startIndex + listSize; + } + + @Override + public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() { + if (bukkitEntity == null) { + bukkitEntity = new OpenPlayerInventory(this); + } + return bukkitEntity; + } + + @Override + public @Nullable OpenChestMenu createMenu(Player player, int i, boolean viewOnly) { + if (player instanceof ServerPlayer serverPlayer) { + return new OpenInventoryMenu(this, serverPlayer, i, viewOnly); + } + return null; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java new file mode 100644 index 00000000..9cd8fbc0 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventory.java @@ -0,0 +1,225 @@ +package com.lishid.openinv.internal.paper1_21_4.container.bukkit; + +import com.google.common.base.Preconditions; +import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory; +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Inventory; +import org.bukkit.craftbukkit.inventory.CraftInventory; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenPlayerInventory extends CraftInventory implements PlayerInventory { + + public OpenPlayerInventory(@NotNull OpenInventory inventory) { + super(inventory); + } + + @Override + public @NotNull OpenInventory getInventory() { + return (OpenInventory) super.getInventory(); + } + + @Override + public ItemStack @NotNull [] getContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents()); + } + + @Override + public void setContents(ItemStack[] items) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + int size = internal.getContainerSize(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + + for (int index = 0; index < size; ++index) { + if (index < items.length) { + internal.setItem(index, CraftItemStack.asNMSCopy(items[index])); + } else { + internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + + @Override + public ItemStack @NotNull [] getStorageContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().items); + } + + @Override + public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { + NonNullList list = getInventory().getOwnerHandle().getInventory().items; + int size = list.size(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + list.set(index, CraftItemStack.asNMSCopy(items[index])); + } + } + + @Override + public @NotNull InventoryType getType() { + return InventoryType.PLAYER; + } + + @Override + public @NotNull Player getHolder() { + return getInventory().getOwner(); + } + + @Override + public @NotNull ItemStack @NotNull [] getArmorContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().armor); + } + + @Override + public void setArmorContents(ItemStack @NotNull [] items) { + NonNullList list = getInventory().getOwnerHandle().getInventory().armor; + int size = list.size(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + list.set(index, CraftItemStack.asNMSCopy(items[index])); + } + } + + @Override + public @NotNull ItemStack @NotNull [] getExtraContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand); + } + + @Override + public void setExtraContents(ItemStack @NotNull [] items) { + NonNullList list = getInventory().getOwnerHandle().getInventory().offhand; + int size = list.size(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + list.set(index, CraftItemStack.asNMSCopy(items[index])); + } + } + + @Override + public @NotNull ItemStack getHelmet() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory() + .getArmor(EquipmentSlot.HEAD.getIndex())); + } + + @Override + public void setHelmet(@Nullable ItemStack helmet) { + getInventory().getOwnerHandle().getInventory().armor + .set(EquipmentSlot.HEAD.getIndex(), CraftItemStack.asNMSCopy(helmet)); + } + + @Override + public @NotNull ItemStack getChestplate() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory() + .getArmor(EquipmentSlot.CHEST.getIndex())); + } + + @Override + public void setChestplate(@Nullable ItemStack chestplate) { + getInventory().getOwnerHandle().getInventory().armor + .set(EquipmentSlot.CHEST.getIndex(), CraftItemStack.asNMSCopy(chestplate)); + } + + @Override + public @NotNull ItemStack getLeggings() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory() + .getArmor(EquipmentSlot.LEGS.getIndex())); + } + + @Override + public void setLeggings(@Nullable ItemStack leggings) { + getInventory().getOwnerHandle().getInventory().armor + .set(EquipmentSlot.LEGS.getIndex(), CraftItemStack.asNMSCopy(leggings)); + } + + @Override + public @NotNull ItemStack getBoots() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory() + .getArmor(EquipmentSlot.FEET.getIndex())); + } + + @Override + public void setBoots(@Nullable ItemStack boots) { + getInventory().getOwnerHandle().getInventory().armor + .set(EquipmentSlot.FEET.getIndex(), CraftItemStack.asNMSCopy(boots)); + } + + @Override + public @NotNull ItemStack getItemInMainHand() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return CraftItemStack.asCraftMirror(internal.getItem(internal.selected)); + } + + @Override + public void setItemInMainHand(@Nullable ItemStack item) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + internal.setItem(internal.selected, CraftItemStack.asNMSCopy(item)); + } + + @Override + public @NotNull ItemStack getItemInOffHand() { + return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand.getFirst()); + } + + @Override + public void setItemInOffHand(@Nullable ItemStack item) { + getInventory().getOwnerHandle().getInventory().offhand.set(0, CraftItemStack.asNMSCopy(item)); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public @NotNull ItemStack getItemInHand() { + return getItemInMainHand(); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public void setItemInHand(@Nullable ItemStack stack) { + setItemInMainHand(stack); + } + + @Override + public int getHeldItemSlot() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return internal.items.size() - 9 + internal.selected; + } + + @Override + public void setHeldItemSlot(int slot) { + slot %= 9; + getInventory().getOwnerHandle().getInventory().selected = slot; + } + + @Override + public @NotNull ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) { + return switch (slot) { + case HAND -> getItemInMainHand(); + case OFF_HAND -> getItemInOffHand(); + case FEET -> getBoots(); + case LEGS -> getLeggings(); + case CHEST -> getChestplate(); + case HEAD -> getHelmet(); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + }; + } + + @Override + public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) { + switch (slot) { + case HAND -> setItemInMainHand(item); + case OFF_HAND -> setItemInOffHand(item); + case FEET -> setBoots(item); + case LEGS -> setLeggings(item); + case CHEST -> setChestplate(item); + case HEAD -> setHelmet(item); + default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot); + } + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java new file mode 100644 index 00000000..d6e0ce0b --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/bukkit/OpenPlayerInventorySelf.java @@ -0,0 +1,26 @@ +package com.lishid.openinv.internal.paper1_21_4.container.bukkit; + +import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class OpenPlayerInventorySelf extends OpenPlayerInventory { + + private final int offset; + + public OpenPlayerInventorySelf(@NotNull OpenInventory inventory, int offset) { + super(inventory); + this.offset = offset; + } + + @Override + public ItemStack getItem(int index) { + return super.getItem(offset + index); + } + + @Override + public void setItem(int index, ItemStack item) { + super.setItem(offset + index, item); + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java new file mode 100644 index 00000000..6ba23c14 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenEnderChestMenu.java @@ -0,0 +1,61 @@ +package com.lishid.openinv.internal.paper1_21_4.container.menu; + +import com.lishid.openinv.internal.common.container.OpenEnderChest; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class OpenEnderChestMenu extends OpenSyncMenu { + + public OpenEnderChestMenu( + @NotNull OpenEnderChest enderChest, + @NotNull ServerPlayer viewer, + int containerId, + boolean viewOnly + ) { + super( + OpenChestMenu.getChestMenuType(enderChest.getContainerSize()), + containerId, + enderChest, + viewer, + viewOnly + ); + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + if (viewOnly) { + return ItemStack.EMPTY; + } + + // See ChestMenu + Slot slot = this.slots.get(index); + + if (slot.isFake() || !slot.hasItem()) { + return ItemStack.EMPTY; + } + + ItemStack itemStack = slot.getItem(); + ItemStack original = itemStack.copy(); + + if (index < topSize) { + if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) { + return ItemStack.EMPTY; + } + } else if (!this.moveItemStackTo(itemStack, 0, topSize, false)) { + return ItemStack.EMPTY; + } + + if (itemStack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + return original; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java new file mode 100644 index 00000000..1bdbf469 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenInventoryMenu.java @@ -0,0 +1,266 @@ +package com.lishid.openinv.internal.paper1_21_4.container.menu; + +import com.google.common.base.Preconditions; +import com.lishid.openinv.internal.common.container.bukkit.OpenDummyPlayerInventory; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.container.slot.ContentDrop; +import com.lishid.openinv.internal.common.container.slot.SlotViewOnly; +import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory; +import com.lishid.openinv.internal.paper1_21_4.container.bukkit.OpenPlayerInventorySelf; +import com.lishid.openinv.internal.paper1_21_4.container.slot.ContentEquipment; +import com.lishid.openinv.util.Permissions; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.craftbukkit.inventory.CraftInventoryView; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventoryMenu extends OpenSyncMenu { + + private int offset; + + public OpenInventoryMenu(OpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) { + super(getMenuType(inventory, viewer), i, inventory, viewer, viewOnly); + } + + private static MenuType getMenuType(OpenInventory inventory, ServerPlayer viewer) { + int size = inventory.getContainerSize(); + // Disallow duplicate access to own main inventory contents. + if (inventory.getOwnerHandle().equals(viewer)) { + size -= viewer.getInventory().items.size(); + size = ((int) Math.ceil(size / 9.0)) * 9; + } + + return OpenChestMenu.getChestMenuType(size); + } + + @Override + protected void preSlotSetup() { + offset = ownContainer ? viewer.getInventory().items.size() : 0; + } + + @Override + protected @NotNull Slot getUpperSlot(int index, int x, int y) { + index += offset; + Slot slot = container.getMenuSlot(index, x, y); + + // If the slot cannot be interacted with there's nothing to configure. + if (slot.getClass().equals(SlotViewOnly.class)) { + return slot; + } + + // Remove drop slot if viewer is not allowed to use it. + if (slot instanceof ContentDrop.SlotDrop + && (viewOnly || !Permissions.INVENTORY_SLOT_DROP.hasPermission(viewer.getBukkitEntity()))) { + return new SlotViewOnly(container, index, x, y); + } + + if (slot instanceof ContentEquipment.SlotEquipment equipment) { + if (viewOnly) { + return SlotViewOnly.wrap(slot); + } + + Permissions perm = switch (equipment.getEquipmentSlot()) { + case HEAD -> Permissions.INVENTORY_SLOT_HEAD_ANY; + case CHEST -> Permissions.INVENTORY_SLOT_CHEST_ANY; + case LEGS -> Permissions.INVENTORY_SLOT_LEGS_ANY; + case FEET -> Permissions.INVENTORY_SLOT_FEET_ANY; + // Off-hand can hold anything, not just equipment. + default -> null; + }; + + // If the viewer doesn't have permission, only allow equipment the viewee can equip in the slot. + if (perm != null && !perm.hasPermission(viewer.getBukkitEntity())) { + equipment.onlyEquipmentFor(container.getOwnerHandle()); + } + + // Equipment slots are a core part of the inventory, so they will always be shown. + return slot; + } + + // When viewing own inventory, only allow access to equipment and drop slots (equipment allowed above). + if (ownContainer && !(slot instanceof ContentDrop.SlotDrop)) { + return new SlotViewOnly(container, index, x, y); + } + + if (viewOnly) { + return SlotViewOnly.wrap(slot); + } + + return slot; + } + + @Override + protected @NotNull CraftInventoryView, Inventory> createBukkitEntity() { + org.bukkit.inventory.Inventory bukkitInventory; + if (viewOnly) { + bukkitInventory = new OpenDummyPlayerInventory(container); + } else if (ownContainer) { + bukkitInventory = new OpenPlayerInventorySelf(container, offset); + } else { + bukkitInventory = container.getBukkitInventory(); + } + + return new CraftInventoryView<>(viewer.getBukkitEntity(), bukkitInventory, this) { + @Override + public org.bukkit.inventory.ItemStack getItem(int index) { + if (viewOnly || index < 0) { + return null; + } + + Slot slot = slots.get(index); + return CraftItemStack.asCraftMirror(slot.hasItem() ? slot.getItem() : ItemStack.EMPTY); + } + + @Override + public boolean isInTop(int rawSlot) { + return rawSlot < topSize; + } + + @Override + public @Nullable Inventory getInventory(int rawSlot) { + if (viewOnly) { + return null; + } + if (rawSlot == InventoryView.OUTSIDE || rawSlot == -1) { + return null; + } + Preconditions.checkArgument( + rawSlot >= 0 && rawSlot < topSize + offset + BOTTOM_INVENTORY_SIZE, + "Slot %s outside of inventory", + rawSlot + ); + if (rawSlot > topSize) { + return getBottomInventory(); + } + Slot slot = slots.get(rawSlot); + if (slot.isFake()) { + return null; + } + return getTopInventory(); + } + + @Override + public int convertSlot(int rawSlot) { + if (viewOnly) { + return InventoryView.OUTSIDE; + } + if (rawSlot < 0) { + return rawSlot; + } + if (rawSlot < topSize) { + Slot slot = slots.get(rawSlot); + if (slot.isFake()) { + return InventoryView.OUTSIDE; + } + return rawSlot; + } + + int slot = rawSlot - topSize; + + if (slot >= 27) { + slot -= 27; + } else { + slot += 9; + } + + return slot; + } + + @Override + public @NotNull InventoryType.SlotType getSlotType(int slot) { + if (viewOnly || slot < 0) { + return InventoryType.SlotType.OUTSIDE; + } + if (slot >= topSize) { + slot -= topSize; + if (slot >= 27) { + return InventoryType.SlotType.QUICKBAR; + } + return InventoryType.SlotType.CONTAINER; + } + return OpenInventoryMenu.this.container.getSlotType(offset + slot); + } + + @Override + public int countSlots() { + return topSize + BOTTOM_INVENTORY_SIZE; + } + }; + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player player, int index) { + if (viewOnly) { + return ItemStack.EMPTY; + } + + // See ChestMenu and InventoryMenu + Slot slot = this.slots.get(index); + + if (!slot.hasItem() || slot.isFake()) { + return ItemStack.EMPTY; + } + + ItemStack itemStack = slot.getItem(); + ItemStack originalStack = itemStack.copy(); + + if (index < topSize) { + // If we're moving top to bottom, do a normal transfer. + if (!this.moveItemStackTo(itemStack, topSize, this.slots.size(), true)) { + return ItemStack.EMPTY; + } + } else { + EquipmentSlot equipmentSlot = player.getEquipmentSlotForItem(itemStack); + boolean movedGear = switch (equipmentSlot) { + // If this is gear, try to move it to the correct slot first. + case OFFHAND, FEET, LEGS, CHEST, HEAD -> { + // Locate the correct slot in the contents following the main inventory. + for (int extra = container.getOwnerHandle().getInventory().items.size() - offset; extra < topSize; ++extra) { + Slot extraSlot = getSlot(extra); + if (extraSlot instanceof ContentEquipment.SlotEquipment equipSlot + && equipSlot.getEquipmentSlot() == equipmentSlot) { + // If we've found a matching slot, try to move to it. + // If this succeeds, even partially, we will not attempt to move to other slots. + // Otherwise, armor is already occupied, so we'll fall through to main inventory. + yield this.moveItemStackTo(itemStack, extra, extra + 1, false); + } + } + yield false; + } + // Non-gear gets no special treatment. + default -> false; + }; + + // If main inventory is not available, there's nowhere else to move. + if (offset != 0) { + if (!movedGear) { + return ItemStack.EMPTY; + } + } else { + // If we didn't move to a gear slot, try to move to a main inventory slot. + if (!movedGear && !this.moveItemStackTo(itemStack, 0, container.getOwnerHandle().getInventory().items.size(), true)) { + return ItemStack.EMPTY; + } + } + } + + if (itemStack.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + + return originalStack; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java new file mode 100644 index 00000000..5a2d6162 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/menu/OpenSyncMenu.java @@ -0,0 +1,229 @@ +package com.lishid.openinv.internal.paper1_21_4.container.menu; + +import com.google.common.base.Suppliers; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.InternalOwned; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.ContainerListener; +import net.minecraft.world.inventory.ContainerSynchronizer; +import net.minecraft.world.inventory.DataSlot; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}. + */ +@SuppressWarnings("HidingField") +public abstract class OpenSyncMenu> + extends OpenChestMenu { + + // Syncher fields + private @Nullable ContainerSynchronizer synchronizer; + private final List dataSlots = new ArrayList<>(); + private final IntList remoteDataSlots = new IntArrayList(); + private final List containerListeners = new ArrayList<>(); + private ItemStack remoteCarried = ItemStack.EMPTY; + private boolean suppressRemoteUpdates; + + protected OpenSyncMenu( + @NotNull MenuType type, + int containerCounter, + @NotNull T container, + @NotNull ServerPlayer viewer, + boolean viewOnly + ) { + super(type, containerCounter, container, viewer, viewOnly); + } + + // Overrides from here on are purely to modify the sync process to send placeholder items. + @Override + protected @NotNull Slot addSlot(@NotNull Slot slot) { + slot.index = this.slots.size(); + this.slots.add(slot); + this.lastSlots.add(ItemStack.EMPTY); + this.remoteSlots.add(ItemStack.EMPTY); + return slot; + } + + @Override + protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) { + this.dataSlots.add(dataSlot); + this.remoteDataSlots.add(0); + return dataSlot; + } + + @Override + protected void addDataSlots(ContainerData containerData) { + for (int i = 0; i < containerData.getCount(); i++) { + this.addDataSlot(DataSlot.forContainer(containerData, i)); + } + } + + @Override + public void addSlotListener(@NotNull ContainerListener containerListener) { + if (!this.containerListeners.contains(containerListener)) { + this.containerListeners.add(containerListener); + this.broadcastChanges(); + } + } + + @Override + public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) { + this.synchronizer = containerSynchronizer; + this.sendAllDataToRemote(); + } + + @Override + public void sendAllDataToRemote() { + for (int index = 0; index < slots.size(); ++index) { + Slot slot = slots.get(index); + this.remoteSlots.set(index, (slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem()).copy()); + } + + remoteCarried = getCarried().copy(); + + for (int index = 0; index < this.dataSlots.size(); ++index) { + this.remoteDataSlots.set(index, this.dataSlots.get(index).get()); + } + + if (this.synchronizer != null) { + this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray()); + } + } + + @Override + public void broadcastCarriedItem() { + this.remoteCarried = this.getCarried().copy(); + if (this.synchronizer != null) { + this.synchronizer.sendCarriedChange(this, this.remoteCarried); + } + } + + @Override + public void removeSlotListener(@NotNull ContainerListener containerListener) { + this.containerListeners.remove(containerListener); + } + + @Override + public void broadcastChanges() { + for (int index = 0; index < this.slots.size(); ++index) { + Slot slot = this.slots.get(index); + ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem(); + Supplier supplier = Suppliers.memoize(itemstack::copy); + this.triggerSlotListeners(index, itemstack, supplier); + this.synchronizeSlotToRemote(index, itemstack, supplier); + } + + this.synchronizeCarriedToRemote(); + + for (int index = 0; index < this.dataSlots.size(); ++index) { + DataSlot dataSlot = this.dataSlots.get(index); + int j = dataSlot.get(); + if (dataSlot.checkAndClearUpdateFlag()) { + this.updateDataSlotListeners(index, j); + } + + this.synchronizeDataSlotToRemote(index, j); + } + } + + @Override + public void broadcastFullState() { + for (int index = 0; index < this.slots.size(); ++index) { + ItemStack itemstack = this.slots.get(index).getItem(); + this.triggerSlotListeners(index, itemstack, itemstack::copy); + } + + for (int index = 0; index < this.dataSlots.size(); ++index) { + DataSlot containerproperty = this.dataSlots.get(index); + if (containerproperty.checkAndClearUpdateFlag()) { + this.updateDataSlotListeners(index, containerproperty.get()); + } + } + + this.sendAllDataToRemote(); + } + + private void updateDataSlotListeners(int i, int j) { + for (ContainerListener containerListener : this.containerListeners) { + containerListener.dataChanged(this, i, j); + } + } + + private void triggerSlotListeners(int index, ItemStack itemStack, Supplier supplier) { + ItemStack itemStack1 = this.lastSlots.get(index); + if (!ItemStack.matches(itemStack1, itemStack)) { + ItemStack itemStack2 = supplier.get(); + this.lastSlots.set(index, itemStack2); + + for (ContainerListener containerListener : this.containerListeners) { + containerListener.slotChanged(this, index, itemStack2); + } + } + } + + private void synchronizeSlotToRemote(int i, ItemStack itemStack, Supplier supplier) { + if (!this.suppressRemoteUpdates) { + ItemStack itemStack1 = this.remoteSlots.get(i); + if (!ItemStack.matches(itemStack1, itemStack)) { + ItemStack itemstack2 = supplier.get(); + this.remoteSlots.set(i, itemstack2); + if (this.synchronizer != null) { + this.synchronizer.sendSlotChange(this, i, itemstack2); + } + } + } + } + + private void synchronizeDataSlotToRemote(int index, int value) { + if (!this.suppressRemoteUpdates) { + int existing = this.remoteDataSlots.getInt(index); + if (existing != value) { + this.remoteDataSlots.set(index, value); + if (this.synchronizer != null) { + this.synchronizer.sendDataChange(this, index, value); + } + } + } + } + + private void synchronizeCarriedToRemote() { + if (!this.suppressRemoteUpdates && !ItemStack.matches(this.getCarried(), this.remoteCarried)) { + this.remoteCarried = this.getCarried().copy(); + if (this.synchronizer != null) { + this.synchronizer.sendCarriedChange(this, this.remoteCarried); + } + } + } + + @Override + public void setRemoteCarried(ItemStack itemstack) { + this.remoteCarried = itemstack.copy(); + } + + @Override + public void suppressRemoteUpdates() { + this.suppressRemoteUpdates = true; + } + + @Override + public void resumeRemoteUpdates() { + this.suppressRemoteUpdates = false; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java new file mode 100644 index 00000000..b5513499 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentEquipment.java @@ -0,0 +1,80 @@ +package com.lishid.openinv.internal.paper1_21_4.container.slot; + +import com.lishid.openinv.internal.common.container.slot.ContentList; +import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder; +import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot for equipment that displays placeholders if empty. + */ +public class ContentEquipment extends ContentList { + + private final ItemStack placeholder; + private final EquipmentSlot equipmentSlot; + + public ContentEquipment(ServerPlayer holder, int index, EquipmentSlot equipmentSlot) { + super(holder, index, InventoryType.SlotType.ARMOR); + placeholder = switch (equipmentSlot) { + case HEAD -> Placeholders.emptyHelmet; + case CHEST -> Placeholders.emptyChestplate; + case LEGS -> Placeholders.emptyLeggings; + case FEET -> Placeholders.emptyBoots; + default -> Placeholders.emptyOffHand; + }; + this.equipmentSlot = equipmentSlot; + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.items = holder.getInventory().armor; + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y); + } + + public class SlotEquipment extends SlotPlaceholder { + + private ServerPlayer viewer; + + SlotEquipment(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + ItemStack itemStack = getItem(); + if (!itemStack.isEmpty()) { + return itemStack; + } + return placeholder; + } + + public EquipmentSlot getEquipmentSlot() { + return equipmentSlot; + } + + public void onlyEquipmentFor(ServerPlayer viewer) { + this.viewer = viewer; + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + if (viewer == null) { + return true; + } + + return equipmentSlot == EquipmentSlot.OFFHAND || viewer.getEquipmentSlotForItem(itemStack) == equipmentSlot; + } + + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java new file mode 100644 index 00000000..31da3618 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/ContentOffHand.java @@ -0,0 +1,53 @@ +package com.lishid.openinv.internal.paper1_21_4.container.slot; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +/** + * A slot for equipment that updates held items if necessary. + */ +public class ContentOffHand extends ContentEquipment { + + private ServerPlayer holder; + + public ContentOffHand(ServerPlayer holder, int localIndex) { + super(holder, localIndex, EquipmentSlot.OFFHAND); + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.items = holder.getInventory().offhand; + this.holder = holder; + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.QUICKBAR; + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y) { + @Override + public void setChanged() { + if (BaseOpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) { + holder.connection.send( + new ClientboundContainerSetSlotPacket( + holder.inventoryMenu.containerId, + holder.inventoryMenu.incrementStateId(), + InventoryMenu.SHIELD_SLOT, + holder.getOffhandItem() + )); + } + } + }; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java new file mode 100644 index 00000000..33dd3a21 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelBase.java @@ -0,0 +1,41 @@ +package com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder; + +import com.lishid.openinv.internal.common.container.slot.placeholder.PlaceholderLoaderBase; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.TagParser; +import net.minecraft.util.Unit; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomModelData; +import net.minecraft.world.item.component.DyedItemColor; +import org.jetbrains.annotations.NotNull; + +public class CustomModelBase extends PlaceholderLoaderBase { + + private final @NotNull CustomModelData defaultCustomModelData; + + public CustomModelBase(@NotNull CustomModelData defaultCustomModelData) { + this.defaultCustomModelData = defaultCustomModelData; + } + + @Override + protected @NotNull CompoundTag parseTag(@NotNull String itemText) throws Exception { + return TagParser.parseTag(itemText); + } + + @Override + protected void addModelData(@NotNull ItemStack itemStack) { + itemStack.set(DataComponents.CUSTOM_MODEL_DATA, defaultCustomModelData); + } + + @Override + protected void hideTooltip(@NotNull ItemStack itemStack) { + itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE); + } + + @Override + protected DyedItemColor getDye(int rgb) { + return new DyedItemColor(rgb, false); + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java new file mode 100644 index 00000000..d6c2c0b5 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/container/slot/placeholder/CustomModelPlaceholderLoader.java @@ -0,0 +1,13 @@ +package com.lishid.openinv.internal.paper1_21_4.container.slot.placeholder; + +import net.minecraft.world.item.component.CustomModelData; + +import java.util.List; + +public class CustomModelPlaceholderLoader extends CustomModelBase { + + public CustomModelPlaceholderLoader() { + super(new CustomModelData(List.of(), List.of(), List.of("openinv:custom"), List.of())); + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java new file mode 100644 index 00000000..3f1fe082 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/OpenPlayer.java @@ -0,0 +1,36 @@ +package com.lishid.openinv.internal.paper1_21_4.player; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenPlayer extends com.lishid.openinv.internal.paper1_21_5.player.OpenPlayer { + + protected OpenPlayer( + CraftServer server, ServerPlayer entity, + PlayerManager manager + ) { + super(server, entity, manager); + } + + @Contract("null -> new") + @Override + protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) { + if (oldData == null) { + return new CompoundTag(); + } + + // Copy old data. This is a deep clone, so operating on it should be safe. + oldData = oldData.copy(); + + // Remove vanilla/server data that is not written every time. + oldData.getAllKeys() + .removeIf(key -> RESET_TAGS.contains(key) || key.startsWith("Bukkit")); + + return oldData; + } + +} diff --git a/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java new file mode 100644 index 00000000..4172afd7 --- /dev/null +++ b/internal/paper1_21_4/src/main/java/com/lishid/openinv/internal/paper1_21_4/player/PlayerManager.java @@ -0,0 +1,73 @@ +package com.lishid.openinv.internal.paper1_21_4.player; + +import com.mojang.serialization.Dynamic; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.DimensionType; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; +import java.util.logging.Logger; + +public class PlayerManager extends com.lishid.openinv.internal.paper1_21_5.player.PlayerManager { + + public PlayerManager(@NotNull Logger logger) { + super(logger); + } + + @Override + protected void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) { + // See PlayerList#placeNewPlayer + World bukkitWorld; + if (loadedData.contains("WorldUUIDMost") && loadedData.contains("WorldUUIDLeast")) { + // Modern Bukkit world. + bukkitWorld = Bukkit.getServer().getWorld(new UUID(loadedData.getLong("WorldUUIDMost"), loadedData.getLong("WorldUUIDLeast"))); + } else if (loadedData.contains("world", net.minecraft.nbt.Tag.TAG_STRING)) { + // Legacy Bukkit world. + bukkitWorld = Bukkit.getServer().getWorld(loadedData.getString("world")); + } else { + // Vanilla player data. + DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension"))) + .resultOrPartial(logger::warning) + .map(player.server::getLevel) + // If ServerLevel exists, set, otherwise move to spawn. + .ifPresentOrElse(player::setServerLevel, () -> spawnInDefaultWorld(player.server, player)); + return; + } + if (bukkitWorld == null) { + spawnInDefaultWorld(player.server, player); + return; + } + player.setServerLevel(((CraftWorld) bukkitWorld).getHandle()); + } + + @Override + protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + ServerLevel level = server.getLevel(Level.OVERWORLD); + if (level != null) { + // Adjust player to default spawn (in keeping with Paper handling) when world not found. + player.moveTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F); + player.spawnIn(level); + } else { + logger.warning("Tried to load player with invalid world when no fallback was available!"); + } + } + + @Override + protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this)); + } + +} diff --git a/internal/paper1_21_5/build.gradle.kts b/internal/paper1_21_5/build.gradle.kts new file mode 100644 index 00000000..2b1471c1 --- /dev/null +++ b/internal/paper1_21_5/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + implementation(project(":openinvadapterpaper1_21_8")) + + paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java new file mode 100644 index 00000000..e0552bd2 --- /dev/null +++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/InternalAccessor.java @@ -0,0 +1,37 @@ +package com.lishid.openinv.internal.paper1_21_5; + +import com.lishid.openinv.internal.paper1_21_5.container.slot.placeholder.PlaceholderLoaderLegacyParse; +import com.lishid.openinv.internal.paper1_21_5.player.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class InternalAccessor extends com.lishid.openinv.internal.paper1_21_8.InternalAccessor { + + private final @NotNull PlayerManager manager; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + super(logger, lang); + manager = new PlayerManager(logger); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public void reload(@NotNull ConfigurationSection config) { + ConfigurationSection placeholders = config.getConfigurationSection("placeholders"); + try { + // Reset placeholders to defaults and try to load configuration. + new PlaceholderLoaderLegacyParse().load(placeholders); + } catch (Exception e) { + logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e); + } + } + +} diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java new file mode 100644 index 00000000..3b11c697 --- /dev/null +++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/container/slot/placeholder/PlaceholderLoaderLegacyParse.java @@ -0,0 +1,36 @@ +package com.lishid.openinv.internal.paper1_21_5.container.slot.placeholder; + +import com.lishid.openinv.internal.common.container.slot.placeholder.PlaceholderLoader; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; + +public class PlaceholderLoaderLegacyParse extends PlaceholderLoader { + + @Override + protected @NotNull ItemStack parse( + @Nullable ConfigurationSection section, + @NotNull String path, + @NotNull ItemStack defaultStack + ) throws Exception { + if (section == null) { + return defaultStack; + } + + String itemText = section.getString(path); + + if (itemText == null) { + return defaultStack; + } + + CompoundTag compoundTag = parseTag(itemText); + Optional parsed = ItemStack.parse(CraftRegistry.getMinecraftRegistry(), compoundTag); + return parsed.filter(itemStack -> !itemStack.isEmpty()).orElse(defaultStack); + } + +} diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java new file mode 100644 index 00000000..46b4aa87 --- /dev/null +++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/OpenPlayer.java @@ -0,0 +1,46 @@ +package com.lishid.openinv.internal.paper1_21_5.player; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.storage.PlayerDataStorage; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; + +public class OpenPlayer extends BaseOpenPlayer { + + protected OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) { + super(server, entity, manager); + } + + @Override + protected void trySave(ServerPlayer player) { + // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman) + try { + PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo; + + CompoundTag oldData = isOnline() ? null : worldNBTStorage.load(player.getName().getString(), player.getStringUUID()).orElse(null); + CompoundTag playerData = getWritableTag(oldData); + + playerData = player.saveWithoutId(playerData); + + saveSafe(player, oldData, playerData, worldNBTStorage); + } catch (Exception e) { + LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e); + } + } + + @Override + protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) { + net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile); + } + + @Override + protected void remove(@NotNull CompoundTag tag, @NotNull String key) { + tag.remove(key); + } + +} diff --git a/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java new file mode 100644 index 00000000..9637b062 --- /dev/null +++ b/internal/paper1_21_5/src/main/java/com/lishid/openinv/internal/paper1_21_5/player/PlayerManager.java @@ -0,0 +1,106 @@ +package com.lishid.openinv.internal.paper1_21_5.player; + +import com.mojang.serialization.Dynamic; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.dimension.DimensionType; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; +import java.util.UUID; +import java.util.logging.Logger; + +public class PlayerManager extends com.lishid.openinv.internal.paper1_21_8.player.PlayerManager { + + public PlayerManager(@NotNull Logger logger) { + super(logger); + } + + @Override + protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + // See CraftPlayer#loadData + CompoundTag loadedData = server.getPlayerList().playerIo.load(player).orElse(null); + + if (loadedData == null) { + // Exceptions with loading are logged. + return false; + } + + // Read basic data into the player. + player.load(loadedData); + // Game type settings are also loaded separately. + player.loadGameTypes(loadedData); + + // World is not loaded by ServerPlayer#load(CompoundTag) on Paper. + parseWorld(player, loadedData); + + return true; + } + + protected void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) { + // See PlayerList#placeNewPlayer + World bukkitWorld; + Optional msbs = loadedData.getLong("WorldUUIDMost"); + Optional lsbs = loadedData.getLong("WorldUUIDLeast"); + if (msbs.isPresent() && lsbs.isPresent()) { + // Modern Bukkit world. + bukkitWorld = Bukkit.getServer().getWorld(new UUID(msbs.get(), lsbs.get())); + } else { + Optional worldName = loadedData.getString("world"); + if (worldName.isPresent()) { + // Legacy Bukkit world. + bukkitWorld = Bukkit.getServer().getWorld(worldName.get()); + } else { + // Vanilla player data. + DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension"))) + .resultOrPartial(logger::warning) + .map(player.server::getLevel) + // If ServerLevel exists, set, otherwise move to spawn. + .ifPresentOrElse(player::setServerLevel, () -> spawnInDefaultWorld(player.server, player)); + return; + } + } + if (bukkitWorld == null) { + spawnInDefaultWorld(player.server, player); + return; + } + player.setServerLevel(((CraftWorld) bukkitWorld).getHandle()); + } + + @Override + public @NotNull Player inject(@NotNull Player player) { + try { + ServerPlayer nmsPlayer = getHandle(player); + if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) { + return openPlayer; + } + injectPlayer(nmsPlayer.server, nmsPlayer); + return nmsPlayer.getBukkitEntity(); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + return player; + } + } + + @Override + protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + + bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this)); + } + +} diff --git a/internal/paper1_21_8/build.gradle.kts b/internal/paper1_21_8/build.gradle.kts new file mode 100644 index 00000000..306db103 --- /dev/null +++ b/internal/paper1_21_8/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `openinv-base` + alias(libs.plugins.paperweight) +} + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val paper = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "paper-api" + } + } + if (paper != null) { + select(paper) + } + because("module is written for Paper servers") + } +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + + paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT") +} diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java new file mode 100644 index 00000000..0c68893d --- /dev/null +++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/InternalAccessor.java @@ -0,0 +1,31 @@ +package com.lishid.openinv.internal.paper1_21_8; + +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.internal.paper1_21_8.container.OpenInventory; +import com.lishid.openinv.internal.paper1_21_8.player.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor { + + private final @NotNull PlayerManager manager; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + super(logger, lang); + manager = new PlayerManager(logger); + } + + @Override + public @NotNull PlayerManager getPlayerManager() { + return manager; + } + + @Override + public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) { + return new OpenInventory(player); + } + +} diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java new file mode 100644 index 00000000..076d69a8 --- /dev/null +++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/container/OpenInventory.java @@ -0,0 +1,49 @@ +package com.lishid.openinv.internal.paper1_21_8.container; + +import com.lishid.openinv.internal.common.container.BaseOpenInventory; +import com.lishid.openinv.internal.common.container.menu.OpenChestMenu; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenInventory extends BaseOpenInventory { + + public OpenInventory(@NotNull Player bukkitPlayer) { + super(bukkitPlayer); + } + + @Override + public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu menu) { + MutableComponent component = Component.empty(); + // Prefix for use with custom bitmap image fonts. + if (owner.equals(viewer)) { + component.append( + Component.translatableWithFallback("openinv.container.inventory.self", "") + .withStyle(style -> style + .withFont(ResourceLocation.parse("openinv:font/inventory")) + .withColor(ChatFormatting.WHITE))); + } else { + component.append( + Component.translatableWithFallback("openinv.container.inventory.other", "") + .withStyle(style -> style + .withFont(ResourceLocation.parse("openinv:font/inventory")) + .withColor(ChatFormatting.WHITE))); + } + if (menu != null && menu.isViewOnly()) { + component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] ")); + } else { + component.append(Component.translatableWithFallback("openinv.container.inventory.editable", "")); + } + // Normal title: "Inventory - OwnerName" + component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName())) + .append(Component.translatable("container.inventory")) + .append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName())); + return component; + } + +} diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java new file mode 100644 index 00000000..0cad37a7 --- /dev/null +++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/OpenPlayer.java @@ -0,0 +1,58 @@ +package com.lishid.openinv.internal.paper1_21_8.player; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.level.storage.PlayerDataStorage; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.level.storage.ValueOutput; +import org.bukkit.craftbukkit.CraftServer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.nio.file.Path; + +public class OpenPlayer extends BaseOpenPlayer { + + protected OpenPlayer( + CraftServer server, + ServerPlayer entity, + PlayerManager manager + ) { + super(server, entity, manager); + } + + @Override + protected void trySave(ServerPlayer player) { + Logger logger = LogUtils.getLogger(); + // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman) + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) { + PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo; + + CompoundTag oldData = isOnline() + ? null + : worldNbtStorage.load(player.getName().getString(), player.getStringUUID(), scopedCollector).orElse(null); + CompoundTag playerData = getWritableTag(oldData); + + ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData); + player.saveWithoutId(valueOutput); + + saveSafe(player, oldData, playerData, worldNbtStorage); + } catch (Exception e) { + LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e); + } + } + + @Override + protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) { + net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile); + } + + @Override + protected void remove(@NotNull CompoundTag tag, @NotNull String key) { + tag.remove(key); + } + +} diff --git a/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java new file mode 100644 index 00000000..dc3ad219 --- /dev/null +++ b/internal/paper1_21_8/src/main/java/com/lishid/openinv/internal/paper1_21_8/player/PlayerManager.java @@ -0,0 +1,100 @@ +package com.lishid.openinv.internal.paper1_21_8.player; + +import com.lishid.openinv.internal.common.player.BaseOpenPlayer; +import com.lishid.openinv.util.JulLoggerAdapter; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.ValueInput; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager { + + public PlayerManager(@NotNull Logger logger) { + super(logger); + } + + @Override + protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + // See CraftPlayer#loadData + + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) { + ValueInput loadedData = server.getPlayerList().playerIo.load(player, scopedCollector).orElse(null); + + if (loadedData == null) { + // Exceptions with loading are logged. + return false; + } + + // Read basic data into the player. + player.load(loadedData); + // Game type settings are loaded separately. + player.loadGameTypes(loadedData); + + // World is not loaded by ServerPlayer#load(CompoundTag) on Paper. + parseWorld(server, player, loadedData); + } + + return true; + } + + @Override + protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) { + ServerLevel level = server.getLevel(Level.OVERWORLD); + if (level != null) { + // Adjust player to default spawn (in keeping with Paper handling) when world not found. + player.snapTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F); + player.spawnIn(level); + } else { + logger.warning("Tried to load player with invalid world when no fallback was available!"); + } + } + + @Override + protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + + bukkitEntity.set(player, new OpenPlayer(server.server, player, this)); + } + + @Override + public @NotNull Player inject(@NotNull Player player) { + try { + ServerPlayer nmsPlayer = getHandle(player); + if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) { + return openPlayer; + } + MinecraftServer server = nmsPlayer.getServer(); + if (server == null) { + if (!(Bukkit.getServer() instanceof CraftServer craftServer)) { + logger.warning(() -> + "Unable to inject ServerPlayer, certain player data may be lost when saving! Server is not a CraftServer: " + + Bukkit.getServer().getClass().getName()); + return player; + } + server = craftServer.getServer(); + } + injectPlayer(server, nmsPlayer); + return nmsPlayer.getBukkitEntity(); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + return player; + } + } + +} diff --git a/internal/pom.xml b/internal/pom.xml deleted file mode 100644 index fd28dce6..00000000 --- a/internal/pom.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - 4.0.0 - - - com.lishid - openinvparent - 4.1.6-SNAPSHOT - - - openinvinternal - OpenInvInternal - - pom - - - - - all - - v1_16_R3 - - - - - - diff --git a/internal/spigot/build.gradle.kts b/internal/spigot/build.gradle.kts new file mode 100644 index 00000000..5a74fb87 --- /dev/null +++ b/internal/spigot/build.gradle.kts @@ -0,0 +1,44 @@ +import com.github.jikoo.openinv.SpigotDependencyExtension +import com.github.jikoo.openinv.SpigotReobf +import com.github.jikoo.openinv.SpigotSetup + +plugins { + `openinv-base` + alias(libs.plugins.shadow) +} + +apply() +apply() + +val spigotVer = "1.21.11-R0.1-SNAPSHOT" +// Used by common adapter to relocate Craftbukkit classes to a versioned package. +rootProject.extra["craftbukkitPackage"] = "v1_21_R7" + +configurations.all { + resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") { + val spigot = candidates.firstOrNull { + it.id.let { id -> + id is ModuleComponentIdentifier && id.module == "spigot-api" + } + } + if (spigot != null) { + select(spigot) + } + because("module is written for Spigot servers") + } +} + +dependencies { + compileOnly(libs.spigotapi) + extensions.getByType(SpigotDependencyExtension::class.java).version = spigotVer + + compileOnly(project(":openinvapi")) + compileOnly(project(":openinvcommon")) + + // Reduce duplicate code by lightly remapping common adapter. + implementation(project(":openinvadaptercommon", configuration = "spigotRelocated")) +} + +tasks.shadowJar { + relocate("com.lishid.openinv.internal.common", "com.lishid.openinv.internal.reobf") +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java new file mode 100644 index 00000000..5b3c1559 --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/AnySilentContainer.java @@ -0,0 +1,200 @@ +package com.lishid.openinv.internal.reobf.container; + +import com.lishid.openinv.internal.AnySilentContainerBase; +import com.lishid.openinv.internal.reobf.container.menu.OpenChestMenu; +import com.lishid.openinv.internal.reobf.player.PlayerManager; +import com.lishid.openinv.util.ReflectionHelper; +import com.lishid.openinv.util.lang.LanguageManager; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.BarrelBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import net.minecraft.world.level.block.TrappedChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.logging.Logger; + +public class AnySilentContainer extends AnySilentContainerBase { + + private final @NotNull Logger logger; + private final @NotNull LanguageManager lang; + private @Nullable Field serverPlayerGameModeGameType; + + public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) { + this.logger = logger; + this.lang = lang; + try { + try { + this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("gameModeForPlayer"); + this.serverPlayerGameModeGameType.setAccessible(true); + } catch (NoSuchFieldException e) { + logger.warning("The field ServerPlayerGameMode#gameModeForPlayer is no longer present!"); + logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues"); + logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail."); + // N.B. gameModeForPlayer is (for now) declared before previousGameModeForPlayer so silent shouldn't break. + this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class); + } + } catch (SecurityException e) { + logger.warning("Unable to directly write player game mode! SilentContainer will fail."); + logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e); + } + } + + @Override + public boolean activateContainer( + @NotNull final Player bukkitPlayer, + final boolean silentchest, + @NotNull final org.bukkit.block.Block bukkitBlock + ) { + + // Silent ender chest is API-only + if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) { + bukkitPlayer.openInventory(bukkitPlayer.getEnderChest()); + bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); + return true; + } + + ServerPlayer player = PlayerManager.getHandle(bukkitPlayer); + + final net.minecraft.world.level.Level level = player.level(); + final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ()); + final BlockEntity blockEntity = level.getBlockEntity(blockPos); + + if (blockEntity == null) { + return false; + } + + if (blockEntity instanceof EnderChestBlockEntity enderChestTile) { + // Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock + PlayerEnderChestContainer enderChest = player.getEnderChestInventory(); + enderChest.setActiveChest(enderChestTile); + player.openMenu( + new SimpleMenuProvider( + (containerCounter, playerInventory, ignored) -> { + MenuType containers = OpenChestMenu.getChestMenuType(enderChest.getContainerSize()); + int rows = enderChest.getContainerSize() / 9; + return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows); + }, + Component.translatable("container.enderchest") + ) + ); + bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); + return true; + } + + if (!(blockEntity instanceof MenuProvider menuProvider)) { + return false; + } + + BlockState blockState = level.getBlockState(blockPos); + Block block = blockState.getBlock(); + + if (block instanceof ChestBlock chestBlock) { + + // boolean flag: do not check if chest is blocked + menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true); + + if (menuProvider == null) { + lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); + return false; + } + + if (block instanceof TrappedChestBlock) { + bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED); + } else { + bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED); + } + } + + if (block instanceof ShulkerBoxBlock) { + bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED); + } + + if (block instanceof BarrelBlock) { + bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL); + } + + // AnyChest only - SilentChest not active, container unsupported, or unnecessary. + if (!silentchest || player.gameMode.getGameModeForPlayer() == GameType.SPECTATOR) { + player.openMenu(menuProvider); + return true; + } + + // SilentChest requires access to setting players' game mode directly. + if (this.serverPlayerGameModeGameType == null) { + return false; + } + + if (blockEntity instanceof RandomizableContainerBlockEntity lootable) { + if (lootable.lootTable != null) { + lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); + return false; + } + } + + GameType gameType = player.gameMode.getGameModeForPlayer(); + this.forceGameType(player, GameType.SPECTATOR); + player.openMenu(menuProvider); + this.forceGameType(player, gameType); + return true; + } + + @Override + public void deactivateContainer(@NotNull final Player bukkitPlayer) { + if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) { + return; + } + + ServerPlayer player = PlayerManager.getHandle(bukkitPlayer); + + // Force game mode change without informing plugins or players. + // Regular game mode set calls GameModeChangeEvent and is cancellable. + GameType gameType = player.gameMode.getGameModeForPlayer(); + this.forceGameType(player, GameType.SPECTATOR); + + // ServerPlayer#closeContainer cannot be called without entering an + // infinite loop because this method is called during inventory close. + // From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent + player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity()); + // From ServerPlayer#closeContainer + player.doCloseContainer(); + // Regular inventory close will handle the rest - packet sending, etc. + + // Revert forced game mode. + this.forceGameType(player, gameType); + } + + private void forceGameType(final ServerPlayer player, final GameType gameMode) { + if (this.serverPlayerGameModeGameType == null) { + // No need to warn repeatedly, error on startup and lack of function should be enough. + return; + } + try { + this.serverPlayerGameModeGameType.setAccessible(true); + this.serverPlayerGameModeGameType.set(player.gameMode, gameMode); + } catch (IllegalArgumentException | IllegalAccessException e) { + logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e); + } + } + +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java new file mode 100644 index 00000000..77aefe24 --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/bukkit/OpenPlayerInventory.java @@ -0,0 +1,190 @@ +package com.lishid.openinv.internal.reobf.container.bukkit; + +import com.google.common.base.Preconditions; +import com.lishid.openinv.internal.reobf.container.BaseOpenInventory; +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.player.Inventory; +import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftInventory; +import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class OpenPlayerInventory extends CraftInventory implements PlayerInventory { + + public OpenPlayerInventory(@NotNull BaseOpenInventory inventory) { + super(inventory); + } + + @Override + public @NotNull BaseOpenInventory getInventory() { + return (BaseOpenInventory) super.getInventory(); + } + + @Override + public ItemStack @NotNull [] getContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents()); + } + + @Override + public void setContents(ItemStack[] items) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + int size = internal.getContainerSize(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + + for (int index = 0; index < size; ++index) { + if (index < items.length) { + internal.setItem(index, CraftItemStack.asNMSCopy(items[index])); + } else { + internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY); + } + } + } + + @Override + public ItemStack @NotNull [] getStorageContents() { + return asCraftMirror(getInventory().getOwnerHandle().getInventory().getNonEquipmentItems()); + } + + @Override + public void setStorageContents(ItemStack[] items) throws IllegalArgumentException { + NonNullList list = getInventory().getOwnerHandle().getInventory().getNonEquipmentItems(); + int size = list.size(); + Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size); + for (int index = 0; index < items.length; ++index) { + list.set(index, CraftItemStack.asNMSCopy(items[index])); + } + } + + @Override + public @NotNull InventoryType getType() { + return InventoryType.PLAYER; + } + + @Override + public @NotNull Player getHolder() { + return getInventory().getOwner(); + } + + @Override + public @NotNull ItemStack @NotNull [] getArmorContents() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getArmorContents(); + } + + @Override + public void setArmorContents(ItemStack @NotNull [] items) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setArmorContents(items); + } + + @Override + public @NotNull ItemStack @NotNull [] getExtraContents() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getExtraContents(); + } + + @Override + public void setExtraContents(ItemStack @NotNull [] items) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setExtraContents(items); + } + + @Override + public @Nullable ItemStack getHelmet() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getHelmet(); + } + + @Override + public void setHelmet(@Nullable ItemStack helmet) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setHelmet(helmet); + } + + @Override + public @Nullable ItemStack getChestplate() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getChestplate(); + } + + @Override + public void setChestplate(@Nullable ItemStack chestplate) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setChestplate(chestplate); + } + + @Override + public @Nullable ItemStack getLeggings() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getLeggings(); + } + + @Override + public void setLeggings(@Nullable ItemStack leggings) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setLeggings(leggings); + } + + @Override + public @Nullable ItemStack getBoots() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getBoots(); + } + + @Override + public void setBoots(@Nullable ItemStack boots) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setBoots(boots); + } + + @Override + public @NotNull ItemStack getItemInMainHand() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return CraftItemStack.asCraftMirror(internal.getSelectedItem()); + } + + @Override + public void setItemInMainHand(@Nullable ItemStack item) { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + internal.setSelectedItem(CraftItemStack.asNMSCopy(item)); + } + + @Override + public @NotNull ItemStack getItemInOffHand() { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItemInOffHand(); + } + + @Override + public void setItemInOffHand(@Nullable ItemStack item) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItemInOffHand(item); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public @NotNull ItemStack getItemInHand() { + return getItemInMainHand(); + } + + @SuppressWarnings("InlineMeSuggester") + @Deprecated + @Override + public void setItemInHand(@Nullable ItemStack stack) { + setItemInMainHand(stack); + } + + @Override + public int getHeldItemSlot() { + Inventory internal = getInventory().getOwnerHandle().getInventory(); + return internal.getNonEquipmentItems().size() - 9 + internal.getSelectedSlot(); + } + + @Override + public void setHeldItemSlot(int slot) { + slot %= 9; + getInventory().getOwnerHandle().getInventory().setSelectedSlot(slot); + } + + @Override + public @Nullable ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) { + return getInventory().getOwnerHandle().getBukkitEntity().getInventory().getItem(slot); + } + + @Override + public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) { + getInventory().getOwnerHandle().getBukkitEntity().getInventory().setItem(slot, item); + } + +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java new file mode 100644 index 00000000..bfec218e --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentEquipment.java @@ -0,0 +1,116 @@ +package com.lishid.openinv.internal.reobf.container.slot; + +import com.lishid.openinv.internal.reobf.container.slot.placeholder.Placeholders; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.bukkit.craftbukkit.v1_21_R7.CraftEquipmentSlot; +import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.PlayerInventory; +import org.jetbrains.annotations.NotNull; + +public class ContentEquipment implements Content { + + private PlayerInventory equipment; + private final ItemStack placeholder; + private final org.bukkit.inventory.EquipmentSlot equipmentSlot; + + public ContentEquipment(ServerPlayer holder, EquipmentSlot equipmentSlot) { + setHolder(holder); + placeholder = switch (equipmentSlot) { + case HEAD -> Placeholders.emptyHelmet; + case CHEST -> Placeholders.emptyChestplate; + case LEGS -> Placeholders.emptyLeggings; + case FEET -> Placeholders.emptyBoots; + default -> Placeholders.emptyOffHand; + }; + this.equipmentSlot = CraftEquipmentSlot.getSlot(equipmentSlot); + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + this.equipment = holder.getBukkitEntity().getInventory(); + } + + @Override + public ItemStack get() { + return CraftItemStack.asNMSCopy(equipment.getItem(equipmentSlot)); + } + + @Override + public ItemStack remove() { + org.bukkit.inventory.ItemStack old = equipment.getItem(equipmentSlot); + equipment.setItem(equipmentSlot, null); + return CraftItemStack.asNMSCopy(old); + } + + @Override + public ItemStack removePartial(int amount) { + if (amount <= 0) { + return ItemStack.EMPTY; + } + ItemStack current = get(); + if (current.isEmpty()) { + return ItemStack.EMPTY; + } + ItemStack split = current.split(amount); + set(current); + return split; + } + + @Override + public void set(ItemStack itemStack) { + equipment.setItem(equipmentSlot, CraftItemStack.asCraftMirror(itemStack)); + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y); + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.ARMOR; + } + + public class SlotEquipment extends SlotPlaceholder { + + private ServerPlayer viewer; + + SlotEquipment(Container container, int index, int x, int y) { + super(container, index, x, y); + } + + @Override + public ItemStack getOrDefault() { + ItemStack itemStack = getItem(); + if (!itemStack.isEmpty()) { + return itemStack; + } + return placeholder; + } + + public EquipmentSlot getEquipmentSlot() { + return CraftEquipmentSlot.getNMS(equipmentSlot); + } + + public void onlyEquipmentFor(ServerPlayer viewer) { + this.viewer = viewer; + } + + @Override + public boolean mayPlace(@NotNull ItemStack itemStack) { + if (viewer == null) { + return true; + } + + return equipmentSlot == org.bukkit.inventory.EquipmentSlot.OFF_HAND + || viewer.getEquipmentSlotForItem(itemStack) == CraftEquipmentSlot.getNMS(equipmentSlot); + } + + } + +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java new file mode 100644 index 00000000..5052987a --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/container/slot/ContentOffHand.java @@ -0,0 +1,50 @@ +package com.lishid.openinv.internal.reobf.container.slot; + +import com.lishid.openinv.internal.reobf.player.OpenPlayer; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.inventory.InventoryMenu; +import net.minecraft.world.inventory.Slot; +import org.bukkit.event.inventory.InventoryType; +import org.jetbrains.annotations.NotNull; + +public class ContentOffHand extends ContentEquipment { + + private ServerPlayer holder; + + public ContentOffHand(ServerPlayer holder) { + super(holder, EquipmentSlot.OFFHAND); + } + + @Override + public void setHolder(@NotNull ServerPlayer holder) { + super.setHolder(holder); + this.holder = holder; + } + + @Override + public InventoryType.SlotType getSlotType() { + return InventoryType.SlotType.QUICKBAR; + } + + @Override + public Slot asSlot(Container container, int slot, int x, int y) { + return new SlotEquipment(container, slot, x, y) { + @Override + public void setChanged() { + if (OpenPlayer.isConnected(holder.connection) && holder.containerMenu != holder.inventoryMenu) { + holder.connection.send( + new ClientboundContainerSetSlotPacket( + holder.inventoryMenu.containerId, + holder.inventoryMenu.incrementStateId(), + InventoryMenu.SHIELD_SLOT, + holder.getOffhandItem() + )); + } + } + }; + } + +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java new file mode 100644 index 00000000..33c630ac --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/OpenPlayer.java @@ -0,0 +1,216 @@ +package com.lishid.openinv.internal.reobf.player; + +import com.lishid.openinv.event.OpenEvents; +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.Tag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import net.minecraft.util.ProblemReporter; +import net.minecraft.util.Util; +import net.minecraft.world.level.storage.PlayerDataStorage; +import net.minecraft.world.level.storage.TagValueOutput; +import net.minecraft.world.level.storage.ValueOutput; +import org.bukkit.craftbukkit.v1_21_R7.CraftServer; +import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +public class OpenPlayer extends CraftPlayer { + + /** + * List of tags to always reset when saving. + * + * @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput) + * @see ServerPlayer#addAdditionalSaveData(ValueOutput) + * @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(ValueOutput) + * @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(ValueOutput) + */ + @Unmodifiable + protected static final Set RESET_TAGS = Set.of( + // Entity#saveWithoutId(CompoundTag) + "CustomName", + "CustomNameVisible", + "Silent", + "NoGravity", + "Glowing", + "TicksFrozen", + "HasVisualFire", + "Tags", + "Passengers", + // ServerPlayer#addAdditionalSaveData(CompoundTag) + // Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle + "warden_spawn_tracker", + "entered_nether_pos", // Replaces enteredNetherPosition + "enteredNetherPosition", + "respawn", // Replaces SpawnXyz fields as of 1.21.6 + "SpawnX", + "SpawnY", + "SpawnZ", + "SpawnForced", + "SpawnAngle", + "SpawnDimension", + "raid_omen_position", + "ender_pearls", + // Player#addAdditionalSaveData(CompoundTag) + "ShoulderEntityLeft", + "ShoulderEntityRight", + "LastDeathLocation", + "current_explosion_impact_pos", + // LivingEntity#addAdditionalSaveData(CompoundTag) + "active_effects", + "sleeping_pos", // Replaces SleepingXyz fields as of 1.21.6 + "SleepingX", + "SleepingY", + "SleepingZ", + "Brain", + "last_hurt_by_player", + "last_hurt_by_player_memory_time", + "last_hurt_by_mob", + "ticks_since_last_hurt_by_mob", + "equipment", + "locator_bar_icon" + ); + + private final PlayerManager manager; + + protected OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) { + super(server, entity); + this.manager = manager; + } + + @Override + public void loadData() { + manager.loadData(getHandle()); + } + + @Override + public void saveData() { + if (OpenEvents.saveCancelled(this)) { + return; + } + + ServerPlayer player = this.getHandle(); + Logger logger = LogUtils.getLogger(); + // See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman) + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) { + PlayerDataStorage worldNBTStorage = server.getServer().getPlayerList().playerIo; + + CompoundTag oldData = isOnline() + ? null + : worldNBTStorage.load(player.nameAndId()).orElse(null); + CompoundTag playerData = getWritableTag(oldData); + + ValueOutput valueOutput = TagValueOutput.createWithContext(scopedCollector, player.registryAccess()); + Field tagValueOutputOutput = TagValueOutput.class.getDeclaredField("output"); + tagValueOutputOutput.setAccessible(true); + CompoundTag newPlayerData = (CompoundTag) tagValueOutputOutput.get(valueOutput); + // Add existing old data. + newPlayerData.merge(playerData); + playerData = newPlayerData; + + player.saveWithoutId(valueOutput); + + if (oldData != null) { + // Revert certain special data values when offline. + revertSpecialValues(playerData, oldData); + } + + Path playerDataDir = worldNBTStorage.getPlayerDir().toPath(); + Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat"); + NbtIo.writeCompressed(playerData, tempFile); + Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat"); + Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old"); + Util.safeReplaceFile(dataFile, tempFile, backupFile); + } catch (Exception e) { + LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e); + } + } + + @Contract("null -> new") + protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) { + if (oldData == null) { + return new CompoundTag(); + } + + // Copy old data. This is a deep clone, so operating on it should be safe. + oldData = oldData.copy(); + + // Remove vanilla/server data that is not written every time. + oldData.keySet().removeIf( + key -> RESET_TAGS.contains(key) + || key.startsWith("Bukkit") + || (key.startsWith("Paper") && key.length() > 5) + ); + + return oldData; + } + + protected void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) { + // Revert automatic updates to play timestamps. + copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class); + copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class); + copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class); + } + + private void copyValue( + @NotNull CompoundTag source, + @NotNull CompoundTag target, + @NotNull String container, + @NotNull String key, + @SuppressWarnings("SameParameterValue") @NotNull Class tagType + ) { + CompoundTag oldContainer = getTag(source, container, CompoundTag.class); + CompoundTag newContainer = getTag(target, container, CompoundTag.class); + + // New container being null means the server implementation doesn't store this data. + if (newContainer == null) { + return; + } + + // If old tag exists, copy it to new location, removing otherwise. + setTag(newContainer, key, getTag(oldContainer, key, tagType)); + } + + private @Nullable T getTag( + @Nullable CompoundTag container, + @NotNull String key, + @NotNull Class dataType + ) { + if (container == null) { + return null; + } + Tag value = container.get(key); + if (value == null || !dataType.isAssignableFrom(value.getClass())) { + return null; + } + return dataType.cast(value); + } + + private void setTag( + @NotNull CompoundTag container, + @NotNull String key, + @Nullable T data + ) { + if (data == null) { + container.remove(key); + } else { + container.put(key, data); + } + } + + public static boolean isConnected(@Nullable ServerGamePacketListenerImpl connection) { + return connection != null && !connection.isDisconnected(); + } + +} diff --git a/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java new file mode 100644 index 00000000..d619f847 --- /dev/null +++ b/internal/spigot/src/main/java/com/lishid/openinv/internal/reobf/player/PlayerManager.java @@ -0,0 +1,237 @@ +package com.lishid.openinv.internal.reobf.player; + +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.reobf.container.OpenEnderChest; +import com.lishid.openinv.internal.reobf.container.OpenInventory; +import com.lishid.openinv.internal.reobf.container.menu.OpenChestMenu; +import com.lishid.openinv.util.JulLoggerAdapter; +import com.mojang.authlib.GameProfile; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ParticleStatus; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.ChatVisiblity; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.ValueInput; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.craftbukkit.v1_21_R7.CraftServer; +import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer; +import org.bukkit.craftbukkit.v1_21_R7.event.CraftEventFactory; +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.util.logging.Logger; + +public class PlayerManager implements com.lishid.openinv.internal.PlayerManager { + + private final @NotNull Logger logger; + private @Nullable Field bukkitEntity; + + public PlayerManager(@NotNull Logger logger) { + this.logger = logger; + try { + bukkitEntity = Entity.class.getDeclaredField("bukkitEntity"); + } catch (NoSuchFieldException e) { + logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!"); + logger.log(java.util.logging.Level.WARNING, e.getMessage(), e); + bukkitEntity = null; + } + } + + public static @NotNull ServerPlayer getHandle(final Player player) { + if (player instanceof CraftPlayer craftPlayer) { + return craftPlayer.getHandle(); + } + + Server server = player.getServer(); + ServerPlayer nmsPlayer = null; + + if (server instanceof CraftServer craftServer) { + nmsPlayer = craftServer.getHandle().getPlayer(player.getUniqueId()); + } + + if (nmsPlayer == null) { + // Could use reflection to examine fields, but it's honestly not worth the bother. + throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName()); + } + + return nmsPlayer; + } + + @Override + public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { + if (!(Bukkit.getServer() instanceof CraftServer craftServer)) { + return null; + } + + MinecraftServer server = craftServer.getServer(); + ServerLevel worldServer = server.getLevel(Level.OVERWORLD); + + if (worldServer == null) { + return null; + } + + // Create a new ServerPlayer. + ServerPlayer entity = createNewPlayer(server, worldServer, offline); + + // Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak. + entity.getAdvancements().stopListening(); + + // Try to load the player's data. + if (loadData(entity)) { + // If data is loaded successfully, return the Bukkit entity. + return entity.getBukkitEntity(); + } + + return null; + } + + private @NotNull ServerPlayer createNewPlayer( + @NotNull MinecraftServer server, + @NotNull ServerLevel worldServer, + @NotNull final OfflinePlayer offline + ) { + // See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile) + // See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket) + GameProfile profile = new GameProfile(offline.getUniqueId(), + offline.getName() != null ? offline.getName() : offline.getUniqueId().toString() + ); + + ClientInformation dummyInfo = new ClientInformation( + "en_us", + 1, // Reduce distance just in case. + ChatVisiblity.HIDDEN, // Don't accept chat. + false, + ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION, + ServerPlayer.DEFAULT_MAIN_HAND, + true, + false, // Don't list in player list (not that this player is in the list anyway). + ParticleStatus.MINIMAL + ); + + ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo); + + try { + injectPlayer(entity); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + } + + return entity; + } + + boolean loadData(@NotNull ServerPlayer player) { + // See CraftPlayer#loadData + + try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) { + CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null); + + if (loadedData == null) { + // Exceptions with loading are logged. + return false; + } + + ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData); + + // Read basic data into the player. + player.load(valueInput); + } + + return true; + } + + private void injectPlayer(ServerPlayer player) throws IllegalAccessException { + if (bukkitEntity == null) { + return; + } + + bukkitEntity.setAccessible(true); + + bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this)); + } + + @Override + public @NotNull Player inject(@NotNull Player player) { + try { + ServerPlayer nmsPlayer = getHandle(player); + if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) { + return openPlayer; + } + injectPlayer(nmsPlayer); + return nmsPlayer.getBukkitEntity(); + } catch (IllegalAccessException e) { + logger.log( + java.util.logging.Level.WARNING, + e, + () -> "Unable to inject ServerPlayer, certain player data may be lost when saving!" + ); + return player; + } + } + + @Override + public @Nullable InventoryView openInventory( + @NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory, + boolean viewOnly + ) { + ServerPlayer player = getHandle(bukkitPlayer); + + if (!OpenPlayer.isConnected(player.connection)) { + return null; + } + + // See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider) + OpenChestMenu menu; + Component title; + if (inventory instanceof OpenInventory playerInv) { + menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly); + title = playerInv.getTitle(player, menu); + } else if (inventory instanceof OpenEnderChest enderChest) { + menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly); + title = enderChest.getTitle(menu); + } else { + return null; + } + + // Should never happen, player is a ServerPlayer with an active connection. + if (menu == null) { + return null; + } + + // Set up title. Title can only be set once for a menu, and is set during the open process. + // Further title changes are a hack where the client is sent a "new" inventory with the same ID, + // resulting in a title change but no other state modifications (like cursor position). + menu.setTitle(title); + + AbstractContainerMenu opened = CraftEventFactory.callInventoryOpenEvent(player, menu, false); + + // Menu is null if event is cancelled. + if (opened == null) { + return null; + } + + player.containerMenu = opened; + player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), opened.getTitle())); + player.initMenu(opened); + + return opened.getBukkitView(); + } + +} diff --git a/internal/v1_16_R3/pom.xml b/internal/v1_16_R3/pom.xml deleted file mode 100644 index 74905edd..00000000 --- a/internal/v1_16_R3/pom.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - 4.0.0 - - - com.lishid - openinvinternal - 4.1.6-SNAPSHOT - - - openinvadapter1_16_R3 - OpenInvAdapter1_16_R3 - - - - org.spigotmc - spigot - 1.16.5-R0.1-SNAPSHOT - provided - - - com.lishid - openinvplugincore - 4.1.6-SNAPSHOT - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.2 - - true - - - - package - - shade - - - - - - - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - - - - - - diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java deleted file mode 100644 index 5d78617d..00000000 --- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/AnySilentContainer.java +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal.v1_16_R3; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.internal.IAnySilentContainer; -import java.lang.reflect.Field; -import net.minecraft.server.v1_16_R3.Block; -import net.minecraft.server.v1_16_R3.BlockBarrel; -import net.minecraft.server.v1_16_R3.BlockChest; -import net.minecraft.server.v1_16_R3.BlockChestTrapped; -import net.minecraft.server.v1_16_R3.BlockPosition; -import net.minecraft.server.v1_16_R3.BlockPropertyChestType; -import net.minecraft.server.v1_16_R3.BlockShulkerBox; -import net.minecraft.server.v1_16_R3.ChatMessage; -import net.minecraft.server.v1_16_R3.Container; -import net.minecraft.server.v1_16_R3.ContainerChest; -import net.minecraft.server.v1_16_R3.Containers; -import net.minecraft.server.v1_16_R3.EntityHuman; -import net.minecraft.server.v1_16_R3.EntityPlayer; -import net.minecraft.server.v1_16_R3.EnumGamemode; -import net.minecraft.server.v1_16_R3.IBlockData; -import net.minecraft.server.v1_16_R3.IChatBaseComponent; -import net.minecraft.server.v1_16_R3.ITileInventory; -import net.minecraft.server.v1_16_R3.InventoryEnderChest; -import net.minecraft.server.v1_16_R3.InventoryLargeChest; -import net.minecraft.server.v1_16_R3.PlayerInteractManager; -import net.minecraft.server.v1_16_R3.PlayerInventory; -import net.minecraft.server.v1_16_R3.TileEntity; -import net.minecraft.server.v1_16_R3.TileEntityChest; -import net.minecraft.server.v1_16_R3.TileEntityEnderChest; -import net.minecraft.server.v1_16_R3.TileEntityLootable; -import net.minecraft.server.v1_16_R3.TileInventory; -import net.minecraft.server.v1_16_R3.World; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.block.Barrel; -import org.bukkit.block.BlockFace; -import org.bukkit.block.BlockState; -import org.bukkit.block.EnderChest; -import org.bukkit.block.ShulkerBox; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Directional; -import org.bukkit.block.data.type.Chest; -import org.bukkit.entity.Cat; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryView; -import org.bukkit.util.BoundingBox; -import org.jetbrains.annotations.NotNull; - -public class AnySilentContainer implements IAnySilentContainer { - - private Field playerInteractManagerGamemode; - - public AnySilentContainer() { - try { - this.playerInteractManagerGamemode = PlayerInteractManager.class.getDeclaredField("gamemode"); - this.playerInteractManagerGamemode.setAccessible(true); - } catch (NoSuchFieldException | SecurityException e) { - System.err.println("[OpenInv] Unable to directly write player gamemode! SilentChest will fail."); - e.printStackTrace(); - } - } - - @Override - public boolean isAnySilentContainer(@NotNull final org.bukkit.block.Block bukkitBlock) { - if (bukkitBlock.getType() == Material.ENDER_CHEST) { - return true; - } - BlockState state = bukkitBlock.getState(); - return state instanceof org.bukkit.block.Chest - || state instanceof org.bukkit.block.ShulkerBox - || state instanceof org.bukkit.block.Barrel; - } - - @Override - public boolean isAnyContainerNeeded(@NotNull final Player p, @NotNull final org.bukkit.block.Block block) { - BlockState blockState = block.getState(); - - // Barrels do not require AnyContainer. - if (blockState instanceof Barrel) { - return false; - } - - // Enderchests require a non-occluding block on top to open. - if (blockState instanceof EnderChest) { - return block.getRelative(0, 1, 0).getType().isOccluding(); - } - - // Shulker boxes require 1/2 a block clear in the direction they open. - if (blockState instanceof ShulkerBox) { - BoundingBox boundingBox = block.getBoundingBox(); - if (boundingBox.getVolume() > 1) { - // Shulker box is already open. - return false; - } - - BlockData blockData = block.getBlockData(); - if (!(blockData instanceof Directional)) { - // Shouldn't be possible. Just in case, demand AnyChest. - return true; - } - - Directional directional = (Directional) blockData; - BlockFace face = directional.getFacing(); - boundingBox.shift(face.getDirection()); - // Return whether or not bounding boxes overlap. - return block.getRelative(face, 1).getBoundingBox().overlaps(boundingBox); - } - - if (!(blockState instanceof org.bukkit.block.Chest)) { - return false; - } - - if (isBlockedChest(block)) { - return true; - } - - BlockData blockData = block.getBlockData(); - if (!(blockData instanceof Chest) || ((Chest) blockData).getType() == Chest.Type.SINGLE) { - return false; - } - - Chest chest = (Chest) blockData; - int ordinal = (chest.getFacing().ordinal() + 4 + (chest.getType() == Chest.Type.RIGHT ? -1 : 1)) % 4; - BlockFace relativeFace = BlockFace.values()[ordinal]; - org.bukkit.block.Block relative = block.getRelative(relativeFace); - - if (relative.getType() != block.getType()) { - return false; - } - - BlockData relativeData = relative.getBlockData(); - if (!(relativeData instanceof Chest)) { - return false; - } - - Chest relativeChest = (Chest) relativeData; - if (relativeChest.getFacing() != chest.getFacing() - || relativeChest.getType() != (chest.getType() == Chest.Type.RIGHT ? Chest.Type.LEFT : Chest.Type.RIGHT)) { - return false; - } - - return isBlockedChest(relative); - } - - private boolean isBlockedChest(org.bukkit.block.Block block) { - org.bukkit.block.Block relative = block.getRelative(0, 1, 0); - return relative.getType().isOccluding() - || block.getWorld().getNearbyEntities(BoundingBox.of(relative), entity -> entity instanceof Cat).size() > 0; - } - - @Override - public boolean activateContainer(@NotNull final Player bukkitPlayer, final boolean silentchest, - @NotNull final org.bukkit.block.Block bukkitBlock) { - - // Silent ender chest is API-only - if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) { - bukkitPlayer.openInventory(bukkitPlayer.getEnderChest()); - bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); - return true; - } - - EntityPlayer player = PlayerDataManager.getHandle(bukkitPlayer); - - final World world = player.world; - final BlockPosition blockPosition = new BlockPosition(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ()); - final TileEntity tile = world.getTileEntity(blockPosition); - - if (tile == null) { - return false; - } - - if (tile instanceof TileEntityEnderChest) { - // Anychest ender chest. See net.minecraft.server.BlockEnderChest - InventoryEnderChest enderChest = player.getEnderChest(); - enderChest.a((TileEntityEnderChest) tile); - player.openContainer(new TileInventory((containerCounter, playerInventory, ignored) -> { - Containers containers = PlayerDataManager.getContainers(enderChest.getSize()); - int rows = enderChest.getSize() / 9; - return new ContainerChest(containers, containerCounter, playerInventory, enderChest, rows); - }, new ChatMessage("container.enderchest"))); - bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED); - return true; - } - - if (!(tile instanceof ITileInventory)) { - return false; - } - - ITileInventory tileInventory = (ITileInventory) tile; - IBlockData blockData = world.getType(blockPosition); - Block block = blockData.getBlock(); - - if (block instanceof BlockChest) { - - BlockPropertyChestType chestType = blockData.get(BlockChest.c); - - if (chestType != BlockPropertyChestType.SINGLE) { - - BlockPosition adjacentBlockPosition = blockPosition.shift(BlockChest.h(blockData)); - IBlockData adjacentBlockData = world.getType(adjacentBlockPosition); - - if (adjacentBlockData.getBlock() == block) { - - BlockPropertyChestType adjacentChestType = adjacentBlockData.get(BlockChest.c); - - if (adjacentChestType != BlockPropertyChestType.SINGLE && chestType != adjacentChestType - && adjacentBlockData.get(BlockChest.FACING) == blockData.get(BlockChest.FACING)) { - - TileEntity adjacentTile = world.getTileEntity(adjacentBlockPosition); - - if (adjacentTile instanceof TileEntityChest && tileInventory instanceof TileEntityChest) { - TileEntityChest rightChest = chestType == BlockPropertyChestType.RIGHT ? ((TileEntityChest) tileInventory) : (TileEntityChest) adjacentTile; - TileEntityChest leftChest = chestType == BlockPropertyChestType.RIGHT ? (TileEntityChest) adjacentTile : ((TileEntityChest) tileInventory); - - if (silentchest && (rightChest.lootTable != null || leftChest.lootTable != null)) { - OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); - return false; - } - - tileInventory = new ITileInventory() { - public Container createMenu(int containerCounter, PlayerInventory playerInventory, EntityHuman entityHuman) { - leftChest.d(playerInventory.player); - rightChest.d(playerInventory.player); - return ContainerChest.b(containerCounter, playerInventory, new InventoryLargeChest(rightChest, leftChest)); - } - - public IChatBaseComponent getScoreboardDisplayName() { - if (leftChest.hasCustomName()) { - return leftChest.getScoreboardDisplayName(); - } - if (rightChest.hasCustomName()) { - return rightChest.getScoreboardDisplayName(); - } - return new ChatMessage("container.chestDouble"); - } - }; - } - } - } - } - - if (block instanceof BlockChestTrapped) { - bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED); - } else { - bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED); - } - } - - if (block instanceof BlockShulkerBox) { - bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED); - } - - if (block instanceof BlockBarrel) { - bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL); - } - - // AnyChest only - SilentChest not active, container unsupported, or unnecessary. - if (!silentchest || player.playerInteractManager.getGameMode() == EnumGamemode.SPECTATOR) { - player.openContainer(tileInventory); - return true; - } - - // SilentChest requires access to setting players' gamemode directly. - if (this.playerInteractManagerGamemode == null) { - return false; - } - - if (tile instanceof TileEntityLootable) { - TileEntityLootable lootable = (TileEntityLootable) tile; - if (lootable.lootTable != null) { - OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated"); - return false; - } - } - - EnumGamemode gamemode = player.playerInteractManager.getGameMode(); - this.forceGameMode(player, EnumGamemode.SPECTATOR); - player.openContainer(tileInventory); - this.forceGameMode(player, gamemode); - return true; - } - - @Override - public void deactivateContainer(@NotNull final Player bukkitPlayer) { - if (this.playerInteractManagerGamemode == null) { - return; - } - - InventoryView view = bukkitPlayer.getOpenInventory(); - switch (view.getType()) { - case CHEST: - case ENDER_CHEST: - case SHULKER_BOX: - case BARREL: - break; - default: - return; - } - - EntityPlayer player = PlayerDataManager.getHandle(bukkitPlayer); - - EnumGamemode gamemode = player.playerInteractManager.getGameMode(); - this.forceGameMode(player, EnumGamemode.SPECTATOR); - player.activeContainer.b(player); - player.activeContainer.a(player, false); - player.activeContainer.transferTo(player.defaultContainer, player.getBukkitEntity()); - player.activeContainer = player.defaultContainer; - this.forceGameMode(player, gamemode); - } - - private void forceGameMode(final EntityPlayer player, final EnumGamemode gameMode) { - if (this.playerInteractManagerGamemode == null) { - // No need to warn repeatedly, error on startup and lack of function should be enough. - return; - } - try { - if (!this.playerInteractManagerGamemode.isAccessible()) { - // Just in case, ensure accessible. - this.playerInteractManagerGamemode.setAccessible(true); - } - this.playerInteractManagerGamemode.set(player.playerInteractManager, gameMode); - } catch (IllegalArgumentException | IllegalAccessException e) { - e.printStackTrace(); - } - } - -} diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java deleted file mode 100644 index def3f960..00000000 --- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/OpenPlayer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2011-2021 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal.v1_16_R3; - -import java.io.File; -import java.io.FileOutputStream; -import net.minecraft.server.v1_16_R3.EntityPlayer; -import net.minecraft.server.v1_16_R3.NBTCompressedStreamTools; -import net.minecraft.server.v1_16_R3.NBTTagCompound; -import net.minecraft.server.v1_16_R3.WorldNBTStorage; -import org.apache.logging.log4j.LogManager; -import org.bukkit.craftbukkit.v1_16_R3.CraftServer; -import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer; - -public class OpenPlayer extends CraftPlayer { - - public OpenPlayer(CraftServer server, EntityPlayer entity) { - super(server, entity); - } - - @Override - public void saveData() { - super.saveData(); - EntityPlayer player = this.getHandle(); - // See net.minecraft.server.WorldNBTStorage#save(EntityPlayer) - try { - WorldNBTStorage worldNBTStorage = player.server.getPlayerList().playerFileData; - - NBTTagCompound playerData = player.save(new NBTTagCompound()); - - if (!isOnline()) { - // Special case: save old vehicle data - NBTTagCompound oldData = worldNBTStorage.load(player); - - if (oldData != null && oldData.hasKeyOfType("RootVehicle", 10)) { - // See net.minecraft.server.PlayerList#a(NetworkManager, EntityPlayer) and net.minecraft.server.EntityPlayer#b(NBTTagCompound) - playerData.set("RootVehicle", oldData.getCompound("RootVehicle")); - } - } - - File file = new File(worldNBTStorage.getPlayerDir(), player.getUniqueIDString() + ".dat.tmp"); - File file1 = new File(worldNBTStorage.getPlayerDir(), player.getUniqueIDString() + ".dat"); - - NBTCompressedStreamTools.a(playerData, new FileOutputStream(file)); - - if (file1.exists() && !file1.delete() || !file.renameTo(file1)) { - LogManager.getLogger().warn("Failed to save player data for {}", player.getDisplayName().getString()); - } - - } catch (Exception e) { - LogManager.getLogger().warn("Failed to save player data for {}", player.getDisplayName().getString()); - } - } - -} diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java deleted file mode 100644 index 5a29ceb5..00000000 --- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/PlayerDataManager.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal.v1_16_R3; - -import com.lishid.openinv.internal.IPlayerDataManager; -import com.lishid.openinv.internal.ISpecialInventory; -import com.lishid.openinv.internal.OpenInventoryView; -import com.mojang.authlib.GameProfile; -import java.lang.reflect.Field; -import net.minecraft.server.v1_16_R3.ChatComponentText; -import net.minecraft.server.v1_16_R3.Container; -import net.minecraft.server.v1_16_R3.Containers; -import net.minecraft.server.v1_16_R3.Entity; -import net.minecraft.server.v1_16_R3.EntityPlayer; -import net.minecraft.server.v1_16_R3.MinecraftServer; -import net.minecraft.server.v1_16_R3.PacketPlayOutOpenWindow; -import net.minecraft.server.v1_16_R3.PlayerInteractManager; -import net.minecraft.server.v1_16_R3.World; -import net.minecraft.server.v1_16_R3.WorldServer; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.bukkit.craftbukkit.v1_16_R3.CraftServer; -import org.bukkit.craftbukkit.v1_16_R3.entity.CraftPlayer; -import org.bukkit.craftbukkit.v1_16_R3.event.CraftEventFactory; -import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftContainer; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class PlayerDataManager implements IPlayerDataManager { - - private @Nullable Field bukkitEntity; - - public PlayerDataManager() { - try { - bukkitEntity = Entity.class.getDeclaredField("bukkitEntity"); - } catch (NoSuchFieldException e) { - System.out.println("Unable to obtain field to inject custom save process - players' mounts may be deleted when loaded."); - e.printStackTrace(); - bukkitEntity = null; - } - } - - @NotNull - public static EntityPlayer getHandle(final Player player) { - if (player instanceof CraftPlayer) { - return ((CraftPlayer) player).getHandle(); - } - - Server server = player.getServer(); - EntityPlayer nmsPlayer = null; - - if (server instanceof CraftServer) { - nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getName()); - } - - if (nmsPlayer == null) { - // Could use reflection to examine fields, but it's honestly not worth the bother. - throw new RuntimeException("Unable to fetch EntityPlayer from provided Player implementation"); - } - - return nmsPlayer; - } - - @Nullable - @Override - public Player loadPlayer(@NotNull final OfflinePlayer offline) { - // Ensure player has data - if (!offline.hasPlayedBefore()) { - return null; - } - - // Create a profile and entity to load the player data - // See net.minecraft.server.PlayerList#attemptLogin - GameProfile profile = new GameProfile(offline.getUniqueId(), - offline.getName() != null ? offline.getName() : offline.getUniqueId().toString()); - MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer(); - WorldServer worldServer = server.getWorldServer(World.OVERWORLD); - - if (worldServer == null) { - return null; - } - - EntityPlayer entity = new EntityPlayer(server, worldServer, profile, new PlayerInteractManager(worldServer)); - - try { - injectPlayer(entity); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } - - // Get the bukkit entity - Player target = entity.getBukkitEntity(); - if (target != null) { - // Load data - target.loadData(); - } - // Return the entity - return target; - } - - void injectPlayer(EntityPlayer player) throws IllegalAccessException { - if (bukkitEntity == null) { - return; - } - - bukkitEntity.setAccessible(true); - - bukkitEntity.set(player, new OpenPlayer(player.server.server, player)); - } - - @NotNull - @Override - public Player inject(@NotNull Player player) { - try { - EntityPlayer nmsPlayer = getHandle(player); - injectPlayer(nmsPlayer); - return nmsPlayer.getBukkitEntity(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - return player; - } - } - - @Nullable - @Override - public InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) { - - EntityPlayer nmsPlayer = getHandle(player); - - if (nmsPlayer.playerConnection == null) { - return null; - } - - InventoryView view = getView(player, inventory); - - if (view == null) { - return player.openInventory(inventory.getBukkitInventory()); - } - - Container container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) { - @Override - public Containers getType() { - return getContainers(inventory.getBukkitInventory().getSize()); - } - }; - - container.setTitle(new ChatComponentText(view.getTitle())); - container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container); - - if (container == null) { - return null; - } - - nmsPlayer.playerConnection.sendPacket(new PacketPlayOutOpenWindow(container.windowId, container.getType(), - new ChatComponentText(container.getBukkitView().getTitle()))); - nmsPlayer.activeContainer = container; - container.addSlotListener(nmsPlayer); - - return container.getBukkitView(); - - } - - private @Nullable InventoryView getView(Player player, ISpecialInventory inventory) { - if (inventory instanceof SpecialEnderChest) { - return new OpenInventoryView(player, inventory, "container.enderchest", "'s Ender Chest"); - } else if (inventory instanceof SpecialPlayerInventory) { - return new OpenInventoryView(player, inventory, "container.player", "'s Inventory"); - } else { - return null; - } - } - - static @NotNull Containers getContainers(int inventorySize) { - switch (inventorySize) { - case 9: - return Containers.GENERIC_9X1; - case 18: - return Containers.GENERIC_9X2; - case 36: - return Containers.GENERIC_9X4; - case 41: // PLAYER - case 45: - return Containers.GENERIC_9X5; - case 54: - return Containers.GENERIC_9X6; - case 27: - default: - return Containers.GENERIC_9X3; - } - } - - @Override - public int convertToPlayerSlot(InventoryView view, int rawSlot) { - int topSize = view.getTopInventory().getSize(); - if (topSize <= rawSlot) { - // Slot is not inside special inventory, use Bukkit logic. - return view.convertSlot(rawSlot); - } - - // Main inventory, slots 0-26 -> 9-35 - if (rawSlot < 27) { - return rawSlot + 9; - } - // Hotbar, slots 27-35 -> 0-8 - if (rawSlot < 36) { - return rawSlot - 27; - } - // Armor, slots 36-39 -> 39-36 - if (rawSlot < 40) { - return 36 + (39 - rawSlot); - } - // Off hand - if (rawSlot == 40) { - return 40; - } - // Drop slots, "out of inventory" - return -1; - } - -} diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java deleted file mode 100644 index 7fe8beea..00000000 --- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialEnderChest.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal.v1_16_R3; - -import com.lishid.openinv.internal.ISpecialEnderChest; -import java.util.List; -import net.minecraft.server.v1_16_R3.AutoRecipeStackManager; -import net.minecraft.server.v1_16_R3.ContainerUtil; -import net.minecraft.server.v1_16_R3.EntityHuman; -import net.minecraft.server.v1_16_R3.EntityPlayer; -import net.minecraft.server.v1_16_R3.IInventoryListener; -import net.minecraft.server.v1_16_R3.InventoryEnderChest; -import net.minecraft.server.v1_16_R3.ItemStack; -import net.minecraft.server.v1_16_R3.NonNullList; -import org.bukkit.Location; -import org.bukkit.craftbukkit.v1_16_R3.entity.CraftHumanEntity; -import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftInventory; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryHolder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class SpecialEnderChest extends InventoryEnderChest implements ISpecialEnderChest { - - private final CraftInventory inventory; - private EntityPlayer owner; - private NonNullList items; - private boolean playerOnline; - - public SpecialEnderChest(final Player player, final Boolean online) { - super(PlayerDataManager.getHandle(player)); - this.inventory = new CraftInventory(this); - this.owner = PlayerDataManager.getHandle(player); - this.playerOnline = online; - this.items = this.owner.getEnderChest().items; - } - - @Override - public @NotNull CraftInventory getBukkitInventory() { - return inventory; - } - - @Override - public boolean isInUse() { - return !this.getViewers().isEmpty(); - } - - @Override - public void setPlayerOffline() { - this.playerOnline = false; - } - - @Override - public void setPlayerOnline(@NotNull final Player player) { - if (!this.playerOnline) { - try { - this.owner = PlayerDataManager.getHandle(player); - InventoryEnderChest enderChest = owner.getEnderChest(); - for (int i = 0; i < enderChest.getSize(); ++i) { - enderChest.setItem(i, this.items.get(i)); - } - this.items = enderChest.items; - } catch (Exception ignored) {} - this.playerOnline = true; - } - } - - @Override - public void update() { - this.owner.getEnderChest().update(); - } - - @Override - public List getContents() { - return this.items; - } - - @Override - public void onOpen(CraftHumanEntity who) { - this.owner.getEnderChest().onOpen(who); - } - - @Override - public void onClose(CraftHumanEntity who) { - this.owner.getEnderChest().onClose(who); - } - - @Override - public List getViewers() { - return this.owner.getEnderChest().getViewers(); - } - - @Override - public void setMaxStackSize(int i) { - this.owner.getEnderChest().setMaxStackSize(i); - } - - @Override - public InventoryHolder getOwner() { - return this.owner.getEnderChest().getOwner(); - } - - @Override - public @Nullable Location getLocation() { - return null; - } - - @Override - public void a(IInventoryListener iinventorylistener) { - this.owner.getEnderChest().a(iinventorylistener); - } - - @Override - public void b(IInventoryListener iinventorylistener) { - this.owner.getEnderChest().b(iinventorylistener); - } - - @Override - public ItemStack getItem(int i) { - return i >= 0 && i < this.items.size() ? this.items.get(i) : ItemStack.b; - } - - @Override - public ItemStack splitStack(int i, int j) { - ItemStack itemstack = ContainerUtil.a(this.items, i, j); - if (!itemstack.isEmpty()) { - this.update(); - } - - return itemstack; - } - - @Override - public ItemStack a(ItemStack itemstack) { - ItemStack itemstack1 = itemstack.cloneItemStack(); - - for (int i = 0; i < this.getSize(); ++i) { - ItemStack itemstack2 = this.getItem(i); - if (itemstack2.isEmpty()) { - this.setItem(i, itemstack1); - this.update(); - return ItemStack.b; - } - - if (ItemStack.c(itemstack2, itemstack1)) { - int j = Math.min(this.getMaxStackSize(), itemstack2.getMaxStackSize()); - int k = Math.min(itemstack1.getCount(), j - itemstack2.getCount()); - if (k > 0) { - itemstack2.add(k); - itemstack1.subtract(k); - if (itemstack1.isEmpty()) { - this.update(); - return ItemStack.b; - } - } - } - } - - if (itemstack1.getCount() != itemstack.getCount()) { - this.update(); - } - - return itemstack1; - } - - @Override - public ItemStack splitWithoutUpdate(int i) { - ItemStack itemstack = this.items.get(i); - if (itemstack.isEmpty()) { - return ItemStack.b; - } else { - this.items.set(i, ItemStack.b); - return itemstack; - } - } - - @Override - public void setItem(int i, ItemStack itemstack) { - this.items.set(i, itemstack); - if (!itemstack.isEmpty() && itemstack.getCount() > this.getMaxStackSize()) { - itemstack.setCount(this.getMaxStackSize()); - } - - this.update(); - } - - @Override - public int getSize() { - return this.owner.getEnderChest().getSize(); - } - - @Override - public boolean isEmpty() { - - for (ItemStack itemstack : this.items) { - if (!itemstack.isEmpty()) { - return false; - } - } - - return true; - } - - @Override - public int getMaxStackSize() { - return 64; - } - - @Override - public boolean a(EntityHuman entityhuman) { - return true; - } - - @Override - public void startOpen(EntityHuman entityhuman) { - } - - @Override - public void closeContainer(EntityHuman entityhuman) { - } - - @Override - public boolean b(int i, ItemStack itemstack) { - return true; - } - - @Override - public void clear() { - this.items.clear(); - } - - @Override - public void a(AutoRecipeStackManager autorecipestackmanager) { - - for (ItemStack itemstack : this.items) { - autorecipestackmanager.b(itemstack); - } - - } - -} diff --git a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java b/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java deleted file mode 100644 index ada345c1..00000000 --- a/internal/v1_16_R3/src/main/java/com/lishid/openinv/internal/v1_16_R3/SpecialPlayerInventory.java +++ /dev/null @@ -1,733 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal.v1_16_R3; - -import com.google.common.collect.ImmutableList; -import com.lishid.openinv.internal.ISpecialPlayerInventory; -import java.util.Iterator; -import java.util.List; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import net.minecraft.server.v1_16_R3.AutoRecipeStackManager; -import net.minecraft.server.v1_16_R3.ChatMessage; -import net.minecraft.server.v1_16_R3.ContainerUtil; -import net.minecraft.server.v1_16_R3.CrashReport; -import net.minecraft.server.v1_16_R3.CrashReportSystemDetails; -import net.minecraft.server.v1_16_R3.DamageSource; -import net.minecraft.server.v1_16_R3.EntityHuman; -import net.minecraft.server.v1_16_R3.EntityPlayer; -import net.minecraft.server.v1_16_R3.EnumItemSlot; -import net.minecraft.server.v1_16_R3.IBlockData; -import net.minecraft.server.v1_16_R3.IChatBaseComponent; -import net.minecraft.server.v1_16_R3.IInventory; -import net.minecraft.server.v1_16_R3.Item; -import net.minecraft.server.v1_16_R3.ItemArmor; -import net.minecraft.server.v1_16_R3.ItemStack; -import net.minecraft.server.v1_16_R3.NBTTagCompound; -import net.minecraft.server.v1_16_R3.NBTTagList; -import net.minecraft.server.v1_16_R3.NonNullList; -import net.minecraft.server.v1_16_R3.PacketPlayOutSetSlot; -import net.minecraft.server.v1_16_R3.PlayerInventory; -import net.minecraft.server.v1_16_R3.ReportedException; -import net.minecraft.server.v1_16_R3.World; -import org.bukkit.Location; -import org.bukkit.craftbukkit.v1_16_R3.entity.CraftHumanEntity; -import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftInventory; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryHolder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public class SpecialPlayerInventory extends PlayerInventory implements ISpecialPlayerInventory { - - private final CraftInventory inventory; - private boolean playerOnline; - private EntityHuman player; - private NonNullList items, armor, extraSlots; - private List> f; - - public SpecialPlayerInventory(final Player bukkitPlayer, final Boolean online) { - super(PlayerDataManager.getHandle(bukkitPlayer)); - this.inventory = new CraftInventory(this); - this.playerOnline = online; - this.player = super.player; - this.items = this.player.inventory.items; - this.armor = this.player.inventory.armor; - this.extraSlots = this.player.inventory.extraSlots; - this.f = ImmutableList.of(this.items, this.armor, this.extraSlots); - } - - @Override - public void setPlayerOnline(@NotNull final Player player) { - if (!this.playerOnline) { - EntityPlayer entityPlayer = PlayerDataManager.getHandle(player); - entityPlayer.inventory.transaction.addAll(this.transaction); - this.player = entityPlayer; - for (int i = 0; i < getSize(); ++i) { - this.player.inventory.setItem(i, getRawItem(i)); - } - this.player.inventory.itemInHandIndex = this.itemInHandIndex; - this.items = this.player.inventory.items; - this.armor = this.player.inventory.armor; - this.extraSlots = this.player.inventory.extraSlots; - this.f = ImmutableList.of(this.items, this.armor, this.extraSlots); - this.playerOnline = true; - } - } - - @Override - public boolean a(final EntityHuman entityhuman) { - return true; - } - - @Override - public @NotNull CraftInventory getBukkitInventory() { - return this.inventory; - } - - @Override - public ItemStack getItem(int i) { - List list = this.items; - - if (i >= list.size()) { - i -= list.size(); - list = this.armor; - } else { - i = this.getReversedItemSlotNum(i); - } - - if (i >= list.size()) { - i -= list.size(); - list = this.extraSlots; - } else if (list == this.armor) { - i = this.getReversedArmorSlotNum(i); - } - - if (i >= list.size()) { - return ItemStack.b; - } - - return list.get(i); - } - - private ItemStack getRawItem(int i) { - NonNullList list = null; - for (NonNullList next : this.f) { - if (i < next.size()) { - list = next; - break; - } - i -= next.size(); - } - - return list == null ? ItemStack.b : list.get(i); - } - - @Override - public IChatBaseComponent getDisplayName() { - return new ChatMessage(this.player.getName()); - } - - @Override - public boolean hasCustomName() { - return false; - } - - private int getReversedArmorSlotNum(final int i) { - if (i == 0) { - return 3; - } - if (i == 1) { - return 2; - } - if (i == 2) { - return 1; - } - if (i == 3) { - return 0; - } - return i; - } - - private int getReversedItemSlotNum(final int i) { - if (i >= 27) { - return i - 27; - } - return i + 9; - } - - @Override - public int getSize() { - return 45; - } - - @Override - public boolean isInUse() { - return !this.getViewers().isEmpty(); - } - - @Override - public void setItem(int i, final ItemStack itemstack) { - List list = this.items; - - if (i >= list.size()) { - i -= list.size(); - list = this.armor; - } else { - i = this.getReversedItemSlotNum(i); - } - - if (i >= list.size()) { - i -= list.size(); - list = this.extraSlots; - } else if (list == this.armor) { - i = this.getReversedArmorSlotNum(i); - } - - if (i >= list.size()) { - this.player.drop(itemstack, true); - return; - } - - list.set(i, itemstack); - } - - @Override - public void setPlayerOffline() { - this.playerOnline = false; - } - - @Override - public ItemStack splitStack(int i, final int j) { - List list = this.items; - - if (i >= list.size()) { - i -= list.size(); - list = this.armor; - } else { - i = this.getReversedItemSlotNum(i); - } - - if (i >= list.size()) { - i -= list.size(); - list = this.extraSlots; - } else if (list == this.armor) { - i = this.getReversedArmorSlotNum(i); - } - - if (i >= list.size()) { - return ItemStack.b; - } - - return list.get(i).isEmpty() ? ItemStack.b : ContainerUtil.a(list, i, j); - } - - @Override - public ItemStack splitWithoutUpdate(int i) { - List list = this.items; - - if (i >= list.size()) { - i -= list.size(); - list = this.armor; - } else { - i = this.getReversedItemSlotNum(i); - } - - if (i >= list.size()) { - i -= list.size(); - list = this.extraSlots; - } else if (list == this.armor) { - i = this.getReversedArmorSlotNum(i); - } - - if (i >= list.size()) { - return ItemStack.b; - } - - if (!list.get(i).isEmpty()) { - ItemStack itemstack = list.get(i); - - list.set(i, ItemStack.b); - return itemstack; - } - - return ItemStack.b; - } - - @Override - public List getContents() { - return this.f.stream().flatMap(List::stream).collect(Collectors.toList()); - } - - @Override - public List getArmorContents() { - return this.armor; - } - - @Override - public void onOpen(CraftHumanEntity who) { - this.transaction.add(who); - } - - @Override - public void onClose(CraftHumanEntity who) { - this.transaction.remove(who); - } - - @Override - public List getViewers() { - return this.transaction; - } - - @Override - public InventoryHolder getOwner() { - return this.player.getBukkitEntity(); - } - - @Override - public Location getLocation() { - return this.player.getBukkitEntity().getLocation(); - } - - @Override - public ItemStack getItemInHand() { - return d(this.itemInHandIndex) ? this.items.get(this.itemInHandIndex) : ItemStack.b; - } - - private boolean isSimilarAndNotFull(ItemStack itemstack, ItemStack itemstack1) { - return !itemstack.isEmpty() && this.b(itemstack, itemstack1) && itemstack.isStackable() && itemstack.getCount() < itemstack.getMaxStackSize() && itemstack.getCount() < this.getMaxStackSize(); - } - - private boolean b(ItemStack itemstack, ItemStack itemstack1) { - return itemstack.getItem() == itemstack1.getItem() && ItemStack.equals(itemstack, itemstack1); - } - - @Override - public int canHold(ItemStack itemstack) { - int remains = itemstack.getCount(); - - for (int i = 0; i < this.items.size(); ++i) { - ItemStack itemstack1 = this.getItem(i); - if (itemstack1.isEmpty()) { - return itemstack.getCount(); - } - - if (!this.isSimilarAndNotFull(itemstack, itemstack1)) { - remains -= Math.min(itemstack1.getMaxStackSize(), this.getMaxStackSize()) - itemstack1.getCount(); - } - - if (remains <= 0) { - return itemstack.getCount(); - } - } - - ItemStack offhandItemStack = this.getItem(this.items.size() + this.armor.size()); - if (this.isSimilarAndNotFull(offhandItemStack, itemstack)) { - remains -= Math.min(offhandItemStack.getMaxStackSize(), this.getMaxStackSize()) - offhandItemStack.getCount(); - } - - return itemstack.getCount() - remains; - } - - @Override - public int getFirstEmptySlotIndex() { - for (int i = 0; i < this.items.size(); ++i) { - if (this.items.get(i).isEmpty()) { - return i; - } - } - - return -1; - } - - @Override - public void c(int i) { - this.itemInHandIndex = this.i(); - ItemStack itemstack = this.items.get(this.itemInHandIndex); - this.items.set(this.itemInHandIndex, this.items.get(i)); - this.items.set(i, itemstack); - } - - @Override - public int c(ItemStack itemstack) { - for (int i = 0; i < this.items.size(); ++i) { - ItemStack itemstack1 = this.items.get(i); - if (!this.items.get(i).isEmpty() && this.b(itemstack, this.items.get(i)) && !this.items.get(i).f() && !itemstack1.hasEnchantments() && !itemstack1.hasName()) { - return i; - } - } - - return -1; - } - - @Override - public int i() { - int i; - int j; - for (j = 0; j < 9; ++j) { - i = (this.itemInHandIndex + j) % 9; - if (this.items.get(i).isEmpty()) { - return i; - } - } - - for (j = 0; j < 9; ++j) { - i = (this.itemInHandIndex + j) % 9; - if (!this.items.get(i).hasEnchantments()) { - return i; - } - } - - return this.itemInHandIndex; - } - - @Override - public int a(Predicate predicate, int i, IInventory iinventory) { - byte b0 = 0; - boolean flag = i == 0; - int j = b0 + ContainerUtil.a(this, predicate, i - b0, flag); - j += ContainerUtil.a(iinventory, predicate, i - j, flag); - j += ContainerUtil.a(this.getCarried(), predicate, i - j, flag); - if (this.getCarried().isEmpty()) { - this.setCarried(ItemStack.b); - } - - return j; - } - - private int i(ItemStack itemstack) { - int i = this.firstPartial(itemstack); - if (i == -1) { - i = this.getFirstEmptySlotIndex(); - } - - return i == -1 ? itemstack.getCount() : this.d(i, itemstack); - } - - private int d(int i, ItemStack itemstack) { - Item item = itemstack.getItem(); - int j = itemstack.getCount(); - ItemStack itemstack1 = this.getItem(i); - if (itemstack1.isEmpty()) { - itemstack1 = new ItemStack(item, 0); - NBTTagCompound tag = itemstack.getTag(); - if (tag != null) { - itemstack1.setTag(tag.clone()); - } - - this.setItem(i, itemstack1); - } - - int k = j; - if (j > itemstack1.getMaxStackSize() - itemstack1.getCount()) { - k = itemstack1.getMaxStackSize() - itemstack1.getCount(); - } - - if (k > this.getMaxStackSize() - itemstack1.getCount()) { - k = this.getMaxStackSize() - itemstack1.getCount(); - } - - if (k != 0) { - j -= k; - itemstack1.add(k); - itemstack1.d(5); - } - return j; - } - - @Override - public int firstPartial(ItemStack itemstack) { - if (this.isSimilarAndNotFull(this.getItem(this.itemInHandIndex), itemstack)) { - return this.itemInHandIndex; - } else if (this.isSimilarAndNotFull(this.getItem(40), itemstack)) { - return 40; - } else { - for (int i = 0; i < this.items.size(); ++i) { - if (this.isSimilarAndNotFull(this.items.get(i), itemstack)) { - return i; - } - } - - return -1; - } - } - - @Override - public void j() { - - for (List itemStacks : this.f) { - for (int i = 0; i < itemStacks.size(); ++i) { - if (!itemStacks.get(i).isEmpty()) { - itemStacks.get(i).a(this.player.world, this.player, i, this.itemInHandIndex == i); - } - } - } - - } - - @Override - public boolean pickup(ItemStack itemstack) { - return this.c(-1, itemstack); - } - - @Override - public boolean c(int i, ItemStack itemstack) { - if (itemstack.isEmpty()) { - return false; - } else { - try { - if (itemstack.f()) { - if (i == -1) { - i = this.getFirstEmptySlotIndex(); - } - - if (i >= 0) { - this.items.set(i, itemstack.cloneItemStack()); - this.items.get(i).d(5); - itemstack.setCount(0); - return true; - } else if (this.player.abilities.canInstantlyBuild) { - itemstack.setCount(0); - return true; - } else { - return false; - } - } else { - int j; - do { - j = itemstack.getCount(); - if (i == -1) { - itemstack.setCount(this.i(itemstack)); - } else { - itemstack.setCount(this.d(i, itemstack)); - } - } while(!itemstack.isEmpty() && itemstack.getCount() < j); - - if (itemstack.getCount() == j && this.player.abilities.canInstantlyBuild) { - itemstack.setCount(0); - return true; - } else { - return itemstack.getCount() < j; - } - } - } catch (Throwable var6) { - CrashReport crashreport = CrashReport.a(var6, "Adding item to inventory"); - CrashReportSystemDetails crashreportsystemdetails = crashreport.a("Item being added"); - crashreportsystemdetails.a("Item ID", Item.getId(itemstack.getItem())); - crashreportsystemdetails.a("Item data", itemstack.getDamage()); - crashreportsystemdetails.a("Item name", () -> itemstack.getName().getString()); - throw new ReportedException(crashreport); - } - } - } - - @Override - public void a(World world, ItemStack itemstack) { - if (!world.isClientSide) { - while(!itemstack.isEmpty()) { - int i = this.firstPartial(itemstack); - if (i == -1) { - i = this.getFirstEmptySlotIndex(); - } - - if (i == -1) { - this.player.drop(itemstack, false); - break; - } - - int j = itemstack.getMaxStackSize() - this.getItem(i).getCount(); - if (this.c(i, itemstack.cloneAndSubtract(j))) { - ((EntityPlayer)this.player).playerConnection.sendPacket(new PacketPlayOutSetSlot(-2, i, this.getItem(i))); - } - } - } - - } - - @Override - public void f(ItemStack itemstack) { - - for (List list : this.f) { - for (int i = 0; i < list.size(); ++i) { - if (list.get(i) == itemstack) { - list.set(i, ItemStack.b); - break; - } - } - } - } - - @Override - public float a(IBlockData iblockdata) { - return this.items.get(this.itemInHandIndex).a(iblockdata); - } - - @Override - public NBTTagList a(NBTTagList nbttaglist) { - NBTTagCompound nbttagcompound; - int i; - for (i = 0; i < this.items.size(); ++i) { - if (!this.items.get(i).isEmpty()) { - nbttagcompound = new NBTTagCompound(); - nbttagcompound.setByte("Slot", (byte) i); - this.items.get(i).save(nbttagcompound); - nbttaglist.add(nbttagcompound); - } - } - - for (i = 0; i < this.armor.size(); ++i) { - if (!this.armor.get(i).isEmpty()) { - nbttagcompound = new NBTTagCompound(); - nbttagcompound.setByte("Slot", (byte) (i + 100)); - this.armor.get(i).save(nbttagcompound); - nbttaglist.add(nbttagcompound); - } - } - - for (i = 0; i < this.extraSlots.size(); ++i) { - if (!this.extraSlots.get(i).isEmpty()) { - nbttagcompound = new NBTTagCompound(); - nbttagcompound.setByte("Slot", (byte) (i + 150)); - this.extraSlots.get(i).save(nbttagcompound); - nbttaglist.add(nbttagcompound); - } - } - - return nbttaglist; - } - - @Override - public void b(NBTTagList nbttaglist) { - this.items.clear(); - this.armor.clear(); - this.extraSlots.clear(); - - for(int i = 0; i < nbttaglist.size(); ++i) { - NBTTagCompound nbttagcompound = nbttaglist.getCompound(i); - int j = nbttagcompound.getByte("Slot") & 255; - ItemStack itemstack = ItemStack.a(nbttagcompound); - if (!itemstack.isEmpty()) { - if (j < this.items.size()) { - this.items.set(j, itemstack); - } else if (j >= 100 && j < this.armor.size() + 100) { - this.armor.set(j - 100, itemstack); - } else if (j >= 150 && j < this.extraSlots.size() + 150) { - this.extraSlots.set(j - 150, itemstack); - } - } - } - - } - - @Override - public boolean isEmpty() { - Iterator iterator = this.items.iterator(); - - ItemStack itemstack; - while (iterator.hasNext()) { - itemstack = iterator.next(); - if (!itemstack.isEmpty()) { - return false; - } - } - - iterator = this.armor.iterator(); - - while (iterator.hasNext()) { - itemstack = iterator.next(); - if (!itemstack.isEmpty()) { - return false; - } - } - - iterator = this.extraSlots.iterator(); - - while (iterator.hasNext()) { - itemstack = iterator.next(); - if (!itemstack.isEmpty()) { - return false; - } - } - - return true; - } - - @Nullable - @Override - public IChatBaseComponent getCustomName() { - return null; - } - - @Override - public void a(DamageSource damagesource, float f) { - if (f > 0.0F) { - f /= 4.0F; - if (f < 1.0F) { - f = 1.0F; - } - - for (int i = 0; i < this.armor.size(); ++i) { - ItemStack itemstack = this.armor.get(0); - int index = i; - if ((!damagesource.isFire() || !itemstack.getItem().u()) && itemstack.getItem() instanceof ItemArmor) { - itemstack.damage((int) f, this.player, (entityHuman) -> entityHuman.broadcastItemBreak(EnumItemSlot.a(EnumItemSlot.Function.ARMOR, index))); - } - } - } - } - - @Override - public void dropContents() { - for (List itemStacks : this.f) { - for (int i = 0; i < itemStacks.size(); ++i) { - ItemStack itemstack = itemStacks.get(i); - if (!itemstack.isEmpty()) { - itemStacks.set(i, ItemStack.b); - this.player.a(itemstack, true, false); - } - } - } - } - - @Override - public boolean h(ItemStack itemstack) { - return this.f.stream().flatMap(List::stream).anyMatch(itemStack1 -> !itemStack1.isEmpty() && itemStack1.doMaterialsMatch(itemstack)); - } - - @Override - public void a(PlayerInventory playerinventory) { - for (int i = 0; i < playerinventory.getSize(); ++i) { - this.setItem(i, playerinventory.getItem(i)); - } - - this.itemInHandIndex = playerinventory.itemInHandIndex; - } - - @Override - public void clear() { - this.f.forEach(List::clear); - } - - @Override - public void a(AutoRecipeStackManager autorecipestackmanager) { - for (ItemStack itemstack : this.items) { - autorecipestackmanager.a(itemstack); - } - } - -} diff --git a/jitpack.yml b/jitpack.yml new file mode 100644 index 00000000..4130c60e --- /dev/null +++ b/jitpack.yml @@ -0,0 +1,6 @@ +before_install: + - sdk update + - sdk install java 21-tem + - sdk use java 21-tem +install: + - ./gradlew -Djitpack=true :openinvapi:publishJitpackPublicationToMavenLocal diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 00000000..d00a60a6 --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,53 @@ +import com.github.jikoo.openinv.SpigotReobf + +plugins { + `openinv-base` + alias(libs.plugins.shadow) +} + +repositories { + maven("https://jitpack.io") +} + +dependencies { + implementation(project(":openinvapi")) + implementation(project(":openinvcommon")) + implementation(project(":openinvadaptercommon")) + implementation(project(":openinvadapterpaper1_21_10")) + implementation(project(":openinvadapterpaper1_21_8")) + implementation(project(":openinvadapterpaper1_21_5")) + implementation(project(":openinvadapterpaper1_21_4")) + implementation(project(":openinvadapterpaper1_21_3")) + implementation(project(":openinvadapterpaper1_21_1")) + implementation(project(":openinvadapterspigot", configuration = SpigotReobf.ARTIFACT_CONFIG)) + implementation(libs.planarwrappers) + implementation(libs.folia.scheduler.wrapper) +} + +tasks.processResources { + expand("version" to version) +} + +tasks.jar { + manifest.attributes("paperweight-mappings-namespace" to "mojang") +} + +tasks.shadowJar { + relocate("me.nahu.scheduler.wrapper", "com.github.jikoo.openinv.lib.nahu.scheduler-wrapper") + relocate("com.github.jikoo.planarwrappers", "com.github.jikoo.openinv.lib.planarwrappers") + minimize { + exclude(":openinv**") + exclude(dependency(libs.folia.scheduler.wrapper.get())) + } +} + +tasks.register("distributePlugin") { + into(rootProject.layout.projectDirectory.dir("dist")) + from(tasks.shadowJar) + rename("openinvplugin.*\\.jar", "OpenInv.jar") +} + +tasks.assemble { + dependsOn(tasks.shadowJar) + dependsOn(tasks.named("distributePlugin")) +} diff --git a/plugin/pom.xml b/plugin/pom.xml deleted file mode 100644 index 358b3962..00000000 --- a/plugin/pom.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - 4.0.0 - - - com.lishid - openinvparent - 4.1.6-SNAPSHOT - - - openinvplugincore - OpenInvPlugin - - - - com.lishid - openinvapi - 4.1.6-SNAPSHOT - - - org.spigotmc - spigot-api - 1.15.2-R0.1-SNAPSHOT - provided - - - - - - - src/main/resources - true - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.2 - - true - - - - package - - shade - - - - - - - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - - - - - - diff --git a/plugin/src/main/java/com/lishid/openinv/OpenInv.java b/plugin/src/main/java/com/lishid/openinv/OpenInv.java index 81375843..e56d9d22 100644 --- a/plugin/src/main/java/com/lishid/openinv/OpenInv.java +++ b/plugin/src/main/java/com/lishid/openinv/OpenInv.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2022 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,35 +16,30 @@ package com.lishid.openinv; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; -import com.lishid.openinv.commands.ContainerSettingCommand; -import com.lishid.openinv.commands.OpenInvCommand; -import com.lishid.openinv.commands.SearchContainerCommand; -import com.lishid.openinv.commands.SearchEnchantCommand; -import com.lishid.openinv.commands.SearchInvCommand; +import com.lishid.openinv.command.ClearInvCommand; +import com.lishid.openinv.command.ContainerSettingCommand; +import com.lishid.openinv.command.OpenInvCommand; +import com.lishid.openinv.command.SearchContainerCommand; +import com.lishid.openinv.command.SearchEnchantCommand; +import com.lishid.openinv.command.SearchInvCommand; import com.lishid.openinv.internal.IAnySilentContainer; import com.lishid.openinv.internal.ISpecialEnderChest; import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; -import com.lishid.openinv.listeners.InventoryListener; -import com.lishid.openinv.listeners.PlayerListener; -import com.lishid.openinv.listeners.PluginListener; -import com.lishid.openinv.util.Cache; -import com.lishid.openinv.util.ConfigUpdater; +import com.lishid.openinv.listener.ContainerListener; +import com.lishid.openinv.listener.ToggleListener; +import com.lishid.openinv.util.AccessEqualMode; import com.lishid.openinv.util.InternalAccessor; -import com.lishid.openinv.util.LanguageManager; +import com.lishid.openinv.util.InventoryManager; import com.lishid.openinv.util.Permissions; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.function.Consumer; -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.TextComponent; -import org.bukkit.Bukkit; +import com.lishid.openinv.util.PlayerLoader; +import com.lishid.openinv.util.config.Config; +import com.lishid.openinv.util.config.ConfigUpdater; +import com.lishid.openinv.util.lang.LangMigrator; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.setting.PlayerToggle; +import com.lishid.openinv.util.setting.PlayerToggles; +import me.nahu.scheduler.wrapper.FoliaWrappedJavaPlugin; import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -52,516 +47,272 @@ import org.bukkit.command.PluginCommand; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; -import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.nio.file.Path; +import java.util.Locale; +import java.util.UUID; +import java.util.function.Consumer; + /** - * Open other player's inventory - * - * @author lishid + * The main class for OpenInv. */ -public class OpenInv extends JavaPlugin implements IOpenInv { - - private final Map inventories = new HashMap<>(); - private final Map enderChests = new HashMap<>(); - private final Multimap> pluginUsage = HashMultimap.create(); - - private final Cache playerCache = new Cache<>(300000L, - value -> { - String key = OpenInv.this.getPlayerID(value); - - return OpenInv.this.inventories.containsKey(key) - && OpenInv.this.inventories.get(key).isInUse() - || OpenInv.this.enderChests.containsKey(key) - && OpenInv.this.enderChests.get(key).isInUse() - || OpenInv.this.pluginUsage.containsKey(key); - }, - value -> { - String key = OpenInv.this.getPlayerID(value); - - // Check if inventory is stored, and if it is, remove it and eject all viewers - if (OpenInv.this.inventories.containsKey(key)) { - Inventory inv = OpenInv.this.inventories.remove(key).getBukkitInventory(); - List viewers = inv.getViewers(); - for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) { - entity.closeInventory(); - } - } - - // Check if ender chest is stored, and if it is, remove it and eject all viewers - if (OpenInv.this.enderChests.containsKey(key)) { - Inventory inv = OpenInv.this.enderChests.remove(key).getBukkitInventory(); - List viewers = inv.getViewers(); - for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) { - entity.closeInventory(); - } - } - - if (!OpenInv.this.disableSaving() && !value.isOnline()) { - value.saveData(); - } - }); - - private InternalAccessor accessor; - private LanguageManager languageManager; - - /** - * Evicts all viewers lacking cross-world permissions from a Player's inventory. - * - * @param player the Player - */ - public void changeWorld(final Player player) { - - String key = this.getPlayerID(player); - - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { - return; - } - - if (this.inventories.containsKey(key)) { - Iterator iterator = this.inventories.get(key).getBukkitInventory().getViewers().iterator(); - //noinspection WhileLoopReplaceableByForEach - while (iterator.hasNext()) { - HumanEntity human = iterator.next(); - // If player has permission or is in the same world, allow continued access - // Just in case, also allow null worlds. - if (Permissions.CROSSWORLD.hasPermission(human) || human.getWorld().equals(player.getWorld())) { - continue; - } - human.closeInventory(); - } - } - - if (this.enderChests.containsKey(key)) { - Iterator iterator = this.enderChests.get(key).getBukkitInventory().getViewers().iterator(); - //noinspection WhileLoopReplaceableByForEach - while (iterator.hasNext()) { - HumanEntity human = iterator.next(); - if (Permissions.CROSSWORLD.hasPermission(human) || human.getWorld().equals(player.getWorld())) { - continue; - } - human.closeInventory(); - } - } - } - - /** - * Convert a raw slot number into a player inventory slot number. - * - *

Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular - * player inventory slot number. - * - * @param view the open inventory view - * @param rawSlot the raw slot in the view - * @return the converted slot number - */ - public int convertToPlayerSlot(InventoryView view, int rawSlot) { - return this.accessor.getPlayerDataManager().convertToPlayerSlot(view, rawSlot); - } - - @Override - public boolean disableSaving() { - return this.getConfig().getBoolean("settings.disable-saving", false); - } - - @NotNull - @Override - public IAnySilentContainer getAnySilentContainer() { - return this.accessor.getAnySilentContainer(); - } - - @Override - public boolean getPlayerAnyChestStatus(@NotNull final OfflinePlayer player) { - boolean defaultState = false; - - if (player.isOnline()) { - Player onlinePlayer = player.getPlayer(); - if (onlinePlayer != null) { - defaultState = Permissions.ANY_DEFAULT.hasPermission(onlinePlayer); - } - } - - return this.getConfig().getBoolean("toggles.any-chest." + this.getPlayerID(player), defaultState); - } - - @Override - public boolean getPlayerSilentChestStatus(@NotNull final OfflinePlayer offline) { - boolean defaultState = false; - - if (offline.isOnline()) { - Player onlinePlayer = offline.getPlayer(); - if (onlinePlayer != null) { - defaultState = Permissions.SILENT_DEFAULT.hasPermission(onlinePlayer); - } - } - - return this.getConfig().getBoolean("toggles.silent-chest." + this.getPlayerID(offline), defaultState); - } - - @NotNull - @Override - public ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) - throws InstantiationException { - String id = this.getPlayerID(player); - if (this.enderChests.containsKey(id)) { - return this.enderChests.get(id); - } - ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online); - this.enderChests.put(id, inv); - this.playerCache.put(id, player); - return inv; - } - - @NotNull - @Override - public ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) - throws InstantiationException { - String id = this.getPlayerID(player); - if (this.inventories.containsKey(id)) { - return this.inventories.get(id); - } - ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online); - this.inventories.put(id, inv); - this.playerCache.put(id, player); - return inv; - } - - @Override - public boolean isSupportedVersion() { - return this.accessor != null && this.accessor.isSupported(); - } - - @Override - public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { - - String key = this.getPlayerID(offline); - if (this.playerCache.containsKey(key)) { - return this.playerCache.get(key); - } - - Player player = offline.getPlayer(); - if (player != null) { - this.playerCache.put(key, player); - return player; - } - - if (!this.isSupportedVersion()) { - return null; - } - - if (Bukkit.isPrimaryThread()) { - return this.accessor.getPlayerDataManager().loadPlayer(offline); - } - - Future future = Bukkit.getScheduler().callSyncMethod(this, - () -> OpenInv.this.accessor.getPlayerDataManager().loadPlayer(offline)); - - try { - player = future.get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - return null; - } - - if (player != null) { - this.playerCache.put(key, player); - } - - return player; - } - - @Override - public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) { - return this.accessor.getPlayerDataManager().openInventory(player, inventory); - } - - public void sendMessage(@NotNull CommandSender sender, @NotNull String key) { - String message = this.languageManager.getValue(key, getLocale(sender)); - - if (message != null && !message.isEmpty()) { - sender.sendMessage(message); - } - } - - public void sendMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) { - String message = this.languageManager.getValue(key, getLocale(sender), replacements); - - if (message != null && !message.isEmpty()) { - sender.sendMessage(message); - } - } - - public void sendSystemMessage(@NotNull Player player, @NotNull String key) { - String message = this.languageManager.getValue(key, getLocale(player)); - - if (message == null) { - return; - } - - int newline = message.indexOf('\n'); - if (newline != -1) { - // No newlines in action bar chat. - message = message.substring(0, newline); - } - - if (message.isEmpty()) { - return; - } - - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); - } - - public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key) { - return this.languageManager.getValue(key, getLocale(sender)); - } - - public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) { - return this.languageManager.getValue(key, getLocale(sender), replacements); - } - - private @Nullable String getLocale(@NotNull CommandSender sender) { - if (sender instanceof Player) { - return ((Player) sender).getLocale(); - } else { - return this.getConfig().getString("settings.locale", "en_us"); - } - } - - @Override - public boolean notifyAnyChest() { - return this.getConfig().getBoolean("notify.any-chest", true); - } - - @Override - public boolean notifySilentChest() { - return this.getConfig().getBoolean("notify.silent-chest", true); - } - - @Override - public void onDisable() { - - if (this.disableSaving()) { - return; - } - - if (this.isSupportedVersion()) { - this.playerCache.invalidateAll(); - } - } - - @Override - public void onEnable() { - - // Save default configuration if not present. - this.saveDefaultConfig(); - - // Get plugin manager - PluginManager pm = this.getServer().getPluginManager(); - - this.accessor = new InternalAccessor(this); - - this.languageManager = new LanguageManager(this, "en_us"); - - // Version check - if (this.accessor.isSupported()) { - // Update existing configuration. May require internal access. - new ConfigUpdater(this).checkForUpdates(); - - // Register listeners - pm.registerEvents(new PlayerListener(this), this); - pm.registerEvents(new PluginListener(this), this); - pm.registerEvents(new InventoryListener(this), this); - - // Register commands to their executors - this.setCommandExecutor(new OpenInvCommand(this), "openinv", "openender"); - this.setCommandExecutor(new SearchContainerCommand(this), "searchcontainer"); - this.setCommandExecutor(new SearchInvCommand(this), "searchinv", "searchender"); - this.setCommandExecutor(new SearchEnchantCommand(this), "searchenchant"); - this.setCommandExecutor(new ContainerSettingCommand(this), "silentcontainer", "anycontainer"); - - } else { - this.sendVersionError(this.getLogger()::warning); - } - - } - - private void sendVersionError(Consumer messageMethod) { - messageMethod.accept("Your server version (" + this.accessor.getVersion() + ") is not supported."); - messageMethod.accept("Please obtain an appropriate version here: " + accessor.getReleasesLink()); - } - - private void setCommandExecutor(CommandExecutor executor, String... commands) { - for (String commandName : commands) { - PluginCommand command = this.getCommand(commandName); - if (command != null) { - command.setExecutor(executor); - } - } - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!this.accessor.isSupported()) { - this.sendVersionError(sender::sendMessage); - return true; - } - return false; - } - - public void releaseAllPlayers(final Plugin plugin) { - Iterator>> iterator = this.pluginUsage.entries().iterator(); - - if (!iterator.hasNext()) { - return; - } - - for (Map.Entry> entry = iterator.next(); iterator.hasNext(); entry = iterator.next()) { - if (entry.getValue().equals(plugin.getClass())) { - iterator.remove(); - } - } - } - - @Override - public void releasePlayer(@NotNull final Player player, @NotNull final Plugin plugin) { - String key = this.getPlayerID(player); - - if (!this.pluginUsage.containsEntry(key, plugin.getClass())) { - return; - } - - this.pluginUsage.remove(key, plugin.getClass()); - } - - @Override - public void retainPlayer(@NotNull final Player player, @NotNull final Plugin plugin) { - String key = this.getPlayerID(player); - - if (this.pluginUsage.containsEntry(key, plugin.getClass())) { - return; - } - - this.pluginUsage.put(key, plugin.getClass()); - } - - @Override - public void setPlayerAnyChestStatus(@NotNull final OfflinePlayer offline, final boolean status) { - this.getConfig().set("toggles.any-chest." + this.getPlayerID(offline), status); - this.saveConfig(); - } - - /** - * Method for handling a Player going offline. - * - * @param player the Player - * @throws IllegalStateException if the server version is unsupported - */ - public void setPlayerOffline(final Player player) { - - String key = this.getPlayerID(player); - - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { - return; - } - - // Replace stored player with our own version - this.playerCache.put(key, this.accessor.getPlayerDataManager().inject(player)); - - if (this.inventories.containsKey(key)) { - this.inventories.get(key).setPlayerOffline(); - } - - if (this.enderChests.containsKey(key)) { - this.enderChests.get(key).setPlayerOffline(); - } - } - - /** - * Method for handling a Player coming online. - * - * @param player the Player - * @throws IllegalStateException if the server version is unsupported - */ - public void setPlayerOnline(final Player player) { - - String key = this.getPlayerID(player); - - // Check if the player is cached. If not, neither of their inventories is open. - if (!this.playerCache.containsKey(key)) { - return; - } - - this.playerCache.put(key, player); - - if (this.inventories.containsKey(key)) { - this.inventories.get(key).setPlayerOnline(player); - new BukkitRunnable() { - @Override - public void run() { - if (player.isOnline()) { - player.updateInventory(); - } - } - }.runTask(this); - } - - if (this.enderChests.containsKey(key)) { - this.enderChests.get(key).setPlayerOnline(player); - } - } - - @Override - public void setPlayerSilentChestStatus(@NotNull final OfflinePlayer offline, final boolean status) { - this.getConfig().set("toggles.silent-chest." + this.getPlayerID(offline), status); - this.saveConfig(); - } - - /** - * Displays all applicable help for OpenInv commands. - * - * @param player the Player to help - */ - public void showHelp(final Player player) { - // Get registered commands - for (String commandName : this.getDescription().getCommands().keySet()) { - PluginCommand command = this.getCommand(commandName); - - // Ensure command is successfully registered and player can use it - if (command == null || !command.testPermissionSilent(player)) { - continue; - } - - // Send usage - player.sendMessage(command.getUsage().replace("", commandName)); - - List aliases = command.getAliases(); - if (aliases.isEmpty()) { - continue; - } - - // Assemble alias list - StringBuilder aliasBuilder = new StringBuilder(" (aliases: "); - for (String alias : aliases) { - aliasBuilder.append(alias).append(", "); - } - aliasBuilder.delete(aliasBuilder.length() - 2, aliasBuilder.length()).append(')'); - - // Send all aliases - player.sendMessage(aliasBuilder.toString()); - } - } - - @Override - public void unload(@NotNull final OfflinePlayer offline) { - this.playerCache.invalidate(this.getPlayerID(offline)); - } +public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv { + + private InternalAccessor accessor; + private Config config; + private InventoryManager inventoryManager; + private LanguageManager languageManager; + private PlayerLoader playerLoader; + private boolean isSpigot = false; + + @Override + public void reloadConfig() { + super.reloadConfig(); + config.reload(getConfig()); + languageManager.reload(); + if (accessor != null && accessor.isSupported()) { + accessor.reload(getConfig()); + } + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!isSpigot || !this.accessor.isSupported()) { + this.sendVersionError(sender::sendMessage); + return true; + } + return false; + } + + @Override + public void onDisable() { + inventoryManager.evictAll(); + } + + @Override + public void onEnable() { + // Save default configuration if not present. + this.saveDefaultConfig(); + + // Migrate locale files to a subfolder. + Path dataFolder = getDataFolder().toPath(); + new LangMigrator(dataFolder, dataFolder.resolve("locale"), getLogger()).migrate(); + + // Set up configurable features. Note that #reloadConfig is called on the first call to #getConfig! + // Configuration values should not be accessed until after all of these have been set up. + config = new Config(); + languageManager = new LanguageManager(this, "en"); + accessor = new InternalAccessor(getLogger(), languageManager); + + // Perform initial config load. + reloadConfig(); + + inventoryManager = new InventoryManager(this, config, accessor); + playerLoader = new PlayerLoader(this, config, inventoryManager, accessor, getLogger()); + + try { + Class.forName("org.bukkit.entity.Player$Spigot"); + isSpigot = true; + } catch (ClassNotFoundException e) { + isSpigot = false; + } + + // Version check + if (isSpigot && this.accessor.isSupported()) { + reloadConfig(); + + // Update existing configuration. May require internal access. + new ConfigUpdater(this).checkForUpdates(); + + // Register relevant event listeners. + registerEvents(); + + // Register commands to their executors. + registerCommands(); + + } else { + this.sendVersionError(this.getLogger()::warning); + } + + } + + private void registerEvents() { + PluginManager pluginManager = this.getServer().getPluginManager(); + pluginManager.registerEvents(playerLoader, this); + pluginManager.registerEvents(inventoryManager, this); + pluginManager.registerEvents(new ContainerListener(accessor, languageManager), this); + pluginManager.registerEvents(new ToggleListener(), this); + } + + private void registerCommands() { + this.setCommandExecutor(new OpenInvCommand(this, config, inventoryManager, languageManager, playerLoader), "openinv", "openender"); + this.setCommandExecutor(new SearchContainerCommand(this, languageManager), "searchcontainer"); + this.setCommandExecutor(new SearchInvCommand(languageManager), "searchinv", "searchender"); + this.setCommandExecutor(new SearchEnchantCommand(languageManager), "searchenchant"); + this.setCommandExecutor(new ClearInvCommand(this, config, inventoryManager, languageManager, playerLoader), "clearinv", "clearender"); + + ContainerSettingCommand settingCommand = new ContainerSettingCommand(languageManager); + for (PlayerToggle toggle : PlayerToggles.get()) { + setCommandExecutor(settingCommand, toggle.getName().toLowerCase(Locale.ENGLISH)); + } + } + + private void setCommandExecutor(@NotNull CommandExecutor executor, String @NotNull ... commands) { + for (String commandName : commands) { + PluginCommand command = this.getCommand(commandName); + if (command != null) { + command.setExecutor(executor); + } + } + } + + private void sendVersionError(@NotNull Consumer messageMethod) { + if (!accessor.isSupported()) { + messageMethod.accept("Your server version (" + accessor.getVersion() + ") is not supported."); + messageMethod.accept("Please download the correct version of OpenInv here: " + accessor.getReleasesLink()); + } + if (!isSpigot) { + messageMethod.accept("OpenInv requires that you use Spigot or a Spigot fork. Per the 1.14 update thread"); + messageMethod.accept("(https://www.spigotmc.org/threads/369724/ \"A Note on CraftBukkit\"), if you are"); + messageMethod.accept("encountering an inconsistency with vanilla that prevents you from using Spigot,"); + messageMethod.accept("that is considered a Spigot bug and should be reported as such."); + } + } + + @Override + public boolean isSupportedVersion() { + return this.accessor != null && this.accessor.isSupported(); + } + + @Override + public boolean disableSaving() { + return config.isSaveDisabled(); + } + + @Override + public boolean disableOfflineAccess() { + return config.isOfflineDisabled(); + } + + @Override + public boolean noArgsOpensSelf() { + return config.doesNoArgsOpenSelf(); + } + + @Override + public @NotNull IAnySilentContainer getAnySilentContainer() { + return this.accessor.getAnySilentContainer(); + } + + @Override + public boolean getAnyContainerStatus(@NotNull final OfflinePlayer offline) { + return PlayerToggles.any().is(offline.getUniqueId()); + } + + @Override + public void setAnyContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) { + PlayerToggles.any().set(offline.getUniqueId(), status); + } + + @Override + public boolean getSilentContainerStatus(@NotNull final OfflinePlayer offline) { + return PlayerToggles.silent().is(offline.getUniqueId()); + } + + @Override + public void setSilentContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) { + PlayerToggles.silent().set(offline.getUniqueId(), status); + } + + @Override + public @NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online) { + return inventoryManager.getEnderChest(player); + } + + @Override + public @NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online) { + return inventoryManager.getInventory(player); + } + + @Override + @Deprecated(forRemoval = true) + public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) { + Permissions edit = null; + HumanEntity target = inventory.getPlayer(); + boolean ownContainer = player.equals(target); + if (inventory instanceof ISpecialPlayerInventory) { + edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER; + } else if (inventory instanceof ISpecialEnderChest) { + edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_EDIT_OTHER; + } + + boolean viewOnly = edit != null && !edit.hasPermission(player); + + if (ownContainer) { + return this.accessor.openInventory(player, inventory, viewOnly); + } + + AccessEqualMode accessMode = AccessEqualMode.getByPerm(player, config); + + for (int level = 4; level > 0; --level) { + String permission = "openinv.access.level." + level; + // If the target doesn't have this access level... + if (!target.hasPermission(permission)) { + // If the viewer does have the access level, all good. + if (player.hasPermission(permission)) { + break; + } + // Otherwise check next access level. + continue; + } + + // If the viewer doesn't have an equal access level or equal access is a denial, deny. + if (!player.hasPermission(permission) || accessMode == AccessEqualMode.DENY) { + return null; + } + + // Since this is a tie, setting decides view state. + if (accessMode == AccessEqualMode.VIEW) { + viewOnly = true; + } + break; + } + + return openInventory(player, inventory, viewOnly); + } + + @Override + public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly) { + return this.accessor.openInventory(player, inventory, viewOnly); + } + + @Override + public boolean isPlayerLoaded(@NotNull UUID playerUuid) { + return inventoryManager.getLoadedPlayer(playerUuid) != null; + } + + @Override + public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) { + return playerLoader.load(offline); + } + + @Override + public @Nullable OfflinePlayer matchPlayer(@NotNull String name) { + return playerLoader.match(name); + } + + @Override + public void unload(@NotNull final OfflinePlayer offline) { + inventoryManager.unload(offline.getUniqueId()); + } } diff --git a/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java new file mode 100644 index 00000000..1aa2da15 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/ClearInvCommand.java @@ -0,0 +1,129 @@ +package com.lishid.openinv.command; + +import com.lishid.openinv.OpenInv; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.util.InventoryManager; +import com.lishid.openinv.util.Permissions; +import com.lishid.openinv.util.PlayerLoader; +import com.lishid.openinv.util.config.Config; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.logging.Level; + +public class ClearInvCommand extends PlayerLookupCommand { + + private final @NotNull InventoryManager manager; + + public ClearInvCommand( + @NotNull OpenInv plugin, + @NotNull Config config, + @NotNull InventoryManager manager, + @NotNull LanguageManager lang, + @NotNull PlayerLoader playerLoader + ) { + super(plugin, lang, config, playerLoader); + this.manager = manager; + } + + @Override + protected boolean isAccessInventory(@NotNull Command command) { + return command.getName().equals("clearinv"); + } + + @Override + protected @Nullable String getTargetIdentifer( + @NotNull CommandSender sender, + @NotNull Command command, + @Nullable String argument, + boolean accessInv + ) { + if (!Permissions.CLEAR_OTHER.hasPermission(sender)) { + // If the sender does not have permissions to clear others, use self. + if (sender instanceof Player player) { + return player.getUniqueId().toString(); + } + + // Console can't target itself. Send error. + lang.sendMessage(sender, "messages.error.permissionOpenOther"); + return null; + } + + // If argument is provided, use it. + if (argument != null) { + return argument; + } + + // For players, default to self. + if (sender instanceof Player player) { + return player.getUniqueId().toString(); + } + + // For console, argument is required. Send usage. + sender.sendMessage(command.getUsage()); + return null; + } + + @Override + protected @Nullable OfflinePlayer getTarget(@NotNull String identifier) { + return playerLoader.matchExact(identifier); + } + + @Override + protected boolean deniedCommand(@NotNull CommandSender sender, @NotNull Player onlineTarget, boolean accessInv) { + if (onlineTarget.equals(sender)) { + return !Permissions.CLEAR_SELF.hasPermission(sender); + } + return !Permissions.CLEAR_OTHER.hasPermission(sender); + } + + @Override + protected void handle( + @NotNull CommandSender sender, + @NotNull PlayerAccess playerAccess, + boolean accessInv, + @NotNull String @NotNull [] args + ) { + Player onlineTarget = playerAccess.player(); + // Create the inventory + final ISpecialInventory inv; + try { + inv = accessInv ? manager.getInventory(onlineTarget) : manager.getEnderChest(onlineTarget); + } catch (Exception e) { + lang.sendMessage(sender, "messages.error.commandException"); + plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e); + return; + } + + // Clear the inventory + inv.getBukkitInventory().clear(); + manager.save(onlineTarget.getUniqueId()); + lang.sendMessage( + sender, + accessInv ? "messages.info.clear.inventory" : "messages.info.clear.enderchest", + new Replacement("%target%", onlineTarget.getDisplayName()) + ); + } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!Permissions.CLEAR_OTHER.hasPermission(sender)) { + return List.of(); + } + + return super.onTabComplete(sender, command, label, args); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java b/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java new file mode 100644 index 00000000..e7b1c6b8 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/ContainerSettingCommand.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.command; + +import com.lishid.openinv.event.OpenEvents; +import com.lishid.openinv.util.TabCompleter; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import com.lishid.openinv.util.setting.PlayerToggle; +import com.lishid.openinv.util.setting.PlayerToggles; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.UUID; + +public class ContainerSettingCommand implements TabExecutor { + + private final @NotNull LanguageManager lang; + + public ContainerSettingCommand(@NotNull LanguageManager lang) { + this.lang = lang; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!(sender instanceof Player player)) { + lang.sendMessage(sender, "messages.error.consoleUnsupported"); + return true; + } + + PlayerToggle toggle = PlayerToggles.get(command.getName()); + + // Shouldn't be possible. + if (toggle == null) { + JavaPlugin.getProvidingPlugin(getClass()).getLogger().warning("Command /" + command.getName() + " registered with no corresponding toggle!"); + return false; + } + + UUID playerId = player.getUniqueId(); + + if (args.length > 0) { + args[0] = args[0].toLowerCase(Locale.ENGLISH); + + if (args[0].equals("on")) { + set(toggle, playerId, true); + } else if (args[0].equals("off")) { + set(toggle, playerId, false); + } else if (!args[0].equals("check")) { + // Invalid argument, show usage. + return false; + } + + } else { + set(toggle, playerId, !toggle.is(playerId)); + } + + String onOff = lang.getLocalizedMessage(player, toggle.is(playerId) ? "messages.info.on" : "messages.info.off"); + if (onOff == null) { + onOff = String.valueOf(toggle.is(playerId)); + } + + lang.sendMessage( + sender, + "messages.info.settingState", + new Replacement("%setting%", toggle.getName()), + new Replacement("%state%", onOff) + ); + + return true; + } + + private void set(@NotNull PlayerToggle toggle, @NotNull UUID uuid, boolean state) { + if (toggle.set(uuid, state)) { + OpenEvents.notifyPlayerToggle(toggle, uuid, state); + } + } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!command.testPermissionSilent(sender) || args.length != 1) { + return Collections.emptyList(); + } + + return TabCompleter.completeString(args[0], new String[]{"check", "on", "off"}); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java new file mode 100644 index 00000000..14abf66b --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/OpenInvCommand.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.command; + +import com.lishid.openinv.OpenInv; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.util.InventoryManager; +import com.lishid.openinv.util.Permissions; +import com.lishid.openinv.util.PlayerLoader; +import com.lishid.openinv.util.config.Config; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.PluginCommand; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import java.util.WeakHashMap; +import java.util.logging.Level; + +public class OpenInvCommand extends PlayerLookupCommand { + + private final @NotNull InventoryManager manager; + private final Map openInvHistory = new WeakHashMap<>(); + private final Map openEnderHistory = new WeakHashMap<>(); + + public OpenInvCommand( + @NotNull OpenInv plugin, + @NotNull Config config, + @NotNull InventoryManager manager, + @NotNull LanguageManager lang, + @NotNull PlayerLoader playerLoader + ) { + super(plugin, lang, config, playerLoader); + this.manager = manager; + } + + @Override + protected boolean isAccessInventory(@NotNull Command command) { + return command.getName().equals("openinv"); + } + + @Override + protected @Nullable String getTargetIdentifer( + @NotNull CommandSender sender, + @NotNull Command command, + @Nullable String argument, + boolean accessInv + ) { + // /openinv help + if (accessInv && argument != null && (argument.equalsIgnoreCase("help") || argument.equals("?"))) { + this.showHelp(sender); + return null; + } + + // Command is player-only. + if (!(sender instanceof Player player)) { + lang.sendMessage(sender, "messages.error.consoleUnsupported"); + return null; + } + + // Use fallthrough for no name provided. + if (argument == null) { + if (config.doesNoArgsOpenSelf()) { + return player.getUniqueId().toString(); + } + return (accessInv ? this.openInvHistory : this.openEnderHistory) + .computeIfAbsent(player, localPlayer -> localPlayer.getUniqueId().toString()); + } + + if (!config.doesNoArgsOpenSelf()) { + // History management + (accessInv ? this.openInvHistory : this.openEnderHistory).put(player, argument); + } + + return argument; + } + + private void showHelp(@NotNull CommandSender sender) { + // Get registered commands + for (String commandName : plugin.getDescription().getCommands().keySet()) { + PluginCommand command = plugin.getCommand(commandName); + + // Ensure command is successfully registered and sender can use it + if (command == null || !command.testPermissionSilent(sender)) { + continue; + } + + // Send usage + sender.sendMessage(command.getUsage().replace("", commandName)); + + List aliases = command.getAliases(); + if (!aliases.isEmpty()) { + // Assemble alias list + StringJoiner aliasJoiner = new StringJoiner(", ", " (aliases: ", ")"); + for (String alias : aliases) { + aliasJoiner.add(alias); + } + + // Send all aliases + sender.sendMessage(aliasJoiner.toString()); + } + + } + } + + @Override + protected @Nullable OfflinePlayer getTarget(@NotNull String identifier) { + return playerLoader.match(identifier); + } + + @Override + protected boolean deniedCommand(@NotNull CommandSender sender, @NotNull Player onlineTarget, boolean accessInv) { + if (onlineTarget.equals(sender)) { + // Permission for opening own inventory. + if (!(accessInv ? Permissions.INVENTORY_OPEN_SELF : Permissions.ENDERCHEST_OPEN_SELF).hasPermission(sender)) { + lang.sendMessage(sender, "messages.error.permissionOpenSelf"); + return true; + + } + } else { + // Permission for opening others' inventories. + if (!(accessInv ? Permissions.INVENTORY_OPEN_OTHER : Permissions.ENDERCHEST_OPEN_OTHER).hasPermission(sender)) { + lang.sendMessage(sender, "messages.error.permissionOpenOther"); + return true; + } + } + + return false; + } + + @Override + protected void handle( + @NotNull CommandSender sender, + @NotNull PlayerAccess playerAccess, + boolean accessInv, + @NotNull String @NotNull [] args + ) { + Player player = (Player) sender; + Player target = playerAccess.player(); + if (!config.doesNoArgsOpenSelf()) { + // Record the target + (accessInv ? this.openInvHistory : this.openEnderHistory).put(player, target.getUniqueId().toString()); + } + + // Create the inventory + final ISpecialInventory inv; + try { + inv = accessInv ? manager.getInventory(target) : manager.getEnderChest(target); + } catch (Exception e) { + lang.sendMessage(player, "messages.error.commandException"); + plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e); + return; + } + + // Open the inventory + plugin.openInventory(player, inv, playerAccess.viewOnly()); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java b/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java new file mode 100644 index 00000000..9081ad1f --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/PlayerLookupCommand.java @@ -0,0 +1,316 @@ +package com.lishid.openinv.command; + +import com.lishid.openinv.OpenInv; +import com.lishid.openinv.util.AccessEqualMode; +import com.lishid.openinv.util.Permissions; +import com.lishid.openinv.util.PlayerLoader; +import com.lishid.openinv.util.TabCompleter; +import com.lishid.openinv.util.config.Config; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import me.nahu.scheduler.wrapper.runnable.WrappedRunnable; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; + +/** + * A command abstraction for performing actions after looking up and loading a player. + */ +public abstract class PlayerLookupCommand implements TabExecutor { + + protected final @NotNull OpenInv plugin; + protected final @NotNull LanguageManager lang; + protected final @NotNull Config config; + protected final @NotNull PlayerLoader playerLoader; + + public PlayerLookupCommand( + @NotNull OpenInv plugin, + @NotNull LanguageManager lang, + @NotNull Config config, + @NotNull PlayerLoader playerLoader + ) { + this.plugin = plugin; + this.lang = lang; + this.config = config; + this.playerLoader = playerLoader; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String @NotNull [] args + ) { + + // Inventory or ender chest? + boolean accessInv = isAccessInventory(command); + + // Get target identifier from parameters. + String targetId = getTargetIdentifer(sender, command, args.length > 0 ? args[0] : null, accessInv); + if (targetId == null) { + return true; + } + + new WrappedRunnable() { + @Override + public void run() { + // Get target from identifier. + final OfflinePlayer target = getTarget(targetId); + + if (target == null || (!target.hasPlayedBefore() && !target.isOnline())) { + lang.sendMessage(sender, "messages.error.invalidPlayer"); + return; + } + + new WrappedRunnable() { + @Override + public void run() { + // Ensure sender still exists. + if ((sender instanceof Player player) && !player.isValid()) { + return; + } + + // Perform access checks and load target if necessary. + PlayerAccess onlineTarget = access(sender, target, accessInv); + + if (onlineTarget != null) { + handle(sender, onlineTarget, accessInv, args); + } + } + }.runTask(PlayerLookupCommand.this.plugin); + + } + }.runTaskAsynchronously(this.plugin); + + return true; + } + + /** + * Get whether a player inventory or ender chest is accessed by the {@link Command} executed. + * + * @param command the {@code Command} being executed + * @return {@code true} if the command is for inventories, {@code false} for ender chests + */ + protected abstract boolean isAccessInventory(@NotNull Command command); + + /** + * Determine the target identifier from the first command argument. + * + *

Implementation note: a return value of {@code null} will cause the command to cease + * execution with no feedback. Appropriate feedback should be sent in the implementation.

+ * + * @param sender the sender of the command + * @param argument the argument, or {@code null} if none provided + * @param accessInv {@code true} if an inventory is being accessed, {@code false} for ender chest + * @return an updated target identifier or {@code null} if no target is available + */ + protected abstract @Nullable String getTargetIdentifer( + @NotNull CommandSender sender, + @NotNull Command command, + @Nullable String argument, + boolean accessInv + ); + + /** + * Get an {@link OfflinePlayer} by identifier. + * + * @param identifier the identifier + * @return the corresponding player or {@code null} if no match was found + */ + protected abstract @Nullable OfflinePlayer getTarget(@NotNull String identifier); + + /** + * Attempt to access the target as an online player. Performs feedback in the event of denial. + * + * @param sender the {@link CommandSender} attempting access + * @param target the {@link OfflinePlayer} being targeted by the command + * @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest + * @return the {@link Player} loaded or {@code null} if target is not accessible + */ + protected @Nullable PlayerAccess access( + @NotNull CommandSender sender, + @NotNull OfflinePlayer target, + boolean invPerms + ) { + // Attempt to load online player dependent on permissions and configuration. + Player onlineTarget = accessAsPlayer(sender, target); + + if (onlineTarget == null) { + return null; + } + + // Permissions checks. + if (deniedCommand(sender, onlineTarget, invPerms)) { + return null; + } + + return accessGeneralized(sender, onlineTarget, invPerms); + } + + /** + * Helper for accessing target as an online {@link Player}. Performs checks + * and feedback for configuration and online/offline permissions. + * + * @param sender the {@link CommandSender} attempting access + * @param target the {@link OfflinePlayer} being targeted by the command + * @return the {@link Player} loaded or {@code null} if target is not accessible + */ + protected @Nullable Player accessAsPlayer(@NotNull CommandSender sender, @NotNull OfflinePlayer target) { + Player onlineTarget; + + if (!target.isOnline()) { + if (!config.isOfflineDisabled() && Permissions.ACCESS_OFFLINE.hasPermission(sender)) { + // Try loading the player's data. + onlineTarget = playerLoader.load(target); + } else { + lang.sendMessage(sender, "messages.error.permissionPlayerOffline"); + return null; + } + } else { + if (Permissions.ACCESS_ONLINE.hasPermission(sender)) { + onlineTarget = target.getPlayer(); + } else { + lang.sendMessage(sender, "messages.error.permissionPlayerOnline"); + return null; + } + } + + if (onlineTarget == null) { + lang.sendMessage(sender, "messages.error.invalidPlayer"); + return null; + } + + return onlineTarget; + } + + /** + * Check for a lack of permissions related to the specific command being executed for the sender. + * For example, {@link Permissions#INVENTORY_OPEN_OTHER} might be required if the target and sender differ. + * + * @param sender the {@link CommandSender} attempting access + * @param onlineTarget the {@link Player} being targeted by the command + * @param accessInv {@code true} to use inventory permissions, {@code false} for ender chest + * @return {@code true} if the sender does not have the correct execution-specific permission + */ + protected abstract boolean deniedCommand( + @NotNull CommandSender sender, + @NotNull Player onlineTarget, + boolean accessInv + ); + + /** + * Check for generalized permissions for accessing the target. + * By default, this is access levels and cross-world restrictions. + * + * @param sender the {@link CommandSender} attempting access + * @param onlineTarget the {@link Player} being targeted by the command + * @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest + * @return a {@link PlayerAccess} containing the accessed player and view mode, or {@code null} if denied + */ + protected @Nullable PlayerAccess accessGeneralized( + @NotNull CommandSender sender, + @NotNull Player onlineTarget, + boolean invPerms + ) { + + boolean ownContainer = sender.equals(onlineTarget); + Permissions edit; + if (invPerms) { + edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER; + } else { + edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_EDIT_OTHER; + } + + boolean viewOnly = !edit.hasPermission(sender); + + if (ownContainer) { + // Skip other access checks for self. + return new PlayerAccess(onlineTarget, viewOnly); + } + + // Crossworld check + if (sender instanceof Player player + && !Permissions.ACCESS_CROSSWORLD.hasPermission(sender) + && !onlineTarget.getWorld().equals(player.getWorld())) { + lang.sendMessage( + sender, + "messages.error.permissionCrossWorld", + new Replacement("%target%", onlineTarget.getDisplayName()) + ); + return null; + } + + AccessEqualMode accessMode = AccessEqualMode.getByPerm(sender, config); + + for (int level = 4; level > 0; --level) { + String permission = "openinv.access.level." + level; + // If the target doesn't have this access level... + if (!onlineTarget.hasPermission(permission)) { + // If the viewer does have the access level, all good. + if (sender.hasPermission(permission)) { + break; + } + // Otherwise check next access level. + continue; + } + + // If the viewer doesn't have an equal access level or equal access is a denial, deny. + if (!sender.hasPermission(permission) || accessMode == AccessEqualMode.DENY) { + lang.sendMessage( + sender, + "messages.error.permissionExempt", + new Replacement("%target%", onlineTarget.getDisplayName()) + ); + return null; + } + + // Since this is a tie, setting decides view state. + if (accessMode == AccessEqualMode.VIEW) { + viewOnly = true; + } + break; + } + + return new PlayerAccess(onlineTarget, viewOnly); + } + + /** + * Perform main command functionality. + * + * @param sender the {@link CommandSender} executing the command + * @param target the {@link Player} being targeted + * @param accessInv {@code true} if an inventory is being accessed, {@code false} for ender chest + * @param args the original command arguments + */ + protected abstract void handle( + @NotNull CommandSender sender, + @NotNull PlayerAccess target, + boolean accessInv, + @NotNull String @NotNull [] args + ); + + @Override + public List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!command.testPermissionSilent(sender) || args.length != 1) { + return Collections.emptyList(); + } + + return TabCompleter.completeOnlinePlayer(sender, args[0]); + } + + protected record PlayerAccess(Player player, boolean viewOnly) {} + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java new file mode 100644 index 00000000..1b8eb1b2 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/SearchContainerCommand.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.command; + +import com.lishid.openinv.util.TabCompleter; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import com.lishid.openinv.util.SearchHelper; +import org.bukkit.Chunk; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Command for searching containers in a radius of chunks. + */ +public class SearchContainerCommand implements TabExecutor { + + private final @NotNull Plugin plugin; + private final @NotNull LanguageManager lang; + + public SearchContainerCommand(@NotNull Plugin plugin, @NotNull LanguageManager lang) { + this.plugin = plugin; + this.lang = lang; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!(sender instanceof Player senderPlayer)) { + lang.sendMessage(sender, "messages.error.consoleUnsupported"); + return true; + } + + if (args.length < 1) { + // Must supply material + return false; + } + + Material material = Material.matchMaterial(args[0]); + + if (material == null) { + lang.sendMessage( + sender, + "messages.error.invalidMaterial", + new Replacement("%target%", args[0]) + ); + return false; + } + + int radius = 5; + + if (args.length > 1) { + try { + radius = Integer.parseInt(args[1]); + } catch (NumberFormatException e) { + // Invalid radius supplied + return false; + } + } + + // Clamp radius. + int configMax = plugin.getConfig().getInt("settings.command.searchcontainer.max-radius", 10); + radius = Math.max(0, Math.min(radius, configMax)); + + World world = senderPlayer.getWorld(); + Chunk centerChunk = senderPlayer.getLocation().getChunk(); + StringBuilder locations = new StringBuilder(); + + for (int dX = -radius; dX <= radius; ++dX) { + for (int dZ = -radius; dZ <= radius; ++dZ) { + if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) { + continue; + } + Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ); + for (BlockState tileEntity : chunk.getTileEntities()) { + if (!(tileEntity instanceof InventoryHolder holder)) { + continue; + } + if (!SearchHelper.findMatch(holder.getInventory(), itemStack -> itemStack.getType() == material)) { + continue; + } + locations.append(holder.getInventory().getType().name().toLowerCase(Locale.ENGLISH)).append(" (") + .append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',') + .append(tileEntity.getZ()).append("), "); + } + } + } + + // Matches found, delete trailing comma and space + if (!locations.isEmpty()) { + locations.delete(locations.length() - 2, locations.length()); + } else { + lang.sendMessage( + sender, + "messages.info.container.noMatches", + new Replacement("%target%", material.name()) + ); + return true; + } + + lang.sendMessage( + sender, + "messages.info.container.matches", + new Replacement("%target%", material.name()), + new Replacement("%detail%", locations.toString()) + ); + return true; + } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + String[] args + ) { + if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) { + return Collections.emptyList(); + } + + String argument = args[args.length - 1]; + if (args.length == 1) { + return TabCompleter.completeEnum(argument, Material.class); + } else { + return TabCompleter.completeInteger(argument); + } + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java new file mode 100644 index 00000000..1844c4ea --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/SearchEnchantCommand.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.command; + +import com.lishid.openinv.util.TabCompleter; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import com.lishid.openinv.util.SearchHelper; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Command adding the ability to search online players' inventories for enchantments of a specific + * type at or above the level specified. + * + * @author Jikoo + */ +public class SearchEnchantCommand implements TabExecutor { + + private final @NotNull LanguageManager lang; + + public SearchEnchantCommand(@NotNull LanguageManager lang) { + this.lang = lang; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (args.length == 0) { + return false; + } + + Enchantment enchant = null; + int level = 0; + + for (String argument : args) { + try { + level = Integer.parseInt(argument); + continue; + } catch (NumberFormatException ignored) { + // Not a level being specified. + } + + argument = argument.toLowerCase(Locale.ENGLISH); + NamespacedKey key = NamespacedKey.fromString(argument); + if (key == null) { + continue; + } + + Enchantment localEnchant = Registry.ENCHANTMENT.get(key); + if (localEnchant != null) { + enchant = localEnchant; + } + } + + // Arguments not set correctly + if (level == 0 && enchant == null) { + return false; + } + + StringBuilder players = new StringBuilder(); + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level); + boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level); + + // No matches, continue + if (!flagInventory && !flagEnder) { + continue; + } + + // Matches, append details + players.append(player.getName()).append(" ("); + if (flagInventory) { + players.append("inv"); + } + if (flagEnder) { + if (flagInventory) { + players.append(','); + } + players.append("ender"); + } + players.append("), "); + } + + if (!players.isEmpty()) { + // Matches found, delete trailing comma and space + players.delete(players.length() - 2, players.length()); + } else { + lang.sendMessage( + sender, + "messages.info.player.noMatches", + new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level) + ); + return true; + } + + lang.sendMessage( + sender, + "messages.info.player.matches", + new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level), + new Replacement("%detail%", players.toString()) + ); + return true; + } + + private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) { + return SearchHelper.findMatch( + inventory, + itemStack -> { + // Ensure meta is available and has enchantments. + if (!itemStack.hasItemMeta()) { + return false; + } + ItemMeta meta = itemStack.getItemMeta(); + if (meta == null || !meta.hasEnchants()) { + return false; + } + + // If enchantment is provided, use it. + if (enchant != null) { + return meta.getEnchantLevel(enchant) >= minLevel; + } + + // Otherwise, check all enchantment levels. + for (int enchLevel : meta.getEnchants().values()) { + if (enchLevel >= minLevel) { + return true; + } + } + + return false; + } + ); + } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) { + return Collections.emptyList(); + } + + if (args.length == 1) { + return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Registry.ENCHANTMENT.stream().toArray(Enchantment[]::new)); + } else { + return TabCompleter.completeInteger(args[1]); + } + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java b/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java new file mode 100644 index 00000000..c2a5a32d --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/command/SearchInvCommand.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.command; + +import com.lishid.openinv.util.TabCompleter; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.lang.Replacement; +import com.lishid.openinv.util.SearchHelper; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabExecutor; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class SearchInvCommand implements TabExecutor { + + private final @NotNull LanguageManager lang; + + public SearchInvCommand(@NotNull LanguageManager lang) { + this.lang = lang; + } + + @Override + public boolean onCommand( + @NotNull CommandSender sender, + @NotNull Command command, + @NotNull String label, + @NotNull String[] args + ) { + + Material material = null; + + if (args.length >= 1) { + material = Material.matchMaterial(args[0]); + } + + if (material == null) { + lang.sendMessage( + sender, + "messages.error.invalidMaterial", + new Replacement("%target%", args.length > 0 ? args[0] : "null") + ); + return false; + } + + int count = 1; + + if (args.length >= 2) { + try { + count = Integer.parseInt(args[1]); + } catch (NumberFormatException ex) { + lang.sendMessage( + sender, + "messages.error.invalidNumber", + new Replacement("%target%", args[1]) + ); + return false; + } + } + + StringBuilder players = new StringBuilder(); + boolean searchInv = command.getName().equals("searchinv"); + for (Player player : Bukkit.getServer().getOnlinePlayers()) { + Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest(); + if (findMatch(inventory, material, count)) { + players.append(player.getName()).append(", "); + break; + } + } + + // Matches found, delete trailing comma and space + if (!players.isEmpty()) { + players.delete(players.length() - 2, players.length()); + } else { + lang.sendMessage( + sender, + "messages.info.player.noMatches", + new Replacement("%target%", material.name()) + ); + return true; + } + + lang.sendMessage( + sender, + "messages.info.player.matches", + new Replacement("%target%", material.name()), + new Replacement("%detail%", players.toString()) + ); + return true; + } + + private boolean findMatch(@NotNull Inventory inventory, @NotNull Material material, int count) { + AtomicInteger total = new AtomicInteger(); + return SearchHelper.findMatch( + inventory, + itemStack -> { + if (itemStack.getType() == material && itemStack.getAmount() > 0) { + return total.addAndGet(itemStack.getAmount()) >= count; + } + return false; + } + ); + } + + @Override + public List onTabComplete( + @NotNull CommandSender sender, @NotNull Command command, @NotNull String label, + @NotNull String[] args + ) { + if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) { + return Collections.emptyList(); + } + + String argument = args[args.length - 1]; + if (args.length == 1) { + return TabCompleter.completeEnum(argument, Material.class); + } else { + return TabCompleter.completeInteger(argument); + } + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java deleted file mode 100644 index 1a765432..00000000 --- a/plugin/src/main/java/com/lishid/openinv/commands/ContainerSettingCommand.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.commands; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.util.TabCompleter; -import java.util.Collections; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Predicate; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public class ContainerSettingCommand implements TabExecutor { - - private final OpenInv plugin; - - public ContainerSettingCommand(final OpenInv plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player)) { - plugin.sendMessage(sender, "messages.error.consoleUnsupported"); - return true; - } - - Player player = (Player) sender; - boolean any = command.getName().startsWith("any"); - Predicate getSetting = any ? plugin::getPlayerAnyChestStatus : plugin::getPlayerSilentChestStatus; - BiConsumer setSetting = any ? plugin::setPlayerAnyChestStatus : plugin::setPlayerSilentChestStatus; - - if (args.length > 0) { - args[0] = args[0].toLowerCase(); - - if (args[0].equals("on")) { - setSetting.accept(player, true); - } else if (args[0].equals("off")) { - setSetting.accept(player, false); - } else if (!args[0].equals("check")) { - // Invalid argument, show usage. - return false; - } - - } else { - setSetting.accept(player, !getSetting.test(player)); - } - - String onOff = plugin.getLocalizedMessage(player, getSetting.test(player) ? "messages.info.on" : "messages.info.off"); - if (onOff == null) { - onOff = String.valueOf(getSetting.test(player)); - } - - plugin.sendMessage(sender, "messages.info.settingState","%setting%", any ? "AnyContainer" : "SilentContainer", "%state%", onOff); - - return true; - } - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!command.testPermissionSilent(sender) || args.length != 1) { - return Collections.emptyList(); - } - - return TabCompleter.completeString(args[0], new String[] {"check", "on", "off"}); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java deleted file mode 100644 index 4571df81..00000000 --- a/plugin/src/main/java/com/lishid/openinv/commands/OpenInvCommand.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.commands; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.internal.ISpecialInventory; -import com.lishid.openinv.util.Permissions; -import com.lishid.openinv.util.TabCompleter; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; - -public class OpenInvCommand implements TabExecutor { - - private final OpenInv plugin; - private final HashMap openInvHistory = new HashMap<>(); - private final HashMap openEnderHistory = new HashMap<>(); - - public OpenInvCommand(final OpenInv plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull final CommandSender sender, @NotNull final Command command, @NotNull final String label, @NotNull final String[] args) { - if (!(sender instanceof Player)) { - plugin.sendMessage(sender, "messages.error.consoleUnsupported"); - return true; - } - - if (args.length > 0 && (args[0].equalsIgnoreCase("help") || args[0].equals("?"))) { - this.plugin.showHelp((Player) sender); - return true; - } - - final Player player = (Player) sender; - final boolean openinv = command.getName().equals("openinv"); - - // History management - String history = (openinv ? this.openInvHistory : this.openEnderHistory).get(player); - - if (history == null || history.isEmpty()) { - history = player.getName(); - (openinv ? this.openInvHistory : this.openEnderHistory).put(player, history); - } - - final String name; - - // Read from history if target is not named - if (args.length < 1) { - name = history; - } else { - name = args[0]; - } - - new BukkitRunnable() { - @Override - public void run() { - final OfflinePlayer offlinePlayer = OpenInvCommand.this.plugin.matchPlayer(name); - - if (offlinePlayer == null || !offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline()) { - plugin.sendMessage(player, "messages.error.invalidPlayer"); - return; - } - - new BukkitRunnable() { - @Override - public void run() { - if (!player.isOnline()) { - return; - } - OpenInvCommand.this.openInventory(player, offlinePlayer, openinv); - } - }.runTask(OpenInvCommand.this.plugin); - - } - }.runTaskAsynchronously(this.plugin); - - return true; - } - - private void openInventory(final Player player, final OfflinePlayer target, boolean openinv) { - Player onlineTarget; - boolean online = target.isOnline(); - - if (!online) { - if (Permissions.OPENOFFLINE.hasPermission(player)) { - // Try loading the player's data - onlineTarget = this.plugin.loadPlayer(target); - } else { - plugin.sendMessage(player, "messages.error.permissionPlayerOffline"); - return; - } - } else { - if (Permissions.OPENONLINE.hasPermission(player)) { - onlineTarget = target.getPlayer(); - } else { - plugin.sendMessage(player, "messages.error.permissionPlayerOnline"); - return; - } - } - - if (onlineTarget == null) { - plugin.sendMessage(player, "messages.error.invalidPlayer"); - return; - } - - // Permissions checks - if (onlineTarget.equals(player)) { - // Inventory: Additional permission required to open own inventory - if (openinv && !Permissions.OPENSELF.hasPermission(player)) { - plugin.sendMessage(player, "messages.error.permissionOpenSelf"); - return; - } - } else { - // Enderchest: Additional permission required to open others' ender chests - if (!openinv && !Permissions.ENDERCHEST_ALL.hasPermission(player)) { - plugin.sendMessage(player, "messages.error.permissionEnderAll"); - return; - } - - // Protected check - if (!Permissions.OVERRIDE.hasPermission(player) - && Permissions.EXEMPT.hasPermission(onlineTarget)) { - plugin.sendMessage(player, "messages.error.permissionExempt", - "%target%", onlineTarget.getDisplayName()); - return; - } - - // Crossworld check - if (!Permissions.CROSSWORLD.hasPermission(player) - && !onlineTarget.getWorld().equals(player.getWorld())) { - plugin.sendMessage(player, "messages.error.permissionCrossWorld", - "%target%", onlineTarget.getDisplayName()); - return; - } - } - - // Record the target - (openinv ? this.openInvHistory : this.openEnderHistory).put(player, this.plugin.getPlayerID(target)); - - // Create the inventory - final ISpecialInventory inv; - try { - inv = openinv ? this.plugin.getSpecialInventory(onlineTarget, online) : this.plugin.getSpecialEnderChest(onlineTarget, online); - } catch (Exception e) { - plugin.sendMessage(player, "messages.error.commandException"); - e.printStackTrace(); - return; - } - - // Open the inventory - plugin.openInventory(player, inv); - } - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!command.testPermissionSilent(sender) || args.length != 1) { - return Collections.emptyList(); - } - - return TabCompleter.completeOnlinePlayer(sender, args[0]); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java deleted file mode 100644 index 7242ee94..00000000 --- a/plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.commands; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.util.TabCompleter; -import java.util.Collections; -import java.util.List; -import org.bukkit.Chunk; -import org.bukkit.Material; -import org.bukkit.World; -import org.bukkit.block.BlockState; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryHolder; -import org.jetbrains.annotations.NotNull; - -/** - * Command for searching containers in a radius of chunks. - */ -public class SearchContainerCommand implements TabExecutor { - - private final OpenInv plugin; - - public SearchContainerCommand(OpenInv plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!(sender instanceof Player)) { - plugin.sendMessage(sender, "messages.error.consoleUnsupported"); - return true; - } - - if (args.length < 1) { - // Must supply material - return false; - } - - Material material = Material.getMaterial(args[0].toUpperCase()); - - if (material == null) { - plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args[0]); - return false; - } - - int radius = 5; - - if (args.length > 1) { - try { - radius = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - // Invalid radius supplied - return false; - } - } - - Player senderPlayer = (Player) sender; - World world = senderPlayer.getWorld(); - Chunk centerChunk = senderPlayer.getLocation().getChunk(); - StringBuilder locations = new StringBuilder(); - - for (int dX = -radius; dX <= radius; ++dX) { - for (int dZ = -radius; dZ <= radius; ++dZ) { - if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) { - continue; - } - Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ); - for (BlockState tileEntity : chunk.getTileEntities()) { - if (!(tileEntity instanceof InventoryHolder)) { - continue; - } - InventoryHolder holder = (InventoryHolder) tileEntity; - if (!holder.getInventory().contains(material)) { - continue; - } - locations.append(holder.getInventory().getType().name().toLowerCase()).append(" (") - .append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',') - .append(tileEntity.getZ()).append("), "); - } - } - } - - // Matches found, delete trailing comma and space - if (locations.length() > 0) { - locations.delete(locations.length() - 2, locations.length()); - } else { - plugin.sendMessage(sender, "messages.info.container.noMatches", - "%target%", material.name()); - return true; - } - - plugin.sendMessage(sender, "messages.info.container.matches", - "%target%", material.name(), "%detail%", locations.toString()); - return true; - } - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) { - return Collections.emptyList(); - } - - String argument = args[args.length - 1]; - if (args.length == 1) { - return TabCompleter.completeEnum(argument, Material.class); - } else { - return TabCompleter.completeInteger(argument); - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java deleted file mode 100644 index 7f5eca95..00000000 --- a/plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.commands; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.util.TabCompleter; -import java.util.Collections; -import java.util.List; -import org.bukkit.Material; -import org.bukkit.NamespacedKey; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Command adding the ability to search online players' inventories for enchantments of a specific - * type at or above the level specified. - * - * @author Jikoo - */ -public class SearchEnchantCommand implements TabExecutor { - - private final OpenInv plugin; - - public SearchEnchantCommand(OpenInv plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (args.length == 0) { - return false; - } - - Enchantment enchant = null; - int level = 0; - - for (String argument : args) { - try { - level = Integer.parseInt(argument); - continue; - } catch (NumberFormatException ignored) {} - - argument = argument.toLowerCase(); - int colon = argument.indexOf(':'); - NamespacedKey key; - try { - if (colon > -1 && colon < argument.length() - 1) { - key = new NamespacedKey(argument.substring(0, colon), argument.substring(colon + 1)); - } else { - key = NamespacedKey.minecraft(argument); - } - } catch (IllegalArgumentException ignored) { - continue; - } - - Enchantment localEnchant = Enchantment.getByKey(key); - if (localEnchant != null) { - enchant = localEnchant; - } - } - - // Arguments not set correctly - if (level == 0 && enchant == null) { - return false; - } - - StringBuilder players = new StringBuilder(); - for (Player player : plugin.getServer().getOnlinePlayers()) { - boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level); - boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level); - - // No matches, continue - if (!flagInventory && !flagEnder) { - continue; - } - - // Matches, append details - players.append(player.getName()).append(" ("); - if (flagInventory) { - players.append("inv"); - } - if (flagEnder) { - if (flagInventory) { - players.append(','); - } - players.append("ender"); - } - players.append("), "); - } - - if (players.length() > 0) { - // Matches found, delete trailing comma and space - players.delete(players.length() - 2, players.length()); - } else { - plugin.sendMessage(sender, "messages.info.player.noMatches", - "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level); - return true; - } - - plugin.sendMessage(sender, "messages.info.player.matches", - "%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level, - "%detail%", players.toString()); - return true; - } - - private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) { - for (ItemStack item : inventory.getContents()) { - //noinspection ConstantConditions // Spigot improperly annotated, should be ItemStack @NotNull [] - if (item == null || item.getType() == Material.AIR) { - continue; - } - if (enchant != null) { - if (item.containsEnchantment(enchant) && item.getEnchantmentLevel(enchant) >= minLevel) { - return true; - } - } else { - if (!item.hasItemMeta()) { - continue; - } - ItemMeta meta = item.getItemMeta(); - if (meta == null || !meta.hasEnchants()) { - continue; - } - for (int enchLevel : meta.getEnchants().values()) { - if (enchLevel >= minLevel) { - return true; - } - } - } - } - return false; - } - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) { - return Collections.emptyList(); - } - - if (args.length == 1) { - return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Enchantment.values()); - } else { - return TabCompleter.completeInteger(args[1]); - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java b/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java deleted file mode 100644 index c471b30f..00000000 --- a/plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.commands; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.util.TabCompleter; -import java.util.Collections; -import java.util.List; -import org.bukkit.Material; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabExecutor; -import org.bukkit.entity.Player; -import org.bukkit.inventory.Inventory; -import org.jetbrains.annotations.NotNull; - -public class SearchInvCommand implements TabExecutor { - - private final OpenInv plugin; - - public SearchInvCommand(OpenInv plugin) { - this.plugin = plugin; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - - Material material = null; - - if (args.length >= 1) { - material = Material.getMaterial(args[0].toUpperCase()); - } - - if (material == null) { - plugin.sendMessage(sender, "messages.error.invalidMaterial", "%target%", args.length > 0 ? args[0] : "null"); - return false; - } - - int count = 1; - - if (args.length >= 2) { - try { - count = Integer.parseInt(args[1]); - } catch (NumberFormatException ex) { - plugin.sendMessage(sender, "messages.error.invalidNumber", "%target%", args[1]); - return false; - } - } - - StringBuilder players = new StringBuilder(); - boolean searchInv = command.getName().equals("searchinv"); - for (Player player : plugin.getServer().getOnlinePlayers()) { - Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest(); - if (inventory.contains(material, count)) { - players.append(player.getName()).append(", "); - } - } - - // Matches found, delete trailing comma and space - if (players.length() > 0) { - players.delete(players.length() - 2, players.length()); - } else { - plugin.sendMessage(sender, "messages.info.player.noMatches", - "%target%", material.name()); - return true; - } - - plugin.sendMessage(sender, "messages.info.player.matches", - "%target%", material.name(), "%detail%", players.toString()); - return true; - } - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) { - return Collections.emptyList(); - } - - String argument = args[args.length - 1]; - if (args.length == 1) { - return TabCompleter.completeEnum(argument, Material.class); - } else { - return TabCompleter.completeInteger(argument); - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java b/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java deleted file mode 100644 index 1a81491f..00000000 --- a/plugin/src/main/java/com/lishid/openinv/internal/IPlayerDataManager.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal; - -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.bukkit.inventory.InventoryView; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -public interface IPlayerDataManager { - - /** - * Loads a Player for an OfflinePlayer. - *

- * This method is potentially blocking, and should not be called on the main thread. - * - * @param offline the OfflinePlayer - * @return the Player loaded - */ - @Nullable Player loadPlayer(@NotNull OfflinePlayer offline); - - /** - * Creates a new Player from an existing one that will function slightly better offline. - * - * @return the Player - */ - @NotNull Player inject(@NotNull Player player); - - /** - * Opens an ISpecialInventory for a Player. - * - * @param player the Player opening the ISpecialInventory - * @param inventory the Inventory - *` - * @return the InventoryView opened - */ - @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory); - - /** - * Convert a raw slot number into a player inventory slot number. - * - *

Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular - * player inventory slot number. - * - * @param view the open inventory view - * @param rawSlot the raw slot in the view - * @return the converted slot number - */ - int convertToPlayerSlot(InventoryView view, int rawSlot); - -} diff --git a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java b/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java deleted file mode 100644 index 8fc6f09d..00000000 --- a/plugin/src/main/java/com/lishid/openinv/internal/OpenInventoryView.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2011-2021 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.internal; - -import com.lishid.openinv.OpenInv; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.jetbrains.annotations.NotNull; - -public class OpenInventoryView extends InventoryView { - - private final Player player; - private final ISpecialInventory inventory; - private final String titleKey; - private final String titleDefaultSuffix; - private String title; - - public OpenInventoryView(Player player, ISpecialInventory inventory, String titleKey, String titleDefaultSuffix) { - this.player = player; - this.inventory = inventory; - this.titleKey = titleKey; - this.titleDefaultSuffix = titleDefaultSuffix; - } - - @Override - public @NotNull Inventory getTopInventory() { - return inventory.getBukkitInventory(); - } - - @Override - public @NotNull Inventory getBottomInventory() { - return getPlayer().getInventory(); - } - - @Override - public @NotNull HumanEntity getPlayer() { - return player; - } - - @Override - public @NotNull InventoryType getType() { - return inventory.getBukkitInventory().getType(); - } - - @Override - public @NotNull String getTitle() { - if (title == null) { - HumanEntity owner = getPlayer(); - - String localTitle = OpenInv.getPlugin(OpenInv.class) - .getLocalizedMessage( - owner, - titleKey, - "%player%", - owner.getName()); - if (localTitle != null) { - title = localTitle; - } else { - title = owner.getName() + titleDefaultSuffix; - } - } - - return title; - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java b/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java new file mode 100644 index 00000000..ae3dcc1e --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/listener/ContainerListener.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.listener; + +import com.google.errorprone.annotations.Keep; +import com.lishid.openinv.internal.ViewOnly; +import com.lishid.openinv.util.InternalAccessor; +import com.lishid.openinv.util.Permissions; +import com.lishid.openinv.util.lang.LanguageManager; +import com.lishid.openinv.util.setting.PlayerToggles; +import org.bukkit.GameMode; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.Event.Result; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.event.inventory.InventoryInteractEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +/** + * A listener managing AnyContainer, SilentContainer, and more. + */ +public class ContainerListener implements Listener { + + private final @NotNull InternalAccessor accessor; + private final @NotNull LanguageManager lang; + + public ContainerListener(@NotNull InternalAccessor accessor, @NotNull LanguageManager lang) { + this.accessor = accessor; + this.lang = lang; + } + + @Keep + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + private void onPlayerInteract(@NotNull PlayerInteractEvent event) { + // Ignore events from other plugins. + if (!PlayerInteractEvent.class.equals(event.getClass())) { + return; + } + + if (event.getAction() != Action.RIGHT_CLICK_BLOCK + || event.getPlayer().isSneaking() + || event.useInteractedBlock() == Result.DENY + || event.getClickedBlock() == null + || !accessor.getAnySilentContainer().isAnySilentContainer(event.getClickedBlock())) { + return; + } + + Player player = event.getPlayer(); + UUID playerId = player.getUniqueId(); + boolean any = Permissions.CONTAINER_ANY.hasPermission(player) && PlayerToggles.any().is(playerId); + boolean needsAny = accessor.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock()); + + if (!any && needsAny) { + return; + } + + boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && PlayerToggles.silent().is(playerId); + + // If anycontainer or silentcontainer is active + if (any || silent) { + if (accessor.getAnySilentContainer().activateContainer(player, silent, event.getClickedBlock())) { + if (silent && needsAny) { + lang.sendSystemMessage(player, "messages.info.containerBlockedSilent"); + } else if (needsAny) { + lang.sendSystemMessage(player, "messages.info.containerBlocked"); + } else if (silent) { + lang.sendSystemMessage(player, "messages.info.containerSilent"); + } + } + event.setCancelled(true); + } + } + + @Keep + @EventHandler + private void onInventoryClose(@NotNull final InventoryCloseEvent event) { + if (!(event.getPlayer() instanceof Player player)) { + return; + } + + InventoryHolder holder = event.getInventory().getHolder(); + if (PlayerToggles.silent().is(player.getUniqueId()) + && holder != null + && this.accessor.getAnySilentContainer().isAnySilentContainer(holder)) { + this.accessor.getAnySilentContainer().deactivateContainer(player); + } + } + + @Keep + @EventHandler(priority = EventPriority.LOWEST) + private void onInventoryClick(@NotNull final InventoryClickEvent event) { + handleInventoryInteract(event); + } + + @Keep + @EventHandler(priority = EventPriority.LOWEST) + private void onInventoryDrag(@NotNull final InventoryDragEvent event) { + handleInventoryInteract(event); + } + + private void handleInventoryInteract(@NotNull final InventoryInteractEvent event) { + HumanEntity entity = event.getWhoClicked(); + + // Un-cancel spectator interactions. + if (entity.getGameMode() == GameMode.SPECTATOR && Permissions.SPECTATE_CLICK.hasPermission(entity)) { + event.setCancelled(false); + } + + if (event.isCancelled()) { + return; + } + + Inventory inventory = event.getView().getTopInventory(); + if (inventory instanceof ViewOnly) { + event.setCancelled(true); + } + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java b/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java new file mode 100644 index 00000000..b39535ae --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/listener/ToggleListener.java @@ -0,0 +1,22 @@ +package com.lishid.openinv.listener; + +import com.google.errorprone.annotations.Keep; +import com.lishid.openinv.util.setting.PlayerToggles; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class ToggleListener implements Listener { + + @Keep + @EventHandler + private void onPlayerQuit(@NotNull PlayerQuitEvent event) { + UUID playerId = event.getPlayer().getUniqueId(); + PlayerToggles.any().set(playerId, false); + PlayerToggles.silent().set(playerId, false); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java deleted file mode 100644 index eee66861..00000000 --- a/plugin/src/main/java/com/lishid/openinv/listeners/InventoryListener.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.listeners; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.internal.ISpecialPlayerInventory; -import com.lishid.openinv.util.InventoryAccess; -import com.lishid.openinv.util.Permissions; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import org.bukkit.GameMode; -import org.bukkit.entity.HumanEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryAction; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryCloseEvent; -import org.bukkit.event.inventory.InventoryDragEvent; -import org.bukkit.event.inventory.InventoryInteractEvent; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryView; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * Listener for inventory-related events to prevent modification of inventories where not allowed. - * - * @author Jikoo - */ -public class InventoryListener implements Listener { - - private final OpenInv plugin; - - public InventoryListener(final OpenInv plugin) { - this.plugin = plugin; - } - - @EventHandler - public void onInventoryClose(@NotNull final InventoryCloseEvent event) { - if (!(event.getPlayer() instanceof Player)) { - return; - } - - Player player = (Player) event.getPlayer(); - - if (this.plugin.getPlayerSilentChestStatus(player)) { - this.plugin.getAnySilentContainer().deactivateContainer(player); - } - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onInventoryClick(@NotNull final InventoryClickEvent event) { - if (handleInventoryInteract(event)) { - return; - } - - // Safe cast - has to be a player to be the holder of a special player inventory. - Player player = (Player) event.getWhoClicked(); - - if (event.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) { - // All own-inventory interactions require updates to display properly. - // Update in same tick after event completion. - this.plugin.getServer().getScheduler().runTask(this.plugin, player::updateInventory); - return; - } - - // Extra handling for MOVE_TO_OTHER_INVENTORY - apparently Mojang no longer removes the item from the target - // inventory prior to adding it to existing stacks. - ItemStack currentItem = event.getCurrentItem(); - if (currentItem == null) { - // Other plugin doing some sort of handling (would be NOTHING for null item otherwise), ignore. - return; - } - - ItemStack clone = currentItem.clone(); - event.setCurrentItem(null); - - // Complete add action in same tick after event completion. - this.plugin.getServer().getScheduler().runTask(this.plugin, () -> { - player.getInventory().addItem(clone); - player.updateInventory(); - }); - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onInventoryDrag(@NotNull final InventoryDragEvent event) { - if (handleInventoryInteract(event)) { - return; - } - - InventoryView view = event.getView(); - int topSize = view.getTopInventory().getSize(); - - // Get bottom inventory active slots as player inventory slots. - Set slots = event.getRawSlots().stream() - .filter(slot -> slot >= topSize) - .map(slot -> plugin.convertToPlayerSlot(view, slot)).collect(Collectors.toSet()); - - int overlapLosses = 0; - - // Count overlapping slots. - for (Map.Entry newItem : event.getNewItems().entrySet()) { - int rawSlot = newItem.getKey(); - - // Skip bottom inventory slots. - if (rawSlot >= topSize) { - continue; - } - - int convertedSlot = plugin.convertToPlayerSlot(view, rawSlot); - - if (slots.contains(convertedSlot)) { - overlapLosses += getCountDiff(view.getItem(rawSlot), newItem.getValue()); - } - } - - // Allow no overlap to proceed as usual. - if (overlapLosses < 1) { - return; - } - - ItemStack cursor = event.getCursor(); - if (cursor != null) { - cursor.setAmount(cursor.getAmount() + overlapLosses); - } else { - cursor = event.getOldCursor().clone(); - cursor.setAmount(overlapLosses); - } - - event.setCursor(cursor); - } - - private int getCountDiff(@Nullable ItemStack original, @NotNull ItemStack result) { - if (original == null || original.getType() != result.getType()) { - return result.getAmount(); - } - - return result.getAmount() - original.getAmount(); - } - - /** - * Handle common InventoryInteractEvent functions. - * - * @param event the InventoryInteractEvent - * @return true unless the top inventory is the holder's own inventory - */ - private boolean handleInventoryInteract(@NotNull final InventoryInteractEvent event) { - HumanEntity entity = event.getWhoClicked(); - - // Un-cancel spectator interactions. - if (Permissions.SPECTATE.hasPermission(entity) && entity.getGameMode() == GameMode.SPECTATOR) { - event.setCancelled(false); - } - - if (event.isCancelled()) { - return true; - } - - Inventory inventory = event.getView().getTopInventory(); - - // Is the inventory a special ender chest? - if (InventoryAccess.isEnderChest(inventory)) { - // Disallow ender chest interaction for users without edit permission. - if (!Permissions.EDITENDER.hasPermission(entity)) { - event.setCancelled(true); - } - return true; - } - - ISpecialPlayerInventory playerInventory = InventoryAccess.getPlayerInventory(inventory); - - // Ignore inventories other than special player inventories. - if (playerInventory == null) { - return true; - } - - // Disallow player inventory interaction for users without edit permission. - if (!Permissions.EDITINV.hasPermission(entity)) { - event.setCancelled(true); - return true; - } - - // Only specially handle actions in the player's own inventory. - return !event.getWhoClicked().equals(event.getView().getTopInventory().getHolder()); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java deleted file mode 100644 index 5f6712a4..00000000 --- a/plugin/src/main/java/com/lishid/openinv/listeners/PlayerListener.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.listeners; - -import com.lishid.openinv.OpenInv; -import com.lishid.openinv.util.Permissions; -import org.bukkit.entity.Player; -import org.bukkit.event.Event.Result; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.player.PlayerChangedWorldEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; - -public class PlayerListener implements Listener { - - private final OpenInv plugin; - - public PlayerListener(OpenInv plugin) { - this.plugin = plugin; - } - - @EventHandler(priority = EventPriority.LOWEST) - public void onPlayerJoin(final PlayerJoinEvent event) { - plugin.setPlayerOnline(event.getPlayer()); - } - - @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerQuit(PlayerQuitEvent event) { - plugin.setPlayerOffline(event.getPlayer()); - } - - @EventHandler - public void onWorldChange(PlayerChangedWorldEvent event) { - plugin.changeWorld(event.getPlayer()); - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onPlayerInteract(PlayerInteractEvent event) { - - // Do not cancel 3rd party plugins' custom events - if (!PlayerInteractEvent.class.equals(event.getClass())) { - return; - } - - if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getPlayer().isSneaking() - || event.useInteractedBlock() == Result.DENY || event.getClickedBlock() == null - || !plugin.getAnySilentContainer().isAnySilentContainer(event.getClickedBlock())) { - return; - } - - Player player = event.getPlayer(); - boolean any = Permissions.ANYCHEST.hasPermission(player) && plugin.getPlayerAnyChestStatus(player); - boolean needsAny = plugin.getAnySilentContainer().isAnyContainerNeeded(player, event.getClickedBlock()); - - if (!any && needsAny) { - return; - } - - boolean silent = Permissions.SILENT.hasPermission(player) && plugin.getPlayerSilentChestStatus(player); - - // If anycontainer or silentcontainer is active - if (any || silent) { - if (plugin.getAnySilentContainer().activateContainer(player, silent, event.getClickedBlock())) { - if (silent && plugin.notifySilentChest() && needsAny && plugin.notifyAnyChest()) { - plugin.sendSystemMessage(player, "messages.info.containerBlockedSilent"); - } else if (needsAny && plugin.notifyAnyChest()) { - plugin.sendSystemMessage(player, "messages.info.containerBlocked"); - } else if (silent && plugin.notifySilentChest()) { - plugin.sendSystemMessage(player, "messages.info.containerSilent"); - } - } - event.setCancelled(true); - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java b/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java deleted file mode 100644 index 546ae261..00000000 --- a/plugin/src/main/java/com/lishid/openinv/listeners/PluginListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.listeners; - -import com.lishid.openinv.OpenInv; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.server.PluginDisableEvent; - -/** - * Listener for plugin-related events. - * - * @author Jikoo - */ -public class PluginListener implements Listener { - - private final OpenInv plugin; - - public PluginListener(OpenInv plugin) { - this.plugin = plugin; - } - - @EventHandler - public void onPluginDisable(PluginDisableEvent event) { - plugin.releaseAllPlayers(event.getPlugin()); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/AccessEqualMode.java b/plugin/src/main/java/com/lishid/openinv/util/AccessEqualMode.java new file mode 100644 index 00000000..a68914d9 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/AccessEqualMode.java @@ -0,0 +1,38 @@ +package com.lishid.openinv.util; + +import com.lishid.openinv.util.config.Config; +import org.bukkit.permissions.Permissible; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public enum AccessEqualMode { + + DENY, ALLOW, VIEW; + + public static @NotNull AccessEqualMode of(@Nullable String value) { + if (value == null) { + return VIEW; + } + return switch (value.toLowerCase(Locale.ENGLISH)) { + case "deny", "false" -> DENY; + case "allow", "true" -> ALLOW; + default -> VIEW; + }; + } + + public static @NotNull AccessEqualMode getByPerm(@NotNull Permissible permissible, @NotNull Config config) { + if (Permissions.ACCESS_EQUAL_EDIT.hasPermission(permissible)) { + return AccessEqualMode.ALLOW; + } + if (Permissions.ACCESS_EQUAL_VIEW.hasPermission(permissible)) { + return AccessEqualMode.VIEW; + } + if (Permissions.ACCESS_EQUAL_DENY.hasPermission(permissible)) { + return AccessEqualMode.DENY; + } + return config.getAccessEqualMode(); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/Cache.java b/plugin/src/main/java/com/lishid/openinv/util/Cache.java deleted file mode 100644 index fc8c60e5..00000000 --- a/plugin/src/main/java/com/lishid/openinv/util/Cache.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -import com.google.common.collect.Multimap; -import com.google.common.collect.TreeMultimap; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Predicate; - -/** - * A minimal thread-safe time-based cache implementation backed by a HashMap and TreeMultimap. - * - * @author Jikoo - */ -public class Cache { - - private final Map internal; - private final Multimap expiry; - private final long retention; - private final Predicate inUseCheck; - private final Consumer postRemoval; - - /** - * Constructs a Cache with the specified retention duration, in use function, and post-removal function. - * - * @param retention duration after which keys are automatically invalidated if not in use - * @param inUseCheck Predicate used to check if a key is considered in use - * @param postRemoval Consumer used to perform any operations required when a key is invalidated - */ - public Cache(final long retention, final Predicate inUseCheck, final Consumer postRemoval) { - this.internal = new HashMap<>(); - - this.expiry = TreeMultimap.create(Long::compareTo, (k1, k2) -> Objects.equals(k1, k2) ? 0 : 1); - - this.retention = retention; - this.inUseCheck = inUseCheck; - this.postRemoval = postRemoval; - } - - /** - * Set a key and value pair. Keys are unique. Using an existing key will cause the old value to - * be overwritten and the expiration timer to be reset. - * - * @param key key with which the specified value is to be associated - * @param value value to be associated with the specified key - */ - public void put(final K key, final V value) { - // Invalidate key - runs lazy check and ensures value won't be cleaned up early - this.invalidate(key); - - synchronized (this.internal) { - this.internal.put(key, value); - this.expiry.put(System.currentTimeMillis() + this.retention, key); - } - } - - /** - * Returns the value to which the specified key is mapped, or null if no value is mapped for the key. - * - * @param key the key whose associated value is to be returned - * @return the value to which the specified key is mapped, or null if no value is mapped for the key - */ - public V get(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - return this.internal.get(key); - } - } - - /** - * Returns true if the specified key is mapped to a value. - * - * @param key key to check if a mapping exists for - * @return true if a mapping exists for the specified key - */ - public boolean containsKey(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - return this.internal.containsKey(key); - } - } - - /** - * Forcibly invalidates a key, even if it is considered to be in use. - * - * @param key key to invalidate - */ - public void invalidate(final K key) { - // Run lazy check to clean cache - this.lazyCheck(); - - synchronized (this.internal) { - if (!this.internal.containsKey(key)) { - // Value either not present or cleaned by lazy check. Either way, we're good - return; - } - - // Remove stored object - this.internal.remove(key); - - // Remove expiration entry - prevents more work later, plus prevents issues with values invalidating early - for (Iterator> iterator = this.expiry.entries().iterator(); iterator.hasNext();) { - if (key.equals(iterator.next().getValue())) { - iterator.remove(); - break; - } - } - } - } - - /** - * Forcibly invalidates all keys, even if they are considered to be in use. - */ - public void invalidateAll() { - synchronized (this.internal) { - for (V value : this.internal.values()) { - this.postRemoval.accept(value); - } - this.expiry.clear(); - this.internal.clear(); - } - } - - /** - * Invalidate all expired keys that are not considered in use. If a key is expired but is - * considered in use by the provided Function, its expiration time is reset. - */ - private void lazyCheck() { - long now = System.currentTimeMillis(); - synchronized (this.internal) { - List inUse = new ArrayList<>(); - for (Iterator> iterator = this.expiry.entries().iterator(); iterator - .hasNext();) { - Map.Entry entry = iterator.next(); - - if (entry.getKey() > now) { - break; - } - - iterator.remove(); - - if (this.inUseCheck.test(this.internal.get(entry.getValue()))) { - inUse.add(entry.getValue()); - continue; - } - - V value = this.internal.remove(entry.getValue()); - - if (value == null) { - continue; - } - - this.postRemoval.accept(value); - } - - long nextExpiry = now + this.retention; - for (K value : inUse) { - this.expiry.put(nextExpiry, value); - } - } - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java b/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java deleted file mode 100644 index faecfc8c..00000000 --- a/plugin/src/main/java/com/lishid/openinv/util/ConfigUpdater.java +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -import com.lishid.openinv.OpenInv; -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import org.bukkit.OfflinePlayer; -import org.bukkit.configuration.ConfigurationSection; - -public class ConfigUpdater { - - private final OpenInv plugin; - - public ConfigUpdater(OpenInv plugin) { - this.plugin = plugin; - } - - public void checkForUpdates() { - final int version = plugin.getConfig().getInt("config-version", 1); - ConfigurationSection defaults = plugin.getConfig().getDefaults(); - if (defaults == null || version >= defaults.getInt("config-version")) { - return; - } - - plugin.getLogger().info("Configuration update found! Performing update..."); - - // Backup the old config file - try { - plugin.getConfig().save(new File(plugin.getDataFolder(), "config_old.yml")); - plugin.getLogger().info("Backed up config.yml to config_old.yml before updating."); - } catch (IOException e) { - plugin.getLogger().warning("Could not back up config.yml before updating!"); - } - - plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> { - if (version < 2) { - updateConfig1To2(); - } - if (version < 3) { - updateConfig2To3(); - } - if (version < 4) { - updateConfig3To4(); - } - - plugin.getServer().getScheduler().runTask(plugin, () -> { - plugin.saveConfig(); - plugin.getLogger().info("Configuration update complete!"); - }); - }); - } - - private void updateConfig3To4() { - plugin.getServer().getScheduler().runTask(plugin, () -> { - plugin.getConfig().set("notify", null); - plugin.getConfig().set("settings.locale", "en_US"); - plugin.getConfig().set("config-version", 4); - }); - } - - private void updateConfig2To3() { - plugin.getServer().getScheduler().runTask(plugin, () -> { - plugin.getConfig().set("config-version", 3); - plugin.getConfig().set("items.open-inv", null); - plugin.getConfig().set("ItemOpenInv", null); - plugin.getConfig().set("toggles.items.open-inv", null); - plugin.getConfig().set("settings.disable-saving", - plugin.getConfig().getBoolean("DisableSaving", false)); - plugin.getConfig().set("DisableSaving", null); - }); - } - - private void updateConfig1To2() { - plugin.getServer().getScheduler().runTask(plugin, () -> { - // Get the old config settings - boolean notifySilentChest = plugin.getConfig().getBoolean("NotifySilentChest", true); - boolean notifyAnyChest = plugin.getConfig().getBoolean("NotifyAnyChest", true); - plugin.getConfig().set("ItemOpenInvItemID", null); - plugin.getConfig().set("NotifySilentChest", null); - plugin.getConfig().set("NotifyAnyChest", null); - plugin.getConfig().set("config-version", 2); - plugin.getConfig().set("notify.any-chest", notifyAnyChest); - plugin.getConfig().set("notify.silent-chest", notifySilentChest); - }); - - updateToggles("AnyChest", "toggles.any-chest"); - updateToggles("SilentChest", "toggles.silent-chest"); - } - - private void updateToggles(final String sectionName, final String newSectionName) { - ConfigurationSection section = plugin.getConfig().getConfigurationSection(sectionName); - // Ensure section exists - if (section == null) { - return; - } - - Set keys = section.getKeys(false); - - // Ensure section has content - if (keys.isEmpty()) { - return; - } - - final Map toggles = new HashMap<>(); - - for (String playerName : keys) { - OfflinePlayer player = plugin.matchPlayer(playerName); - if (player != null) { - toggles.put(plugin.getPlayerID(player), section.getBoolean(playerName + ".toggle", false)); - } - } - - plugin.getServer().getScheduler().runTask(plugin, () -> { - // Wipe old ConfigurationSection - plugin.getConfig().set(sectionName, null); - - // Prepare new ConfigurationSection - ConfigurationSection newSection = plugin.getConfig().getConfigurationSection(newSectionName); - if (newSection == null) { - newSection = plugin.getConfig().createSection(newSectionName); - } - // Set new values - for (Map.Entry entry : toggles.entrySet()) { - newSection.set(entry.getKey(), entry.getValue()); - } - }); - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java b/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java index fb245521..f5d77139 100644 --- a/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java +++ b/plugin/src/main/java/com/lishid/openinv/util/InternalAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2023 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,204 +16,294 @@ package com.lishid.openinv.util; +import com.github.jikoo.planarwrappers.util.version.BukkitVersions; +import com.github.jikoo.planarwrappers.util.version.Version; +import com.lishid.openinv.internal.Accessor; import com.lishid.openinv.internal.IAnySilentContainer; -import com.lishid.openinv.internal.IPlayerDataManager; import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.ISpecialInventory; import com.lishid.openinv.internal.ISpecialPlayerInventory; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; +import com.lishid.openinv.internal.PlayerManager; +import com.lishid.openinv.util.lang.LanguageManager; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; +import org.bukkit.inventory.InventoryView; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.logging.Logger; public class InternalAccessor { - private final Plugin plugin; - private final String version; - private boolean supported = false; - private IPlayerDataManager playerDataManager; - private IAnySilentContainer anySilentContainer; - - public InternalAccessor(final Plugin plugin) { - this.plugin = plugin; - - String packageName = plugin.getServer().getClass().getPackage().getName(); - this.version = packageName.substring(packageName.lastIndexOf('.') + 1); - - try { - Class.forName("com.lishid.openinv.internal." + this.version + ".SpecialPlayerInventory"); - Class.forName("com.lishid.openinv.internal." + this.version + ".SpecialEnderChest"); - this.playerDataManager = this.createObject(IPlayerDataManager.class, "PlayerDataManager"); - this.anySilentContainer = this.createObject(IAnySilentContainer.class, "AnySilentContainer"); - this.supported = InventoryAccess.isUsable(); - } catch (Exception ignored) {} - } - - public String getReleasesLink() { - switch (version) { - case "1_4_5": - case "1_4_6": - case "v1_4_R1": - case "v1_5_R2": - case "v1_5_R3": - case "v1_6_R1": - case "v1_6_R2": - case "v1_6_R3": - case "v1_7_R1": - case "v1_7_R2": - case "v1_7_R3": - case "v1_7_R4": - case "v1_8_R1": - case "v1_8_R2": - case "v1_9_R1": - case "v1_9_R2": - case "v1_10_R1": - case "v1_11_R1": - case "v1_12_R1": - return "https://github.com/lishid/OpenInv/releases/tag/4.0.0 (OpenInv-legacy)"; - case "v1_13_R1": - return "https://github.com/lishid/OpenInv/releases/tag/4.0.0"; - case "v1_13_R2": - return "https://github.com/lishid/OpenInv/releases/tag/4.0.7"; - case "v1_14_R1": - return "https://github.com/lishid/OpenInv/releases/tag/4.1.1"; - case "v1_16_R1": - return "https://github.com/lishid/OpenInv/releases/tag/4.1.4"; - case "v1_8_R3": - case "v1_15_R1": - case "v1_16_R2": - return "https://github.com/lishid/OpenInv/releases/tag/4.1.5"; - case "v1_16_R3": - default: - return "https://github.com/lishid/OpenInv/releases"; - } - } - - private T createObject(final Class assignableClass, final String className, - final Object... params) throws ClassCastException, ClassNotFoundException, - InstantiationException, IllegalAccessException, IllegalArgumentException, - InvocationTargetException, NoSuchMethodException, SecurityException { - // Fetch internal class if it exists. - Class internalClass = Class.forName("com.lishid.openinv.internal." + this.version + "." + className); - if (!assignableClass.isAssignableFrom(internalClass)) { - String message = String.format("Found class %s but cannot cast to %s!", internalClass.getName(), assignableClass.getName()); - this.plugin.getLogger().warning(message); - throw new IllegalStateException(message); - } - - // Quick return: no parameters, no need to fiddle about finding the correct constructor. - if (params.length == 0) { - return assignableClass.cast(internalClass.getConstructor().newInstance()); - } - - // Search constructors for one matching the given parameters - nextConstructor: for (Constructor constructor : internalClass.getConstructors()) { - Class[] requiredClasses = constructor.getParameterTypes(); - if (requiredClasses.length != params.length) { - continue; - } - for (int i = 0; i < params.length; ++i) { - if (!requiredClasses[i].isAssignableFrom(params[i].getClass())) { - continue nextConstructor; - } - } - return assignableClass.cast(constructor.newInstance(params)); - } - - StringBuilder builder = new StringBuilder("Found class ").append(internalClass.getName()) - .append(" but cannot find any matching constructors for ["); - for (Object object : params) { - builder.append(object.getClass().getName()).append(", "); - } - builder.delete(builder.length() - 2, builder.length()); - - String message = builder.append(']').toString(); - this.plugin.getLogger().warning(message); - - throw new IllegalArgumentException(message); - } - - /** - * Creates an instance of the IAnySilentContainer implementation for the current server version. - * - * @return the IAnySilentContainer - * @throws IllegalStateException if server version is unsupported - */ - public IAnySilentContainer getAnySilentContainer() { - if (!this.supported) { - throw new IllegalStateException(String.format("Unsupported server version %s!", this.version)); - } - return this.anySilentContainer; - } - - /** - * Creates an instance of the IPlayerDataManager implementation for the current server version. - * - * @return the IPlayerDataManager - * @throws IllegalStateException if server version is unsupported - */ - public IPlayerDataManager getPlayerDataManager() { - if (!this.supported) { - throw new IllegalStateException(String.format("Unsupported server version %s!", this.version)); - } - return this.playerDataManager; - } - - /** - * Gets the server implementation version. If not initialized, returns the string "null" - * instead. - * - * @return the version, or "null" - */ - public String getVersion() { - return this.version != null ? this.version : "null"; - } - - /** - * Checks if the server implementation is supported. - * - * @return true if initialized for a supported server version - */ - public boolean isSupported() { - return this.supported; - } - - /** - * Creates an instance of the ISpecialEnderChest implementation for the given Player, or - * null if the current version is unsupported. - * - * @param player the Player - * @param online true if the Player is online - * @return the ISpecialEnderChest created - * @throws InstantiationException if the ISpecialEnderChest could not be instantiated - */ - public ISpecialEnderChest newSpecialEnderChest(final Player player, final boolean online) throws InstantiationException { - if (!this.supported) { - throw new IllegalStateException(String.format("Unsupported server version %s!", this.version)); - } - try { - return this.createObject(ISpecialEnderChest.class, "SpecialEnderChest", player, online); - } catch (Exception e) { - throw new InstantiationException(String.format("Unable to create a new ISpecialEnderChest: %s", e.getMessage())); - } - } - - /** - * Creates an instance of the ISpecialPlayerInventory implementation for the given Player.. - * - * @param player the Player - * @param online true if the Player is online - * @return the ISpecialPlayerInventory created - * @throws InstantiationException if the ISpecialPlayerInventory could not be instantiated - */ - public ISpecialPlayerInventory newSpecialPlayerInventory(final Player player, final boolean online) throws InstantiationException { - if (!this.supported) { - throw new IllegalStateException(String.format("Unsupported server version %s!", this.version)); - } - try { - return this.createObject(ISpecialPlayerInventory.class, "SpecialPlayerInventory", player, online); - } catch (Exception e) { - throw new InstantiationException(String.format("Unable to create a new ISpecialPlayerInventory: %s", e.getMessage())); - } + private static final boolean PAPER; + + static { + boolean paper = false; + try { + Class.forName("io.papermc.paper.configuration.GlobalConfiguration"); + paper = true; + } catch (ClassNotFoundException ignored) { + // Expect remapped server. + } + PAPER = paper; + } + + private @Nullable Accessor internal; + + public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + try { + internal = getAccessor(logger, lang); + + if (internal != null) { + InventoryAccess.setProvider(internal::get); + } + } catch (NoClassDefFoundError | Exception e) { + internal = null; + InventoryAccess.setProvider(null); + } + } + + private @Nullable Accessor getAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) { + Version maxSupported = Version.of(1, 21, 11); + Version minSupported = Version.of(1, 21, 1); + + // Ensure version is in supported range. + if (BukkitVersions.MINECRAFT.greaterThan(maxSupported) || BukkitVersions.MINECRAFT.lessThan(minSupported)) { + return null; + } + + // Load Spigot accessor. + if (!PAPER) { + if (BukkitVersions.MINECRAFT.equals(maxSupported)) { + // Current Spigot, remapped internals are available. + return new com.lishid.openinv.internal.reobf.InternalAccessor(logger, lang); + } else { + // Older Spigot; unsupported. + return null; + } + } + + // Paper or a Paper fork, can use Mojang-mapped internals. + if (BukkitVersions.MINECRAFT.equals(maxSupported)) { // 1.21.11 + return new com.lishid.openinv.internal.common.InternalAccessor(logger, lang); + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10)) + && BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 9))) { // 1.21.9, 1.21.10 + return new com.lishid.openinv.internal.paper1_21_10.InternalAccessor(logger, lang); + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8)) + && BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 6))) { // 1.21.6, 1.21.7, 1.21.8 + return new com.lishid.openinv.internal.paper1_21_8.InternalAccessor(logger, lang); + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 5))) { // 1.21.5 + return new com.lishid.openinv.internal.paper1_21_5.InternalAccessor(logger, lang); + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 4))) { // 1.21.4 + return new com.lishid.openinv.internal.paper1_21_4.InternalAccessor(logger, lang); + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 21, 2))) { + // 1.21.1-1.21.2 placeholder format + return new com.lishid.openinv.internal.paper1_21_1.InternalAccessor(logger, lang); + } + + // 1.21.2, 1.21.3 + return new com.lishid.openinv.internal.paper1_21_3.InternalAccessor(logger, lang); + } + + public String getReleasesLink() { + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 4, 4))) { // Good luck. + return "https://dev.bukkit.org/projects/openinv/files?&sort=datecreated"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 8, 8))) { // 1.8.8 + return "https://github.com/lishid/OpenInv/releases/tag/4.1.5"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 13))) { // 1.4.4+ had versioned packages. + return "https://github.com/lishid/OpenInv/releases/tag/4.0.0 (OpenInv-legacy)"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 13))) { // 1.13 + return "https://github.com/lishid/OpenInv/releases/tag/4.0.0"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 14))) { // 1.13.1, 1.13.2 + return "https://github.com/lishid/OpenInv/releases/tag/4.0.7"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 14))) { // 1.14 to 1.14.1 had no revision bump. + return "https://github.com/lishid/OpenInv/releases/tag/4.0.0"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 14, 1))) { // 1.14.1 to 1.14.2 had no revision bump. + return "https://github.com/lishid/OpenInv/releases/tag/4.0.1"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 15))) { // 1.14.2 + return "https://github.com/lishid/OpenInv/releases/tag/4.1.1"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 15, 1))) { // 1.15, 1.15.1 + return "https://github.com/lishid/OpenInv/releases/tag/4.1.5"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 16))) { // 1.15.2 + return "https://github.com/Jikoo/OpenInv/commit/502f661be39ee85d300851dd571f3da226f12345 (never released)"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 16, 1))) { // 1.16, 1.16.1 + return "https://github.com/lishid/OpenInv/releases/tag/4.1.4"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 16, 3))) { // 1.16.2, 1.16.3 + return "https://github.com/lishid/OpenInv/releases/tag/4.1.5"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 17))) { // 1.16.4, 1.16.5 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.1.8"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 18, 1))) { // 1.17, 1.18, 1.18.1 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.1.10"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 19))) { // 1.18.2 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.3.0"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 19))) { // 1.19 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.2.0"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 19, 1))) { // 1.19.1 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.2.2"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 19, 3))) { // 1.19.2, 1.19.3 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.3.0"; + } + if (BukkitVersions.MINECRAFT.lessThan(Version.of(1, 20))) { // 1.19.4 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.3"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 20, 1))) { // 1.20, 1.20.1 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.1"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 20, 3))) { // 1.20.2, 1.20.3 + return "https://github.com/Jikoo/OpenInv/releases/tag/4.4.3"; + } + if (BukkitVersions.MINECRAFT.equals(Version.of(1, 20, 5))) { // 1.20.5 + return "Unsupported; upgrade to 1.20.6: https://github.com/Jikoo/OpenInv/releases/tag/5.1.2"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21))) { // 1.20.4, 1.20.6, 1.21 + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.2"; + } + if (!PAPER) { + return getSpigotReleaseLink(); + } + // Paper 1.21.1+ + return "https://github.com/Jikoo/OpenInv/releases"; + } + + private String getSpigotReleaseLink() { + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 2))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.3"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 3))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.6"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 4))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.9"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 5))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.11"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 6))) { + return "Unsupported; upgrade to 1.21.7: https://github.com/Jikoo/OpenInv/releases"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.13"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 9))) { + return "Unsupported; upgrade to 1.21.10: https://github.com/Jikoo/OpenInv/releases/tag/5.1.15"; + } + if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10))) { + return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.15"; + } + + return "https://github.com/Jikoo/OpenInv/releases"; + } + + /** + * Reload internal features. + */ + public void reload(ConfigurationSection config) { + if (internal != null) { + internal.reload(config); + } + } + + /** + * Gets the server implementation version. + * + * @return the version + */ + public @NotNull String getVersion() { + return BukkitVersions.MINECRAFT.toString(); + } + + /** + * Checks if the server implementation is supported. + * + * @return true if initialized for a supported server version + */ + public boolean isSupported() { + return internal != null; + } + + /** + * Get the instance of the IAnySilentContainer implementation for the current server version. + * + * @return the IAnySilentContainer + * @throws IllegalStateException if server version is unsupported + */ + public @NotNull IAnySilentContainer getAnySilentContainer() { + if (internal == null) { + throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT)); + } + return internal.getAnySilentContainer(); + } + + public @Nullable InventoryView openInventory( + @NotNull Player player, + @NotNull ISpecialInventory inventory, + boolean viewOnly + ) { + if (internal == null) { + throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT)); + } + return internal.getPlayerManager().openInventory(player, inventory, viewOnly); + } + + /** + * Get the instance of the IPlayerDataManager implementation for the current server version. + * + * @return the IPlayerDataManager + * @throws IllegalStateException if server version is unsupported + */ + @NotNull PlayerManager getPlayerDataManager() { + if (internal == null) { + throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT)); + } + return internal.getPlayerManager(); + } + + /** + * Creates an instance of the ISpecialEnderChest implementation for the given Player. + * + * @param player the Player + * @return the ISpecialEnderChest created + */ + ISpecialEnderChest createEnderChest(final Player player) { + if (internal == null) { + throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT)); + } + return internal.createEnderChest(player); + } + + /** + * Creates an instance of the ISpecialPlayerInventory implementation for the given Player. + * + * @param player the Player + * @return the ISpecialPlayerInventory created + */ + ISpecialPlayerInventory createInventory(final Player player) { + if (internal == null) { + throw new IllegalStateException(String.format("Unsupported server version %s!", BukkitVersions.MINECRAFT)); } + return internal.createPlayerInventory(player); + } } diff --git a/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java b/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java new file mode 100644 index 00000000..d6460197 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/InventoryManager.java @@ -0,0 +1,277 @@ +package com.lishid.openinv.util; + +import com.google.errorprone.annotations.Keep; +import com.lishid.openinv.OpenInv; +import com.lishid.openinv.event.OpenEvents; +import com.lishid.openinv.internal.ISpecialEnderChest; +import com.lishid.openinv.internal.ISpecialInventory; +import com.lishid.openinv.internal.ISpecialPlayerInventory; +import com.lishid.openinv.util.config.Config; +import org.bukkit.entity.HumanEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryCloseEvent; +import org.bukkit.event.inventory.InventoryOpenEvent; +import org.bukkit.event.player.PlayerChangedWorldEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.stream.Stream; + +/** + * A manager for special inventories. Delegates creation and tracks copies in use. + */ +public class InventoryManager implements Listener { + + private final Map inventories = new ConcurrentHashMap<>(); + private final Map enderChests = new ConcurrentHashMap<>(); + private final Set expectedCloses = new HashSet<>(); + private final @NotNull OpenInv plugin; + private final @NotNull Config config; + private final @NotNull InternalAccessor accessor; + + public InventoryManager(@NotNull OpenInv plugin, @NotNull Config config, @NotNull InternalAccessor accessor) { + this.plugin = plugin; + this.config = config; + this.accessor = accessor; + } + + public void evictAll() { + Stream.concat(inventories.values().stream(), enderChests.values().stream()) + .map(inventory -> { + // Rather than iterate twice, evict all viewers during remapping. + for (HumanEntity viewer : List.copyOf(inventory.getBukkitInventory().getViewers())) { + expectedCloses.add(viewer.getUniqueId()); + viewer.closeInventory(); + } + // If saving is prevented, return a null value for the player to save. + if (config.isSaveDisabled() || OpenEvents.saveCancelled(inventory)) { + return null; + } + if (inventory.getPlayer() instanceof Player player) { + return player; + } + return null; + }) + .filter(Objects::nonNull) + .distinct() + .forEach(player -> { + if (!player.isOnline()) { + accessor.getPlayerDataManager().inject(player).saveData(); + } + }); + inventories.clear(); + enderChests.clear(); + expectedCloses.clear(); + } + + public @NotNull ISpecialPlayerInventory getInventory(@NotNull Player player) { + return inventories.computeIfAbsent(player.getUniqueId(), uuid -> accessor.createInventory(player)); + } + + public @NotNull ISpecialEnderChest getEnderChest(@NotNull Player player) { + return enderChests.computeIfAbsent(player.getUniqueId(), uuid -> accessor.createEnderChest(player)); + } + + public @Nullable Player getLoadedPlayer(@NotNull UUID uuid) { + ISpecialInventory inUse = inventories.get(uuid); + if (inUse != null) { + return (Player) inUse.getPlayer(); + } + inUse = enderChests.get(uuid); + if (inUse != null) { + return (Player) inUse.getPlayer(); + } + return null; + } + + public void unload(@NotNull UUID uuid) { + inventories.computeIfPresent(uuid, this::remove); + enderChests.computeIfPresent(uuid, this::remove); + } + + public void save(@NotNull UUID uuid) { + consumeLoaded(uuid, inventory -> {}); + } + + @Keep + @EventHandler(priority = EventPriority.LOWEST) + private void onPlayerJoin(@NotNull PlayerJoinEvent event) { + consumeLoaded( + event.getPlayer().getUniqueId(), + inventory -> { + inventory.setPlayerOnline(event.getPlayer()); + checkViewerAccess(inventory, true); + } + ); + } + + @Keep + @EventHandler(priority = EventPriority.MONITOR) + private void onPlayerQuit(@NotNull PlayerQuitEvent event) { + consumeLoaded( + event.getPlayer().getUniqueId(), + inventory -> checkViewerAccess(inventory, false) + ); + } + + @Keep + @EventHandler + private void onWorldChanged(@NotNull PlayerChangedWorldEvent event) { + Player player = event.getPlayer(); + consumeLoaded(player.getUniqueId(), inventory -> checkViewerAccess(inventory, player.isOnline())); + } + + @Keep + @EventHandler + private void onInventoryClose(@NotNull InventoryCloseEvent event) { + ISpecialInventory inventory = InventoryAccess.getInventory(event.getInventory()); + + // If this is not an ISpecialInventory or the inventory was closed elsewhere internally, don't handle. + if (inventory == null || expectedCloses.remove(event.getPlayer().getUniqueId())) { + return; + } + + // Fetch the active ISpecialInventory of this type. + Map map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests; + UUID key = inventory.getPlayer().getUniqueId(); + ISpecialInventory loaded = map.get(key); + + // If there is no loaded inventory, it has already been removed and saved. + if (loaded == null) { + return; + } + + // This should only be possible if a plugin is going to extreme lengths to mess with our inventories. + if (loaded != inventory) { + // Immediately remove affected inventory, then dump all viewers. We don't want to risk duplication bugs. + map.remove(key); + remove(key, loaded); + remove(key, inventory); + // The loaded one is "correct" as far as we're concerned, so save that. + save(loaded); + } + + // Schedule task to check in use status later this tick. Closing user is still in viewer list. + plugin.getScheduler().runTask(() -> { + if (loaded.isInUse()) { + return; + } + + // Re-fetch from map to reduce odds of a duplicate save. + ISpecialInventory current = map.remove(key); + + if (current != null) { + save(current); + } + }); + } + + @Keep + @EventHandler(priority = EventPriority.HIGHEST) + private void onInventoryOpen(@NotNull InventoryOpenEvent event) { + ISpecialInventory inventory = InventoryAccess.getInventory(event.getInventory()); + if (inventory == null) { + return; + } + + Map map = inventory instanceof ISpecialPlayerInventory ? inventories : enderChests; + UUID key = inventory.getPlayer().getUniqueId(); + ISpecialInventory loaded = map.get(key); + + if (!inventory.equals(loaded)) { + event.setCancelled(true); + plugin.getLogger().log( + Level.WARNING, + "Prevented a plugin from opening an untracked ISpecialInventory!", + new Throwable("Untracked ISpecialInventory") + ); + } + } + + private void checkViewerAccess(@NotNull T inventory, boolean online) { + + Player owner = (Player) inventory.getPlayer(); + Permissions connectedState = online ? Permissions.ACCESS_ONLINE : Permissions.ACCESS_OFFLINE; + boolean alwaysDenied = !online && config.isOfflineDisabled(); + + // Copy viewers so we don't modify the list we're iterating over when closing inventories. + List viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers()); + + for (HumanEntity viewer : viewers) { + if (alwaysDenied + || !connectedState.hasPermission(viewer) + || (!Objects.equals(owner.getWorld(), viewer.getWorld()) && !Permissions.ACCESS_CROSSWORLD.hasPermission(viewer))) { + expectedCloses.add(viewer.getUniqueId()); + viewer.closeInventory(); + } + } + } + + private void consumeLoaded(@NotNull UUID key, @NotNull Consumer<@NotNull ISpecialInventory> consumer) { + boolean saved = consumeLoaded(inventories, key, false, consumer); + consumeLoaded(enderChests, key, saved, consumer); + } + + private boolean consumeLoaded( + @NotNull Map map, + @NotNull UUID key, + boolean saved, + @NotNull Consumer<@NotNull ISpecialInventory> consumer + ) { + T inventory = map.get(key); + + if (inventory == null) { + return saved; + } + + consumer.accept(inventory); + if (!inventory.isInUse()) { + map.remove(key); + + if (!saved) { + save(inventory); + return true; + } + } + + return saved; + } + + private void save(@NotNull ISpecialInventory inventory) { + if (config.isSaveDisabled()) { + return; + } + + Player player = (Player) inventory.getPlayer(); + + if (!player.isOnline() && !OpenEvents.saveCancelled(inventory)) { + accessor.getPlayerDataManager().inject(player).saveData(); + } + } + + @Contract("_, _ -> null") + private @Nullable T remove(@NotNull UUID key, @NotNull T inventory) { + for (HumanEntity viewer : List.copyOf(inventory.getBukkitInventory().getViewers())) { + expectedCloses.add(viewer.getUniqueId()); + viewer.closeInventory(); + } + return null; + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java b/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java deleted file mode 100644 index 8eadcb9f..00000000 --- a/plugin/src/main/java/com/lishid/openinv/util/LanguageManager.java +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright (C) 2011-2020 Jikoo. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -import com.lishid.openinv.OpenInv; -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import org.bukkit.ChatColor; -import org.bukkit.configuration.file.YamlConfiguration; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * A simple language manager supporting both custom and bundled languages. - * - * @author Jikoo - */ -public class LanguageManager { - - private final OpenInv plugin; - private final String defaultLocale; - private final Map locales; - - public LanguageManager(@NotNull OpenInv plugin, @NotNull String defaultLocale) { - this.plugin = plugin; - this.defaultLocale = defaultLocale; - this.locales = new HashMap<>(); - getOrLoadLocale(defaultLocale); - } - - private YamlConfiguration getOrLoadLocale(@NotNull String locale) { - YamlConfiguration loaded = locales.get(locale); - if (loaded != null) { - return loaded; - } - - InputStream resourceStream = plugin.getResource(locale + ".yml"); - YamlConfiguration localeConfigDefaults; - if (resourceStream == null) { - localeConfigDefaults = new YamlConfiguration(); - } else { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceStream))) { - localeConfigDefaults = YamlConfiguration.loadConfiguration(reader); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to load resource " + locale + ".yml", e); - localeConfigDefaults = new YamlConfiguration(); - } - } - - File file = new File(plugin.getDataFolder(), locale + ".yml"); - YamlConfiguration localeConfig; - - if (!file.exists()) { - localeConfig = localeConfigDefaults; - try { - localeConfigDefaults.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e); - } - } else { - localeConfig = YamlConfiguration.loadConfiguration(file); - - // Add new language keys - List newKeys = new ArrayList<>(); - for (String key : localeConfigDefaults.getKeys(true)) { - if (localeConfigDefaults.isConfigurationSection(key)) { - continue; - } - - if (localeConfig.isSet(key)) { - continue; - } - - localeConfig.set(key, localeConfigDefaults.get(key)); - newKeys.add(key); - } - - if (!newKeys.isEmpty()) { - plugin.getLogger().info("[LanguageManager] Added new language keys: " + String.join(", ", newKeys)); - try { - localeConfig.save(file); - } catch (IOException e) { - plugin.getLogger().log(Level.WARNING, "[LanguageManager] Unable to save resource " + locale + ".yml", e); - } - } - } - - if (!locale.equals(defaultLocale)) { - localeConfigDefaults = locales.get(defaultLocale); - - // Check for missing keys - List newKeys = new ArrayList<>(); - for (String key : localeConfigDefaults.getKeys(true)) { - if (localeConfigDefaults.isConfigurationSection(key)) { - continue; - } - - if (localeConfig.isSet(key)) { - continue; - } - - newKeys.add(key); - } - - if (!newKeys.isEmpty()) { - plugin.getLogger().info("[LanguageManager] Missing translations from " + locale + ".yml: " + String.join(", ", newKeys)); - } - - // Fall through to default locale - localeConfig.setDefaults(localeConfigDefaults); - } - - locales.put(locale, localeConfig); - return localeConfig; - } - - @Nullable - public String getValue(@NotNull String key, @Nullable String locale) { - String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase()).getString(key); - if (value == null || value.isEmpty()) { - return null; - } - - value = ChatColor.translateAlternateColorCodes('&', value); - - return value; - } - - @Nullable - public String getValue(@NotNull String key, @Nullable String locale, @NotNull String... replacements) { - if (replacements.length % 2 != 0) { - plugin.getLogger().log(Level.WARNING, "[LanguageManager] Replacement data is uneven", new Exception()); - } - - String value = getValue(key, locale); - - if (value == null) { - return null; - } - - for (int i = 0; i < replacements.length; i += 2) { - value = value.replace(replacements[i], replacements[i + 1]); - } - - return value; - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java b/plugin/src/main/java/com/lishid/openinv/util/Permissions.java deleted file mode 100644 index 859ba139..00000000 --- a/plugin/src/main/java/com/lishid/openinv/util/Permissions.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2011-2020 lishid. All rights reserved. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lishid.openinv.util; - -import org.bukkit.permissions.Permissible; - -public enum Permissions { - - OPENINV("openinv"), - OVERRIDE("override"), - EXEMPT("exempt"), - CROSSWORLD("crossworld"), - SILENT("silent"), - SILENT_DEFAULT("silent.default", true), - ANYCHEST("anychest"), - ANY_DEFAULT("any.default", true), - ENDERCHEST("openender"), - ENDERCHEST_ALL("openenderall"), - SEARCH("search"), - EDITINV("editinv"), - EDITENDER("editender"), - OPENSELF("openself"), - OPENONLINE("openonline"), - OPENOFFLINE("openoffline"), - SPECTATE("spectate"); - - private final String permission; - private final boolean uninheritable; - - Permissions(String permission) { - this(permission, false); - } - - Permissions(String permission, boolean uninheritable) { - this.permission = "OpenInv." + permission; - this.uninheritable = uninheritable; - } - - public boolean hasPermission(Permissible permissible) { - - boolean hasPermission = permissible.hasPermission(permission); - if (uninheritable || hasPermission || permissible.isPermissionSet(permission)) { - return hasPermission; - } - - StringBuilder permissionDestroyer = new StringBuilder(permission); - for (int lastPeriod = permissionDestroyer.lastIndexOf("."); lastPeriod > 0; - lastPeriod = permissionDestroyer.lastIndexOf(".")) { - permissionDestroyer.delete(lastPeriod + 1, permissionDestroyer.length()).append('*'); - - hasPermission = permissible.hasPermission(permissionDestroyer.toString()); - if (hasPermission || permissible.isPermissionSet(permissionDestroyer.toString())) { - return hasPermission; - } - - permissionDestroyer.delete(lastPeriod, permissionDestroyer.length()); - - } - - return permissible.hasPermission("*"); - - } - -} diff --git a/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java b/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java new file mode 100644 index 00000000..0930c589 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/PlayerLoader.java @@ -0,0 +1,236 @@ +package com.lishid.openinv.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.errorprone.annotations.Keep; +import com.lishid.openinv.OpenInv; +import com.lishid.openinv.util.config.Config; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.profile.PlayerProfile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Iterator; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A utility for looking up and loading players. + */ +public class PlayerLoader implements Listener { + + private final @NotNull OpenInv plugin; + private final @NotNull Config config; + private final @NotNull InventoryManager inventoryManager; + private final @NotNull InternalAccessor internalAccessor; + private final @NotNull Logger logger; + private final @NotNull Cache lookupCache; + + public PlayerLoader( + @NotNull OpenInv plugin, + @NotNull Config config, + @NotNull InventoryManager inventoryManager, + @NotNull InternalAccessor internalAccessor, + @NotNull Logger logger + ) { + this.plugin = plugin; + this.config = config; + this.inventoryManager = inventoryManager; + this.internalAccessor = internalAccessor; + this.logger = logger; + this.lookupCache = CacheBuilder.newBuilder().maximumSize(20).build(); + } + + /** + * Load a {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for + * the server is not loaded, this will return {@code null}. + * + * @param offline the {@code OfflinePlayer} to load a {@code Player} for + * @return the loaded {@code Player} + * @throws IllegalStateException if the server version is unsupported + */ + public @Nullable Player load(@NotNull OfflinePlayer offline) { + UUID key = offline.getUniqueId(); + + Player player = offline.getPlayer(); + if (player != null) { + return player; + } + + player = inventoryManager.getLoadedPlayer(key); + if (player != null) { + return player; + } + + if (config.isOfflineDisabled() || !internalAccessor.isSupported()) { + return null; + } + + if (Bukkit.isPrimaryThread()) { + return internalAccessor.getPlayerDataManager().loadPlayer(offline); + } + + CompletableFuture future = new CompletableFuture<>(); + plugin.getScheduler().runTask(() -> future.complete(internalAccessor.getPlayerDataManager().loadPlayer(offline))); + + try { + player = future.get(); + } catch (InterruptedException | ExecutionException e) { + logger.log(Level.WARNING, e.getMessage(), e); + return null; + } + + return player; + } + + public @Nullable OfflinePlayer matchExact(@NotNull String name) { + // Warn if called on the main thread - if we resort to searching offline players, this may take several seconds. + if (Bukkit.getServer().isPrimaryThread()) { + logger.warning("Call to PlayerSearchCache#matchPlayer made on the main thread!"); + logger.warning("This can cause the server to hang, potentially severely."); + logger.log(Level.WARNING, "Current stack trace", new Throwable("Current stack trace")); + } + + OfflinePlayer player; + + try { + UUID uuid = UUID.fromString(name); + player = Bukkit.getOfflinePlayer(uuid); + // Ensure player is an existing player. + if (player.hasPlayedBefore() || player.isOnline()) { + return player; + } + // Return null otherwise. + return null; + } catch (IllegalArgumentException ignored) { + // Not a UUID + } + + // Exact online match first. + player = Bukkit.getServer().getPlayerExact(name); + + if (player != null) { + return player; + } + + // Cached offline match. + PlayerProfile cachedResult = lookupCache.getIfPresent(name); + if (cachedResult != null && cachedResult.getUniqueId() != null) { + player = Bukkit.getOfflinePlayer(cachedResult.getUniqueId()); + // Ensure player is an existing player. + if (player.hasPlayedBefore() || player.isOnline()) { + return player; + } + // Return null otherwise. + return null; + } + + // Exact offline match second - ensure offline access works when matchable users are online. + player = Bukkit.getServer().getOfflinePlayer(name); + + if (player.hasPlayedBefore()) { + lookupCache.put(name, player.getPlayerProfile()); + return player; + } + + return null; + } + + public @Nullable OfflinePlayer match(@NotNull String name) { + OfflinePlayer player = this.matchExact(name); + + if (player != null) { + return player; + } + + // Inexact online match. + player = Bukkit.getServer().getPlayer(name); + + if (player != null) { + return player; + } + + // Finally, inexact offline match. + float bestMatch = 0; + for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) { + if (offline.getName() == null) { + // Loaded by UUID only, name has never been looked up. + continue; + } + + float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName()); + + if (currentMatch == 1.0F) { + return offline; + } + + if (currentMatch > bestMatch) { + bestMatch = currentMatch; + player = offline; + } + } + + if (player != null) { + // If a match was found, store it. + lookupCache.put(name, player.getPlayerProfile()); + return player; + } + + // No players have ever joined the server. + return null; + } + + @Keep + @EventHandler + private void updateMatches(@NotNull PlayerJoinEvent event) { + // If player is not new, any cached values are valid. + if (event.getPlayer().hasPlayedBefore()) { + return; + } + + // New player may have a name that already points to someone else in lookup cache. + String name = event.getPlayer().getName(); + lookupCache.invalidate(name); + + // If the cache is empty, nothing to do. Don't hit scheduler. + if (lookupCache.size() == 0) { + return; + } + + plugin.getScheduler().runTaskLaterAsynchronously( + () -> { + Iterator> iterator = lookupCache.asMap().entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + String oldMatch = entry.getValue().getName(); + + // Shouldn't be possible - all profiles should be complete. + if (oldMatch == null) { + iterator.remove(); + continue; + } + + String lookup = entry.getKey(); + float oldMatchScore = StringMetric.compareJaroWinkler(lookup, oldMatch); + float newMatchScore = StringMetric.compareJaroWinkler(lookup, name); + + // If new match exceeds old match, delete old match. + if (newMatchScore > oldMatchScore) { + iterator.remove(); + } + } + }, + 7L + ); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java b/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java new file mode 100644 index 00000000..e62ad300 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/SearchHelper.java @@ -0,0 +1,66 @@ +package com.lishid.openinv.util; + +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockStateMeta; +import org.bukkit.inventory.meta.BundleMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; + +public final class SearchHelper { + + public static boolean findMatch(@NotNull Inventory inventory, @NotNull Predicate<@NotNull ItemStack> predicate) { + for (ItemStack content : inventory.getContents()) { + if (findMatch(content, predicate)) { + return true; + } + } + return false; + } + + private static boolean findMatch(@Nullable ItemStack itemStack, @NotNull Predicate<@NotNull ItemStack> predicate) { + if (itemStack == null || itemStack.getType().isAir()) { + return false; + } + + // If the item is the search target, done. + if (predicate.test(itemStack)) { + return true; + } + + // If the item doesn't have meta, it cannot contain items. + if (!itemStack.hasItemMeta()) { + return false; + } + + ItemMeta meta = itemStack.getItemMeta(); + + // Container meta with items (primarily shulkers). + if (meta instanceof BlockStateMeta stateMeta) { + if (!stateMeta.hasBlockState() || !(stateMeta.getBlockState() instanceof InventoryHolder holder)) { + return false; + } + Inventory inventory = holder.getInventory(); + return findMatch(inventory, predicate); + } + + // Bundle meta. + if (meta instanceof BundleMeta bundleMeta) { + for (ItemStack subStack : bundleMeta.getItems()) { + if (findMatch(subStack, predicate)) { + return true; + } + } + } + + return false; + } + + private SearchHelper() { + throw new IllegalStateException("Cannot create instance of utility class."); + } +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java b/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java new file mode 100644 index 00000000..8feaca81 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/StringMetric.java @@ -0,0 +1,164 @@ +/* + * This file is an amalgamation of code from the Simmetrics authors. + * The originals may be found here: + * https://github.com/Simmetrics/simmetrics/blob/master/simmetrics-core/src/main/java/org/simmetrics/metrics/JaroWinkler.java + * https://github.com/Simmetrics/simmetrics/blob/master/simmetrics-core/src/main/java/org/simmetrics/metrics/Jaro.java + * + * Copyright (C) 2014 - 2016 Simmetrics Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.lishid.openinv.util; + +public class StringMetric { + + public static float compareJaroWinkler(String a, String b) { + final float jaroScore = compareJaro(a, b); + + if (jaroScore < (float) 0.7) { + return jaroScore; + } + + String prefix = commonPrefix(a, b); + int prefixLength = Math.min(prefix.codePointCount(0, prefix.length()), 4); + + return jaroScore + (prefixLength * (float) 0.1 * (1.0f - jaroScore)); + + } + + private static float compareJaro(String a, String b) { + if (a.isEmpty() && b.isEmpty()) { + return 1.0f; + } + + if (a.isEmpty() || b.isEmpty()) { + return 0.0f; + } + + final int[] charsA = a.codePoints().toArray(); + final int[] charsB = b.codePoints().toArray(); + + // Intentional integer division to round down. + final int halfLength = Math.max(0, Math.max(charsA.length, charsB.length) / 2 - 1); + + final int[] commonA = getCommonCodePoints(charsA, charsB, halfLength); + final int[] commonB = getCommonCodePoints(charsB, charsA, halfLength); + + // commonA and commonB will always contain the same multi-set of + // characters. Because getCommonCharacters has been optimized, commonA + // and commonB are -1-padded. So in this loop we count transposition + // and use commonCharacters to determine the length of the multi-set. + float transpositions = 0; + int commonCharacters = 0; + for ( + int length = commonA.length; + commonCharacters < length && commonA[commonCharacters] > -1; + commonCharacters++ + ) { + if (commonA[commonCharacters] != commonB[commonCharacters]) { + transpositions++; + } + } + + if (commonCharacters == 0) { + return 0.0f; + } + + float aCommonRatio = commonCharacters / (float) charsA.length; + float bCommonRatio = commonCharacters / (float) charsB.length; + float transpositionRatio = (commonCharacters - transpositions / 2.0f) / commonCharacters; + + return (aCommonRatio + bCommonRatio + transpositionRatio) / 3.0f; + } + + /* + * Returns an array of code points from a within b. A character in b is + * counted as common when it is within separation distance from the position + * in a. + */ + private static int[] getCommonCodePoints(final int[] charsA, final int[] charsB, final int separation) { + final int[] common = new int[Math.min(charsA.length, charsB.length)]; + final boolean[] matched = new boolean[charsB.length]; + + // Iterate of string a and find all characters that occur in b within + // the separation distance. Mark any matches found to avoid + // duplicate matchings. + int commonIndex = 0; + for (int i = 0, length = charsA.length; i < length; i++) { + final int character = charsA[i]; + final int index = indexOf( + character, + charsB, + i - separation, + i + separation + 1, + matched + ); + if (index > -1) { + common[commonIndex++] = character; + matched[index] = true; + } + } + + if (commonIndex < common.length) { + common[commonIndex] = -1; + } + + // Both invocations will yield the same multi-set terminated by -1, so + // they can be compared for transposition without making a copy. + return common; + } + + /* + * Search for code point in buffer starting at fromIndex to toIndex - 1. + * + * Returns -1 when not found. + */ + private static int indexOf(int character, int[] buffer, int fromIndex, int toIndex, boolean[] matched) { + + // compare char with range of characters to either side + for (int j = Math.max(0, fromIndex), length = Math.min(toIndex, buffer.length); j < length; j++) { + // check if found + if (buffer[j] == character && !matched[j]) { + return j; + } + } + + return -1; + } + + private static String commonPrefix(CharSequence a, CharSequence b) { + int maxPrefixLength = Math.min(a.length(), b.length()); + + int p; + + p = 0; + while (p < maxPrefixLength && a.charAt(p) == b.charAt(p)) { + ++p; + } + + if (validSurrogatePairAt(a, p - 1) || validSurrogatePairAt(b, p - 1)) { + --p; + } + + return a.subSequence(0, p).toString(); + } + + private static boolean validSurrogatePairAt(CharSequence string, int index) { + return index >= 0 && index <= string.length() - 2 && Character.isHighSurrogate(string.charAt(index)) && Character.isLowSurrogate(string.charAt(index + 1)); + } + + private StringMetric() { + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java b/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java index 77538f95..f55ae192 100644 --- a/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java +++ b/plugin/src/main/java/com/lishid/openinv/util/TabCompleter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2011-2020 lishid. All rights reserved. + * Copyright (C) 2011-2021 lishid. All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,132 +16,151 @@ package com.lishid.openinv.util; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.util.StringUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.function.Function; -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.util.StringUtil; /** * Utility class for common tab completions. */ -public class TabCompleter { - - /** - * Offer tab completions for whole numbers. - * - * @param argument the argument to complete - * @return integer options - */ - public static List completeInteger(String argument) { - // Ensure existing argument is actually a number - if (!argument.isEmpty()) { - try { - Integer.parseInt(argument); - } catch (NumberFormatException e) { - return Collections.emptyList(); - } - } - - List completions = new ArrayList<>(10); - for (int i = 0; i < 10; ++i) { - completions.add(argument + i); - } - - return completions; +public final class TabCompleter { + + /** + * Offer tab completions for whole numbers. + * + * @param argument the argument to complete + * @return integer options + */ + public static @NotNull @Unmodifiable List completeInteger(@NotNull String argument) { + // Ensure existing argument is actually a number + if (!argument.isEmpty()) { + try { + Integer.parseInt(argument); + } catch (NumberFormatException e) { + return List.of(); + } } - /** - * Offer tab completions for a given Enum. - * - * @param argument the argument to complete - * @param enumClazz the Enum to complete for - * @return the matching Enum values - */ - public static List completeEnum(String argument, Class> enumClazz) { - argument = argument.toLowerCase(Locale.ENGLISH); - List completions = new ArrayList<>(); - - for (Enum enumConstant : enumClazz.getEnumConstants()) { - String name = enumConstant.name().toLowerCase(); - if (name.startsWith(argument)) { - completions.add(name); - } - } - - return completions; + List completions = new ArrayList<>(10); + for (int i = 0; i < 10; ++i) { + completions.add(argument + i); } - /** - * Offer tab completions for a given array of Strings. - * - * @param argument the argument to complete - * @param options the Strings which may be completed - * @return the matching Strings - */ - public static List completeString(String argument, String[] options) { - argument = argument.toLowerCase(Locale.ENGLISH); - List completions = new ArrayList<>(); - - for (String option : options) { - if (option.startsWith(argument)) { - completions.add(option); - } - } - - return completions; + return Collections.unmodifiableList(completions); + } + + /** + * Offer tab completions for a given Enum. + * + * @param argument the argument to complete + * @param enumClazz the Enum to complete for + * @return the matching Enum values + */ + public static @NotNull List completeEnum( + @NotNull String argument, + @NotNull Class> enumClazz + ) { + argument = argument.toLowerCase(Locale.ENGLISH); + List completions = new ArrayList<>(); + + for (Enum enumConstant : enumClazz.getEnumConstants()) { + String name = enumConstant.name().toLowerCase(Locale.ENGLISH); + if (name.startsWith(argument)) { + completions.add(name); + } } - /** - * Offer tab completions for visible online Players' names. - * - * @param sender the command's sender - * @param argument the argument to complete - * @return the matching Players' names - */ - public static List completeOnlinePlayer(CommandSender sender, String argument) { - List completions = new ArrayList<>(); - Player senderPlayer = sender instanceof Player ? (Player) sender : null; - - for (Player player : Bukkit.getOnlinePlayers()) { - if (senderPlayer != null && !senderPlayer.canSee(player)) { - continue; - } - - if (StringUtil.startsWithIgnoreCase(player.getName(), argument)) { - completions.add(player.getName()); - } - } - - return completions; + return completions; + } + + /** + * Offer tab completions for a given array of Strings. + * + * @param argument the argument to complete + * @param options the Strings which may be completed + * @return the matching Strings + */ + public static @NotNull List completeString( + @NotNull String argument, + @NotNull String @NotNull [] options + ) { + argument = argument.toLowerCase(Locale.ENGLISH); + List completions = new ArrayList<>(); + + for (String option : options) { + if (option.startsWith(argument)) { + completions.add(option); + } } - /** - * Offer tab completions for a given array of Objects. - * - * @param argument the argument to complete - * @param converter the Function for converting the Object into a comparable String - * @param options the Objects which may be completed - * @return the matching Strings - */ - public static List completeObject(String argument, Function converter, T[] options) { - argument = argument.toLowerCase(Locale.ENGLISH); - List completions = new ArrayList<>(); - - for (T option : options) { - String optionString = converter.apply(option).toLowerCase(); - if (optionString.startsWith(argument)) { - completions.add(optionString); - } - } - - return completions; + return completions; + } + + /** + * Offer tab completions for visible online Players' names. + * + * @param sender the command's sender + * @param argument the argument to complete + * @return the matching Players' names + */ + public static List completeOnlinePlayer( + @Nullable CommandSender sender, + @NotNull String argument + ) { + List completions = new ArrayList<>(); + Player senderPlayer = sender instanceof Player player ? player : null; + + for (Player player : Bukkit.getOnlinePlayers()) { + if (senderPlayer != null && !senderPlayer.canSee(player)) { + continue; + } + + if (StringUtil.startsWithIgnoreCase(player.getName(), argument)) { + completions.add(player.getName()); + } } - private TabCompleter() {} + return completions; + } + + /** + * Offer tab completions for a given array of Objects. + * + * @param argument the argument to complete + * @param converter the Function for converting the Object into a comparable String + * @param options the Objects which may be completed + * @return the matching Strings + */ + public static List completeObject( + @NotNull String argument, + @NotNull Function<@NotNull T, @NotNull String> converter, + @NotNull T @NotNull[] options + ) { + argument = argument.toLowerCase(Locale.ENGLISH); + List completions = new ArrayList<>(); + + for (T option : options) { + String optionString = converter.apply(option).toLowerCase(Locale.ENGLISH); + if (optionString.startsWith(argument)) { + completions.add(optionString); + } + } + + return completions; + } + + private TabCompleter() { + throw new IllegalStateException("Cannot create instance of utility class."); + } } diff --git a/plugin/src/main/java/com/lishid/openinv/util/config/Config.java b/plugin/src/main/java/com/lishid/openinv/util/config/Config.java new file mode 100644 index 00000000..ad5f8f92 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/config/Config.java @@ -0,0 +1,43 @@ +package com.lishid.openinv.util.config; + +import com.lishid.openinv.util.AccessEqualMode; +import org.bukkit.configuration.Configuration; +import org.bukkit.configuration.MemoryConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class Config { + + private @NotNull Configuration root; + private @Nullable AccessEqualMode accessEqualMode; + + public Config() { + root = new MemoryConfiguration(); + } + + public void reload(@NotNull Configuration configuration) { + root = configuration; + accessEqualMode = null; + } + + public boolean isSaveDisabled() { + return root.getBoolean("settings.disable-saving", false); + } + + public boolean isOfflineDisabled() { + return root.getBoolean("settings.disable-offline-access", false); + } + + public boolean doesNoArgsOpenSelf() { + return root.getBoolean("settings.command.open.no-args-opens-self", false); + } + + public @NotNull AccessEqualMode getAccessEqualMode() { + if (accessEqualMode == null) { + accessEqualMode = AccessEqualMode.of(root.getString("settings.equal-access")); + } + + return accessEqualMode; + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java b/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java new file mode 100644 index 00000000..228cfd07 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/config/ConfigUpdater.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2011-2022 lishid. All rights reserved. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lishid.openinv.util.config; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; + +public record ConfigUpdater(@NotNull Plugin plugin) { + + public void checkForUpdates() { + final int version = plugin.getConfig().getInt("config-version", 1); + ConfigurationSection defaults = plugin.getConfig().getDefaults(); + if (defaults == null || version >= defaults.getInt("config-version")) { + return; + } + + plugin.getLogger().info("Configuration update found! Performing update..."); + + // Backup the old config file + try { + plugin.getConfig().save(new File(plugin.getDataFolder(), "config_old.yml")); + plugin.getLogger().info("Backed up config.yml to config_old.yml before updating."); + } catch (IOException e) { + plugin.getLogger().warning("Could not back up config.yml before updating!"); + } + + if (version < 2) { + updateConfig1To2(); + } + if (version < 3) { + updateConfig2To3(); + } + if (version < 4) { + updateConfig3To4(); + } + if (version < 5) { + updateConfig4To5(); + } + if (version < 6) { + updateConfig5To6(); + } + if (version < 7) { + updateConfig6To7(); + } + if (version < 8) { + updateConfig7To8(); + } + + plugin.saveConfig(); + plugin.getLogger().info("Configuration update complete!"); + } + + private void updateConfig7To8() { + FileConfiguration config = plugin.getConfig(); + config.set("settings.equal-access", "view"); + config.set("config-version", 8); + } + + private void updateConfig6To7() { + FileConfiguration config = plugin.getConfig(); + config.set("toggles", null); + String consoleLocale = config.getString("settings.locale", "en"); + if (consoleLocale.isBlank() || consoleLocale.equalsIgnoreCase("en_us")) { + consoleLocale = "en"; + } + config.set("settings.console-locale", consoleLocale); + config.set("settings.locale", null); + config.set("config-version", 7); + } + + private void updateConfig5To6() { + FileConfiguration config = plugin.getConfig(); + config.set("settings.command.open.no-args-opens-self", false); + config.set("settings.command.searchcontainer.max-radius", 10); + config.set("config-version", 6); + } + + private void updateConfig4To5() { + FileConfiguration config = plugin.getConfig(); + config.set("settings.disable-offline-access", false); + config.set("config-version", 5); + } + + private void updateConfig3To4() { + FileConfiguration config = plugin.getConfig(); + config.set("notify", null); + config.set("config-version", 4); + } + + private void updateConfig2To3() { + FileConfiguration config = plugin.getConfig(); + config.set("items", null); + config.set("ItemOpenInv", null); + config.set("toggles", null); + config.set("settings.disable-saving", config.getBoolean("DisableSaving", false)); + config.set("DisableSaving", null); + config.set("config-version", 3); + } + + private void updateConfig1To2() { + FileConfiguration config = plugin.getConfig(); + config.set("ItemOpenInvItemID", null); + config.set("NotifySilentChest", null); + config.set("NotifyAnyChest", null); + config.set("AnyChest", null); + config.set("SilentChest", null); + config.set("config-version", 2); + } + +} diff --git a/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java b/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java new file mode 100644 index 00000000..2ae68159 --- /dev/null +++ b/plugin/src/main/java/com/lishid/openinv/util/lang/LangMigrator.java @@ -0,0 +1,87 @@ +package com.lishid.openinv.util.lang; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LangMigrator { + + private final @NotNull Path oldFolder; + private final @NotNull Path newFolder; + private final @NotNull Logger logger; + + public LangMigrator(@NotNull Path oldFolder, @NotNull Path newFolder, @NotNull Logger logger) { + this.oldFolder = oldFolder; + this.newFolder = newFolder; + this.logger = logger; + } + + public void migrate() { + if (!Files.exists(oldFolder.resolve("en_us.yml"))) { + // Probably already migrated. + return; + } + + logger.info(() -> String.format("[LanguageManager] Migrating language files to %s", newFolder)); + + if (!Files.exists(newFolder)) { + try { + Files.createDirectories(newFolder); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to create language subdirectory!", e); + } + } + + try (DirectoryStream files = Files.newDirectoryStream(oldFolder)) { + files.forEach(path -> { + if (path == null) { + return; + } + + String fileName = path.getFileName().toString(); + + if (fileName.startsWith("config") || !fileName.endsWith(".yml")) { + return; + } + + // Migrate certain files to be parent languages. + fileName = switch (fileName) { + case "en_us.yml" -> "en.yml"; + case "de_de.yml" -> "de.yml"; + case "es_es.yml" -> "es.yml"; + case "pt_br.yml" -> "pt.yml"; + default -> fileName; + }; + + try { + Files.copy(path, newFolder.resolve(fileName)); + Files.delete(path); + } catch (FileAlreadyExistsException e1) { + // File already migrated? + try { + Files.copy(path, newFolder.resolve("old_" + fileName)); + Files.delete(path); + } catch (IOException e2) { + // If it fails again, just re-throw. + throw new UncheckedIOException(e2); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } catch (UncheckedIOException e) { + logger.log(Level.WARNING, "Unable to migrate languages to subdirectory!", e.getCause()); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to migrate languages to subdirectory!", e); + } + + } + +} diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml index d8bc7bb0..a69d6d62 100644 --- a/plugin/src/main/resources/config.yml +++ b/plugin/src/main/resources/config.yml @@ -1,4 +1,11 @@ -config-version: 4 +config-version: 8 settings: + equal-access: allow + command: + open: + no-args-opens-self: false + searchcontainer: + max-radius: 10 + disable-offline-access: false disable-saving: false - locale: 'en_us' + console-locale: 'en' diff --git a/plugin/src/main/resources/de_de.yml b/plugin/src/main/resources/locale/de.yml similarity index 92% rename from plugin/src/main/resources/de_de.yml rename to plugin/src/main/resources/locale/de.yml index ea1d0dce..e866e3f9 100644 --- a/plugin/src/main/resources/de_de.yml +++ b/plugin/src/main/resources/locale/de.yml @@ -23,8 +23,5 @@ messages: container: noMatches: 'Keine Container mit %target% gefunden.' matches: 'Container hat %target%: %detail%' - on: 'an' - off: 'aus' -container: - player: '%player%''s Inventar' - enderchest: '%player%''s Endertruhe' + 'on': 'an' + 'off': 'aus' diff --git a/plugin/src/main/resources/en_us.yml b/plugin/src/main/resources/locale/en.yml similarity index 72% rename from plugin/src/main/resources/en_us.yml rename to plugin/src/main/resources/locale/en.yml index 58e66f2c..287c83da 100644 --- a/plugin/src/main/resources/en_us.yml +++ b/plugin/src/main/resources/locale/en.yml @@ -6,16 +6,19 @@ messages: invalidNumber: '&cInvalid number: "%target%"' invalidPlayer: '&cPlayer not found!' permissionOpenSelf: '&cYou''re not allowed to open your own inventory.' - permissionEnderAll: '&cYou''re not allowed to access other players'' ender chests.' + permissionOpenOther: '&cYou''re not allowed to access others'' inventories.' permissionExempt: '&c%target%''s inventory is protected.' permissionCrossWorld: '&c%target% is not in your world.' - permissionPlayerOnline: '&cYou''re not allowed to open the inventory of online players.' - permissionPlayerOffline: '&cYou''re not allowed to open the inventory of offline players.' + permissionPlayerOnline: '&cYou''re not allowed to open inventories of online players.' + permissionPlayerOffline: '&cYou''re not allowed to open inventories of offline players.' commandException: '&cAn error occurred. Please check console for details.' info: containerBlocked: 'You are opening a blocked container.' containerBlockedSilent: 'You are opening a blocked container silently.' containerSilent: 'You are opening a container silently.' + clear: + inventory: 'Cleared %target%''s inventory.' + enderchest: 'Cleared %target%''s ender chest.' settingState: '%setting%: %state%' player: noMatches: 'No players found with %target%.' @@ -23,8 +26,5 @@ messages: container: noMatches: 'No containers found with %target%.' matches: 'Containers holding %target%: %detail%' - on: 'on' - off: 'off' -container: - player: '%player%''s Inventory' - enderchest: '%player%''s Ender Chest' + 'on': 'on' + 'off': 'off' diff --git a/plugin/src/main/resources/es_es.yml b/plugin/src/main/resources/locale/es.yml similarity index 91% rename from plugin/src/main/resources/es_es.yml rename to plugin/src/main/resources/locale/es.yml index 41541fae..d4a2b69c 100644 --- a/plugin/src/main/resources/es_es.yml +++ b/plugin/src/main/resources/locale/es.yml @@ -23,9 +23,5 @@ messages: container: noMatches: 'No se encontraron contenedores con %target%.' matches: 'Contenedores con %target%: %detail%' - on: 'activado' - off: 'desactivado' -container: - player: 'Inventario de %player%' - enderchest: 'Cofre de Ender de %player%' - \ No newline at end of file + 'on': 'activado' + 'off': 'desactivado' diff --git a/plugin/src/main/resources/pt_br.yml b/plugin/src/main/resources/locale/pt.yml similarity index 92% rename from plugin/src/main/resources/pt_br.yml rename to plugin/src/main/resources/locale/pt.yml index cd26a2d7..d0cd5d89 100644 --- a/plugin/src/main/resources/pt_br.yml +++ b/plugin/src/main/resources/locale/pt.yml @@ -23,8 +23,5 @@ messages: container: noMatches: 'Nenhum recipiente encontrado com %target%.' matches: 'Recipientes contendo %target%: %detail%' - on: 'ligado' - off: 'desligado' -container: - player: 'Inventario de %player%' - enderchest: 'Bau de Ender de %player%' + 'on': 'ligado' + 'off': 'desligado' diff --git a/plugin/src/main/resources/locale/zh_cn.yml b/plugin/src/main/resources/locale/zh_cn.yml new file mode 100644 index 00000000..ab0d34cb --- /dev/null +++ b/plugin/src/main/resources/locale/zh_cn.yml @@ -0,0 +1,28 @@ +# Translated into Chinese Simplified by Flandre_tw +messages: + error: + consoleUnsupported: 该命令无法在后台执行。 + lootNotGenerated: '&c奖励箱尚未生成 ! 请关闭 &b/silentcontainer&c。' + invalidMaterial: '&c无效的物品 : "%target%"' + invalidNumber: '&c无效的数字 : "%target%"' + invalidPlayer: '&c玩家不存在 !' + permissionOpenSelf: '&c你无法开启自己的物品栏。' + permissionEnderAll: '&c你无法开启其他玩家的末影箱。' + permissionExempt: '&c%target% 的物品栏受到保护。' + permissionCrossWorld: '&c%target% 不在你所在的世界。' + permissionPlayerOnline: '&c你无法开启线上玩家的物品栏。' + permissionPlayerOffline: '&c你无法开启离线玩家的物品栏。' + commandException: '&c发生错误,请查看后台。' + info: + containerBlocked: 你正在开启受阻挡的储物箱。 + containerBlockedSilent: 你正在悄悄开启受阻挡的储物箱。 + containerSilent: 你正在悄悄开启储物箱。 + settingState: '%setting% : %state%' + player: + noMatches: 找不到持有 %target% 的玩家。 + matches: '找到持有 %target% 的玩家 : %detail%' + container: + noMatches: 找不到放有 %target% 的储物箱。 + matches: '找到放有 %target% 的储物箱 : %detail%' + 'on': '开启' + 'off': '关闭' diff --git a/plugin/src/main/resources/locale/zh_tw.yml b/plugin/src/main/resources/locale/zh_tw.yml new file mode 100644 index 00000000..b6a16d26 --- /dev/null +++ b/plugin/src/main/resources/locale/zh_tw.yml @@ -0,0 +1,28 @@ +# Translated into Chinese Traditional by Flandre_tw +messages: + error: + consoleUnsupported: 該指令無法在控制台執行。 + lootNotGenerated: '&c獎勵箱尚未生成 ! 請關閉 &b/silentcontainer&c。' + invalidMaterial: '&c無效的物品 : "%target%"' + invalidNumber: '&c無效的數字 : "%target%"' + invalidPlayer: '&c玩家不存在 !' + permissionOpenSelf: '&c你無法開啟自己的物品欄。' + permissionEnderAll: '&c你無法開啟其他玩家的終界箱。' + permissionExempt: '&c%target% 的物品欄受到保護。' + permissionCrossWorld: '&c%target% 不在你所在的世界。' + permissionPlayerOnline: '&c你無法開啟線上玩家的物品欄。' + permissionPlayerOffline: '&c你無法開啟離線玩家的物品欄。' + commandException: '&c發生錯誤,請查看控制台。' + info: + containerBlocked: 你正在開啟受阻擋的儲物箱。 + containerBlockedSilent: 你正在悄悄開啟受阻擋的儲物箱。 + containerSilent: 你正在悄悄開啟儲物箱。 + settingState: '%setting% : %state%' + player: + noMatches: 找不到持有 %target% 的玩家。 + matches: '找到持有 %target% 的玩家 : %detail%' + container: + noMatches: 找不到放有 %target% 的儲物箱。 + matches: '找到放有 %target% 的儲物箱 : %detail%' + 'on': '開啟' + 'off': '關閉' diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml index 767b886d..e3da2390 100644 --- a/plugin/src/main/resources/plugin.yml +++ b/plugin/src/main/resources/plugin.yml @@ -1,89 +1,144 @@ name: OpenInv main: com.lishid.openinv.OpenInv -version: ${project.version} +version: ${version} author: lishid -authors: [Jikoo, ShadowRanger] -description: > - This plugin allows you to open a player's inventory as a chest and interact with it in real time. -api-version: "1.16" +authors: [ Jikoo, ShadowRanger ] +description: Open a player's inventory as a chest and interact with it in real time. +api-version: "1.13" +folia-supported: true permissions: - OpenInv.any.default: - description: Permission for AnyContainer to default on prior to toggling. - default: false - OpenInv.silent.default: - description: Permission for SilentContainer to default on prior to toggling. - default: false - OpenInv.*: - description: Permission for all OpenInv features. - default: op - children: - OpenInv.openinv: true - OpenInv.openender: true - OpenInv.search: true - OpenInv.silent: true - OpenInv.anychest: true - OpenInv.searchenchant: true - OpenInv.searchcontainer: true - OpenInv.openonline: true - OpenInv.openoffline: true - OpenInv.spectate: true - OpenInv.openinv: - default: op - children: - OpenInv.openonline: true - OpenInv.openoffline: true - OpenInv.openender: - default: op + + openinv: children: - OpenInv.openonline: true - OpenInv.openoffline: true + # Inventory nodes (/openinv) + openinv.inventory: + children: + openinv.inventory.open: + children: + openinv.inventory.open.self: true + openinv.inventory.open.other: true + openinv.inventory.edit: + children: + openinv.inventory.open: true + openinv.inventory.edit.self: + children: + openinv.inventory.open.self: true + openinv.inventory.edit.other: + children: + openinv.inventory.open.other: true + # Specific slot behaviors inside opened player inventories + openinv.inventory.slot: + default: true + children: + openinv.inventory.slot.head.any: true + openinv.inventory.slot.chest.any: true + openinv.inventory.slot.legs.any: true + openinv.inventory.slot.feet.any: true + openinv.inventory.slot.drop: true + # Ender chest nodes (/openender) + openinv.enderchest: + children: + openinv.enderchest.open: + children: + openinv.enderchest.open.self: true + openinv.enderchest.open.other: true + openinv.enderchest.edit: + children: + openinv.enderchest.edit.self: + children: + openinv.enderchest.open.self: true + openinv.enderchest.edit.other: + children: + openinv.enderchest.open.other: true + # Clear nodes (/clearinv and /clearender) + openinv.clear: + children: + openinv.clear.self: true + openinv.clear.other: true + # Player access + openinv.access: + children: + openinv.access.offline: true + openinv.access.online: true + openinv.access.crossworld: true + openinv.access.level.1: true + openinv.access.level.2: false + openinv.access.level.3: false + openinv.access.level.4: false + openinv.access.equal.edit: false + openinv.access.equal.view: false + openinv.access.equal.deny: false + # Spectate features + openinv.spectate: + children: + openinv.spectate.click: true + # Container features + openinv.container: + children: + openinv.container.any: true + openinv.container.silent: true + # Search functionality + openinv.search: + children: + openinv.search.inventory: true + openinv.search.container: true commands: openinv: - aliases: [oi, inv, open] + aliases: [ oi, inv, open ] description: Open a player's inventory - permission: OpenInv.openinv + permission: openinv.inventory.open.self;openinv.inventory.open.other + usage: |- + / [Player] - Open a player's inventory + clearinv: + description: Clear a player's inventory + permission: openinv.clear.self;openinv.clear.other usage: |- - / [Player] - Open a player's inventory + / [Player] - Clear a player's inventory openender: - aliases: [oe] - description: Opens the enderchest of a player - permission: OpenInv.openender + aliases: [ oe ] + description: Open a player's ender chest + permission: openinv.enderchest.open.self;openinv.enderchest.open.other + usage: |- + / [Player] - Open a player's ender chest + clearender: + description: Clear a player's ender chest + permission: openinv.clear.self;openinv.clear.other usage: |- - / [Player] - Open a player's enderchest + / [Player] - Clear a player's ender chest searchinv: - aliases: [si] + aliases: [ si ] description: Search and list players having a specific item - permission: OpenInv.search + permission: openinv.search.inventory usage: |- - / [MinAmount] - MinAmount is optional, the minimum amount required + / [MinAmount] - MinAmount is optional, the minimum amount required searchender: - aliases: [se] - permission: OpenInv.search - description: Searches and lists players having a specific item in their ender chest + aliases: [ se ] + permission: openinv.search.inventory + description: Search and list players having a specific item in their ender chest usage: |- - / [MinAmount] - MinAmount is optional, the minimum amount required + / [MinAmount] - MinAmount is optional, the minimum amount required silentcontainer: - aliases: [sc, silent, silentchest] + aliases: [ sc, silent, silentchest ] description: SilentContainer stops sounds and animations when using containers. - permission: OpenInv.silent + permission: openinv.container.silent usage: |- - / [check|on|off] - Check, toggle, or set SilentContainer + / [check|on|off] - Check, toggle, or set SilentContainer anycontainer: - aliases: [ac, anychest] + aliases: [ ac, anychest ] description: AnyContainer allows using blocked containers. - permission: OpenInv.anychest + permission: openinv.container.any usage: |- - / [check|on|off] - Check, toggle, or set AnyContainer + / [check|on|off] - Check, toggle, or set AnyContainer searchenchant: - aliases: [searchenchants] + aliases: [ searchenchants ] description: Search and list players with a specific enchantment. - permission: OpenInv.searchenchant + permission: openinv.search.inventory usage: |- - / <[Enchantment] [MinLevel]> - Enchantment is the enchantment type, MinLevel is the minimum level. One is optional + / <[Enchantment] [MinLevel]> - Enchantment is the enchantment type, MinLevel is the minimum level. One is optional searchcontainer: - aliases: [searchchest] + aliases: [ searchchest ] description: Search and list containers with a specific material. - permission: OpenInv.searchcontainer + permission: openinv.search.container usage: / [ChunkRadius] - ChunkRadius is optional, the length that will be searched for matching items. Default 5 diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 6882a320..00000000 --- a/pom.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - 4.0.0 - - com.lishid - openinvparent - OpenInvParent - http://dev.bukkit.org/bukkit-plugins/openinv/ - 4.1.6-SNAPSHOT - - pom - - - UTF-8 - - - - api - plugin - internal - assembly - - - - - - - - all - - - all - true - - - - - - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/groups/public/ - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.2 - - - - *:* - - - META-INF/maven/** - - - - - - - package - - shade - - - - - - - maven-compiler-plugin - 3.8.1 - - 1.8 - 1.8 - - - - - - diff --git a/resource-pack/build.gradle.kts b/resource-pack/build.gradle.kts new file mode 100644 index 00000000..f94f30ea --- /dev/null +++ b/resource-pack/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `base` +} + +tasks.register("buildResourcePack") { + archiveFileName = "openinv-legibility-pack.zip" + destinationDirectory = rootProject.layout.projectDirectory.dir("dist") + + from("openinv-legibility-pack") + with(copySpec { + include("**/*.json", "**/*.png", "pack.mcmeta") + }) +} + +tasks.assemble { + dependsOn(tasks.named("buildResourcePack")) +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json new file mode 100644 index 00000000..482b3af9 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/crafting_output.json @@ -0,0 +1,25 @@ +{ + "texture_size": [ 16, 32 ], + "textures": { + "layer0": "openinv:item/crafting_output", + "particle": "minecraft:block/crafting_table_front" + }, + "elements": [ + { + "from": [ 0, -16, -16 ], + "to": [ 16, 16, -16 ], + "faces": { + "south": { + "uv": [ 0, 0, 16, 16 ], + "texture": "#layer0" + } + } + } + ], + "gui_light": "front", + "display": { + "gui": { + "scale": [ 1.125, 1.125, 1 ] + } + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json new file mode 100644 index 00000000..6c033d83 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/cursor.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/cursor" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json new file mode 100644 index 00000000..b4782491 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/drop.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/drop" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json new file mode 100644 index 00000000..bdb545f8 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/empty_boots" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json new file mode 100644 index 00000000..b407d98c --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/empty_chestplate" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json new file mode 100644 index 00000000..f7cc30f6 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/empty_helmet" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json new file mode 100644 index 00000000..0467df35 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/empty_leggings" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json new file mode 100644 index 00000000..0cf9047a --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/empty_shield.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/empty_shield" + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json new file mode 100644 index 00000000..a4955524 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/assets/openinv/models/item/not_a_slot.json @@ -0,0 +1,11 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "openinv:item/not_a_slot" + }, + "display": { + "gui": { + "scale": [ 1.125, -1.125, -1.125 ] + } + } +} diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png new file mode 100644 index 00000000..a53a4142 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/crafting_output.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png new file mode 100644 index 00000000..af54bc59 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/cursor.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png new file mode 100644 index 00000000..8caf814e Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/drop.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png new file mode 100644 index 00000000..356e615b Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_boots.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png new file mode 100644 index 00000000..be0e904a Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_chestplate.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png new file mode 100644 index 00000000..59c44691 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_helmet.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png new file mode 100644 index 00000000..bea579e0 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_leggings.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png new file mode 100644 index 00000000..35e73102 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/empty_shield.png differ diff --git a/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png new file mode 100644 index 00000000..1d78e569 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/assets/openinv/textures/item/not_a_slot.png differ diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json new file mode 100644 index 00000000..230469c5 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/crafting_table.json @@ -0,0 +1,11 @@ +{ + "parent": "minecraft:block/crafting_table", + "overrides": [ + { + "model": "openinv:item/crafting_output", + "predicate": { + "custom_model_data": 9999 + } + } + ] +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json new file mode 100644 index 00000000..0c8bb744 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/dropper.json @@ -0,0 +1,11 @@ +{ + "parent": "minecraft:block/dropper", + "overrides": [ + { + "model": "openinv:item/drop", + "predicate": { + "custom_model_data": 9999 + } + } + ] +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json new file mode 100644 index 00000000..f9cd4073 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_boots.json @@ -0,0 +1,75 @@ +{ + "parent": "minecraft:item/generated", + "overrides": [ + { + "model": "openinv:item/empty_boots", + "predicate": { + "custom_model_data": 9999 + } + }, + { + "model": "minecraft:item/leather_boots_quartz_trim", + "predicate": { + "trim_type": 0.1 + } + }, + { + "model": "minecraft:item/leather_boots_iron_trim", + "predicate": { + "trim_type": 0.2 + } + }, + { + "model": "minecraft:item/leather_boots_netherite_trim", + "predicate": { + "trim_type": 0.3 + } + }, + { + "model": "minecraft:item/leather_boots_redstone_trim", + "predicate": { + "trim_type": 0.4 + } + }, + { + "model": "minecraft:item/leather_boots_copper_trim", + "predicate": { + "trim_type": 0.5 + } + }, + { + "model": "minecraft:item/leather_boots_gold_trim", + "predicate": { + "trim_type": 0.6 + } + }, + { + "model": "minecraft:item/leather_boots_emerald_trim", + "predicate": { + "trim_type": 0.7 + } + }, + { + "model": "minecraft:item/leather_boots_diamond_trim", + "predicate": { + "trim_type": 0.8 + } + }, + { + "model": "minecraft:item/leather_boots_lapis_trim", + "predicate": { + "trim_type": 0.9 + } + }, + { + "model": "minecraft:item/leather_boots_amethyst_trim", + "predicate": { + "trim_type": 1.0 + } + } + ], + "textures": { + "layer0": "minecraft:item/leather_boots", + "layer1": "minecraft:item/leather_boots_overlay" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json new file mode 100644 index 00000000..d6dc8c5f --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_chestplate.json @@ -0,0 +1,75 @@ +{ + "parent": "minecraft:item/generated", + "overrides": [ + { + "model": "openinv:item/empty_chestplate", + "predicate": { + "custom_model_data": 9999 + } + }, + { + "model": "minecraft:item/leather_chestplate_quartz_trim", + "predicate": { + "trim_type": 0.1 + } + }, + { + "model": "minecraft:item/leather_chestplate_iron_trim", + "predicate": { + "trim_type": 0.2 + } + }, + { + "model": "minecraft:item/leather_chestplate_netherite_trim", + "predicate": { + "trim_type": 0.3 + } + }, + { + "model": "minecraft:item/leather_chestplate_redstone_trim", + "predicate": { + "trim_type": 0.4 + } + }, + { + "model": "minecraft:item/leather_chestplate_copper_trim", + "predicate": { + "trim_type": 0.5 + } + }, + { + "model": "minecraft:item/leather_chestplate_gold_trim", + "predicate": { + "trim_type": 0.6 + } + }, + { + "model": "minecraft:item/leather_chestplate_emerald_trim", + "predicate": { + "trim_type": 0.7 + } + }, + { + "model": "minecraft:item/leather_chestplate_diamond_trim", + "predicate": { + "trim_type": 0.8 + } + }, + { + "model": "minecraft:item/leather_chestplate_lapis_trim", + "predicate": { + "trim_type": 0.9 + } + }, + { + "model": "minecraft:item/leather_chestplate_amethyst_trim", + "predicate": { + "trim_type": 1.0 + } + } + ], + "textures": { + "layer0": "minecraft:item/leather_chestplate", + "layer1": "minecraft:item/leather_chestplate_overlay" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json new file mode 100644 index 00000000..236ae610 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_helmet.json @@ -0,0 +1,75 @@ +{ + "parent": "minecraft:item/generated", + "overrides": [ + { + "model": "openinv:item/empty_helmet", + "predicate": { + "custom_model_data": 9999 + } + }, + { + "model": "minecraft:item/leather_helmet_quartz_trim", + "predicate": { + "trim_type": 0.1 + } + }, + { + "model": "minecraft:item/leather_helmet_iron_trim", + "predicate": { + "trim_type": 0.2 + } + }, + { + "model": "minecraft:item/leather_helmet_netherite_trim", + "predicate": { + "trim_type": 0.3 + } + }, + { + "model": "minecraft:item/leather_helmet_redstone_trim", + "predicate": { + "trim_type": 0.4 + } + }, + { + "model": "minecraft:item/leather_helmet_copper_trim", + "predicate": { + "trim_type": 0.5 + } + }, + { + "model": "minecraft:item/leather_helmet_gold_trim", + "predicate": { + "trim_type": 0.6 + } + }, + { + "model": "minecraft:item/leather_helmet_emerald_trim", + "predicate": { + "trim_type": 0.7 + } + }, + { + "model": "minecraft:item/leather_helmet_diamond_trim", + "predicate": { + "trim_type": 0.8 + } + }, + { + "model": "minecraft:item/leather_helmet_lapis_trim", + "predicate": { + "trim_type": 0.9 + } + }, + { + "model": "minecraft:item/leather_helmet_amethyst_trim", + "predicate": { + "trim_type": 1.0 + } + } + ], + "textures": { + "layer0": "minecraft:item/leather_helmet", + "layer1": "minecraft:item/leather_helmet_overlay" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json new file mode 100644 index 00000000..eb9ddc89 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/leather_leggings.json @@ -0,0 +1,75 @@ +{ + "parent": "minecraft:item/generated", + "overrides": [ + { + "model": "openinv:item/empty_leggings", + "predicate": { + "custom_model_data": 9999 + } + }, + { + "model": "minecraft:item/leather_leggings_quartz_trim", + "predicate": { + "trim_type": 0.1 + } + }, + { + "model": "minecraft:item/leather_leggings_iron_trim", + "predicate": { + "trim_type": 0.2 + } + }, + { + "model": "minecraft:item/leather_leggings_netherite_trim", + "predicate": { + "trim_type": 0.3 + } + }, + { + "model": "minecraft:item/leather_leggings_redstone_trim", + "predicate": { + "trim_type": 0.4 + } + }, + { + "model": "minecraft:item/leather_leggings_copper_trim", + "predicate": { + "trim_type": 0.5 + } + }, + { + "model": "minecraft:item/leather_leggings_gold_trim", + "predicate": { + "trim_type": 0.6 + } + }, + { + "model": "minecraft:item/leather_leggings_emerald_trim", + "predicate": { + "trim_type": 0.7 + } + }, + { + "model": "minecraft:item/leather_leggings_diamond_trim", + "predicate": { + "trim_type": 0.8 + } + }, + { + "model": "minecraft:item/leather_leggings_lapis_trim", + "predicate": { + "trim_type": 0.9 + } + }, + { + "model": "minecraft:item/leather_leggings_amethyst_trim", + "predicate": { + "trim_type": 1.0 + } + } + ], + "textures": { + "layer0": "minecraft:item/leather_leggings", + "layer1": "minecraft:item/leather_leggings_overlay" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json new file mode 100644 index 00000000..5ea7eddd --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/shield.json @@ -0,0 +1,58 @@ +{ + "parent": "builtin/entity", + "gui_light": "front", + "textures": { + "particle": "block/dark_oak_planks" + }, + "display": { + "thirdperson_righthand": { + "rotation": [ 0, 90, 0 ], + "translation": [ 10, 6, -4 ], + "scale": [ 1, 1, 1 ] + }, + "thirdperson_lefthand": { + "rotation": [ 0, 90, 0 ], + "translation": [ 10, 6, 12 ], + "scale": [ 1, 1, 1 ] + }, + "firstperson_righthand": { + "rotation": [ 0, 180, 5 ], + "translation": [ -10, 2, -10 ], + "scale": [ 1.25, 1.25, 1.25 ] + }, + "firstperson_lefthand": { + "rotation": [ 0, 180, 5 ], + "translation": [ 10, 0, -10 ], + "scale": [ 1.25, 1.25, 1.25 ] + }, + "gui": { + "rotation": [ 15, -25, -5 ], + "translation": [ 2, 3, 0 ], + "scale": [ 0.65, 0.65, 0.65 ] + }, + "fixed": { + "rotation": [ 0, 180, 0 ], + "translation": [ -4.5, 4.5, -5], + "scale":[ 0.55, 0.55, 0.55] + }, + "ground": { + "rotation": [ 0, 0, 0 ], + "translation": [ 2, 4, 2], + "scale":[ 0.25, 0.25, 0.25] + } + }, + "overrides": [ + { + "model": "openinv:item/empty_shield", + "predicate": { + "custom_model_data": 9999 + } + }, + { + "predicate": { + "blocking": 1 + }, + "model": "item/shield_blocking" + } + ] +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json new file mode 100644 index 00000000..bc6fadb8 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_banner.json @@ -0,0 +1,11 @@ +{ + "parent": "minecraft:item/template_banner", + "overrides": [ + { + "model": "openinv:item/cursor", + "predicate": { + "custom_model_data": 9999 + } + } + ] +} diff --git a/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json new file mode 100644 index 00000000..e4edacdb --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_34/assets/minecraft/models/item/white_stained_glass_pane.json @@ -0,0 +1,14 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "minecraft:block/white_stained_glass" + }, + "overrides": [ + { + "model": "openinv:item/not_a_slot", + "predicate": { + "custom_model_data": 9999 + } + } + ] +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json new file mode 100644 index 00000000..1e24a414 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/crafting_table.json @@ -0,0 +1,20 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/crafting_output" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:block/crafting_table" + }, + "property": "minecraft:custom_model_data" + }, + "oversized_in_gui": true +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json new file mode 100644 index 00000000..8493cfa9 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/dropper.json @@ -0,0 +1,19 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/drop" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:block/dropper" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json new file mode 100644 index 00000000..c6859113 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_boots.json @@ -0,0 +1,174 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/empty_boots" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_quartz_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:quartz" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_iron_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:iron" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_netherite_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:netherite" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_redstone_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:redstone" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_copper_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:copper" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_gold_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:gold" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_emerald_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:emerald" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_diamond_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:diamond" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_lapis_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:lapis" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_amethyst_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:amethyst" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots_resin_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:resin" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/leather_boots", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "property": "minecraft:trim_material" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json new file mode 100644 index 00000000..10e51d04 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_chestplate.json @@ -0,0 +1,174 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/empty_chestplate" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_quartz_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:quartz" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_iron_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:iron" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_netherite_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:netherite" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_redstone_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:redstone" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_copper_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:copper" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_gold_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:gold" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_emerald_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:emerald" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_diamond_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:diamond" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_lapis_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:lapis" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_amethyst_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:amethyst" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate_resin_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:resin" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/leather_chestplate", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "property": "minecraft:trim_material" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json new file mode 100644 index 00000000..27a56391 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_helmet.json @@ -0,0 +1,174 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/empty_helmet" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_quartz_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:quartz" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_iron_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:iron" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_netherite_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:netherite" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_redstone_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:redstone" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_copper_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:copper" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_gold_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:gold" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_emerald_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:emerald" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_diamond_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:diamond" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_lapis_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:lapis" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_amethyst_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:amethyst" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet_resin_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:resin" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/leather_helmet", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "property": "minecraft:trim_material" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json new file mode 100644 index 00000000..4686e060 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/leather_leggings.json @@ -0,0 +1,174 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/empty_leggings" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_quartz_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:quartz" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_iron_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:iron" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_netherite_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:netherite" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_redstone_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:redstone" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_copper_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:copper" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_gold_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:gold" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_emerald_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:emerald" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_diamond_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:diamond" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_lapis_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:lapis" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_amethyst_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:amethyst" + }, + { + "model": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings_resin_trim", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "when": "minecraft:resin" + } + ], + "fallback": { + "type": "minecraft:model", + "model": "minecraft:item/leather_leggings", + "tints": [ + { + "type": "minecraft:dye", + "default": -6265536 + } + ] + }, + "property": "minecraft:trim_material" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json new file mode 100644 index 00000000..cfac9c50 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/shield.json @@ -0,0 +1,33 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/empty_shield" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:condition", + "on_false": { + "type": "minecraft:special", + "base": "minecraft:item/shield", + "model": { + "type": "minecraft:shield" + } + }, + "on_true": { + "type": "minecraft:special", + "base": "minecraft:item/shield_blocking", + "model": { + "type": "minecraft:shield" + } + }, + "property": "minecraft:using_item" + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json new file mode 100644 index 00000000..9f3d690e --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_banner.json @@ -0,0 +1,23 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "minecraft:model", + "model": "openinv:item/cursor" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "minecraft:special", + "base": "minecraft:item/template_banner", + "model": { + "type": "minecraft:banner", + "color": "white" + } + }, + "property": "minecraft:custom_model_data" + } +} diff --git a/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json new file mode 100644 index 00000000..59f6d812 --- /dev/null +++ b/resource-pack/openinv-legibility-pack/openinv_44/assets/minecraft/items/white_stained_glass_pane.json @@ -0,0 +1,20 @@ +{ + "model": { + "type": "minecraft:select", + "cases": [ + { + "model": { + "type": "model", + "model": "openinv:item/not_a_slot" + }, + "when": "openinv:custom" + } + ], + "fallback": { + "type": "model", + "model": "item/white_stained_glass_pane" + }, + "property": "custom_model_data" + }, + "oversized_in_gui": true +} diff --git a/resource-pack/openinv-legibility-pack/pack.mcmeta b/resource-pack/openinv-legibility-pack/pack.mcmeta new file mode 100644 index 00000000..a8a915fc --- /dev/null +++ b/resource-pack/openinv-legibility-pack/pack.mcmeta @@ -0,0 +1,23 @@ +{ + "pack": { + "description": "Improve OpenInv's legibility", + "min_format": 34, + "max_format": [75, 0], + "pack_format": 64, + "supported_formats": [ 34, 75 ] + }, + "overlays": { + "entries": [ + { + "min_format": 44, + "max_format": [75, 0], + "formats": [ 44, 75 ], + "directory": "openinv_44" + }, + { + "formats": [ 34, 43 ], + "directory": "openinv_34" + } + ] + } +} diff --git a/resource-pack/openinv-legibility-pack/pack.png b/resource-pack/openinv-legibility-pack/pack.png new file mode 100644 index 00000000..536a28f6 Binary files /dev/null and b/resource-pack/openinv-legibility-pack/pack.png differ diff --git a/scripts/generate_changelog.sh b/scripts/generate_changelog.sh deleted file mode 100644 index 72c823d9..00000000 --- a/scripts/generate_changelog.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2011-2021 lishid. All rights reserved. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# A script for generating a changelog from Git. -# -# Note that this script is designed for use in GitHub Actions, and is not -# particularly robust nor configurable. Run from project parent directory. - -# Query GitHub for the username of the given email address. -# Falls through to the given author name. -lookup_email_username() { - lookup=$(curl -G --data-urlencode "q=$1 in:email" https://api.github.com/search/users -H 'Accept: application/vnd.github.v3+json' | grep '"login":' | sed -e 's/^.*": "//g' -e 's/",.*$//g') - - if [[ $lookup ]]; then - echo -n "@$lookup" - else - echo "$2" - fi -} - -# Use formatted log to pull authors list -authors_raw=$(git log --pretty=format:"%ae|%an" "$(git describe --tags --abbrev=0 @^)"..@) -readarray -t authors <<<"$authors_raw" - -declare -A author_data - -for author in "${authors[@]}"; do - # Match author email - author_email=${author%|*} - # Convert to lower case - author_email=${author_email,,} - # Match author name - author_name=${author##*|} - if [[ -n ${author_data[$author_email]} ]]; then - # Skip emails we already have data for - continue - fi - - # Fetch and store author GitHub username by email - author_data[$author_email]=$(lookup_email_username "$author_email" "$author_name") -done - -# Fetch actual formatted changelog -changelog=$(git log --pretty=format:"%s (%h) - %ae" "$(git describe --tags --abbrev=0 @^)"..@) - -for author_email in "${!author_data[@]}"; do - # Ignore case when matching - shopt -s nocasematch - # Match and replace email - changelog=${changelog//$author_email/${author_data[$author_email]}} -done - -echo "GENERATED_CHANGELOG<> "$GITHUB_ENV" diff --git a/scripts/install_spigot_dependencies.sh b/scripts/install_spigot_dependencies.sh deleted file mode 100644 index 09e93e16..00000000 --- a/scripts/install_spigot_dependencies.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash -# -# Copyright (C) 2011-2021 lishid. All rights reserved. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, version 3. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -# A script for installing required Spigot versions. -# -# Note that this script is designed for use in GitHub Actions, and is -# not particularly robust nor configurable. -# In its current state, the script must be run from OpenInv's parent -# project directory and will always install BuildTools to ~/buildtools. - -buildtools_dir=~/buildtools -buildtools=$buildtools_dir/BuildTools.jar - -get_spigot_versions () { - # Get all submodules of internal module - modules=$(mvn help:evaluate -Dexpression=project.modules -q -DforceStdout -P all -pl internal | grep -oP '(?<=)(.*)(?=<\/string>)') - for module in "${modules[@]}"; do - - # Get number of dependencies declared in pom of specified internal module - max_index=$(mvn help:evaluate -Dexpression=project.dependencies -q -DforceStdout -P all -pl internal/"$module" | grep -c "") - - for ((i=0; i < max_index; i++)); do - # Get artifactId of dependency - artifact_id=$(mvn help:evaluate -Dexpression=project.dependencies["$i"].artifactId -q -DforceStdout -P all -pl internal/"$module") - - # Ensure dependency is spigot - if [[ "$artifact_id" == spigot ]]; then - # Get spigot version - spigot_version=$(mvn help:evaluate -Dexpression=project.dependencies["$i"].version -q -DforceStdout -P all -pl internal/"$module") - echo "$spigot_version" - break - fi - done - done -} - -get_buildtools () { - if [[ -d $buildtools_dir && -f $buildtools ]]; then - return - fi - - mkdir $buildtools_dir - wget https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar -O $buildtools -} - -versions=$(get_spigot_versions) -echo Found Spigot dependencies: "$versions" - -for version in "${versions[@]}"; do - set -e - exit_code=0 - mvn dependency:get -Dartifact=org.spigotmc:spigot:"$version" -q -o || exit_code=$? - if [ $exit_code -ne 0 ]; then - echo Installing missing Spigot version "$version" - revision=$(echo "$version" | grep -oP '(\d+\.\d+(\.\d+)?)(?=-R[0-9\.]+-SNAPSHOT)') - get_buildtools - java -jar $buildtools -rev "$revision" - else - echo Spigot "$version" is already installed - fi -done diff --git a/scripts/set_curseforge_env.sh b/scripts/set_curseforge_env.sh new file mode 100755 index 00000000..c75d5202 --- /dev/null +++ b/scripts/set_curseforge_env.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# +# Copyright (C) 2011-2021 lishid. All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Note that this script is designed for use in GitHub Actions, and is not +# particularly robust nor configurable. Run from project parent directory. + +if [[ ! $1 ]]; then + echo "No changelog, no Minecraft versions." + exit 0 +fi + +# Find line declaring Paper versions. +raw=$(grep "**Paper:**" <<< "$1") + +# Enable extended glob pattern to match 0 or more whitespace characters. +shopt -s extglob +# Trim Paper versions identifier prefix. +raw=${raw##*([[:space:]])'**'Paper:'**'*([[:space:]])} +# Replace commas and optional spaces with a newline. +raw=${raw//,*([[:space:]])/$'\n'} +# Turn extglob back off. +shopt -u extglob + +# Split into an array on newlines. +readarray -td $'\n' versions <<< "${raw}" + +for version in "${versions[@]}"; do + # Parse Minecraft minor version by dropping everything from the second period onward. + # CurseForge doesn't usually add patch versions for Bukkit, so we're more likely to + # hit a supported identifier this way. + version="${version%[.-]"${version#*.*[.-]}"}" + + # Skip already listed versions + if [[ "$minecraft_versions" =~ "$version"($|,) ]]; then + continue + fi + + # Append comma if variable is set, then append version. + # Note that Minecraft versions on CurseForge are declared "Minecraft x.y.z" + minecraft_versions="${minecraft_versions:+${minecraft_versions},}Minecraft ${version}" +done + +printf "$minecraft_versions\n" +#echo "CURSEFORGE_MINECRAFT_VERSIONS=$minecraft_versions" >> "$GITHUB_ENV" diff --git a/scripts/tag_release.sh b/scripts/tag_release.sh new file mode 100755 index 00000000..341d3e2a --- /dev/null +++ b/scripts/tag_release.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# +# Copyright (C) 2011-2021 lishid. All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +if [[ ! $1 ]]; then + echo "Please provide a version string." + return +fi + +version="$1" +snapshot="${version%.*}.$((${version##*.} + 1))-SNAPSHOT" + +sed -i s/version=.*/version="$version"/ gradle.properties + +git add gradle.properties +git commit -S -m "Bump version to $version for release" +git tag -s "$version" -m "Release $version" + +./gradlew build + +sed -i s/version=.*/version="$snapshot"/ gradle.properties + +git add gradle.properties +git commit -S -m "Bump version to $snapshot for development" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..efbf5eb0 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,39 @@ +rootProject.name = "openinvparent" + +include(":openinvapi") +project(":openinvapi").projectDir = file("api") + +if (!java.lang.Boolean.getBoolean("jitpack")) { + val addons = listOf( + "togglepersist" + ) + for (addon in addons) { + include(":addon$addon") + val proj = project(":addon$addon") + proj.projectDir = file("addon/$addon") + proj.name = "openinv$addon" + } + + include(":openinvcommon") + project(":openinvcommon").projectDir = file("common") + + val internals = listOf( + "common", + "paper1_21_10", + "paper1_21_8", + "paper1_21_5", + "paper1_21_4", + "paper1_21_3", + "paper1_21_1", + "spigot" + ) + for (internal in internals) { + include(":openinvadapter$internal") + project(":openinvadapter$internal").projectDir = file("internal/$internal") + } + + include(":resource-pack") + + include(":plugin") + project(":plugin").name = "openinvplugin" +}