From 87c91db83ce6acd5c4f53ee072bc6abb485b0f98 Mon Sep 17 00:00:00 2001 From: Nolij Date: Wed, 10 Jul 2024 21:05:28 -0400 Subject: [PATCH] initial commit --- .editorconfig | 308 ++++++++++ .github/workflows/build.yml | 27 + .github/workflows/dco.yml | 38 ++ .github/workflows/pull_request.yml | 28 + .github/workflows/release.yml | 29 + .github/workflows/release_dev.yml | 27 + .github/workflows/release_pre.yml | 27 + .github/workflows/release_rc.yml | 27 + .gitignore | 119 ++++ CHANGELOG.md | 1 + Jenkinsfile | 23 + LICENSE | 172 ++++++ README.md | 53 ++ api/build.gradle.kts | 35 ++ .../impl/INolijiumImplementation.java | 9 + .../dev/nolij/nolijium/impl/Nolijium.java | 33 ++ .../nolijium/impl/config/FileWatcher.java | 106 ++++ .../nolijium/impl/config/HostPlatform.java | 10 + .../nolijium/impl/config/IFileWatcher.java | 9 + .../impl/config/NolijiumConfigImpl.java | 306 ++++++++++ .../nolijium/impl/config/NullFileWatcher.java | 16 + .../nolijium/impl/config/package-info.java | 4 + .../dev/nolij/nolijium/impl/package-info.java | 4 + .../nolij/nolijium/impl/util/Alignment.java | 17 + .../nolij/nolijium/impl/util/DetailLevel.java | 10 + .../nolij/nolijium/impl/util/MathHelper.java | 17 + .../impl/util/MethodHandleHelper.java | 144 +++++ .../nolijium/impl/util/SlidingLongBuffer.java | 108 ++++ .../nolijium/impl/util/package-info.java | 4 + .../resources/assets/nolijium/lang/en_us.json | 61 ++ api/src/main/resources/icon.png | Bin 0 -> 38712 bytes api/src/test/java/SlidingLongArrayTest.java | 90 +++ build.gradle.kts | 530 ++++++++++++++++++ buildSrc/build.gradle.kts | 42 ++ buildSrc/src/main/kotlin/ZumeGradle.kt | 11 + .../dev/nolij/zumegradle/JarCompressing.kt | 338 +++++++++++ .../MixinConfigMergingTransformer.kt | 92 +++ gradle.properties | 65 +++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 249 ++++++++ gradlew.bat | 92 +++ neoforge/build.gradle.kts | 53 ++ .../NolijiumEmbeddiumConfigScreen.java | 305 ++++++++++ .../embeddium/NolijiumOptionsStorage.java | 21 + .../mixin/neoforge/ChatComponentMixin.java | 23 + .../mixin/neoforge/GameRendererMixin.java | 22 + .../mixin/neoforge/LevelLightEngineMixin.java | 33 ++ .../mixin/neoforge/LevelRendererMixin.java | 39 ++ .../mixin/neoforge/LightTextureMixin.java | 36 ++ .../mixin/neoforge/TextureAtlasMixin.java | 24 + .../neoforge/NolijiumHUDRenderLayer.java | 259 +++++++++ .../nolijium/neoforge/NolijiumNeoForge.java | 97 ++++ .../resources/META-INF/neoforge.mods.toml | 39 ++ .../resources/nolijium-neoforge.mixins.json | 17 + proguard.pro | 50 ++ settings.gradle.kts | 15 + .../resources/META-INF/neoforge.mods.toml | 39 ++ stubs/build.gradle.kts | 1 + .../zumegradle/proguard/ProGuardKeep.java | 20 + 60 files changed, 4381 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/dco.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/release_dev.yml create mode 100644 .github/workflows/release_pre.yml create mode 100644 .github/workflows/release_rc.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Jenkinsfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/build.gradle.kts create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/INolijiumImplementation.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/Nolijium.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/FileWatcher.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/HostPlatform.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/IFileWatcher.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/NolijiumConfigImpl.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/NullFileWatcher.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/config/package-info.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/package-info.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/Alignment.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/DetailLevel.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/MathHelper.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/MethodHandleHelper.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/SlidingLongBuffer.java create mode 100644 api/src/main/java/dev/nolij/nolijium/impl/util/package-info.java create mode 100644 api/src/main/resources/assets/nolijium/lang/en_us.json create mode 100644 api/src/main/resources/icon.png create mode 100644 api/src/test/java/SlidingLongArrayTest.java create mode 100644 build.gradle.kts create mode 100644 buildSrc/build.gradle.kts create mode 100644 buildSrc/src/main/kotlin/ZumeGradle.kt create mode 100644 buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt create mode 100644 buildSrc/src/main/kotlin/dev/nolij/zumegradle/MixinConfigMergingTransformer.kt create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 neoforge/build.gradle.kts create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumEmbeddiumConfigScreen.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumOptionsStorage.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/ChatComponentMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/GameRendererMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelLightEngineMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelRendererMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LightTextureMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/TextureAtlasMixin.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumHUDRenderLayer.java create mode 100644 neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumNeoForge.java create mode 100644 neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 neoforge/src/main/resources/nolijium-neoforge.mixins.json create mode 100644 proguard.pro create mode 100644 settings.gradle.kts create mode 100644 src/main/resources/META-INF/neoforge.mods.toml create mode 100644 stubs/build.gradle.kts create mode 100644 stubs/src/main/java/dev/nolij/zumegradle/proguard/ProGuardKeep.java diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..46eeb6a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,308 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = false +max_line_length = 120 +tab_width = 4 +trim_trailing_whitespace = false +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 = true +ij_visual_guides = unset +ij_wrap_on_typing = true +ij_java_keep_indents_on_empty_lines = true +ij_java_keep_line_breaks = true +ij_any_space_within_empty_array_initializer_braces = true +ij_any_space_before_array_initializer_left_brace = true +ij_any_block_brace_style = gnu + +[{*.yml,*.yaml}] +tab_width = 2 +indent_style = space +indent_size = 2 + +[*.java] +ij_java_align_consecutive_assignments = false +ij_java_align_consecutive_variable_declarations = false +ij_java_align_group_field_declarations = false +ij_java_align_multiline_annotation_parameters = false +ij_java_align_multiline_array_initializer_expression = false +ij_java_align_multiline_assignment = false +ij_java_align_multiline_binary_operation = false +ij_java_align_multiline_chained_methods = false +ij_java_align_multiline_deconstruction_list_components = true +ij_java_align_multiline_extends_list = false +ij_java_align_multiline_for = true +ij_java_align_multiline_method_parentheses = false +ij_java_align_multiline_parameters = true +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 = true +ij_java_align_multiline_text_blocks = true +ij_java_align_multiline_throws_list = false +ij_java_align_subsequent_simple_methods = true +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 = false +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_initializer = 1 +ij_java_blank_lines_around_method = 1 +ij_java_blank_lines_around_method_in_interface = 1 +ij_java_blank_lines_before_class_end = 0 +ij_java_blank_lines_before_imports = 1 +ij_java_blank_lines_before_method_body = 0 +ij_java_blank_lines_before_package = 0 +ij_java_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 = none +ij_java_call_parameters_new_line_after_left_paren = false +ij_java_call_parameters_right_paren_on_new_line = false +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 = 10000 +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 = true +ij_java_doc_align_param_comments = true +ij_java_doc_do_not_wrap_if_one_line = false +ij_java_doc_enable_formatting = true +ij_java_doc_enable_leading_asterisks = true +ij_java_doc_indent_on_continuation = false +ij_java_doc_keep_empty_lines = true +ij_java_doc_keep_empty_parameter_tag = true +ij_java_doc_keep_empty_return_tag = true +ij_java_doc_keep_empty_throws_tag = true +ij_java_doc_keep_invalid_tags = true +ij_java_doc_param_description_on_new_line = false +ij_java_doc_preserve_line_breaks = false +ij_java_doc_use_throws_not_exception_tag = true +ij_java_else_on_new_line = false +ij_java_entity_dd_suffix = EJB +ij_java_entity_eb_suffix = Bean +ij_java_entity_hi_suffix = Home +ij_java_entity_lhi_prefix = Local +ij_java_entity_lhi_suffix = Home +ij_java_entity_li_prefix = Local +ij_java_entity_pk_class = java.lang.String +ij_java_entity_vo_suffix = VO +ij_java_enum_constants_wrap = off +ij_java_extends_keyword_wrap = off +ij_java_extends_list_wrap = off +ij_java_field_annotation_wrap = split_into_lines +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 = false +ij_java_for_statement_wrap = off +ij_java_generate_final_locals = false +ij_java_generate_final_parameters = false +ij_java_if_brace_force = never +ij_java_imports_layout = *,|,javax.**,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_multiple_expressions_in_one_line = false +ij_java_keep_simple_blocks_in_one_line = false +ij_java_keep_simple_classes_in_one_line = false +ij_java_keep_simple_lambdas_in_one_line = false +ij_java_keep_simple_methods_in_one_line = false +ij_java_label_indent_absolute = false +ij_java_label_indent_size = 0 +ij_java_lambda_brace_style = end_of_line +ij_java_layout_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_message_dd_suffix = EJB +ij_java_message_eb_suffix = Bean +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 = false +ij_java_method_parameters_right_paren_on_new_line = false +ij_java_method_parameters_wrap = off +ij_java_modifier_list_wrap = false +ij_java_multi_catch_types_wrap = normal +ij_java_names_count_to_use_import_on_demand = 3 +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_packages_to_use_import_on_demand = java.awt.*,javax.swing.* +ij_java_parameter_annotation_wrap = off +ij_java_parentheses_expression_new_line_after_left_paren = false +ij_java_parentheses_expression_right_paren_on_new_line = false +ij_java_place_assignment_sign_on_next_line = false +ij_java_prefer_longer_names = true +ij_java_prefer_parameters_wrap = false +ij_java_record_components_wrap = normal +ij_java_repeat_synchronized = true +ij_java_replace_instanceof_and_cast = false +ij_java_replace_null_check = true +ij_java_replace_sum_lambda_with_method_ref = true +ij_java_resource_list_new_line_after_left_paren = false +ij_java_resource_list_right_paren_on_new_line = false +ij_java_resource_list_wrap = off +ij_java_rparen_on_new_line_in_annotation = false +ij_java_rparen_on_new_line_in_deconstruction_pattern = true +ij_java_rparen_on_new_line_in_record_header = false +ij_java_session_dd_suffix = EJB +ij_java_session_eb_suffix = Bean +ij_java_session_hi_suffix = Home +ij_java_session_lhi_prefix = Local +ij_java_session_lhi_suffix = Home +ij_java_session_li_prefix = Local +ij_java_session_si_suffix = Service +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_within_angle_brackets = false +ij_java_spaces_within_annotation_parentheses = false +ij_java_spaces_within_array_initializer_braces = true +ij_java_spaces_within_braces = true +ij_any_keep_simple_lambdas_in_one_line = true +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_subclass_name_suffix = Impl +ij_java_ternary_operation_signs_on_next_line = false +ij_java_ternary_operation_wrap = off +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 + +[{*.har,*.jsb2,*.jsb3,*.json,*.png.mcmeta,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config,mcmod.info,pack.mcmeta}] +ij_smart_tabs = false +ij_json_array_wrapping = split_into_lines +ij_json_keep_blank_lines_in_code = 1 +ij_json_keep_line_breaks = true +ij_json_keep_trailing_comma = true +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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4f0f1d4 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: build +on: [ push ] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + - name: :build + run: ./gradlew build --stacktrace + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: nolijium + path: | + **/nolijium-*.jar + **/*mappings.txt diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml new file mode 100644 index 0000000..1faa8ab --- /dev/null +++ b/.github/workflows/dco.yml @@ -0,0 +1,38 @@ +name: "DCO Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + DCO: + runs-on: ubuntu-latest + steps: + - name: "DCO Assistant" + if: ( + github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read and hereby affirm the entire contents of the Developer Certificate of Origin.' + ) || github.event_name == 'pull_request_target' + uses: cla-assistant/github-action@v2.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-signatures: "signatures.json" + path-to-document: "https://github.com/Nolij/Nolijium/blob/dco/DCO.txt" + branch: "dco" + create-file-commit-message: "Create DCO signature list" + signed-commit-message: "Add $contributorName to the DCO signature list" + custom-notsigned-prcomment: + "Before we can merge your submission, we require that you read and affirm the contents of the + [Developer Certificate of Origin](https://github.com/Nolij/Nolijium/blob/dco/DCO.txt) by adding a comment containing the below text. + Otherwise, please close this PR." + custom-pr-sign-comment: "I have read and hereby affirm the entire contents of the Developer Certificate of Origin." + custom-allsigned-prcomment: "All contributors have read and affirmed the entire contents of the Developer Certificate of Origin." + use-dco-flag: true diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..9809edc --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,28 @@ +name: pull_request +on: [ pull_request ] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY_PR }} + cache-read-only: true + - name: :build + run: ./gradlew build + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: nolijium + path: | + **/nolijium-*.jar + **/*mappings.txt diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8b72948 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: release +on: [ workflow_dispatch ] + +jobs: + publish: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-read-only: true + - name: :publishMods + run: ./gradlew publishMods -Prelease_channel=RELEASE + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }} + CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/release_dev.yml b/.github/workflows/release_dev.yml new file mode 100644 index 0000000..29231c8 --- /dev/null +++ b/.github/workflows/release_dev.yml @@ -0,0 +1,27 @@ +name: release_dev +on: [ workflow_dispatch ] + +jobs: + publish: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-read-only: true + - name: :publishMods + run: ./gradlew publishMods -Prelease_channel=DEV_BUILD --stacktrace + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/release_pre.yml b/.github/workflows/release_pre.yml new file mode 100644 index 0000000..c5e0045 --- /dev/null +++ b/.github/workflows/release_pre.yml @@ -0,0 +1,27 @@ +name: release_pre +on: [ workflow_dispatch ] + +jobs: + publish: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-read-only: true + - name: :publishMods + run: ./gradlew publishMods -Prelease_channel=PRE_RELEASE + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/.github/workflows/release_rc.yml b/.github/workflows/release_rc.yml new file mode 100644 index 0000000..4e3e3a9 --- /dev/null +++ b/.github/workflows/release_rc.yml @@ -0,0 +1,27 @@ +name: release_rc +on: [ workflow_dispatch ] + +jobs: + publish: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install Packages + run: sudo apt-get install -y advancecomp + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 21 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + cache-read-only: true + - name: :publishMods + run: ./gradlew publishMods -Prelease_channel=RELEASE_CANDIDATE + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISCORD_WEBHOOK: ${{ secrets.DISCORD_DEV_WEBHOOK }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bbef6e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# User-specific stuff +.idea/ +nolijium.json5 + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a38066 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- further improvements to overall system stability and other minor adjustments have been made to enhance the user experience diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..81245a9 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,23 @@ +#!/usr/bin/env groovy + +pipeline { + agent any + + tools { + jdk "jdk-21" + } + + stages { + stage("Setup") { + steps { + sh "chmod +x gradlew" + } + } + + stage(":publish") { + steps { + sh "./gradlew publish -Pexternal_publish=true" + } + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..512ebd3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,172 @@ +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of +authorship (the "Original Work") whose owner (the "Licensor") has placed the +following licensing notice adjacent to the copyright notice for the Original +Work: + + Licensed under the Open Software License version 3.0 + +1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, +non-exclusive, sublicensable license, for the duration of the copyright, to do +the following: + + a) to reproduce the Original Work in copies, either alone or as part of a + collective work; + + b) to translate, adapt, alter, transform, modify, or arrange the Original + Work, thereby creating derivative works ("Derivative Works") based upon the + Original Work; + + c) to distribute or communicate copies of the Original Work and Derivative + Works to the public, with the proviso that copies of Original Work or + Derivative Works that You distribute or communicate shall be licensed under + this Open Software License; + + d) to perform the Original Work publicly; and + + e) to display the Original Work publicly. + +2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, +non-exclusive, sublicensable license, under patent claims owned or controlled +by the Licensor that are embodied in the Original Work as furnished by the +Licensor, for the duration of the patents, to make, use, sell, offer for sale, +have made, and import the Original Work and Derivative Works. + +3) Grant of Source Code License. The term "Source Code" means the preferred +form of the Original Work for making modifications to it and all available +documentation describing how to modify the Original Work. Licensor agrees to +provide a machine-readable copy of the Source Code of the Original Work along +with each copy of the Original Work that Licensor distributes. Licensor +reserves the right to satisfy this obligation by placing a machine-readable +copy of the Source Code in an information repository reasonably calculated to +permit inexpensive and convenient access by You for as long as Licensor +continues to distribute the Original Work. + +4) Exclusions From License Grant. Neither the names of Licensor, nor the names +of any contributors to the Original Work, nor any of their trademarks or +service marks, may be used to endorse or promote products derived from this +Original Work without express prior permission of the Licensor. Except as +expressly stated herein, nothing in this License grants any license to +Licensor's trademarks, copyrights, patents, trade secrets or any other +intellectual property. No patent license is granted to make, use, sell, offer +for sale, have made, or import embodiments of any patent claims other than the +licensed claims defined in Section 2. No license is granted to the trademarks +of Licensor even if such marks are included in the Original Work. Nothing in +this License shall be interpreted to prohibit Licensor from licensing under +terms different from this License any Original Work that Licensor otherwise +would have a right to license. + +5) External Deployment. The term "External Deployment" means the use, +distribution, or communication of the Original Work or Derivative Works in any +way such that the Original Work or Derivative Works may be used by anyone +other than You, whether those works are distributed or communicated to those +persons or made available as an application intended for use over a network. +As an express condition for the grants of license hereunder, You must treat +any External Deployment by You of the Original Work or a Derivative Work as a +distribution under section 1(c). + +6) Attribution Rights. You must retain, in the Source Code of any Derivative +Works that You create, all copyright, patent, or trademark notices from the +Source Code of the Original Work, as well as any notices of licensing and any +descriptive text identified therein as an "Attribution Notice." You must cause +the Source Code for any Derivative Works that You create to carry a prominent +Attribution Notice reasonably calculated to inform recipients that You have +modified the Original Work. + +7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that +the copyright in and to the Original Work and the patent rights granted herein +by Licensor are owned by the Licensor or are sublicensed to You under the +terms of this License with the permission of the contributor(s) of those +copyrights and patent rights. Except as expressly stated in the immediately +preceding sentence, the Original Work is provided under this License on an "AS +IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without +limitation, the warranties of non-infringement, merchantability or fitness for +a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK +IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this +License. No license to the Original Work is granted by this License except +under this disclaimer. + +8) Limitation of Liability. Under no circumstances and under no legal theory, +whether in tort (including negligence), contract, or otherwise, shall the +Licensor be liable to anyone for any indirect, special, incidental, or +consequential damages of any character arising as a result of this License or +the use of the Original Work including, without limitation, damages for loss +of goodwill, work stoppage, computer failure or malfunction, or any and all +other commercial damages or losses. This limitation of liability shall not +apply to the extent applicable law prohibits such limitation. + +9) Acceptance and Termination. If, at any time, You expressly assented to this +License, that assent indicates your clear and irrevocable acceptance of this +License and all of its terms and conditions. If You distribute or communicate +copies of the Original Work or a Derivative Work, You must make a reasonable +effort under the circumstances to obtain the express assent of recipients to +the terms of this License. This License conditions your rights to undertake +the activities listed in Section 1, including your right to create Derivative +Works based upon the Original Work, and doing so without honoring these terms +and conditions is prohibited by copyright law and international treaty. +Nothing in this License is intended to affect copyright exceptions and +limitations (including "fair use" or "fair dealing"). This License shall +terminate immediately and You may no longer exercise any of the rights granted +to You by this License upon your failure to honor the conditions in Section +1(c). + +10) Termination for Patent Action. This License shall terminate automatically +and You may no longer exercise any of the rights granted to You by this +License as of the date You commence an action, including a cross-claim or +counterclaim, against Licensor or any licensee alleging that the Original Work +infringes a patent. This termination provision shall not apply for an action +alleging patent infringement by combinations of the Original Work with other +software or hardware. + +11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this +License may be brought only in the courts of a jurisdiction wherein the +Licensor resides or in which Licensor conducts its primary business, and under +the laws of that jurisdiction excluding its conflict-of-law provisions. The +application of the United Nations Convention on Contracts for the +International Sale of Goods is expressly excluded. Any use of the Original +Work outside the scope of this License or after its termination shall be +subject to the requirements and penalties of copyright or patent law in the +appropriate jurisdiction. This section shall survive the termination of this +License. + +12) Attorneys' Fees. In any action to enforce the terms of this License or +seeking damages relating thereto, the prevailing party shall be entitled to +recover its costs and expenses, including, without limitation, reasonable +attorneys' fees and costs incurred in connection with such action, including +any appeal of such action. This section shall survive the termination of this +License. + +13) Miscellaneous. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent necessary +to make it enforceable. + +14) Definition of "You" in This License. "You" throughout this License, +whether in upper or lower case, means an individual or a legal entity +exercising rights under, and complying with all of the terms of, this License. +For legal entities, "You" includes any entity that controls, is controlled by, +or is under common control with you. For purposes of this definition, +"control" means (i) the power, direct or indirect, to cause the direction or +management of such entity, whether by contract or otherwise, or (ii) ownership +of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial +ownership of such entity. + +15) Right to Use. You may use the Original Work in all ways not otherwise +restricted or conditioned by this License or by law, and Licensor promises not +to interfere with or be responsible for such uses by You. + +16) Modification of This License. This License is Copyright © 2005 Lawrence +Rosen. Permission is granted to copy, distribute, or communicate this License +without modification. Nothing in this License permits You to modify this +License as applied to the Original Work or to Derivative Works. However, You +may modify the text of this License and copy, distribute or communicate your +modified version (the "Modified License") and apply it to other original works +of authorship subject to the following conditions: (i) You may not indicate in +any way that your Modified License is the "Open Software License" or "OSL" and +you may not use those names in the name of your Modified License; (ii) You +must replace the notice specified in the first paragraph above with the notice +"Licensed under " or with a notice of your own +that is not confusingly similar to the notice in this License; and (iii) You +may not claim that your original works are open source software unless your +Modified License has been approved by Open Source Initiative (OSI) and You +comply with its license review and certification process. diff --git a/README.md b/README.md new file mode 100644 index 0000000..516d769 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# IMPORTANT LICENSE NOTICE + +By using this project in any form, you hereby give your "express assent" for the terms of the license of this +project (see [License](#license)), and acknowledge that I (the author of this project) have fulfilled my obligation +under the license to "make a reasonable effort under the circumstances to obtain the express assent of recipients to +the terms of this License". + +# Nolijium + +A collection of various QoL enhancements with (optional) Embeddium support and integration, written by Nolij. + +# FAQ + +#### Q: Some of these features seem familiar... + +A: Inspiration was taken from a few sources, but 100% of the code in this mod was written by me. The only parts of +this mod that were not entirely written by me are the parts of [Zume](https://github.com/Nolij/Zume)'s buildscript +contributed by @rhysdh540, which I used for the buildscript of this mod, and the icon, which 100% of the credit for +goes to the amazing @CelestialAbyss, who also made the equally amazing icons for [Embeddium](https://github. +com/embeddedt/embeddium) and [TauMC](https://github.com/TauMC). + +#### Q: Where is the config? + +A: You'll find the config at `.minecraft/global/nolijium.json5` (note that this is the default `.minecraft` folder, not +the instance `.minecraft`). You can modify the file while the game is running, and the config will be automatically +reloaded. The config button in the NeoForge mods screen will open this text file for you in your system's default +text editor. Nolijium also has optional Embeddium options screen integration, which is probably the easiest way to +modify this mod's config. + +#### Q: discord where +A: https://discord.gg/6ZjX4mvCMR + +#### Q: What version is this for? + +A: Currently this mod only supports 21.x NeoForge. It is built off of [Zume](https://github.com/Nolij/Zume)'s +buildscript though, so it should be feasible to add support for Fabric and/or older Minecraft versions. PRs which +extend platform support are welcome. + +#### Q: What kind of weird license is this? + +A: OSL-3.0 is the closest equivalent to a LAGPL I could find. AGPL and GPL are incompatible with Minecraft, and LGPL +doesn't protect network use. OSL-3.0 protects network use and is compatible with Minecraft. + +#### Q: Why though? It's so strict!!!! + +A: This is, and will remain, free, copyleft software. Any requests to change the license other than to make it even +stronger will be denied immediately (unfortunately GPL and AGPL aren't compatible with Minecraft due to linking +restrictions, as much as I'd like to use them). Even in situations where I use parts of other projects with more +"permissive" licenses, I will treat them as copyleft, free software. + +## License + +This project is licensed under OSL-3.0. For more information, see [LICENSE](LICENSE). diff --git a/api/build.gradle.kts b/api/build.gradle.kts new file mode 100644 index 0000000..13937c2 --- /dev/null +++ b/api/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + id("com.github.gmazzo.buildconfig") +} + +operator fun String.invoke(): String = rootProject.properties[this] as? String ?: error("Property $this not found") + +buildConfig { + className("NolijiumConstants") + packageName("dev.nolij.nolijium.impl") + + useJavaOutput() + + buildConfigField("MOD_ID", "mod_id"()) +} + +dependencies { + compileOnly(project(":stubs")) + + compileOnly("org.apache.logging.log4j:log4j-core:${"log4j_version"()}") + + testImplementation("org.junit.jupiter:junit-jupiter:${"junit_version"()}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() +} + +tasks.build { + dependsOn(tasks.test) +} + +tasks.clean { + finalizedBy(tasks.generateBuildConfig) +} \ No newline at end of file diff --git a/api/src/main/java/dev/nolij/nolijium/impl/INolijiumImplementation.java b/api/src/main/java/dev/nolij/nolijium/impl/INolijiumImplementation.java new file mode 100644 index 0000000..8c1c0ea --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/INolijiumImplementation.java @@ -0,0 +1,9 @@ +package dev.nolij.nolijium.impl; + +import dev.nolij.nolijium.impl.config.NolijiumConfigImpl; + +public interface INolijiumImplementation { + + default void onConfigReload(NolijiumConfigImpl config) {} + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/Nolijium.java b/api/src/main/java/dev/nolij/nolijium/impl/Nolijium.java new file mode 100644 index 0000000..6111db2 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/Nolijium.java @@ -0,0 +1,33 @@ +package dev.nolij.nolijium.impl; + +import dev.nolij.nolijium.impl.config.NolijiumConfigImpl; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.nio.file.Path; + +import static dev.nolij.nolijium.impl.NolijiumConstants.*; + +public class Nolijium { + + //region Constants + public static final Logger LOGGER = LogManager.getLogger(MOD_ID); + public static final String CONFIG_FILE_NAME = MOD_ID + ".json5"; + //endregion + + public static NolijiumConfigImpl config = new NolijiumConfigImpl(); + private static INolijiumImplementation implementation; + + public static void registerImplementation(final INolijiumImplementation implementation, final Path instanceConfigPath) { + if (Nolijium.implementation != null) + throw new AssertionError("Nolijium already initialized!"); + + Nolijium.implementation = implementation; + + NolijiumConfigImpl.init(instanceConfigPath, CONFIG_FILE_NAME, config -> { + Nolijium.config = config; + Nolijium.implementation.onConfigReload(config); + }); + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/FileWatcher.java b/api/src/main/java/dev/nolij/nolijium/impl/config/FileWatcher.java new file mode 100644 index 0000000..c0cc082 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/FileWatcher.java @@ -0,0 +1,106 @@ +// Based off of https://gist.githubusercontent.com/danielflower/f54c2fe42d32356301c68860a4ab21ed/raw/d09c312b4e40b17cdce310992da89dc06aabb98a/FileWatcher.java +// Original License (all modifications are still distributed under this project's license): https://gist.github.com/danielflower/f54c2fe42d32356301c68860a4ab21ed?permalink_comment_id=2352260#gistcomment-2352260 + +package dev.nolij.nolijium.impl.config; + +import dev.nolij.nolijium.impl.Nolijium; + +import java.io.IOException; +import java.nio.file.*; +import java.util.concurrent.Semaphore; + +public class FileWatcher implements IFileWatcher { + + private static final long DEBOUNCE_DURATION_MS = 500L; + + /** + * Starts watching a file and the given path and calls the callback when it is changed. + * A shutdown hook is registered to stop watching. To control this yourself, create an + * instance and use the start/stop methods. + */ + public static FileWatcher onFileChange(Path file, Runnable callback) throws IOException { + final FileWatcher watcher = new FileWatcher(); + watcher.start(file, callback); + + return watcher; + } + + private WatchService watchService; + private Thread thread; + private long debounce = 0L; + private final Semaphore semaphore = new Semaphore(1); + + @Override + public void lock() throws InterruptedException { + synchronized (semaphore) { + semaphore.acquire(); + } + } + + @Override + public boolean tryLock() { + synchronized (semaphore) { + return semaphore.tryAcquire(); + } + } + + @Override + public void unlock() { + synchronized (semaphore) { + if (semaphore.availablePermits() > 0) + return; + + semaphore.release(); + } + } + + public void start(Path file, Runnable callback) throws IOException { + watchService = FileSystems.getDefault().newWatchService(); + Path parent = file.getParent(); + parent.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE); + + thread = new Thread(() -> { + while (true) { + WatchKey wk = null; + try { + wk = watchService.take(); + for (final WatchEvent event : wk.pollEvents()) { + final Path changed = parent.resolve((Path) event.context()); + boolean locked = false; + try { + if ((locked = tryLock()) && + System.currentTimeMillis() > debounce && + Files.exists(changed) && + Files.isSameFile(changed, file)) { + callback.run(); + debounce = System.currentTimeMillis() + DEBOUNCE_DURATION_MS; + break; + } + } catch (NoSuchFileException ignored) { + } catch (IOException e) { + Nolijium.LOGGER.error("Error in config watcher: ", e); + } finally { + if (locked) + unlock(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } finally { + if (wk != null) { + wk.reset(); + } + } + } + }); + thread.setDaemon(true); + thread.start(); + } + + public void stop() { + thread.interrupt(); + watchService.close(); + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/HostPlatform.java b/api/src/main/java/dev/nolij/nolijium/impl/config/HostPlatform.java new file mode 100644 index 0000000..f0be9dd --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/HostPlatform.java @@ -0,0 +1,10 @@ +package dev.nolij.nolijium.impl.config; + +public enum HostPlatform { + + LINUX, + WINDOWS, + MAC_OS, + UNKNOWN, + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/IFileWatcher.java b/api/src/main/java/dev/nolij/nolijium/impl/config/IFileWatcher.java new file mode 100644 index 0000000..c77dee3 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/IFileWatcher.java @@ -0,0 +1,9 @@ +package dev.nolij.nolijium.impl.config; + +public interface IFileWatcher { + + void lock() throws InterruptedException; + boolean tryLock(); + void unlock(); + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/NolijiumConfigImpl.java b/api/src/main/java/dev/nolij/nolijium/impl/config/NolijiumConfigImpl.java new file mode 100644 index 0000000..24f5b8a --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/NolijiumConfigImpl.java @@ -0,0 +1,306 @@ +package dev.nolij.nolijium.impl.config; + +import dev.nolij.nolijium.impl.util.Alignment; +import dev.nolij.nolijium.impl.Nolijium; +import dev.nolij.nolijium.impl.util.DetailLevel; +import dev.nolij.zson.Zson; +import dev.nolij.zson.ZsonField; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.function.Consumer; + +public class NolijiumConfigImpl implements Cloneable { + + //region Options + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean enableGamma = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideAllToasts = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideAdvancementToasts = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideRecipeToasts = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideSystemToasts = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideTutorialToasts = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hideParticles = false; + + @ZsonField(comment = """ + DEFAULT: `[ ]`""") + public ArrayList hideParticlesByID = new ArrayList<>(); + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean disableTextureAnimations = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean revertDamageCameraTilt = false; + + @ZsonField(comment = """ + DEFAULT: `100`""") + public int maxChatHistory = 100; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hudEnabled = false; + + @ZsonField(comment = """ + DEFAULT: `LEFT`""") + public Alignment.X hudAlignmentX = Alignment.X.LEFT; + + @ZsonField(comment = """ + DEFAULT: `TOP`""") + public Alignment.Y hudAlignmentY = Alignment.Y.TOP; + + @ZsonField(comment = """ + DEFAULT: `5`""") + public int hudMarginX = 5; + + @ZsonField(comment = """ + DEFAULT: `5`""") + public int hudMarginY = 5; + + @ZsonField(comment = """ + DEFAULT: `true`""") + public boolean hudBackground = true; + + @ZsonField(comment = """ + DEFAULT: `true`""") + public boolean hudShadow = true; + + @ZsonField(comment = """ + DEFAULT: `5`""") + public int hudRefreshRateTicks = 5; + + @ZsonField(comment = """ + DEFAULT: `SIMPLE`""") + public DetailLevel hudShowFPS = DetailLevel.SIMPLE; + + @ZsonField(comment = """ + DEFAULT: `10.0`""") + public double hudFrameTimeBufferSize = 10D; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hudShowCPU = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hudShowMemory = false; + + @ZsonField(comment = """ + DEFAULT: `false`""") + public boolean hudShowCoordinates = false; + //endregion + + private static final int EXPECTED_VERSION = 1; + + @ZsonField(comment = "Used internally. Don't modify this.") + public int configVersion = EXPECTED_VERSION; + + @Override + public NolijiumConfigImpl clone() throws CloneNotSupportedException { + return (NolijiumConfigImpl) super.clone(); + } + + private static final int MAX_RETRIES = 5; + private static final Zson ZSON = new Zson(); + + private static NolijiumConfigImpl readFromFile(final File configFile) { + if (configFile == null || !configFile.exists()) + return null; + + int i = 0; + while (true) { + try { + //noinspection DataFlowIssue + return Zson.map2Obj(Zson.parse(new FileReader(configFile)), NolijiumConfigImpl.class); + } catch (IllegalArgumentException e) { + if (++i < MAX_RETRIES) { + try { + //noinspection BusyWait + Thread.sleep(i * 200L); + continue; + } catch (InterruptedException ignored) { + return null; + } + } + Nolijium.LOGGER.error("Error parsing config after {} retries: ", i, e); + return null; + } catch (IOException e) { + Nolijium.LOGGER.error("Error reading config: ", e); + return null; + } + } + } + + private static NolijiumConfigImpl readConfigFile() { + NolijiumConfigImpl result = readFromFile(getConfigFile()); + + if (result == null) + result = new NolijiumConfigImpl(); + + return result; + } + + private void writeToFile(final File configFile) { + this.configVersion = EXPECTED_VERSION; + try (final FileWriter configWriter = new FileWriter(configFile)) { + ZSON.write(Zson.obj2Map(this), configWriter); + configWriter.flush(); + } catch (IOException e) { + throw new RuntimeException("Failed to write config file", e); + } + } + + private static Consumer consumer; + private static IFileWatcher instanceWatcher; + private static IFileWatcher globalWatcher; + private static File instanceFile = null; + private static File globalFile = null; + + public static void replace(final NolijiumConfigImpl newConfig) throws InterruptedException { + try { + instanceWatcher.lock(); + try { + globalWatcher.lock(); + + newConfig.writeToFile(getConfigFile()); + consumer.accept(newConfig); + } finally { + globalWatcher.unlock(); + } + } finally { + instanceWatcher.unlock(); + } + } + + + private static final HostPlatform HOST_PLATFORM; + static { + final String OS_NAME = System.getProperty("os.name").toLowerCase(); + + if (OS_NAME.contains("linux")) + HOST_PLATFORM = HostPlatform.LINUX; + else if (OS_NAME.contains("mac")) + HOST_PLATFORM = HostPlatform.MAC_OS; + else if (OS_NAME.contains("win")) + HOST_PLATFORM = HostPlatform.WINDOWS; + else + HOST_PLATFORM = HostPlatform.UNKNOWN; + } + private static final String CONFIG_PATH_OVERRIDE = System.getProperty("nolijium.configPathOverride"); + private static final Path GLOBAL_CONFIG_PATH; + + static { + final Path dotMinecraft = switch (HOST_PLATFORM) { + case LINUX, UNKNOWN -> Paths.get(System.getProperty("user.home"), ".minecraft"); + case WINDOWS -> Paths.get(System.getenv("APPDATA"), ".minecraft"); + case MAC_OS -> Paths.get(System.getProperty("user.home"), "Library", "Application Support", "minecraft"); + }; + + GLOBAL_CONFIG_PATH = dotMinecraft.resolve("global"); + if (Files.notExists(GLOBAL_CONFIG_PATH)) { + try { + Files.createDirectories(GLOBAL_CONFIG_PATH); + } catch (IOException e) { + Nolijium.LOGGER.error("Failed to create global config path: ", e); + } + } + } + + public static File getConfigFile() { + if (CONFIG_PATH_OVERRIDE != null) { + return new File(CONFIG_PATH_OVERRIDE); + } + + if (instanceFile != null && instanceFile.exists()) { + return instanceFile; + } + + return globalFile; + } + + public static void reloadConfig() { + Nolijium.LOGGER.info("Reloading config..."); + + final NolijiumConfigImpl newConfig = readConfigFile(); + + consumer.accept(newConfig); + } + + public static void openConfigFile() { + final File configFile = getConfigFile(); + try { + final String CONFIG_PATH = configFile.getCanonicalPath(); + + final ProcessBuilder builder = new ProcessBuilder().inheritIO(); + + switch (HOST_PLATFORM) { + case LINUX, UNKNOWN -> builder.command("xdg-open", CONFIG_PATH); + case WINDOWS -> builder.command("rundll32", "url.dll,FileProtocolHandler", CONFIG_PATH); + case MAC_OS -> builder.command("open", "-t", CONFIG_PATH); + } + + builder.start(); + } catch (IOException e) { + Nolijium.LOGGER.error("Error opening config file: ", e); + } + } + + public static void init(final Path instanceConfigPath, final String fileName, final Consumer configConsumer) { + if (consumer != null) + throw new AssertionError("Config already initialized!"); + + consumer = configConsumer; + if (CONFIG_PATH_OVERRIDE == null) { + instanceFile = instanceConfigPath.resolve(fileName).toFile(); + globalFile = GLOBAL_CONFIG_PATH.resolve(fileName).toFile(); + } + + NolijiumConfigImpl config = readConfigFile(); + + // write new options and comment updates to disk + config.writeToFile(getConfigFile()); + + consumer.accept(config); + + try { + final IFileWatcher nullWatcher = new NullFileWatcher(); + + if (CONFIG_PATH_OVERRIDE == null) { + instanceWatcher = FileWatcher.onFileChange(instanceFile.toPath(), NolijiumConfigImpl::reloadConfig); + globalWatcher = FileWatcher.onFileChange(globalFile.toPath(), NolijiumConfigImpl::reloadConfig); + } else { + instanceWatcher = nullWatcher; + globalWatcher = FileWatcher.onFileChange(getConfigFile().toPath(), NolijiumConfigImpl::reloadConfig); + } + } catch (IOException e) { + throw new RuntimeException("Failed to create file watcher", e); + } + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/NullFileWatcher.java b/api/src/main/java/dev/nolij/nolijium/impl/config/NullFileWatcher.java new file mode 100644 index 0000000..82082c9 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/NullFileWatcher.java @@ -0,0 +1,16 @@ +package dev.nolij.nolijium.impl.config; + +public final class NullFileWatcher implements IFileWatcher { + + @Override + public void lock() {} + + @Override + public boolean tryLock() { + return true; + } + + @Override + public void unlock() {} + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/config/package-info.java b/api/src/main/java/dev/nolij/nolijium/impl/config/package-info.java new file mode 100644 index 0000000..a1ee6d0 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/config/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package dev.nolij.nolijium.impl.config; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/api/src/main/java/dev/nolij/nolijium/impl/package-info.java b/api/src/main/java/dev/nolij/nolijium/impl/package-info.java new file mode 100644 index 0000000..fcd89d6 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package dev.nolij.nolijium.impl; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/Alignment.java b/api/src/main/java/dev/nolij/nolijium/impl/util/Alignment.java new file mode 100644 index 0000000..5d519ab --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/Alignment.java @@ -0,0 +1,17 @@ +package dev.nolij.nolijium.impl.util; + +import dev.nolij.zumegradle.proguard.ProGuardKeep; + +public final class Alignment { + + @ProGuardKeep.Enum + public enum X { + LEFT, RIGHT + } + + @ProGuardKeep.Enum + public enum Y { + TOP, BOTTOM + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/DetailLevel.java b/api/src/main/java/dev/nolij/nolijium/impl/util/DetailLevel.java new file mode 100644 index 0000000..fc28e10 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/DetailLevel.java @@ -0,0 +1,10 @@ +package dev.nolij.nolijium.impl.util; + +import dev.nolij.zumegradle.proguard.ProGuardKeep; + +@ProGuardKeep.Enum +public enum DetailLevel { + + NONE, SIMPLE, EXTENDED + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/MathHelper.java b/api/src/main/java/dev/nolij/nolijium/impl/util/MathHelper.java new file mode 100644 index 0000000..89d03fa --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/MathHelper.java @@ -0,0 +1,17 @@ +package dev.nolij.nolijium.impl.util; + +import org.jetbrains.annotations.Contract; + +public final class MathHelper { + + @Contract(pure = true) + public static int sign(final int input) { + return input >> (Integer.SIZE - 1) | 1; + } + + @Contract(pure = true) + public static double clamp(final double value, final double min, final double max) { + return Math.max(Math.min(value, max), min); + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/MethodHandleHelper.java b/api/src/main/java/dev/nolij/nolijium/impl/util/MethodHandleHelper.java new file mode 100644 index 0000000..88a6e21 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/MethodHandleHelper.java @@ -0,0 +1,144 @@ +package dev.nolij.nolijium.impl.util; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Arrays; +import java.util.Objects; + +public class MethodHandleHelper { + + public static final MethodHandleHelper PUBLIC = + new MethodHandleHelper(MethodHandleHelper.class.getClassLoader(), MethodHandles.lookup()); + + private final @NotNull ClassLoader classLoader; + private final @NotNull MethodHandles.Lookup lookup; + + public MethodHandleHelper(@NotNull final ClassLoader classLoader, @NotNull final MethodHandles.Lookup lookup) { + this.classLoader = classLoader; + this.lookup = lookup; + } + + @SafeVarargs + public static @Nullable T firstNonNull(@Nullable T... options) { + for (final T option : options) + if (option != null) + return option; + + return null; + } + + public @Nullable Class getClassOrNull(@NotNull final String className) { + try { + return Class.forName(className, true, classLoader); + } catch (ClassNotFoundException ignored) { + return null; + } + } + + public @Nullable Class getClassOrNull(@NotNull String... classNames) { + for (final String className : classNames) { + try { + return Class.forName(className, true, classLoader); + } catch (ClassNotFoundException ignored) { } + } + + return null; + } + + public @Nullable MethodHandle getMethodOrNull(@Nullable final Class clazz, + @NotNull final String methodName, + @Nullable Class... parameterTypes) { + if (clazz == null || Arrays.stream(parameterTypes).anyMatch(Objects::isNull)) + return null; + + try { + return lookup.unreflect(clazz.getMethod(methodName, parameterTypes)); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getMethodOrNull(@Nullable final Class clazz, + @NotNull final String methodName, + @Nullable final MethodType methodType, + @Nullable Class... parameterTypes) { + if (clazz == null || Arrays.stream(parameterTypes).anyMatch(Objects::isNull)) + return null; + + try { + return lookup.unreflect(clazz.getMethod(methodName, parameterTypes)) + .asType(methodType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getConstructorOrNull(@Nullable final Class clazz, + @NotNull final MethodType methodType, + @Nullable Class... parameterTypes) { + if (clazz == null || Arrays.stream(parameterTypes).anyMatch(Objects::isNull)) + return null; + + try { + return lookup.unreflectConstructor(clazz.getConstructor(parameterTypes)) + .asType(methodType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getGetterOrNull(@Nullable final Class clazz, @NotNull final String fieldName, + @Nullable final Class fieldType) { + if (clazz == null || fieldType == null) + return null; + + try { + return lookup.findGetter(clazz, fieldName, fieldType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getGetterOrNull(@Nullable final Class clazz, @NotNull final String fieldName, + @Nullable final Class fieldType, @NotNull final MethodType methodType) { + if (clazz == null || fieldType == null) + return null; + + try { + return lookup.findGetter(clazz, fieldName, fieldType) + .asType(methodType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getSetterOrNull(@Nullable final Class clazz, @NotNull final String fieldName, + @Nullable final Class fieldType) { + if (clazz == null || fieldType == null) + return null; + + try { + return lookup.findSetter(clazz, fieldName, fieldType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + + public @Nullable MethodHandle getSetterOrNull(@Nullable final Class clazz, @NotNull final String fieldName, + @Nullable final Class fieldType, @NotNull final MethodType methodType) { + if (clazz == null || fieldType == null) + return null; + + try { + return lookup.findSetter(clazz, fieldName, fieldType) + .asType(methodType); + } catch (ReflectiveOperationException ignored) { + return null; + } + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/SlidingLongBuffer.java b/api/src/main/java/dev/nolij/nolijium/impl/util/SlidingLongBuffer.java new file mode 100644 index 0000000..35d6c9e --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/SlidingLongBuffer.java @@ -0,0 +1,108 @@ +package dev.nolij.nolijium.impl.util; + +public class SlidingLongBuffer { + + private int maxSize; + private long[] array; + + private int base = 0; + private int size = 0; + + public SlidingLongBuffer(int maxSize) { + this.maxSize = maxSize; + this.array = new long[maxSize + 1]; + } + + private int getIndex(int b, int i) { + return (b + i) % array.length; + } + + private int getIndex(int i) { + return getIndex(base, i); + } + + public boolean isEmpty() { + return size == 0; + } + + public boolean isFull() { + return size == maxSize; + } + + public boolean any() { + return size != 0; + } + + public int maxSize() { + return maxSize; + } + + public int size() { + return size; + } + + public long get(int index) { + if (size == 0 || index < 0 || index >= size) + return 0L; + + return array[getIndex(index)]; + } + + public long peek() { + return get(size - 1); + } + + public long getUnsafe(int index) { + return array[getIndex(index)]; + } + + public long peekUnsafe() { + return getUnsafe(size - 1); + } + + public synchronized void push(long value) { + if (size < maxSize) { + array[getIndex(base, size++)] = value; + } else { + array[getIndex(base++, size)] = value; + base %= array.length; + } + } + + public synchronized void pop() { + base = (base + 1) % array.length; + size--; + } + + public synchronized void resize(int newMaxSize) { + if (newMaxSize == maxSize) + return; + + final long[] newArray = new long[newMaxSize + 1]; + + if (newMaxSize > size) { + copy(size, newArray, base); + } else { + final int offsetBase = getIndex(base, size - newMaxSize); + copy(newMaxSize, newArray, offsetBase); + + size = newMaxSize; + } + + base = 0; + array = newArray; + maxSize = newMaxSize; + } + + private void copy(int amount, long[] newArray, int offsetBase) { + final int distanceToEdge = array.length - offsetBase; + + if (amount <= distanceToEdge) { + System.arraycopy(array, offsetBase, newArray, 0, amount); + } else { + System.arraycopy(array, offsetBase, newArray, 0, distanceToEdge); + System.arraycopy(array, 0, newArray, distanceToEdge, amount - distanceToEdge); + } + } + +} diff --git a/api/src/main/java/dev/nolij/nolijium/impl/util/package-info.java b/api/src/main/java/dev/nolij/nolijium/impl/util/package-info.java new file mode 100644 index 0000000..ae16389 --- /dev/null +++ b/api/src/main/java/dev/nolij/nolijium/impl/util/package-info.java @@ -0,0 +1,4 @@ +@ApiStatus.Internal +package dev.nolij.nolijium.impl.util; + +import org.jetbrains.annotations.ApiStatus; \ No newline at end of file diff --git a/api/src/main/resources/assets/nolijium/lang/en_us.json b/api/src/main/resources/assets/nolijium/lang/en_us.json new file mode 100644 index 0000000..5f9bb38 --- /dev/null +++ b/api/src/main/resources/assets/nolijium/lang/en_us.json @@ -0,0 +1,61 @@ +{ + "nolijium.utilities": "Utilities", + "nolijium.options.enable_gamma.name": "Enable Gamma Brightness", + "nolijium.options.enable_gamma.tooltip": "", + "nolijium.options.max_chat_history.name": "Max Chat History Size", + "nolijium.options.max_chat_history.tooltip": "", + "nolijium.messages": "%d Messages", + "nolijium.unlimited": "Unlimited", + "nolijium.options.hud_enabled.name": "Enable HUD", + "nolijium.options.hud_enabled.tooltip": "", + "nolijium.options.hud_alignment_x.name": "HUD X Alignment", + "nolijium.options.hud_alignment_x.tooltip": "", + "nolijium.options.hud_alignment_y.name": "HUD Y Alignment", + "nolijium.options.hud_alignment_y.tooltip": "", + "nolijium.options.hud_margin_x.name": "HUD X Margin", + "nolijium.options.hud_margin_x.tooltip": "", + "nolijium.options.hud_margin_y.name": "HUD Y Margin", + "nolijium.options.hud_margin_y.tooltip": "", + "nolijium.pixels": "%dpx", + "nolijium.options.hud_background.name": "Draw Background Behind HUD", + "nolijium.options.hud_background.tooltip": "", + "nolijium.options.hud_shadow.name": "Enable Shadow for HUD Text", + "nolijium.options.hud_shadow.tooltip": "", + "nolijium.options.hud_refresh_rate_ticks.name": "HUD Refresh Rate", + "nolijium.options.hud_refresh_rate_ticks.tooltip": "", + "nolijium.ticks": "%d tick(s)", + "nolijium.every_frame": "I paid for the whole CPU", + "nolijium.options.hud_show_fps.name": "Show FPS in HUD", + "nolijium.options.hud_show_fps.tooltip": "", + "nolijium.none": "None", + "nolijium.simple": "Simple", + "nolijium.extended": "Extended", + "nolijium.options.hud_frame_time_buffer_size.name": "Frame Time Buffer Size", + "nolijium.options.hud_frame_time_buffer_size.tooltip": "", + "nolijium.seconds": "%d second(s)", + "nolijium.options.hud_show_cpu.name": "Show CPU Usage in HUD", + "nolijium.options.hud_show_cpu.tooltip": "", + "nolijium.options.hud_show_memory.name": "Show Memory Usage in HUD", + "nolijium.options.hud_show_memory.tooltip": "", + "nolijium.options.hud_show_coordinates.name": "Show Coordinates in HUD", + "nolijium.options.hud_show_coordinates.tooltip": "", + "nolijium.toggles": "Toggles", + "nolijium.options.revert_damage_camera_tilt.name": "Revert Damage Tilt Fix", + "nolijium.options.revert_damage_camera_tilt.tooltip": "", + "nolijium.options.disable_block_animations.name": "Disable Texture Animations", + "nolijium.options.disable_block_animations.tooltip": "", + "nolijium.options.disable_all_toasts.name": "Disable All Toasts", + "nolijium.options.disable_all_toasts.tooltip": "", + "nolijium.options.disable_advancement_toasts.name": "Disable Advancement Toasts", + "nolijium.options.disable_advancement_toasts.tooltip": "", + "nolijium.options.disable_recipe_toasts.name": "Disable Recipe Toasts", + "nolijium.options.disable_recipe_toasts.tooltip": "", + "nolijium.options.disable_system_toasts.name": "Disable System Toasts", + "nolijium.options.disable_system_toasts.tooltip": "", + "nolijium.options.disable_tutorial_toasts.name": "Disable Tutorial Toasts", + "nolijium.options.disable_tutorial_toasts.tooltip": "", + "nolijium.particles": "Particles", + "nolijium.options.disable_particles.name": "Disable All Particles", + "nolijium.options.disable_particles.tooltip": "", + "nolijium.particle_id": "Enable %s particles" +} \ No newline at end of file diff --git a/api/src/main/resources/icon.png b/api/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..33e28e0ebfc67b66928dadf259fdb6d47c2d8888 GIT binary patch literal 38712 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 literal 0 HcmV?d00001 diff --git a/api/src/test/java/SlidingLongArrayTest.java b/api/src/test/java/SlidingLongArrayTest.java new file mode 100644 index 0000000..c52bd40 --- /dev/null +++ b/api/src/test/java/SlidingLongArrayTest.java @@ -0,0 +1,90 @@ +import dev.nolij.nolijium.impl.util.SlidingLongBuffer; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SlidingLongArrayTest { + + @Test + public void testAutoPop() { + final SlidingLongBuffer array = new SlidingLongBuffer(10); + for (int i = 0; i < 1000; i++) { + if (i > 0) + assertEquals(i - 1, array.peek()); + + array.push(i); + + assertEquals(i, array.peek()); + assertEquals(Math.min(i + 1, array.maxSize()), array.size()); + + for (int j = 0; j < array.size(); j++) { + assertEquals(i - array.size() + j + 1, array.get(j)); + } + } + } + + @Test + public void testManualPop() { + final SlidingLongBuffer array = new SlidingLongBuffer(5); + for (int i = 0; i < 6; i++) + array.push(i); + + assertEquals(5, array.peek()); + assertEquals(1, array.get(0)); + assertEquals(5, array.size()); + + array.pop(); + + assertEquals(5, array.peek()); + assertEquals(2, array.get(0)); + assertEquals(4, array.size()); + } + + @Test + public void testResizeExpand() { + for (int padding = 0; padding < 250; padding++) { + System.out.println(padding); + + final SlidingLongBuffer array = new SlidingLongBuffer(100); + + for (int i = 0; i < padding; i++) + array.push(0); + + for (int i = 0; i < 80; i++) + array.push(i); + + array.resize(200); + + assertEquals(80 + Math.min(padding, 20), array.size()); + + int j = -Math.min(20, padding); + for (int i = 0; i < array.size(); i++) { + assertEquals(Math.max(0, j++), array.get(i)); + } + } + } + + @Test + public void testResizeShrink() { + for (int padding = 0; padding < 200; padding++) { + System.out.println(padding); + + final SlidingLongBuffer array = new SlidingLongBuffer(150); + + for (int i = 0; i < padding; i++) + array.push(0); + + for (int i = 0; i < 100; i++) + array.push(i); + + array.resize(20); + + assertEquals(20, array.size()); + + for (int i = 0; i < array.size(); i++) { + assertEquals(80 + i, array.get(i)); + } + } + } + +} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..a6125cb --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,530 @@ +@file:Suppress("UnstableApiUsage") +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import dev.nolij.zumegradle.DeflateAlgorithm +import dev.nolij.zumegradle.JsonShrinkingType +import dev.nolij.zumegradle.MixinConfigMergingTransformer +import dev.nolij.zumegradle.CompressJarTask +import kotlinx.serialization.encodeToString +import me.modmuss50.mpp.HttpUtils +import me.modmuss50.mpp.PublishModTask +import me.modmuss50.mpp.ReleaseType +import me.modmuss50.mpp.platforms.discord.DiscordAPI +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.Request +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.internal.immutableListOf +import org.ajoberstar.grgit.Tag +import xyz.wagyourtail.unimined.api.minecraft.task.RemapJarTask +import xyz.wagyourtail.unimined.api.unimined +import java.nio.file.Files +import java.time.ZonedDateTime + +plugins { + id("java") + id("maven-publish") + id("com.github.johnrengelman.shadow") + id("me.modmuss50.mod-publish-plugin") + id("xyz.wagyourtail.unimined") + id("org.ajoberstar.grgit") +} + +operator fun String.invoke(): String = rootProject.properties[this] as? String ?: error("Property $this not found") + +enum class ReleaseChannel( + val suffix: String? = null, + val releaseType: ReleaseType? = null, + val deflation: DeflateAlgorithm = DeflateAlgorithm.ZOPFLI, + val json: JsonShrinkingType = JsonShrinkingType.MINIFY, + val proguard: Boolean = true, +) { + DEV_BUILD( + suffix = "dev", + deflation = DeflateAlgorithm.SEVENZIP, + json = JsonShrinkingType.PRETTY_PRINT + ), + PRE_RELEASE("pre"), + RELEASE_CANDIDATE("rc"), + RELEASE(releaseType = ReleaseType.STABLE), +} + +//region Git Versioning + +val headDateTime: ZonedDateTime? = grgit.head()?.dateTime + +val branchName = grgit.branch.current().name!! +val releaseTagPrefix = "release/" + +val releaseTags = grgit.tag.list() + .filter { tag -> tag.name.startsWith(releaseTagPrefix) } + .sortedWith { tag1, tag2 -> + if (tag1.commit.dateTime == tag2.commit.dateTime) + if (tag1.name.length != tag2.name.length) + return@sortedWith tag1.name.length.compareTo(tag2.name.length) + else + return@sortedWith tag1.name.compareTo(tag2.name) + else + return@sortedWith tag2.commit.dateTime.compareTo(tag1.commit.dateTime) + } + .dropWhile { tag -> headDateTime != null && tag.commit.dateTime > headDateTime } + +val isExternalCI = (rootProject.properties["external_publish"] as String?).toBoolean() +val isRelease = rootProject.hasProperty("release_channel") || isExternalCI +val releaseIncrement = if (isExternalCI) 0 else 1 +val releaseChannel: ReleaseChannel = + if (isExternalCI) { + val tagName = releaseTags.first().name + val suffix = """\-(\w+)\.\d+$""".toRegex().find(tagName)?.groupValues?.get(1) + if (suffix != null) + ReleaseChannel.values().find { channel -> channel.suffix == suffix }!! + else + ReleaseChannel.RELEASE + } else { + if (isRelease) + ReleaseChannel.valueOf("release_channel"()) + else + ReleaseChannel.DEV_BUILD + } + +println("Release Channel: $releaseChannel") + +val minorVersion = "mod_version"() +val minorTagPrefix = "${releaseTagPrefix}${minorVersion}." + +val patchHistory = releaseTags + .map { tag -> tag.name } + .filter { name -> name.startsWith(minorTagPrefix) } + .map { name -> name.substring(minorTagPrefix.length) } + +val maxPatch = patchHistory.maxOfOrNull { it.substringBefore('-').toInt() } +val patch = + maxPatch?.plus( + if (patchHistory.contains(maxPatch.toString())) + releaseIncrement + else + 0 + ) ?: 0 +var patchAndSuffix = patch.toString() + +if (releaseChannel.suffix != null) { + patchAndSuffix += "-${releaseChannel.suffix}" + + if (isRelease) { + patchAndSuffix += "." + + val maxBuild = patchHistory + .mapNotNull { it.removePrefix(patchAndSuffix).toIntOrNull() } + .maxOrNull() + + val build = (maxBuild?.plus(releaseIncrement)) ?: 1 + patchAndSuffix += build.toString() + } +} + +//endregion + +ZumeGradle.version = "${minorVersion}.${patchAndSuffix}" +println("Zume Version: ${ZumeGradle.version}") + +rootProject.group = "maven_group"() +rootProject.version = ZumeGradle.version + +base { + archivesName = "mod_id"() +} + +fun arrayOfProjects(vararg projectNames: String): Array { + return listOf(*projectNames).filter { p -> findProject(p) != null }.toTypedArray() +} +val uniminedImpls = arrayOfProjects( + "neoforge", +) + +allprojects { + apply(plugin = "java") + apply(plugin = "maven-publish") + + repositories { + mavenCentral { + content { + excludeGroup("ca.weblite") + } + } + maven("https://repo.spongepowered.org/maven") + maven("https://jitpack.io/") + exclusiveContent { + forRepository { maven("https://api.modrinth.com/maven") } + filter { + includeGroup("maven.modrinth") + } + } + maven("https://maven.blamejared.com") + } + + tasks.withType { + if (name !in arrayOf("compileMcLauncherJava", "compilePatchedMcJava")) { + options.encoding = "UTF-8" + sourceCompatibility = "21" + options.release = 21 + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(21) + } + options.compilerArgs.addAll(arrayOf("-Xplugin:Manifold no-bootstrap")) + } + } + + dependencies { + compileOnly("org.jetbrains:annotations:${"jetbrains_annotations_version"()}") + + compileOnly("systems.manifold:manifold-rt:${"manifold_version"()}") + annotationProcessor("systems.manifold:manifold-exceptions:${"manifold_version"()}") + + testCompileOnly("systems.manifold:manifold-rt:${"manifold_version"()}") + testAnnotationProcessor("systems.manifold:manifold-exceptions:${"manifold_version"()}") + } + + tasks.processResources { + inputs.file(rootDir.resolve("gradle.properties")) + inputs.property("version", ZumeGradle.version) + + filteringCharset = "UTF-8" + + val props = mutableMapOf() + props.putAll(rootProject.properties + .filterValues { value -> value is String } + .mapValues { entry -> entry.value as String }) + props["mod_version"] = ZumeGradle.version + + filesMatching(immutableListOf("fabric.mod.json", "META-INF/neoforge.mods.toml")) { + expand(props) + } + } +} + +subprojects { + val subProject = this + val implName = subProject.name + + group = "maven_group"() + version = ZumeGradle.version + + base { + archivesName = "${"mod_id"()}-${subProject.name}" + } + + tasks.withType { + enabled = false + } + + dependencies { + implementation("dev.nolij:zson:${"zson_version"()}") + } + + if (implName in uniminedImpls) { + apply(plugin = "xyz.wagyourtail.unimined") + apply(plugin = "com.github.johnrengelman.shadow") + + unimined.minecraft(sourceSets["main"], lateApply = true) { + combineWith(project(":api").sourceSets.main.get()) + + if (implName != "primitive") { + runs.config("server") { + disabled = true + } + + runs.config("client") { + jvmArgs += "-Dnolijium.configPathOverride=${rootProject.file("nolijium.json5").absolutePath}" + } + } + + defaultRemapJar = true + } + + val outputJar = tasks.register("outputJar") { + group = "build" + + val remapJarTasks = tasks.withType() + dependsOn(remapJarTasks) + mustRunAfter(remapJarTasks) + remapJarTasks.forEach { remapJar -> + remapJar.archiveFile.also { archiveFile -> + from(zipTree(archiveFile)) + inputs.file(archiveFile) + } + } + + configurations = emptyList() + archiveClassifier = "output" + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } + + tasks.assemble { + dependsOn(outputJar) + } + } +} + +unimined.minecraft { + version("minecraft_version"()) + + runs.off = true + + neoForge { + loader("neoforge_version"()) + } + + mappings { + mojmap() + parchment(mcVersion = "minecraft_version"(), version = "parchment_version"()) + } + + defaultRemapJar = false +} + +val shade: Configuration by configurations.creating { + configurations.compileClasspath.get().extendsFrom(this) + configurations.runtimeClasspath.get().extendsFrom(this) +} + +dependencies { + shade("dev.nolij:zson:${"zson_version"()}") + + compileOnly("org.apache.logging.log4j:log4j-core:${"log4j_version"()}") + + compileOnly(project(":stubs")) + + implementation(project(":api")) + + uniminedImpls.forEach { + implementation(project(":${it}")) { isTransitive = false } + } +} + +tasks.jar { + enabled = false +} + +val sourcesJar = tasks.register("sourcesJar") { + group = "build" + + archiveClassifier = "sources" + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + + from("LICENSE") { + rename { "${it}_${"mod_id"()}" } + } + + if (releaseChannel.proguard) { + dependsOn(compressJar) + from(compressJar.get().mappingsFile!!) { + rename { "mappings.txt" } + } + } + + listOf( + sourceSets, + project(":api").sourceSets, + uniminedImpls.flatMap { project(":${it}").sourceSets } + ).flatten().forEach { + from(it.allSource) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + } +} + +tasks.shadowJar { + transform(MixinConfigMergingTransformer::class.java) { + modId = "mod_id"() + packageName = "dev.nolij.nolijium.mixin" + } + + from("LICENSE") { + rename { "${it}_${"mod_id"()}" } + } + + exclude("*.xcf") + exclude("LICENSE_zson") + + configurations = immutableListOf(shade) + archiveClassifier = null + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + + val apiJar = project(":api").tasks.jar + dependsOn(apiJar) + from(zipTree(apiJar.get().archiveFile.get())) { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + + uniminedImpls.map { project(it).tasks.named("outputJar").get() }.forEach { implJarTask -> + dependsOn(implJarTask) + from(zipTree(implJarTask.archiveFile.get())) { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + exclude("fabric.mod.json", "pack.mcmeta") + } + } + + relocate("dev.nolij.zson", "dev.nolij.nolijium.zson") + if (releaseChannel.proguard) { + relocate("dev.nolij.nolijium.mixin", "nolijium.mixin") + } +} + +val compressJar = tasks.register("compressJar") { + dependsOn(tasks.shadowJar) + group = "build" + + val shadowJar = tasks.shadowJar.get() + inputJar = shadowJar.archiveFile.get().asFile + + deflateAlgorithm = releaseChannel.deflation + jsonShrinkingType = releaseChannel.json + if (releaseChannel.proguard) { + useProguard(uniminedImpls.flatMap { implName -> project(":$implName").unimined.minecrafts.values }) + } +} + +tasks.assemble { + dependsOn(compressJar, sourcesJar) +} + +afterEvaluate { + publishing { + repositories { + if (!System.getenv("local_maven_url").isNullOrEmpty()) + maven(System.getenv("local_maven_url")) + } + + publications { + create("mod_id"()) { + artifact(compressJar.get().outputJar) + artifact(sourcesJar) + } + } + } + + tasks.withType { + dependsOn(compressJar, sourcesJar) + } + + fun getChangelog(): String { + return file("CHANGELOG.md").readText() + } + + publishMods { + file = compressJar.get().outputJar + additionalFiles.from(sourcesJar.get().archiveFile) + type = releaseChannel.releaseType ?: ALPHA + displayName = ZumeGradle.version + version = ZumeGradle.version + changelog = getChangelog() + + modLoaders.addAll("neoforge") + dryRun = !isRelease + + github { + accessToken = providers.environmentVariable("GITHUB_TOKEN") + repository = "Nolij/Nolijium" + commitish = branchName + tagName = releaseTagPrefix + ZumeGradle.version + } + + if (dryRun.get() || releaseChannel.releaseType != null) { + modrinth { + accessToken = providers.environmentVariable("MODRINTH_TOKEN") + projectId = "KstN3eSL" + + minecraftVersions.add("1.21") + } + + curseforge { + val cfAccessToken = providers.environmentVariable("CURSEFORGE_TOKEN") + accessToken = cfAccessToken + projectId = "969602" + projectSlug = "nolijium" + + minecraftVersions.add("1.21") + } + + discord { + webhookUrl = providers.environmentVariable("DISCORD_WEBHOOK").orElse("") + + username = "Nolijium Releases" + + avatarUrl = "https://github.com/Nolij/Nolijium/raw/master/api/src/main/resources/icon.png" + + content = changelog.map { changelog -> + "# Nolijium ${ZumeGradle.version} has been released!\nChangelog: ```md\n${changelog}\n```" + } + + setPlatforms(platforms["modrinth"], platforms["github"], platforms["curseforge"]) + } + } + } + + tasks.withType { + dependsOn(compressJar, sourcesJar) + } + + tasks.publishMods { + if (!publishMods.dryRun.get() && releaseChannel.releaseType == null) { + doLast { + val http = HttpUtils() + + val currentTag: Tag? = releaseTags.getOrNull(0) + val buildChangeLog = + grgit.log { + if (currentTag != null) + excludes = listOf(currentTag.name) + includes = listOf("HEAD") + }.joinToString("\n") { commit -> + val id = commit.abbreviatedId + val message = commit.fullMessage.substringBefore('\n').trim() + val author = commit.author.name + "- [${id}] $message (${author})" + } + + val compareStart = currentTag?.name ?: grgit.log().minBy { it.dateTime }.id + val compareEnd = releaseTagPrefix + ZumeGradle.version + val compareLink = "https://github.com/Nolij/Nolijium/compare/${compareStart}...${compareEnd}" + + val webhookUrl = providers.environmentVariable("DISCORD_WEBHOOK") + val releaseChangeLog = getChangelog() + val file = publishMods.file.asFile.get() + + var content = "# [Nolijium Test Build ${ZumeGradle.version}]" + + "() has been released!\n" + + "Changes since last build: <${compareLink}>" + + if (buildChangeLog.isNotBlank()) + content += " ```md\n${buildChangeLog}\n```" + content += "\nChanges since last release: ```md\n${releaseChangeLog}\n```" + + val webhook = DiscordAPI.Webhook( + content, + "Nolijium Test Builds", + "https://github.com/Nolij/Nolijium/raw/master/api/src/main/resources/icon.png" + ) + + val bodyBuilder = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("payload_json", http.json.encodeToString(webhook)) + .addFormDataPart("files[0]", file.name, file.asRequestBody("application/java-archive".toMediaTypeOrNull())) + + var fileIndex = 1 + for (additionalFile in publishMods.additionalFiles) { + bodyBuilder.addFormDataPart( + "files[${fileIndex++}]", + additionalFile.name, + additionalFile.asRequestBody(Files.probeContentType(additionalFile.toPath()).toMediaTypeOrNull()) + ) + } + + val requestBuilder = Request.Builder() + .url(webhookUrl.get()) + .post(bodyBuilder.build()) + .header("Content-Type", "multipart/form-data") + + http.httpClient.newCall(requestBuilder.build()).execute().close() + } + } + } +} \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..675a30f --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,42 @@ +import java.util.* + +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + maven("https://maven.fabricmc.net/") + maven("https://maven.glass-launcher.net/babric") + maven("https://repo.legacyfabric.net/repository/legacyfabric/") + maven("https://maven.wagyourtail.xyz/releases") + maven("https://maven.wagyourtail.xyz/snapshots") + gradlePluginPortal { + content { + excludeGroup("org.apache.logging.log4j") + } + } +} + +fun DependencyHandler.plugin(id: String, version: String) { + this.implementation(group = id, name = "$id.gradle.plugin", version = version) +} + +val properties = Properties().apply { + load(rootDir.parentFile.resolve("gradle.properties").inputStream()) +} + +operator fun String.invoke(): String = properties.getProperty(this) ?: error("Property $this not found") + +dependencies { + implementation("org.ow2.asm:asm-tree:${"asm_version"()}") + implementation("net.fabricmc:mapping-io:${"mapping_io_version"()}") + implementation("org.apache.ant:ant:${"shadow_ant_version"()}") + implementation("com.guardsquare:proguard-base:${"proguard_version"()}") + + plugin(id = "com.github.johnrengelman.shadow", version = "shadow_version"()) + plugin(id = "xyz.wagyourtail.unimined", version = "unimined_version"()) + plugin(id = "com.github.gmazzo.buildconfig", version = "buildconfig_version"()) + plugin(id = "org.ajoberstar.grgit", version = "grgit_version"()) + plugin(id = "me.modmuss50.mod-publish-plugin", version = "mod_publish_version"()) +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/ZumeGradle.kt b/buildSrc/src/main/kotlin/ZumeGradle.kt new file mode 100644 index 0000000..c98e424 --- /dev/null +++ b/buildSrc/src/main/kotlin/ZumeGradle.kt @@ -0,0 +1,11 @@ +@Suppress("MemberVisibilityCanBePrivate") +object ZumeGradle { + private var _version: String? = null + var version: String = "" + get() = _version!! + set(value) { + field = value + if (value.isNotEmpty()) + _version = value + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt new file mode 100644 index 0000000..dc4d274 --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/JarCompressing.kt @@ -0,0 +1,338 @@ +package dev.nolij.zumegradle + +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import net.fabricmc.mappingio.MappingReader +import net.fabricmc.mappingio.format.MappingFormat +import net.fabricmc.mappingio.tree.MappingTree.ClassMapping +import net.fabricmc.mappingio.tree.MemoryMappingTree +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.options.Option +import org.gradle.kotlin.dsl.support.uppercaseFirstChar +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.tree.AnnotationNode +import org.objectweb.asm.tree.ClassNode +import proguard.Configuration +import proguard.ConfigurationParser +import proguard.ProGuard +import xyz.wagyourtail.unimined.api.minecraft.MinecraftConfig +import java.io.File +import java.util.Properties +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream +import java.util.zip.Deflater +import kotlin.collections.HashSet + +enum class DeflateAlgorithm(val id: Int?) { + NONE(null), + LIBDEFLATE(2), + SEVENZIP(3), + ZOPFLI(4), // too slow + ; + + override fun toString() = name.lowercase().uppercaseFirstChar() +} + +enum class JsonShrinkingType { + NONE, MINIFY, PRETTY_PRINT +} + +fun squishJar(jar: File, jsonProcessing: JsonShrinkingType, mappingsFile: File?) { + val contents = linkedMapOf() + JarFile(jar).use { + it.entries().asIterator().forEach { entry -> + if (!entry.isDirectory) { + contents[entry.name] = it.getInputStream(entry).readAllBytes() + } + } + } + + jar.delete() + + val json = JsonSlurper() + + val isObfuscating = mappingsFile?.exists() == true + val mappings = if (isObfuscating) mappings(mappingsFile!!) else null + + JarOutputStream(jar.outputStream()).use { out -> + out.setLevel(Deflater.BEST_COMPRESSION) + contents.forEach { var (name, bytes) = it + + if (name.endsWith("mixins.json") && isObfuscating) { + bytes = remapMixinConfig(bytes, mappings!!) + } + + if (jsonProcessing != JsonShrinkingType.NONE && + name.endsWith(".json") || name.endsWith(".mcmeta") || name == "mcmod.info" + ) { + bytes = when (jsonProcessing) { + JsonShrinkingType.MINIFY -> JsonOutput.toJson(json.parse(bytes)).toByteArray() + JsonShrinkingType.PRETTY_PRINT -> JsonOutput.prettyPrint(JsonOutput.toJson(json.parse(bytes))) + .toByteArray() + + else -> throw AssertionError() + } + } + + if (name.endsWith(".class")) { + bytes = processClassFile(bytes, mappings!!) + } + + out.putNextEntry(JarEntry(name)) + out.write(bytes) + out.closeEntry() + } + out.finish() + } +} + +@Suppress("UNCHECKED_CAST") +private fun remapMixinConfig(bytes: ByteArray, mappings: MemoryMappingTree): ByteArray { + val json = (JsonSlurper().parse(bytes) as Map).toMutableMap() + if (json.containsKey("plugin")) { + val old = json["plugin"] as String + val obf = mappings.obfuscate(old) + json["plugin"] = obf + } + + json["package"] = "nolijium.mixin" + + return JsonOutput.toJson(json).toByteArray() +} + +private fun processClassFile(bytes: ByteArray, mappings: MemoryMappingTree): ByteArray { + val classNode = ClassNode() + ClassReader(bytes).accept(classNode, 0) + + for (annotation in classNode.visibleAnnotations ?: emptyList()) { + if (annotation.desc.endsWith("fml/common/Mod;")) { + for (i in 0 until annotation.values.size step 2) { + if (annotation.values[i] == "guiFactory") { + val old = annotation.values[i + 1] as String + annotation.values[i + 1] = mappings.obfuscate(old) + println("Remapped guiFactory: $old -> ${annotation.values[i + 1]}") + } + } + } + } + + val strippableAnnotations = setOf( + "Lorg/spongepowered/asm/mixin/Dynamic;", + "Lorg/spongepowered/asm/mixin/Final;", + "Ljava/lang/SafeVarargs;", + ) + val canStripAnnotation = { annotationNode: AnnotationNode -> + annotationNode.desc.startsWith("Ldev/nolij/zumegradle/proguard/") || + annotationNode.desc.startsWith("Lorg/jetbrains/annotations/") || + strippableAnnotations.contains(annotationNode.desc) + } + + classNode.invisibleAnnotations?.removeIf(canStripAnnotation) + classNode.visibleAnnotations?.removeIf(canStripAnnotation) + classNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) + classNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) + classNode.fields.forEach { fieldNode -> + fieldNode.invisibleAnnotations?.removeIf(canStripAnnotation) + fieldNode.visibleAnnotations?.removeIf(canStripAnnotation) + fieldNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) + fieldNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) + } + classNode.methods.forEach { methodNode -> + methodNode.invisibleAnnotations?.removeIf(canStripAnnotation) + methodNode.visibleAnnotations?.removeIf(canStripAnnotation) + methodNode.invisibleTypeAnnotations?.removeIf(canStripAnnotation) + methodNode.visibleTypeAnnotations?.removeIf(canStripAnnotation) + methodNode.invisibleLocalVariableAnnotations?.removeIf(canStripAnnotation) + methodNode.visibleLocalVariableAnnotations?.removeIf(canStripAnnotation) + methodNode.invisibleParameterAnnotations?.forEach { parameterAnnotations -> + parameterAnnotations?.removeIf(canStripAnnotation) + } + methodNode.visibleParameterAnnotations?.forEach { parameterAnnotations -> + parameterAnnotations?.removeIf(canStripAnnotation) + } + } + + if (classNode.invisibleAnnotations?.any { it.desc == "Lorg/spongepowered/asm/mixin/Mixin;" } == true) { + classNode.methods.removeAll { it.name == "" && it.instructions.size() <= 3 } // ALOAD, super(), RETURN + } + + val writer = ClassWriter(0) + classNode.accept(writer) + return writer.toByteArray() +} + +val advzipInstalled = try { + ProcessBuilder("advzip", "-V").start().waitFor() == 0 +} catch (e: Exception) { + false +} + +fun deflate(zip: File, type: DeflateAlgorithm) { + if (type == DeflateAlgorithm.NONE) return + if (!advzipInstalled) { + println("advzip is not installed; skipping re-deflation of $zip") + return + } + + try { + val process = ProcessBuilder("advzip", "-z", "-${type.id}", zip.absolutePath).start() + val exitCode = process.waitFor() + if (exitCode != 0) { + error("Failed to compress $zip with $type") + } + } catch (e: Exception) { + error("Failed to compress $zip with $type: ${e.message}") + } +} + +val JAVA_HOME = System.getProperty("java.home") + +@Suppress("UnstableApiUsage") +fun applyProguard(jar: File, minecraftConfigs: List, configDir: File) { + val inputJar = jar.copyTo( + jar.parentFile.resolve(".${jar.nameWithoutExtension}_proguardRunning.jar"), true + ).also { + it.deleteOnExit() + } + + val config = configDir.resolve("proguard.pro") + if (!config.exists()) { + error("proguard.pro not found") + } + val proguardCommand = mutableListOf( + "@${config.absolutePath}", + "-printmapping", jar.parentFile.resolve("${jar.nameWithoutExtension}-mappings.txt").absolutePath, + "-injars", inputJar.absolutePath, + "-outjars", jar.absolutePath, + ) + + val libraries = HashSet() + libraries.add("${JAVA_HOME}/jmods/java.base.jmod") + libraries.add("${JAVA_HOME}/jmods/java.desktop.jmod") + + for (minecraftConfig in minecraftConfigs) { + val prodNamespace = minecraftConfig.mcPatcher.prodNamespace + + libraries.add(minecraftConfig.getMinecraft(prodNamespace, prodNamespace).toFile().absolutePath) + + val minecrafts = listOf( + minecraftConfig.sourceSet.compileClasspath.files, + minecraftConfig.sourceSet.runtimeClasspath.files + ) + .flatten() + .filter { it: File -> !minecraftConfig.isMinecraftJar(it.toPath()) } + .toHashSet() + + libraries += minecraftConfig.mods.getClasspathAs(prodNamespace, prodNamespace, minecrafts) + .filter { it.extension == "jar" && !it.name.startsWith("zume") } + .map { it.absolutePath } + } + + val debug = Properties().apply { + val gradleproperties = configDir.resolve("gradle.properties") + if (gradleproperties.exists()) { + load(gradleproperties.inputStream()) + } + }.getProperty("zumegradle.proguard.keepAttrs").toBoolean() + + if (debug) { + proguardCommand.add("-keepattributes") + proguardCommand.add("*Annotation*,SourceFile,MethodParameters,L*Table") + proguardCommand.add("-dontobfuscate") + } + + proguardCommand.add("-libraryjars") + proguardCommand.add(libraries.joinToString(File.pathSeparator) { "\"$it\"" }) + + val configuration = Configuration() + ConfigurationParser(proguardCommand.toTypedArray(), System.getProperties()) + .parse(configuration) + + try { + ProGuard(configuration).execute() + } catch (ex: Exception) { + throw IllegalStateException("ProGuard failed for $jar", ex) + } finally { + inputJar.delete() + } +} + +open class CompressJarTask : DefaultTask() { + @InputFile + lateinit var inputJar: File + + @Input + var deflateAlgorithm = DeflateAlgorithm.LIBDEFLATE + + @Input + var jsonShrinkingType = JsonShrinkingType.NONE + + @get:Input + val useProguard get() = this.minecraftConfigs.isNotEmpty() + + private var minecraftConfigs: List = emptyList() + + @get:OutputFile + val outputJar get() = inputJar // compressed jar will replace the input jar + + @get:OutputFile + @get:Optional + val mappingsFile + get() = if (useProguard) + inputJar.parentFile.resolve("${inputJar.nameWithoutExtension}-mappings.txt") + else null + + @Option(option = "compression-type", description = "How to recompress the jar") + fun setDeflateAlgorithm(value: String) { + deflateAlgorithm = value.uppercase().let { + if (it.matches(Regex("7Z(?:IP)?"))) DeflateAlgorithm.SEVENZIP + else DeflateAlgorithm.valueOf(it) + } + } + + @Option(option = "json-processing", description = "How to process json files") + fun setJsonShrinkingType(value: String) { + jsonShrinkingType = JsonShrinkingType.valueOf(value.uppercase()) + } + + fun useProguard(minecraftConfigs: List) { + this.minecraftConfigs = minecraftConfigs + } + + @TaskAction + fun compressJar() { + if (useProguard) + applyProguard(inputJar, minecraftConfigs, project.rootDir) + squishJar(inputJar, jsonShrinkingType, mappingsFile) + deflate(outputJar, deflateAlgorithm) + } +} + +fun mappings(file: File, format: MappingFormat = MappingFormat.PROGUARD): MemoryMappingTree { + if (!file.exists()) { + error("Mappings file $file does not exist") + } + + val mappingTree = MemoryMappingTree() + MappingReader.read(file.toPath(), format, mappingTree) + return mappingTree +} + +@Suppress("INACCESSIBLE_TYPE", "NAME_SHADOWING") +fun MemoryMappingTree.obfuscate(src: String): String { + val src = src.replace('.', '/') + val dstNamespaceIndex = getNamespaceId(dstNamespaces[0]) + val classMapping: ClassMapping? = getClass(src) + if (classMapping == null) { + println("Class $src not found in mappings") + return src + } + return classMapping.getDstName(dstNamespaceIndex).replace('/', '.') +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/dev/nolij/zumegradle/MixinConfigMergingTransformer.kt b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/MixinConfigMergingTransformer.kt new file mode 100644 index 0000000..17a499e --- /dev/null +++ b/buildSrc/src/main/kotlin/dev/nolij/zumegradle/MixinConfigMergingTransformer.kt @@ -0,0 +1,92 @@ +package dev.nolij.zumegradle + +import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer +import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import groovy.json.JsonOutput +import groovy.json.JsonSlurper +import org.gradle.api.file.FileTreeElement +import org.gradle.api.tasks.Input +import org.apache.tools.zip.ZipOutputStream +import org.apache.tools.zip.ZipEntry +import org.gradle.api.tasks.Optional + +class MixinConfigMergingTransformer : Transformer { + + private val JSON = JsonSlurper() + + @Input lateinit var modId: String + @Input lateinit var packageName: String + @Input @Optional var mixinPlugin: String? = null + + override fun getName(): String { + return "MixinConfigMergingTransformer" + } + + override fun canTransformResource(element: FileTreeElement?): Boolean { + return element != null && (element.name.endsWith(".mixins.json") || element.name.endsWith("-refmap.json")) + } + + private var transformed = false + + private var mixins = ArrayList() + private var refMaps = HashMap>() + + override fun transform(context: TransformerContext?) { + if (context == null) + return + + this.transformed = true + + val parsed = JSON.parse(context.`is`) as Map<*, *> + if (parsed.contains("client")) { + @Suppress("UNCHECKED_CAST") + mixins.addAll(parsed["client"] as List) + } else { + @Suppress("UNCHECKED_CAST") + refMaps.putAll(parsed["mappings"] as Map>) + } + } + + override fun hasTransformedResource(): Boolean { + return transformed + } + + override fun modifyOutputStream(os: ZipOutputStream?, preserveFileTimestamps: Boolean) { + val mixinConfigEntry = ZipEntry("${modId}.mixins.json") + os!!.putNextEntry(mixinConfigEntry) + + val mixinConfigJson = mutableMapOf( + "required" to true, + "minVersion" to "0.8", + "package" to packageName, + "compatibilityLevel" to "JAVA_8", + "client" to mixins, + "injectors" to mapOf( + "defaultRequire" to 1, + ) + ) + if (mixinPlugin != null) + mixinConfigJson["plugin"] = mixinPlugin!! + + if (refMaps.isNotEmpty()) { + val refmapName = "${modId}-refmap.json" + mixinConfigJson["refmap"] = refmapName + os.putNextEntry(ZipEntry(refmapName)) + os.write( + JsonOutput.prettyPrint( + JsonOutput.toJson( + mapOf( + "mappings" to refMaps, + ) + ) + ).toByteArray() + ) + } + + os.write(JsonOutput.prettyPrint(JsonOutput.toJson(mixinConfigJson)).toByteArray()) + + transformed = false + mixins.clear() + refMaps.clear() + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..d842461 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,65 @@ +# Gradle +org.gradle.jvmargs = -Xmx4G -XX:+EnableDynamicAgentLoading +org.gradle.daemon = true +org.gradle.parallel = true +org.gradle.caching = true +org.gradle.configuration-cache = false + +# Mod Properties +maven_group = dev.nolij +mod_id = nolijium +mod_version = 0.1 +mod_name = Nolijium +mod_description = Various QoL enhancements by Nolij +nolij = Nolij (@xdMatthewbx#1337) +mod_url = https://modrinth.com/mod/nolijium +repo_url = https://github.com/Nolij/Nolijium +issue_url = https://github.com/Nolij/Nolijium/issues + +# Platform-Shared +minecraft_version = 1.21 +parchment_version = 2024.06.23 + +# NeoForge +# https://projects.neoforged.net/neoforged/neoforge +neoforge_minecraft_range = [1.21,) +neoforge_version = 76-beta +neoforge_neoforge_range = [21.0.42-beta,) + +# Dependencies +# https://github.com/Nolij/Zume/releases/latest +zume_version = 1.0.0 +# https://maven.blamejared.com/org/embeddedt/ +## https://maven.blamejared.com/org/embeddedt/embeddium-1.21/ +embeddium_neoforge_version = 1.0.5-beta.314+1.21 +embeddium_range = [1.0.5,) +# https://github.com/Nolij/ZSON/releases/latest +zson_version = 0.4.1 +# https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core +log4j_version = 3.0.0-beta2 +# https://central.sonatype.com/artifact/org.ow2.asm/asm-tree +asm_version = 9.7 +# https://central.sonatype.com/artifact/org.jetbrains/annotations +jetbrains_annotations_version = 24.1.0 +# https://github.com/manifold-systems/manifold +manifold_version = 2024.1.20 +# https://central.sonatype.com/artifact/org.junit.jupiter/junit-jupiter-engine +junit_version = 5.11.0-M2 + +# Gradle Plugins +# https://maven.fabricmc.net/net/fabricmc/mapping-io/ +mapping_io_version = 0.3.0 +# https://github.com/johnrengelman/shadow/blob/main/gradle/dependencies.gradle +shadow_ant_version = 1.10.4 +# https://plugins.gradle.org/plugin/com.github.johnrengelman.shadow +shadow_version = 8.1.1 +# https://central.sonatype.com/artifact/com.guardsquare/proguard-base +proguard_version = 7.5.0 +# https://github.com/unimined/unimined/releases/latest +unimined_version = 1.2.14 +# https://plugins.gradle.org/plugin/com.github.gmazzo.buildconfig +buildconfig_version = 5.3.5 +# https://github.com/ajoberstar/grgit/releases/latest +grgit_version = 5.2.2 +# https://plugins.gradle.org/plugin/me.modmuss50.mod-publish-plugin +mod_publish_version = 0.5.1 \ 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 0000000000000000000000000000000000000000..249e5832f090a2944b7473328c07c9755baa3196 GIT binary patch literal 60756 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 '} + 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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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 + +@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/neoforge/build.gradle.kts b/neoforge/build.gradle.kts new file mode 100644 index 0000000..35a2700 --- /dev/null +++ b/neoforge/build.gradle.kts @@ -0,0 +1,53 @@ +operator fun String.invoke(): String = rootProject.properties[this] as? String ?: error("Property $this not found") + +val modCompileOnly: Configuration by configurations.creating { + configurations.compileClasspath.get().extendsFrom(this) +} +val modRuntimeOnly: Configuration by configurations.creating { + configurations.runtimeClasspath.get().extendsFrom(this) +} +val mod: Configuration by configurations.creating { + configurations.compileClasspath.get().extendsFrom(this) + configurations.runtimeClasspath.get().extendsFrom(this) +} + +unimined.minecraft { + version("minecraft_version"()) + + neoForge { + loader("neoforge_version"()) + } + + source { + sourceGenerator.jvmArgs = listOf("-Xmx4G") + } + + mappings { + mojmap() + parchment(mcVersion = "minecraft_version"(), version = "parchment_version"()) + } + + mods { + remap(modCompileOnly) + remap(modRuntimeOnly) + remap(mod) + } +} + +repositories { + mavenLocal() + maven("https://maven.blamejared.com") +} + +dependencies { + compileOnly(project(":stubs")) + + minecraftLibraries("dev.nolij:zson:${"zson_version"()}") + + modCompileOnly("org.embeddedt:embeddium-1.21:${"embeddium_neoforge_version"()}:api") + modRuntimeOnly("org.embeddedt:embeddium-1.21:${"embeddium_neoforge_version"()}") { + isTransitive = false + } + + modRuntimeOnly("dev.nolij:zume:${"zume_version"()}") +} \ No newline at end of file diff --git a/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumEmbeddiumConfigScreen.java b/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumEmbeddiumConfigScreen.java new file mode 100644 index 0000000..838d22e --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumEmbeddiumConfigScreen.java @@ -0,0 +1,305 @@ +package dev.nolij.nolijium.integration.embeddium; + +import com.google.common.collect.ImmutableList; +import dev.nolij.nolijium.impl.util.Alignment; +import dev.nolij.nolijium.impl.util.DetailLevel; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import org.embeddedt.embeddium.api.OptionGUIConstructionEvent; +import org.embeddedt.embeddium.api.eventbus.EventHandlerRegistrar; +import org.embeddedt.embeddium.api.options.OptionIdentifier; +import org.embeddedt.embeddium.api.options.control.CyclingControl; +import org.embeddedt.embeddium.api.options.control.SliderControl; +import org.embeddedt.embeddium.api.options.control.TickBoxControl; +import org.embeddedt.embeddium.api.options.structure.Option; +import org.embeddedt.embeddium.api.options.structure.OptionGroup; +import org.embeddedt.embeddium.api.options.structure.OptionImpl; +import org.embeddedt.embeddium.api.options.structure.OptionPage; + +import java.util.ArrayList; +import java.util.List; + +import static dev.nolij.nolijium.impl.NolijiumConstants.*; + +public class NolijiumEmbeddiumConfigScreen implements EventHandlerRegistrar.Handler { + + public NolijiumEmbeddiumConfigScreen() { + OptionGUIConstructionEvent.BUS.addListener(this); + } + + private static OptionIdentifier id(String path) { + return id(path, void.class); + } + + private static OptionIdentifier id(String path, Class type) { + return OptionIdentifier.create(MOD_ID, path, type); + } + + @Override + public void acceptEvent(OptionGUIConstructionEvent event) { + final NolijiumOptionsStorage storage = new NolijiumOptionsStorage(); + + final List utilitiesPage = new ArrayList<>(); + final List togglesPage = new ArrayList<>(); + final List particlesPage = new ArrayList<>(); + + utilitiesPage.add(OptionGroup.createBuilder() + .setId(id("utilities")) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("enable_gamma", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.enableGamma = value, + config -> config.enableGamma + ) + .build()) + .add(OptionImpl.createBuilder(int.class, storage) + .setId(id("max_chat_history", int.class)) + .setControl(option -> new SliderControl(option, 0, 2000, 100, + v -> v > 0 + ? Component.translatable("nolijium.messages", v) + : Component.translatable("nolijium.unlimited"))) + .setBinding( + (config, value) -> config.maxChatHistory = value, + config -> config.maxChatHistory) + .build()) + .build()); + + final Option hudEnabledOption; + final Option hudShowFPSOption; + utilitiesPage.add(OptionGroup.createBuilder() + .setId(id("hud")) + .add(hudEnabledOption = OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_enabled", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudEnabled = value, + config -> config.hudEnabled) + .build()) + .add(OptionImpl.createBuilder(Alignment.X.class, storage) + .setId(id("hud_alignment_x", Alignment.X.class)) + .setControl(option -> new CyclingControl<>(option, Alignment.X.class)) + .setBinding( + (config, value) -> config.hudAlignmentX = value, + config -> config.hudAlignmentX) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(Alignment.Y.class, storage) + .setId(id("hud_alignment_y", Alignment.Y.class)) + .setControl(option -> new CyclingControl<>(option, Alignment.Y.class)) + .setBinding( + (config, value) -> config.hudAlignmentY = value, + config -> config.hudAlignmentY) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(int.class, storage) + .setId(id("hud_margin_x", int.class)) + .setControl(option -> + new SliderControl(option, 0, 20, 5, + v -> Component.translatable("nolijium.pixels", v))) + .setBinding( + (config, value) -> config.hudMarginX = value, + config -> config.hudMarginX) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(int.class, storage) + .setId(id("hud_margin_y", int.class)) + .setControl(option -> + new SliderControl(option, 0, 20, 5, + v -> Component.translatable("nolijium.pixels", v))) + .setBinding( + (config, value) -> config.hudMarginY = value, + config -> config.hudMarginY) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_background", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudBackground = value, + config -> config.hudBackground) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_shadow", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudShadow = value, + config -> config.hudShadow) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(int.class, storage) + .setId(id("hud_refresh_rate_ticks", int.class)) + .setControl(option -> + new SliderControl(option, 0, 20, 1, + v -> v != 0 + ? Component.translatable("nolijium.ticks", v) + : Component.translatable("nolijium.every_frame"))) + .setBinding( + (config, value) -> config.hudRefreshRateTicks = value, + config -> config.hudRefreshRateTicks) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(hudShowFPSOption = OptionImpl.createBuilder(DetailLevel.class, storage) + .setId(id("hud_show_fps", DetailLevel.class)) + .setControl(option -> new CyclingControl<>(option, DetailLevel.class)) + .setBinding( + (config, value) -> config.hudShowFPS = value, + config -> config.hudShowFPS) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(int.class, storage) + .setId(id("hud_frame_time_buffer_size", int.class)) + .setControl(option -> + new SliderControl(option, 0, 30, 5, + v -> Component.translatable("nolijium.seconds", Math.max(1, v)))) + .setBinding( + (config, value) -> config.hudFrameTimeBufferSize = Math.max(1, value), + config -> (int) config.hudFrameTimeBufferSize) + .setEnabledPredicate(() -> hudEnabledOption.getValue() && hudShowFPSOption.getValue() == DetailLevel.EXTENDED) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_show_cpu", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudShowCPU = value, + config -> config.hudShowCPU) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_show_memory", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudShowMemory = value, + config -> config.hudShowMemory) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("hud_show_coordinates", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hudShowCoordinates = value, + config -> config.hudShowCoordinates) + .setEnabledPredicate(hudEnabledOption::getValue) + .build()) + .build()); + + togglesPage.add(OptionGroup.createBuilder() + .setId(id("toggles")) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("revert_damage_camera_tilt", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.revertDamageCameraTilt = value, + config -> config.revertDamageCameraTilt) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_block_animations", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.disableTextureAnimations = value, + config -> config.disableTextureAnimations) + .build()) + .build()); + + final Option disableAllToastsOption; + togglesPage.add(OptionGroup.createBuilder() + .setId(id("toasts")) + .add(disableAllToastsOption = OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_all_toasts", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideAllToasts = value, + config -> config.hideAllToasts) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_advancement_toasts", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideAdvancementToasts = value, + config -> config.hideAdvancementToasts) + .setEnabledPredicate(() -> !disableAllToastsOption.getValue()) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_recipe_toasts", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideRecipeToasts = value, + config -> config.hideRecipeToasts) + .setEnabledPredicate(() -> !disableAllToastsOption.getValue()) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_system_toasts", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideSystemToasts = value, + config -> config.hideSystemToasts) + .setEnabledPredicate(() -> !disableAllToastsOption.getValue()) + .build()) + .add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_tutorial_toasts", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideTutorialToasts = value, + config -> config.hideTutorialToasts) + .setEnabledPredicate(() -> !disableAllToastsOption.getValue()) + .build()) + .build()); + + final Option hideAllParticlesOption; + + particlesPage.add(OptionGroup.createBuilder() + .setId(id("particles")) + .add(hideAllParticlesOption = OptionImpl.createBuilder(boolean.class, storage) + .setId(id("disable_particles", boolean.class)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> config.hideParticles = value, + config -> config.hideParticles) + .build()) + .build()); + + final OptionGroup.Builder particlesByIdBuilder = OptionGroup.createBuilder().setId(id("particles_by_id")); + + for (final ResourceLocation particleTypeId : + BuiltInRegistries.PARTICLE_TYPE.keySet().stream().sorted().toArray(ResourceLocation[]::new)) { + final String name = particleTypeId.toString(); + particlesByIdBuilder.add(OptionImpl.createBuilder(boolean.class, storage) + .setId(id("enable_particle/" + name.replace(':', '/'), boolean.class)) + .setName(Component.literal(name)) + .setTooltip(Component.translatable("nolijium.particle_id", name)) + .setControl(TickBoxControl::new) + .setBinding( + (config, value) -> { + final boolean present = config.hideParticlesByID.contains(name); + if (!value && !present) + config.hideParticlesByID.add(name); + else if (value && present) + config.hideParticlesByID.remove(name); + }, + config -> !config.hideParticlesByID.contains(name)) + .setEnabledPredicate(() -> !hideAllParticlesOption.getValue()) + .build()); + } + + particlesPage.add(particlesByIdBuilder.build()); + + event.getPages().add(new OptionPage( + id("utilities"), + Component.translatable("nolijium.utilities"), + ImmutableList.copyOf(utilitiesPage) + )); + event.getPages().add(new OptionPage( + id("toggles"), + Component.translatable("nolijium.toggles"), + ImmutableList.copyOf(togglesPage) + )); + event.getPages().add(new OptionPage( + id("particles"), + Component.translatable("nolijium.particles"), + ImmutableList.copyOf(particlesPage) + )); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumOptionsStorage.java b/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumOptionsStorage.java new file mode 100644 index 0000000..173f88d --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/integration/embeddium/NolijiumOptionsStorage.java @@ -0,0 +1,21 @@ +package dev.nolij.nolijium.integration.embeddium; + +import dev.nolij.nolijium.impl.Nolijium; +import dev.nolij.nolijium.impl.config.NolijiumConfigImpl; +import org.embeddedt.embeddium.api.options.structure.OptionStorage; + +public class NolijiumOptionsStorage implements OptionStorage { + + private final NolijiumConfigImpl storage = Nolijium.config.clone(); + + @Override + public NolijiumConfigImpl getData() { + return storage; + } + + @Override + public void save() { + NolijiumConfigImpl.replace(storage); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/ChatComponentMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/ChatComponentMixin.java new file mode 100644 index 0000000..07075d8 --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/ChatComponentMixin.java @@ -0,0 +1,23 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import dev.nolij.nolijium.impl.Nolijium; +import net.minecraft.client.gui.components.ChatComponent; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +@Mixin(ChatComponent.class) +public class ChatComponentMixin { + + @ModifyConstant(method = { + "addMessageToDisplayQueue", + "addMessageToQueue", + "addRecentChat", + }, constant = @Constant(intValue = 100)) + public int nolijium$MAX_CHAT_HISTORY(int constant) { + return Nolijium.config.maxChatHistory > 0 + ? Nolijium.config.maxChatHistory + : Integer.MAX_VALUE; + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/GameRendererMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/GameRendererMixin.java new file mode 100644 index 0000000..6740297 --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/GameRendererMixin.java @@ -0,0 +1,22 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.nolij.nolijium.impl.Nolijium; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.world.entity.LivingEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GameRenderer.class) +public class GameRendererMixin { + + @WrapOperation(method = "bobHurt", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;getHurtDir()F")) + public float nolijium$bobHurt$getHurtDir(LivingEntity instance, Operation original) { + if (Nolijium.config.revertDamageCameraTilt) + return 0F; + + return original.call(instance); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelLightEngineMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelLightEngineMixin.java new file mode 100644 index 0000000..1e122c1 --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelLightEngineMixin.java @@ -0,0 +1,33 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import dev.nolij.nolijium.impl.Nolijium; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.lighting.LevelLightEngine; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LevelLightEngine.class) +public class LevelLightEngineMixin { + + @Shadow @Final protected LevelHeightAccessor levelHeightAccessor; + + @Inject(method = "checkBlock", at = @At("HEAD"), cancellable = true) + public void nolijium$checkBlock$HEAD(BlockPos blockPos, CallbackInfo ci) { + if (Nolijium.config.enableGamma && levelHeightAccessor instanceof ClientLevel) + ci.cancel(); + } + + @Inject(method = "runLightUpdates", at = @At("HEAD"), cancellable = true) + public void nolijium$runLightUpdates$HEAD(CallbackInfoReturnable cir) { + if (Nolijium.config.enableGamma && levelHeightAccessor instanceof ClientLevel) + cir.setReturnValue(0); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelRendererMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelRendererMixin.java new file mode 100644 index 0000000..e58887a --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LevelRendererMixin.java @@ -0,0 +1,39 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import dev.nolij.nolijium.impl.Nolijium; +import dev.nolij.nolijium.neoforge.NolijiumNeoForge; +import net.minecraft.client.particle.Particle; +import net.minecraft.client.renderer.LevelRenderer; +import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.core.registries.BuiltInRegistries; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LevelRenderer.class) +public class LevelRendererMixin { + + @Inject( + method = "addParticleInternal(Lnet/minecraft/core/particles/ParticleOptions;ZZDDDDDD)Lnet/minecraft/client/particle/Particle;", + at = @At("HEAD"), + cancellable = true + ) + public void nolijium$addParticleInternal$HEAD(ParticleOptions particleOptions, boolean p_109806_, boolean p_109807_, double p_109808_, double p_109809_, double p_109810_, double p_109811_, double p_109812_, double p_109813_, CallbackInfoReturnable cir) { + if (Nolijium.config.hideParticles) { + cir.setReturnValue(null); + return; + } + + if (NolijiumNeoForge.blockedParticleTypeIDs.isEmpty()) + return; + + var key = BuiltInRegistries.PARTICLE_TYPE.getKey(particleOptions.getType()); + if (key == null) + return; + + if (NolijiumNeoForge.blockedParticleTypeIDs.contains(key)) + cir.setReturnValue(null); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LightTextureMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LightTextureMixin.java new file mode 100644 index 0000000..362649c --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/LightTextureMixin.java @@ -0,0 +1,36 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.nolij.nolijium.impl.Nolijium; +import net.minecraft.client.renderer.LightTexture; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(LightTexture.class) +public class LightTextureMixin { + + @WrapOperation( + method = "updateLightTexture", + at = @At( + value = "INVOKE", + target = "Ljava/lang/Double;floatValue()F", + remap = false, + ordinal = 1 + )) + public float nolijium$updateLightTexture$floatValue(Double instance, Operation original) { + if (Nolijium.config.enableGamma) + return 1E7F; + + return original.call(instance); + } + + @Inject(method = "calculateDarknessScale", at = @At("HEAD"), cancellable = true) + public void nolijium$calculateDarknessScale$HEAD(CallbackInfoReturnable cir) { + if (Nolijium.config.enableGamma) + cir.setReturnValue(0F); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/TextureAtlasMixin.java b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/TextureAtlasMixin.java new file mode 100644 index 0000000..0bdfd6e --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/mixin/neoforge/TextureAtlasMixin.java @@ -0,0 +1,24 @@ +package dev.nolij.nolijium.mixin.neoforge; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.nolij.nolijium.impl.Nolijium; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.util.List; + +@Mixin(TextureAtlas.class) +public class TextureAtlasMixin { + + @WrapOperation(method = "cycleAnimationFrames", at = @At(value = "FIELD", target = "Lnet/minecraft/client/renderer/texture/TextureAtlas;animatedTextures:Ljava/util/List;")) + public List nolijium$cycleAnimationFrames$animatedTextures(TextureAtlas instance, Operation> original) { + if (Nolijium.config.disableTextureAnimations) + return List.of(); + + return original.call(instance); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumHUDRenderLayer.java b/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumHUDRenderLayer.java new file mode 100644 index 0000000..b28ddbd --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumHUDRenderLayer.java @@ -0,0 +1,259 @@ +package dev.nolij.nolijium.neoforge; + +import com.sun.management.OperatingSystemMXBean; +import dev.nolij.nolijium.impl.Nolijium; +import dev.nolij.nolijium.impl.util.Alignment; +import dev.nolij.nolijium.impl.util.DetailLevel; +import dev.nolij.nolijium.impl.util.MathHelper; +import dev.nolij.nolijium.impl.util.SlidingLongBuffer; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.LayeredDraw; +import net.minecraft.client.player.LocalPlayer; +import org.jetbrains.annotations.NotNull; + +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; + +public class NolijiumHUDRenderLayer implements LayeredDraw.Layer { + + private static final boolean DEBUG = false; + + private static final int TEXT_COLOUR = 0xFFFFFFFF; + private static final int BACKGROUND_COLOUR = 0x90505050; + + private static final Font FONT = Minecraft.getInstance().font; + private static final int LINE_HEIGHT = FONT.lineHeight + 3; + + private static final OperatingSystemMXBean OS_BEAN = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class); + + private static final int FRAME_TIME_BUFFER_SIZE_MAX = 2 << 16; + private static final int FRAME_TIME_BUFFER_SIZE_MIN = 2 << 6; + private static final int FRAME_TIME_BUFFER_RESIZE_THRESHOLD = 2 << 8; + private static final long FRAME_TIME_BUFFER_RESIZE_INTERVAL = (long) 1E9; + + private long lastUpdateTimestamp = 0L; + private int screenWidth = 0, screenHeight = 0; + + private static class Line { + + public String text; + public int posX; + public int width; + + public Line(String text, int posX, int width) { + this.text = text; + this.posX = posX; + this.width = width; + } + + } + + private List lines = List.of(); + private int posY = 0; + private boolean background = false; + + private final SlidingLongBuffer frameTimeBuffer = new SlidingLongBuffer(FRAME_TIME_BUFFER_SIZE_MIN); + private long lastResizeTimestamp = 0L; + private long lastFrameTimestamp = 0L; + private long lastFrameTime = 0L; + + private void resizeFrameTimeBuffer() { + lastResizeTimestamp = System.nanoTime(); + + if (!frameTimeBuffer.isFull()) + return; + + var duration = 0L; + + for (int i = 0; i < frameTimeBuffer.size(); i++) { + duration += frameTimeBuffer.get(i); + } + + final long target = (long) (Nolijium.config.hudFrameTimeBufferSize * 1E9); + final int newSize = (int) MathHelper.clamp( + (frameTimeBuffer.maxSize() * (double) target / duration), + FRAME_TIME_BUFFER_SIZE_MIN, + FRAME_TIME_BUFFER_SIZE_MAX); + + if (Math.abs(newSize - frameTimeBuffer.maxSize()) > FRAME_TIME_BUFFER_RESIZE_THRESHOLD) { + frameTimeBuffer.resize(newSize); + } + } + + private static int getFrameTimeFPS(long frameTime) { + return (int) (1E9D / frameTime); + } + + private List getLines() { + var result = new ArrayList(); + + if (Nolijium.config.hudShowFPS != DetailLevel.NONE) { + synchronized (frameTimeBuffer) { + final int fps = getFrameTimeFPS(lastFrameTime); + result.add("FPS: %d".formatted(fps)); + + if (Nolijium.config.hudShowFPS == DetailLevel.EXTENDED) { + final String leftPad = Nolijium.config.hudAlignmentX == Alignment.X.LEFT ? " " : ""; + if (!frameTimeBuffer.isEmpty()) { + long max = -Long.MAX_VALUE; + long min = Long.MAX_VALUE; + long avg = 0L; + + for (int i = 0; i < frameTimeBuffer.size(); i++) { + long frameTime = frameTimeBuffer.getUnsafe(i); + max = Math.max(max, frameTime); + min = Math.min(min, frameTime); + avg += frameTime; + } + avg /= frameTimeBuffer.size(); + + result.add("%sMIN: %d".formatted(leftPad, getFrameTimeFPS(max))); + result.add("%sMAX: %d".formatted(leftPad, getFrameTimeFPS(min))); + result.add("%sAVG: %d".formatted(leftPad, getFrameTimeFPS(avg))); + if (DEBUG) + result.add("%sSIZE: %d".formatted(leftPad, frameTimeBuffer.maxSize())); + result.add(""); + } else { + result.add("%sMIN: ???".formatted(leftPad)); + result.add("%sMAX: ???".formatted(leftPad)); + result.add("%sAVG: ???".formatted(leftPad)); + if (DEBUG) + result.add("%sSIZE: %d".formatted(leftPad, frameTimeBuffer.maxSize())); + result.add(""); + } + } + } + } + + if (Nolijium.config.hudShowCPU) { + var cpuUsage = OS_BEAN.getProcessCpuLoad(); + if (cpuUsage == -1) + cpuUsage = OS_BEAN.getCpuLoad(); + + result.add("CPU: %2.2f%%".formatted(cpuUsage * 100D)); + } + + if (Nolijium.config.hudShowMemory) { + final long maxMemory = Runtime.getRuntime().maxMemory(); + final long totalMemory = Runtime.getRuntime().totalMemory(); + final long freeMemory = Runtime.getRuntime().freeMemory(); + final long usedMemory = totalMemory - freeMemory; + + result.add("RAM: %2.2f%% %d/%dMiB" + .formatted( + usedMemory * 100D / maxMemory, + usedMemory / (1024 * 1024), + maxMemory / (1024 * 1024))); + } + + if (Nolijium.config.hudShowCoordinates) { + if (!result.isEmpty()) + result.add(""); + + final LocalPlayer player = Minecraft.getInstance().player; + if (player != null) { + result.add("X: %.2f Y: %.2f Z: %.2f".formatted(player.getX(), player.getY(), player.getZ())); + } else { + result.add("X: ?.?? Y: ?.?? Z: ?.??"); + } + } + + return result + .stream() + .map(x -> new Line(x, 0, FONT.width(x))) + .toList(); + } + + private void onFrame(@NotNull GuiGraphics guiGraphics) { + screenWidth = guiGraphics.guiWidth(); + screenHeight = guiGraphics.guiHeight(); + + if (Nolijium.config.hudShowFPS != DetailLevel.NONE) { + final long timestamp = System.nanoTime(); + + if (lastFrameTimestamp != 0L) { + lastFrameTime = timestamp - lastFrameTimestamp; + if (Nolijium.config.hudShowFPS == DetailLevel.EXTENDED) { + if (timestamp - lastResizeTimestamp > FRAME_TIME_BUFFER_RESIZE_INTERVAL) { + resizeFrameTimeBuffer(); + } + + frameTimeBuffer.push(lastFrameTime); + } + } else { + lastResizeTimestamp = System.nanoTime(); + } + + lastFrameTimestamp = timestamp; + } + } + + private void update() { + lastUpdateTimestamp = System.nanoTime(); + + lines = getLines(); + + background = Nolijium.config.hudBackground; + + if (Nolijium.config.hudAlignmentX == Alignment.X.RIGHT) { + lines.parallelStream().forEach(line -> + line.posX = screenWidth - line.width - Nolijium.config.hudMarginX); + } else { + lines.parallelStream().forEach(line -> + line.posX = Nolijium.config.hudMarginX); + } + + if (Nolijium.config.hudAlignmentY == Alignment.Y.BOTTOM) { + final int height = lines.size() * LINE_HEIGHT; + + posY = screenHeight - height - Nolijium.config.hudMarginY + 2; + } else { + posY = Nolijium.config.hudMarginY - 2; + } + } + + private boolean isHidden() { + return + !Nolijium.config.hudEnabled || + Minecraft.getInstance().options.hideGui || + (Nolijium.config.hudAlignmentY == Alignment.Y.TOP && Minecraft.getInstance().getDebugOverlay().showDebugScreen()); + } + + @Override + public void render(@NotNull GuiGraphics guiGraphics, @NotNull DeltaTracker deltaTracker) { + if (isHidden()) + return; + + this.onFrame(guiGraphics); + + if (Nolijium.config.hudRefreshRateTicks == 0 || + System.nanoTime() - lastUpdateTimestamp > Nolijium.config.hudRefreshRateTicks * 50E6) + update(); + + //noinspection deprecation + guiGraphics.drawManaged(() -> { + var linePosY = posY; + for (var line : lines) { + if (!line.text.isEmpty()) { + if (background) + guiGraphics.fill( + line.posX - 2, linePosY, + line.posX + line.width + (Nolijium.config.hudShadow ? 2 : 1), linePosY + LINE_HEIGHT, + BACKGROUND_COLOUR); + + guiGraphics.drawString( + FONT, line.text, + line.posX, linePosY + 2, + TEXT_COLOUR, Nolijium.config.hudShadow); + } + + linePosY += LINE_HEIGHT; + } + }); + } + +} diff --git a/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumNeoForge.java b/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumNeoForge.java new file mode 100644 index 0000000..ba1a067 --- /dev/null +++ b/neoforge/src/main/java/dev/nolij/nolijium/neoforge/NolijiumNeoForge.java @@ -0,0 +1,97 @@ +package dev.nolij.nolijium.neoforge; + +import dev.nolij.nolijium.impl.INolijiumImplementation; +import dev.nolij.nolijium.impl.Nolijium; +import dev.nolij.nolijium.impl.config.NolijiumConfigImpl; +import dev.nolij.nolijium.impl.util.MethodHandleHelper; +import dev.nolij.nolijium.integration.embeddium.NolijiumEmbeddiumConfigScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.toasts.AdvancementToast; +import net.minecraft.client.gui.components.toasts.RecipeToast; +import net.minecraft.client.gui.components.toasts.SystemToast; +import net.minecraft.client.gui.components.toasts.Toast; +import net.minecraft.client.gui.components.toasts.TutorialToast; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.EventPriority; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLPaths; +import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent; +import net.neoforged.neoforge.client.event.ToastAddEvent; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; +import net.neoforged.neoforge.common.NeoForge; + +import java.lang.invoke.MethodHandles; +import java.util.Set; +import java.util.stream.Collectors; + +import static dev.nolij.nolijium.impl.NolijiumConstants.*; + +@Mod(value = MOD_ID, dist = Dist.CLIENT) +public class NolijiumNeoForge implements INolijiumImplementation { + + private static final MethodHandleHelper METHOD_HANDLE_HELPER = + new MethodHandleHelper(NolijiumNeoForge.class.getClassLoader(), MethodHandles.lookup()); + + public NolijiumNeoForge(IEventBus modEventBus, ModContainer modContainer) { + Nolijium.LOGGER.info("Loading Nolijium..."); + + modContainer.registerExtensionPoint( + IConfigScreenFactory.class, + (minecraft, parent) -> + new Screen(Component.empty()) { + @Override + protected void init() { + NolijiumConfigImpl.openConfigFile(); + Minecraft.getInstance().setScreen(parent); + } + }); + + Nolijium.registerImplementation(this, FMLPaths.CONFIGDIR.get()); + + modEventBus.addListener(this::onRegisterGuiLayers); + NeoForge.EVENT_BUS.addListener(EventPriority.LOWEST, this::onAddToast); + + if (METHOD_HANDLE_HELPER.getClassOrNull("org.embeddedt.embeddium.api.OptionGUIConstructionEvent") != null) + new NolijiumEmbeddiumConfigScreen(); + } + + public static Set blockedParticleTypeIDs = Set.of(); + + @Override + public void onConfigReload(NolijiumConfigImpl config) { + blockedParticleTypeIDs = config.hideParticlesByID + .stream() + .map(ResourceLocation::parse) + .collect(Collectors.toUnmodifiableSet()); + } + + private void onRegisterGuiLayers(RegisterGuiLayersEvent event) { + event.registerAboveAll(ResourceLocation.fromNamespaceAndPath(MOD_ID, "hud"), new NolijiumHUDRenderLayer()); + } + + private void onAddToast(ToastAddEvent event) { + if (event.isCanceled()) + return; + + if (Nolijium.config.hideAllToasts) { + event.setCanceled(true); + return; + } + + final Toast toast = event.getToast(); + + switch (toast) { + case AdvancementToast advancementToast -> event.setCanceled(Nolijium.config.hideAdvancementToasts); + case RecipeToast recipeToast -> event.setCanceled(Nolijium.config.hideRecipeToasts); + case SystemToast systemToast -> event.setCanceled(Nolijium.config.hideSystemToasts); + case TutorialToast tutorialToast -> event.setCanceled(Nolijium.config.hideTutorialToasts); + default -> {} + } + } + +} diff --git a/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..786d70c --- /dev/null +++ b/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,39 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="OSL-3.0" +issueTrackerURL="${issue_url}" + +[[mixins]] +config="nolijium-neoforge.mixins.json" + +[[mods]] +modId="nolijium" +version="${mod_version}" +displayName="${mod_name}" +displayURL="${mod_url}" +logoFile="icon.png" +authors="${nolij}" +description=""" +${mod_description} +""" + +[[dependencies.nolijium]] +modId="neoforge" +type="required" +versionRange="${neoforge_neoforge_range}" +ordering="NONE" +side="CLIENT" + +[[dependencies.nolijium]] +modId="minecraft" +type="required" +versionRange="${neoforge_minecraft_range}" +ordering="NONE" +side="CLIENT" + +[[dependencies.nolijium]] +modId="embeddium" +type="optional" +versionRange="${embeddium_range}" +ordering="AFTER" +side="CLIENT" diff --git a/neoforge/src/main/resources/nolijium-neoforge.mixins.json b/neoforge/src/main/resources/nolijium-neoforge.mixins.json new file mode 100644 index 0000000..f6ef6fa --- /dev/null +++ b/neoforge/src/main/resources/nolijium-neoforge.mixins.json @@ -0,0 +1,17 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.nolij.nolijium.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "neoforge.ChatComponentMixin", + "neoforge.GameRendererMixin", + "neoforge.LevelLightEngineMixin", + "neoforge.LevelRendererMixin", + "neoforge.LightTextureMixin", + "neoforge.TextureAtlasMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/proguard.pro b/proguard.pro new file mode 100644 index 0000000..3f594a1 --- /dev/null +++ b/proguard.pro @@ -0,0 +1,50 @@ +-ignorewarnings +-dontnote +-optimizationpasses 10 +-optimizations !class/merging/*,!method/marking/private,!method/marking/static,!*/specialization/*,!method/removal/parameter +-allowaccessmodification +#noinspection ShrinkerInvalidFlags +-optimizeaggressively +-repackageclasses nolijium +-keepattributes Runtime*Annotations,AnnotationDefault + +-keepclassmembers class dev.nolij.nolijium.impl.config.NolijiumConfigImpl { # dont rename config fields + @dev.nolij.nolijium.zson.ZsonField ; +} +-keepclassmembers,allowoptimization class dev.nolij.nolijium.NolijiumMixinPlugin { + public *; +} +-keep @org.spongepowered.asm.mixin.Mixin class * { + @org.spongepowered.asm.mixin.Overwrite *; + @org.spongepowered.asm.mixin.Shadow *; +} +-keepclassmembers,allowobfuscation @org.spongepowered.asm.mixin.Mixin class * { *; } + +# Forge entrypoints +-keep,allowobfuscation @*.*.fml.common.Mod class dev.nolij.nolijium.** { + public (...); +} + +-adaptclassstrings +-adaptresourcefilecontents fabric.mod.json + +# screens +-keepclassmembers class dev.nolij.nolijium.** extends net.minecraft.class_437, + net.minecraft.client.gui.screens.Screen { + public *; +} + +# Fabric entrypoints +-keep,allowoptimization,allowobfuscation class dev.nolij.nolijium.fabric.NolijiumFabric +-keep,allowoptimization,allowobfuscation class dev.nolij.nolijium.fabric.integration.modmenu.NolijiumModMenuIntegration + +-keep @dev.nolij.zumegradle.proguard.ProGuardKeep class * { *; } +-keepclassmembers class * { @dev.nolij.zumegradle.proguard.ProGuardKeep *; } + +-keep,allowobfuscation @dev.nolij.zumegradle.proguard.ProGuardKeep$WithObfuscation class * { *; } +-keepclassmembers,allowobfuscation class * { @dev.nolij.zumegradle.proguard.ProGuardKeep$WithObfuscation *; } + +-keepclassmembers @dev.nolij.zumegradle.proguard.ProGuardKeep$Enum enum * { + **[] values(); + ** valueOf(...); +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..78323c0 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0") +} + +rootProject.name = "nolijium" + +include("stubs") +include("api") +include("neoforge") \ No newline at end of file diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..f03d9e0 --- /dev/null +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,39 @@ +modLoader="javafml" +loaderVersion="[1,)" +license="OSL-3.0" +issueTrackerURL="${issue_url}" + +[[mixins]] +config="nolijium.mixins.json" + +[[mods]] +modId="nolijium" +version="${mod_version}" +displayName="${mod_name}" +displayURL="${mod_url}" +logoFile="icon.png" +authors="${nolij}" +description=""" +${mod_description} +""" + +[[dependencies.nolijium]] +modId="neoforge" +type="required" +versionRange="${neoforge_neoforge_range}" +ordering="NONE" +side="CLIENT" + +[[dependencies.nolijium]] +modId="minecraft" +type="required" +versionRange="${neoforge_minecraft_range}" +ordering="NONE" +side="CLIENT" + +[[dependencies.nolijium]] +modId="embeddium" +type="optional" +versionRange="${embeddium_range}" +ordering="AFTER" +side="CLIENT" diff --git a/stubs/build.gradle.kts b/stubs/build.gradle.kts new file mode 100644 index 0000000..7a0bde2 --- /dev/null +++ b/stubs/build.gradle.kts @@ -0,0 +1 @@ +operator fun String.invoke(): String = rootProject.properties[this] as? String ?: error("Property $this not found") \ No newline at end of file diff --git a/stubs/src/main/java/dev/nolij/zumegradle/proguard/ProGuardKeep.java b/stubs/src/main/java/dev/nolij/zumegradle/proguard/ProGuardKeep.java new file mode 100644 index 0000000..e566628 --- /dev/null +++ b/stubs/src/main/java/dev/nolij/zumegradle/proguard/ProGuardKeep.java @@ -0,0 +1,20 @@ +package dev.nolij.zumegradle.proguard; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) +public @interface ProGuardKeep { + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD}) + @interface WithObfuscation {} + + @Retention(RetentionPolicy.CLASS) + @Target({ElementType.TYPE}) + @interface Enum {} + +}