diff --git a/.editorconfig b/.editorconfig index a1323c50..a05b73f3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,120 +1,152 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories root = true -[*.{h,c,hpp,cpp,def}] +[CMakeLists.txt] +# Indentation and spacing indent_size = 4 indent_style = space -end_of_line = lf -insert_final_newline = false - -# Project files -[*.{csproj,targets,props}] -indent_size = 2 -indent_style = space +tab_width = 4 # C# files [*.cs] +guidelines = 80, 120 #### Core EditorConfig Options #### # Indentation and spacing indent_size = 4 indent_style = space +tab_width = 4 # New line preferences end_of_line = crlf insert_final_newline = false -max_line_length = 160 #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true +file_header_template = unset # this. and Me. preferences -dotnet_style_qualification_for_event = false:warning -dotnet_style_qualification_for_field = false:warning -dotnet_style_qualification_for_method = false:warning -dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false # Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:warning -dotnet_style_predefined_type_for_member_access = true:warning +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true # Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity # Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members # Expression-level preferences -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_throw_expression = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:warning -dotnet_style_null_propagation = true:warning -dotnet_style_object_initializer = true:suggestion -dotnet_style_prefer_auto_properties = true:warning -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = false +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true # Field preferences -dotnet_style_readonly_field = true:warning +dotnet_style_readonly_field = true # Parameter preferences -dotnet_code_quality_unused_parameters = all:suggestion +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = true:warning -csharp_style_var_for_built_in_types = true:warning -csharp_style_var_when_type_is_apparent = true:warning +csharp_style_var_elsewhere = true +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true # Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = when_on_single_line:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:warning -csharp_style_expression_bodied_methods = false:suggestion -csharp_style_expression_bodied_operators = when_on_single_line:silent -csharp_style_expression_bodied_properties = when_on_single_line:silent +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true # Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true # Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion +csharp_style_conditional_delegate_call = true # Modifier preferences -csharp_prefer_static_local_function = true:suggestion -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true # Code-block preferences -csharp_prefer_braces = when_multiline:silent -csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = file_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true # Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable # 'using' directive preferences -csharp_using_directive_placement = outside_namespace:silent +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true #### C# Formatting Rules #### @@ -167,121 +199,80 @@ csharp_preserve_single_line_statements = true # Naming rules -dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.private_or_internal_field_should_be__camelcase.severity = warning -dotnet_naming_rule.private_or_internal_field_should_be__camelcase.symbols = private_or_internal_field -dotnet_naming_rule.private_or_internal_field_should_be__camelcase.style = _camelcase +dotnet_naming_rule.non_public_fields_should_begin_with_underscoree.severity = suggestion +dotnet_naming_rule.non_public_fields_should_begin_with_underscoree.symbols = non_public_fields +dotnet_naming_rule.non_public_fields_should_begin_with_underscoree.style = begins_with_underscore -dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.severity = warning -dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.symbols = private_or_internal_static_field -dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.style = _camelcase +dotnet_naming_rule.public_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.public_fields_should_be_pascal_case.symbols = public_fields +dotnet_naming_rule.public_fields_should_be_pascal_case.style = pascal_case -dotnet_naming_rule.types_should_be_pascal_case.severity = warning -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +dotnet_naming_rule.const_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.const_fields_should_be_pascal_case.symbols = const_fields +dotnet_naming_rule.const_fields_should_be_pascal_case.style = pascal_case # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field -dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private -dotnet_naming_symbols.private_or_internal_field.required_modifiers = - -dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field -dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private -dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.struct.applicable_kinds = struct +dotnet_naming_symbols.struct.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.struct.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.non_public_fields.applicable_kinds = field +dotnet_naming_symbols.non_public_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_public_fields.required_modifiers = -dotnet_naming_symbols.constant_fields.applicable_kinds = namespace, property, field, event, parameter, class, struct, interface, enum, delegate, method, local_function -dotnet_naming_symbols.constant_fields.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_symbols.public_fields.applicable_kinds = field +dotnet_naming_symbols.public_fields.applicable_accessibilities = public +dotnet_naming_symbols.public_fields.required_modifiers = + +dotnet_naming_symbols.const_fields.applicable_kinds = field +dotnet_naming_symbols.const_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected, public +dotnet_naming_symbols.const_fields.required_modifiers = const # Naming styles -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style._camelcase.required_prefix = _ -dotnet_naming_style._camelcase.required_suffix = -dotnet_naming_style._camelcase.word_separator = -dotnet_naming_style._camelcase.capitalization = camel_case - -dotnet_naming_style.upper_case.required_prefix = -dotnet_naming_style.upper_case.required_suffix = -dotnet_naming_style.upper_case.word_separator = _ -dotnet_naming_style.upper_case.capitalization = all_upper - -# S1125: Boolean literals should not be redundant -dotnet_diagnostic.S1125.severity = none - -# S1135: Track uses of "TODO" tags -# Justification: Keep track of uncompleted work -dotnet_diagnostic.S1135.severity = warning - -# CS0618: Method is obsolete -# Justification: We're deprecating a lot of our own methods right now -dotnet_diagnostic.CS0618.severity = none - -# S3881: Fix this implementation of 'IDisposable' to conform to the dispose pattern. -# Justification: Let implementation decide whether to use dispose pattern. -dotnet_diagnostic.S3881.severity = none +dotnet_naming_style.begins_with_underscore.required_prefix = _ +dotnet_naming_style.begins_with_underscore.required_suffix = +dotnet_naming_style.begins_with_underscore.word_separator = +dotnet_naming_style.begins_with_underscore.capitalization = camel_case -# S3453: Classes should not have only "private" constructors -# Justification: We use types with factory methods -dotnet_diagnostic.S3453.severity = none - -# CA1720: Identifier contains type name -# Justfication: Interop and type handing makes this diagnostic annoying -dotnet_diagnostic.CA1720.severity = none - -# S1172: Unused method parameters should be removed -# Justification: Diagnostic is faulty -dotnet_diagnostic.S1172.severity = none - -# IDE0046: Convert to conditional expression -dotnet_diagnostic.IDE0046.severity = silent - -[**TestMode**/**.cs] +# IDE0290: Use primary constructor +dotnet_diagnostic.IDE0290.severity = silent # CA1822: Mark members as static -# Justification: ECS system events -dotnet_diagnostic.CA1822.severity = none - -# S3218: Inner class members should not shadow outer class "static" or type members -# Justification: command systems "default" groups commands -dotnet_diagnostic.S3218.severity = none - -[**Tests/**.cs] - -# Test projects - -# CA1707: Identifiers should not contain underscores -dotnet_code_quality.CA1707.api_surface = private, internal, friend +# Justification: Event handlers in ECS must be instance methods. +dotnet_diagnostic.CA1822.severity = none \ No newline at end of file diff --git a/.github/workflows/component-linux.yml b/.github/workflows/component-linux.yml index 1e2aaa57..be3dc528 100644 --- a/.github/workflows/component-linux.yml +++ b/.github/workflows/component-linux.yml @@ -1,7 +1,4 @@ -name: CI - Component x64 Linux (Coming Soon) - -# TODO: Update this workflow when component (x64) code is in src/sampsharp-component/ -# For now, this is a placeholder. +name: CI - Component x64 (Linux) on: push: @@ -11,16 +8,38 @@ on: paths: - .github/workflows/component-linux.yml - src/sampsharp-component/** + - external/** + - build.sh pull_request: branches: - master - release/1.* paths: + - .github/workflows/component-linux.yml - src/sampsharp-component/** + - external/** + - build.sh jobs: - placeholder: + build: runs-on: ubuntu-latest + steps: - - run: echo "New component (x64) build on Linux - not yet implemented" + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y gcc g++ cmake + + - name: Build component + run: ./build.sh component publish + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: sampsharp-component-linux + path: build/artifacts/sampsharp-component/ diff --git a/.github/workflows/component-win.yml b/.github/workflows/component-win.yml deleted file mode 100644 index 8dac2609..00000000 --- a/.github/workflows/component-win.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: CI - Component x64 (Coming Soon) - -# TODO: Update this workflow when component (x64) code is in src/sampsharp-component/ -# For now, this is a placeholder. - -on: - push: - branches: - - master - - release/1.* - paths: - - .github/workflows/component-win.yml - - src/sampsharp-component/** - - pull_request: - branches: - - master - - release/1.* - paths: - - src/sampsharp-component/** - -jobs: - placeholder: - runs-on: windows-latest - steps: - - run: echo "New component (x64) build on Windows - not yet implemented" diff --git a/.github/workflows/component-win64.yml b/.github/workflows/component-win64.yml new file mode 100644 index 00000000..6e8ea9f4 --- /dev/null +++ b/.github/workflows/component-win64.yml @@ -0,0 +1,43 @@ +name: CI - Component x64 (Windows) + +on: + push: + branches: + - master + - release/1.* + paths: + - .github/workflows/component-win64.yml + - src/sampsharp-component/** + - external/** + - build.cmd + + pull_request: + branches: + - master + - release/1.* + paths: + - .github/workflows/component-win64.yml + - src/sampsharp-component/** + - external/** + - build.cmd + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup CMake + uses: lukka/get-cmake@latest + + - name: Build component + run: .\build.cmd component publish + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: sampsharp-component-windows + path: build/artifacts/sampsharp-component/ diff --git a/.github/workflows/dotnet-component.yml b/.github/workflows/dotnet-component.yml new file mode 100644 index 00000000..ad7a95f9 --- /dev/null +++ b/.github/workflows/dotnet-component.yml @@ -0,0 +1,50 @@ +name: CI - Component Libraries + +on: + push: + branches: + - master + - release/1.* + paths: + - .github/workflows/dotnet-component.yml + - src/SampSharp.*/** + - src/TestMode.*/** + - SampSharp.sln + - Directory.Build.props + - build.sh + + pull_request: + branches: + - master + - release/1.* + paths: + - .github/workflows/dotnet-component.yml + - src/SampSharp.*/** + - src/TestMode.*/** + - SampSharp.sln + - Directory.Build.props + - build.sh + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Build component libraries + run: ./build.sh component-libraries + + - name: Pack component libraries + run: ./build.sh component-libraries publish + + - name: Upload NuGet packages + uses: actions/upload-artifact@v4 + with: + name: sampsharp-component-nuget-packages + path: build/artifacts/packages/ diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml deleted file mode 100644 index 33b88f9f..00000000 --- a/.github/workflows/dotnet.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: CI - New SampSharp (Coming Soon) - -# TODO: Update this workflow when new SampSharp code is in src/sampsharp/ -# For now, this is a placeholder. - -on: - push: - branches: - - master - - release/1.* - paths: - - .github/workflows/dotnet.yml - - src/sampsharp/** - - pull_request: - branches: - - master - - release/1.* - paths: - - src/sampsharp/** - -jobs: - placeholder: - runs-on: ubuntu-latest - steps: - - run: echo "New SampSharp build workflow - not yet implemented" \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 69e4abe0..8004b0ff 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,115 @@ -name: Release - New SampSharp (Coming Soon) - -# TODO: Update this workflow when new SampSharp code is ready for release -# For now, this is a placeholder. +name: Release - SampSharp on: push: tags: - '1.*' - + jobs: - placeholder: + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup CMake + uses: lukka/get-cmake@latest + + - name: Build open.mp component + run: .\build.cmd component publish + + - name: Upload Windows component + uses: actions/upload-artifact@v4 + with: + name: component-windows + path: build/artifacts/sampsharp-component/ + + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y gcc g++ cmake + + - name: Build open.mp component + run: ./build.sh component publish + + - name: Upload Linux component + uses: actions/upload-artifact@v4 + with: + name: component-linux + path: build/artifacts/sampsharp-component/ + + release: + needs: [build-windows, build-linux] runs-on: ubuntu-latest steps: - - run: echo "New SampSharp release workflow - not yet implemented" \ No newline at end of file + - uses: actions/checkout@v4 + + - name: Set VERSION + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Download Windows component + uses: actions/download-artifact@v4 + with: + name: component-windows + path: ./component-windows + + - name: Download Linux component + uses: actions/download-artifact@v4 + with: + name: component-linux + path: ./component-linux + + - name: Create release package + run: | + mkdir -p SampSharp-${{ env.VERSION }}/components + cp component-windows/SampSharp.dll SampSharp-${{ env.VERSION }}/components/ + cp component-linux/SampSharp.so SampSharp-${{ env.VERSION }}/components/SampSharp.so + zip -r SampSharp-${{ env.VERSION }}.zip SampSharp-${{ env.VERSION }}/ + + - name: Create Release + uses: actions/create-release@v1 + id: create_release + with: + tag_name: ${{ github.ref }} + release_name: SampSharp ${{ env.VERSION }} + draft: true + prerelease: ${{ contains(env.VERSION, 'alpha') || contains(env.VERSION, 'beta') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload component package + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: SampSharp-${{ env.VERSION }}.zip + asset_name: SampSharp-${{ env.VERSION }}.zip + asset_content_type: application/zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Pack NuGet packages + run: ./build.sh component-libraries publish + env: + CiVersion: ${{ env.VERSION }} + + - name: Publish to NuGet + run: | + for nupkg in build/artifacts/packages/*.nupkg; do + dotnet nuget push "$nupkg" \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --skip-duplicate + done diff --git a/.gitignore b/.gitignore index 5d720ce6..8c75dadf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ # Build and output +[Bb]in/ +[Oo]bj/ build/ -bin/ -obj/ +build_*/ +publish/ +PublishProfiles/ *.log # IDE @@ -11,9 +14,7 @@ obj/ *.user *.suo *.sln.DotSettings.user - -# Publishing -PublishProfiles/ +launchSettings.json # OS .DS_Store @@ -24,4 +25,4 @@ desktop.ini *~ *.tmp *.swp -*.swo \ No newline at end of file +*.swo diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..5edfc196 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "external/sdk"] + path = external/sdk + url = https://github.com/openmultiplayer/open.mp-sdk + branch = master diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..da2d5d6c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,91 @@ + + + $(MSBuildProjectName.Contains('.UnitTests')) + $(MSBuildProjectName.Contains('TestMode.')) + True + + + + + $(MSBuildThisFileDirectory)build/bin/$(MSBuildProjectName)/ + $(MSBuildThisFileDirectory)build/obj/$(MSBuildProjectName)/ + $(MSBuildThisFileDirectory)build/artifacts/packages + + + + + Tim Potze + Tim Potze + Copyright (c) Tim Potze 2014-$([System.DateTime]::UtcNow.ToString(yyyy)) + en-GB + + + + https://github.com/ikkentim/SampSharp + sampsharp.png + MIT + gta samp sampsharp + $(AssemblyName) + $(AssemblyName) + true + + + + + + + + + 0.0.0 + local + + + + $(CiVersion.Split('-', 2)[0]) + + $(CiVersion.Split('-', 2)[1]) + + + + + + true + latest + + + Minimum + false + + + false + false + + + true + true + + + + + + + + + + + true + snupkg + + + + true + + + + + + + + true + + \ No newline at end of file diff --git a/SampSharp.sln b/SampSharp.sln new file mode 100644 index 00000000..4bd1f4f3 --- /dev/null +++ b/SampSharp.sln @@ -0,0 +1,106 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34316.72 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampSharp.OpenMp.Core", "src\SampSharp.OpenMp.Core\SampSharp.OpenMp.Core.csproj", "{25744E3B-22E4-4296-A3E7-285FEBE6D8EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampSharp.SourceGenerator", "src\SampSharp.SourceGenerator\SampSharp.SourceGenerator.csproj", "{B46B7075-DE9F-4627-996D-B58A7F518BC7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1A8859FD-936F-4B65-ADCF-D9699094B734}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitignore = .gitignore + Directory.Build.props = Directory.Build.props + LICENSE = LICENSE + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestMode.OpenMp.Core", "src\TestMode.OpenMp.Core\TestMode.OpenMp.Core.csproj", "{4262C3BF-74C9-4BC2-9775-D05866DCCA6D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampSharp.Analyzer", "src\SampSharp.Analyzer\SampSharp.Analyzer.csproj", "{C07D4977-7729-44C1-A4DA-7E2D0249F836}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeAnalysis", "CodeAnalysis", "{1779A01D-61FA-4683-A154-88970557D5F7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampSharp.CodeFixes", "src\SampSharp.CodeFixes\SampSharp.CodeFixes.csproj", "{C1EB9CED-D661-462D-BB8F-F32864B5149F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SampSharp", "SampSharp", "{D523C91A-D20C-4674-9958-FF43E131BBE6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestingModes", "TestingModes", "{B729F640-F930-4631-97EF-B649E23ACE5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampSharp.Sdk", "src\SampSharp.Sdk\SampSharp.Sdk.csproj", "{BAECE101-E831-41BC-BBCB-6464B4AB305A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampSharp.OpenMp.Entities", "src\SampSharp.OpenMp.Entities\SampSharp.OpenMp.Entities.csproj", "{F9D7210F-F87D-4FC1-90DF-A2496B7D0253}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampSharp.OpenMp.Entities.Commands", "src\SampSharp.OpenMp.Entities.Commands\SampSharp.OpenMp.Entities.Commands.csproj", "{A291514D-ACC7-4C69-ABB3-8D71D3691BB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestMode.OpenMp.Entities", "src\TestMode.OpenMp.Entities\TestMode.OpenMp.Entities.csproj", "{EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestMode.UnitTests", "src\TestMode.UnitTests\TestMode.UnitTests.csproj", "{0209A3FA-DA60-4FD7-BD06-38E4FC316F07}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25744E3B-22E4-4296-A3E7-285FEBE6D8EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25744E3B-22E4-4296-A3E7-285FEBE6D8EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25744E3B-22E4-4296-A3E7-285FEBE6D8EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25744E3B-22E4-4296-A3E7-285FEBE6D8EB}.Release|Any CPU.Build.0 = Release|Any CPU + {B46B7075-DE9F-4627-996D-B58A7F518BC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B46B7075-DE9F-4627-996D-B58A7F518BC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B46B7075-DE9F-4627-996D-B58A7F518BC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B46B7075-DE9F-4627-996D-B58A7F518BC7}.Release|Any CPU.Build.0 = Release|Any CPU + {4262C3BF-74C9-4BC2-9775-D05866DCCA6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4262C3BF-74C9-4BC2-9775-D05866DCCA6D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4262C3BF-74C9-4BC2-9775-D05866DCCA6D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4262C3BF-74C9-4BC2-9775-D05866DCCA6D}.Release|Any CPU.Build.0 = Release|Any CPU + {C07D4977-7729-44C1-A4DA-7E2D0249F836}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C07D4977-7729-44C1-A4DA-7E2D0249F836}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C07D4977-7729-44C1-A4DA-7E2D0249F836}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C07D4977-7729-44C1-A4DA-7E2D0249F836}.Release|Any CPU.Build.0 = Release|Any CPU + {C1EB9CED-D661-462D-BB8F-F32864B5149F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1EB9CED-D661-462D-BB8F-F32864B5149F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1EB9CED-D661-462D-BB8F-F32864B5149F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1EB9CED-D661-462D-BB8F-F32864B5149F}.Release|Any CPU.Build.0 = Release|Any CPU + {BAECE101-E831-41BC-BBCB-6464B4AB305A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAECE101-E831-41BC-BBCB-6464B4AB305A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAECE101-E831-41BC-BBCB-6464B4AB305A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAECE101-E831-41BC-BBCB-6464B4AB305A}.Release|Any CPU.Build.0 = Release|Any CPU + {F9D7210F-F87D-4FC1-90DF-A2496B7D0253}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F9D7210F-F87D-4FC1-90DF-A2496B7D0253}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F9D7210F-F87D-4FC1-90DF-A2496B7D0253}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F9D7210F-F87D-4FC1-90DF-A2496B7D0253}.Release|Any CPU.Build.0 = Release|Any CPU + {A291514D-ACC7-4C69-ABB3-8D71D3691BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A291514D-ACC7-4C69-ABB3-8D71D3691BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A291514D-ACC7-4C69-ABB3-8D71D3691BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A291514D-ACC7-4C69-ABB3-8D71D3691BB4}.Release|Any CPU.Build.0 = Release|Any CPU + {EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327}.Release|Any CPU.Build.0 = Release|Any CPU + {0209A3FA-DA60-4FD7-BD06-38E4FC316F07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0209A3FA-DA60-4FD7-BD06-38E4FC316F07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0209A3FA-DA60-4FD7-BD06-38E4FC316F07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0209A3FA-DA60-4FD7-BD06-38E4FC316F07}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {25744E3B-22E4-4296-A3E7-285FEBE6D8EB} = {D523C91A-D20C-4674-9958-FF43E131BBE6} + {B46B7075-DE9F-4627-996D-B58A7F518BC7} = {1779A01D-61FA-4683-A154-88970557D5F7} + {4262C3BF-74C9-4BC2-9775-D05866DCCA6D} = {B729F640-F930-4631-97EF-B649E23ACE5A} + {C07D4977-7729-44C1-A4DA-7E2D0249F836} = {1779A01D-61FA-4683-A154-88970557D5F7} + {C1EB9CED-D661-462D-BB8F-F32864B5149F} = {1779A01D-61FA-4683-A154-88970557D5F7} + {BAECE101-E831-41BC-BBCB-6464B4AB305A} = {D523C91A-D20C-4674-9958-FF43E131BBE6} + {F9D7210F-F87D-4FC1-90DF-A2496B7D0253} = {D523C91A-D20C-4674-9958-FF43E131BBE6} + {A291514D-ACC7-4C69-ABB3-8D71D3691BB4} = {D523C91A-D20C-4674-9958-FF43E131BBE6} + {EA866E1C-37C0-42C7-B0F7-F8B9BF4B3327} = {B729F640-F930-4631-97EF-B649E23ACE5A} + {0209A3FA-DA60-4FD7-BD06-38E4FC316F07} = {B729F640-F930-4631-97EF-B649E23ACE5A} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A40756BA-E781-4BEA-924E-BBE3AC843108} + EndGlobalSection +EndGlobal diff --git a/SampSharp.sln.DotSettings b/SampSharp.sln.DotSettings new file mode 100644 index 00000000..22ad5e2d --- /dev/null +++ b/SampSharp.sln.DotSettings @@ -0,0 +1,19 @@ + + FQN + GTA + ID + INPC + NPC + RPC + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/build.cmd b/build.cmd index b67e2fc4..7d8779f2 100644 --- a/build.cmd +++ b/build.cmd @@ -39,13 +39,33 @@ if /i "%TARGET%"=="legacy-libraries" ( ) if /i "%TARGET%"=="component" ( - echo Component x64 plugin build not yet implemented. - goto end + if "%ACTION%"=="" ( + echo Building open.mp component... + call "%SCRIPTDIR%src\sampsharp-component\build.cmd" + goto end + ) else if /i "%ACTION%"=="publish" ( + echo Building and publishing open.mp component... + call "%SCRIPTDIR%src\sampsharp-component\build.cmd" + if errorlevel 1 exit /b 1 + call "%SCRIPTDIR%src\sampsharp-component\publish.cmd" + goto end + ) else ( + goto usage + ) ) if /i "%TARGET%"=="component-libraries" ( - echo Component C# libraries not yet implemented. - goto end + if "%ACTION%"=="" ( + echo Building C# libraries... + call :build_component_libraries + goto end + ) else if /i "%ACTION%"=="publish" ( + echo Building and packing C# libraries... + call :pack_component_libraries + goto end + ) else ( + goto usage + ) ) if /i "%TARGET%"=="clean" ( @@ -68,8 +88,10 @@ echo build.cmd legacy-plugin - Build legacy x86 plugin echo build.cmd legacy-plugin publish - Build and publish legacy x86 plugin echo build.cmd legacy-libraries - Build legacy C# libraries echo build.cmd legacy-libraries publish - Build and pack legacy C# libraries -echo build.cmd component - Build component x64 plugin (not implemented) -echo build.cmd component-libraries - Build component C# libraries (not implemented) +echo build.cmd component - Build open.mp component +echo build.cmd component publish - Build and publish open.mp component +echo build.cmd component-libraries - Build C# libraries +echo build.cmd component-libraries publish - Build and pack C# libraries echo build.cmd clean - Delete build directory contents exit /b 1 @@ -91,5 +113,23 @@ echo. echo NuGet packages created in: %SCRIPTDIR%build\artifacts\packages exit /b 0 +:build_component_libraries +cd /d "%SCRIPTDIR%" +echo. +echo Building C# libraries... +dotnet build SampSharp.sln -c Release +if errorlevel 1 exit /b 1 +exit /b 0 + +:pack_component_libraries +cd /d "%SCRIPTDIR%" +echo. +echo Packing C# libraries... +dotnet pack SampSharp.sln -c Release +if errorlevel 1 exit /b 1 +echo. +echo NuGet packages created in: %SCRIPTDIR%build\artifacts\packages +exit /b 0 + :end echo Build complete. diff --git a/build.sh b/build.sh index 020f5739..3efd1a96 100755 --- a/build.sh +++ b/build.sh @@ -13,11 +13,42 @@ show_usage() { echo " build.sh legacy-plugin publish - Build and publish legacy x86 plugin" echo " build.sh legacy-libraries - Build legacy C# libraries" echo " build.sh legacy-libraries publish - Build and pack legacy C# libraries" - echo " build.sh component - Build component x64 plugin (not implemented)" - echo " build.sh component-libraries - Build component C# libraries (not implemented)" + echo " build.sh component - Build open.mp component" + echo " build.sh component publish - Build and publish open.mp component" + echo " build.sh component-libraries - Build C# libraries" + echo " build.sh component-libraries publish - Build and pack C# libraries" echo " build.sh clean - Delete build directory contents" } +build_component_libraries() { + local SCRIPTDIR="$1" + cd "$SCRIPTDIR" + + echo "" + echo "Building C# libraries..." + if [ -n "$CiVersion" ]; then + dotnet build SampSharp.sln -c Release "/p:CiVersion=$CiVersion" + else + dotnet build SampSharp.sln -c Release + fi +} + +pack_component_libraries() { + local SCRIPTDIR="$1" + cd "$SCRIPTDIR" + + echo "" + echo "Packing C# libraries..." + if [ -n "$CiVersion" ]; then + dotnet pack SampSharp.sln -c Release "/p:CiVersion=$CiVersion" + else + dotnet pack SampSharp.sln -c Release + fi + + echo "" + echo "NuGet packages created in: $SCRIPTDIR/build/artifacts/packages" +} + build_legacy_libraries() { local SCRIPTDIR="$1" cd "$SCRIPTDIR/src/legacy" @@ -81,10 +112,27 @@ case "$TARGET" in fi ;; component) - echo "Component x64 plugin build not yet implemented." + if [ -z "$ACTION" ]; then + echo "Building open.mp component..." + "$SCRIPTDIR/src/sampsharp-component/build.sh" + elif [ "$ACTION" = "publish" ]; then + echo "Building and publishing open.mp component..." + "$SCRIPTDIR/src/sampsharp-component/build.sh" + "$SCRIPTDIR/src/sampsharp-component/publish.sh" + else + show_usage + exit 1 + fi ;; component-libraries) - echo "Component C# libraries not yet implemented." + if [ -z "$ACTION" ]; then + build_component_libraries "$SCRIPTDIR" + elif [ "$ACTION" = "publish" ]; then + pack_component_libraries "$SCRIPTDIR" + else + show_usage + exit 1 + fi ;; clean) echo "Cleaning build directory..." diff --git a/external/dotnet/README.md b/external/dotnet/README.md new file mode 100644 index 00000000..49348d47 --- /dev/null +++ b/external/dotnet/README.md @@ -0,0 +1,6 @@ +## Source + +Version 10.0.7 from: + +- https://www.nuget.org/packages/Microsoft.NETCore.App.Host.linux-x64 +- https://www.nuget.org/packages/Microsoft.NETCore.App.Host.win-x64 diff --git a/external/dotnet/coreclr_delegates.h b/external/dotnet/coreclr_delegates.h new file mode 100644 index 00000000..1a175803 --- /dev/null +++ b/external/dotnet/coreclr_delegates.h @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_CORECLR_DELEGATES_H +#define HAVE_CORECLR_DELEGATES_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +#if defined(_WIN32) + #define CORECLR_DELEGATE_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define CORECLR_DELEGATE_CALLTYPE + typedef char char_t; +#endif + +#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1) + +// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer +typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_and_get_function_pointer_fn)( + const char_t *assembly_path /* Fully qualified path to assembly */, + const char_t *type_name /* Assembly qualified type name */, + const char_t *method_name /* Public static method name compatible with delegateType */, + const char_t *delegate_type_name /* Assembly qualified delegate type name or null + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void **delegate /* Pointer where to store the function pointer result */); + +// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default) +typedef int (CORECLR_DELEGATE_CALLTYPE *component_entry_point_fn)(void *arg, int32_t arg_size_in_bytes); + +typedef int (CORECLR_DELEGATE_CALLTYPE *get_function_pointer_fn)( + const char_t *type_name /* Assembly qualified type name */, + const char_t *method_name /* Public static method name compatible with delegateType */, + const char_t *delegate_type_name /* Assembly qualified delegate type name or null, + or UNMANAGEDCALLERSONLY_METHOD if the method is marked with + the UnmanagedCallersOnlyAttribute. */, + void *load_context /* Extensibility parameter (currently unused and must be 0) */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */, + /*out*/ void **delegate /* Pointer where to store the function pointer result */); + +typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_fn)( + const char_t *assembly_path /* Fully qualified path to assembly */, + void *load_context /* Extensibility parameter (currently unused and must be 0) */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */); + +typedef int (CORECLR_DELEGATE_CALLTYPE *load_assembly_bytes_fn)( + const void *assembly_bytes /* Bytes of the assembly to load */, + size_t assembly_bytes_len /* Byte length of the assembly to load */, + const void *symbols_bytes /* Optional. Bytes of the symbols for the assembly */, + size_t symbols_bytes_len /* Optional. Byte length of the symbols for the assembly */, + void *load_context /* Extensibility parameter (currently unused and must be 0) */, + void *reserved /* Extensibility parameter (currently unused and must be 0) */); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // HAVE_CORECLR_DELEGATES_H diff --git a/external/dotnet/hostfxr.h b/external/dotnet/hostfxr.h new file mode 100644 index 00000000..3b203a89 --- /dev/null +++ b/external/dotnet/hostfxr.h @@ -0,0 +1,427 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_HOSTFXR_H +#define HAVE_HOSTFXR_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + +#if defined(_WIN32) + #define HOSTFXR_CALLTYPE __cdecl + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #define HOSTFXR_CALLTYPE + typedef char char_t; +#endif + +enum hostfxr_delegate_type +{ + hdt_com_activation, + hdt_load_in_memory_assembly, + hdt_winrt_activation, + hdt_com_register, + hdt_com_unregister, + hdt_load_assembly_and_get_function_pointer, + hdt_get_function_pointer, + hdt_load_assembly, + hdt_load_assembly_bytes, +}; + +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_fn)(const int argc, const char_t **argv); +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_main_startupinfo_fn)( + const int argc, + const char_t **argv, + const char_t *host_path, + const char_t *dotnet_root, + const char_t *app_path); +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)( + const int argc, + const char_t** argv, + const char_t* host_path, + const char_t* dotnet_root, + const char_t* app_path, + int64_t bundle_header_offset); + +typedef void(HOSTFXR_CALLTYPE *hostfxr_error_writer_fn)(const char_t *message); + +// +// Sets a callback which is to be used to write errors to. +// +// Parameters: +// error_writer +// A callback function which will be invoked every time an error is to be reported. +// Or nullptr to unregister previously registered callback and return to the default behavior. +// Return value: +// The previously registered callback (which is now unregistered), or nullptr if no previous callback +// was registered +// +// The error writer is registered per-thread, so the registration is thread-local. On each thread +// only one callback can be registered. Subsequent registrations overwrite the previous ones. +// +// By default no callback is registered in which case the errors are written to stderr. +// +// Each call to the error writer is sort of like writing a single line (the EOL character is omitted). +// Multiple calls to the error writer may occur for one failure. +// +// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer +// will be propagated to hostpolicy for the duration of the call. This means that errors from +// both hostfxr and hostpolicy will be reporter through the same error writer. +// +typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE *hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer); + +typedef void* hostfxr_handle; +struct hostfxr_initialize_parameters +{ + size_t size; + const char_t *host_path; + const char_t *dotnet_root; +}; + +// +// Initializes the hosting components for a dotnet command line running an application +// +// Parameters: +// argc +// Number of argv arguments +// argv +// Command-line arguments for running an application (as if through the dotnet executable). +// Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported. +// For example 'app.dll app_argument_1 app_argument_2`. +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// HostInvalidState - Hosting components are already initialized +// +// This function parses the specified command-line arguments to determine the application to run. It will +// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and +// dependencies and prepare everything needed to load the runtime. +// +// This function only supports arguments for running an application. It does not support SDK commands. +// +// This function does not load the runtime. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_dotnet_command_line_fn)( + int argc, + const char_t **argv, + const struct hostfxr_initialize_parameters *parameters, + /*out*/ hostfxr_handle *host_context_handle); + +// +// Initializes the hosting components using a .runtimeconfig.json file +// +// Parameters: +// runtime_config_path +// Path to the .runtimeconfig.json file +// parameters +// Optional. Additional parameters for initialization +// host_context_handle +// On success, this will be populated with an opaque value representing the initialized host context +// +// Return value: +// Success - Hosting components were successfully initialized +// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components +// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components +// HostIncompatibleConfig - Config is incompatible with already initialized hosting components +// +// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed +// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that +// may be next to the .runtimeconfig.json). +// +// This function does not load the runtime. +// +// If called when the runtime has already been loaded, this function will check if the specified runtime +// config is compatible with the existing runtime. +// +// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful +// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that +// the difference in properties is acceptable. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_initialize_for_runtime_config_fn)( + const char_t *runtime_config_path, + const struct hostfxr_initialize_parameters *parameters, + /*out*/ hostfxr_handle *host_context_handle); + +// +// Gets the runtime property value for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Out parameter. Pointer to a buffer with the property value. +// +// Return value: +// The error code result. +// +// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// property value for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t *name, + /*out*/ const char_t **value); + +// +// Sets the value of a runtime property for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// name +// Runtime property name +// value +// Value to set +// +// Return value: +// The error code result. +// +// Setting properties is only supported for the first host context, before the runtime has been loaded. +// +// If the property already exists in the host context, it will be overwritten. If value is nullptr, the +// property will be removed. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_set_runtime_property_value_fn)( + const hostfxr_handle host_context_handle, + const char_t *name, + const char_t *value); + +// +// Gets all the runtime properties for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// count +// [in] Size of the keys and values buffers +// [out] Number of properties returned (size of keys/values buffers used). If the input value is too +// small or keys/values is nullptr, this is populated with the number of available properties +// keys +// Array of pointers to buffers with runtime property keys +// values +// Array of pointers to buffers with runtime property values +// +// Return value: +// The error code result. +// +// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only +// guaranteed until any of the below occur: +// - a 'run' method is called for the host context +// - properties are changed via hostfxr_set_runtime_property_value +// - the host context is closed via 'hostfxr_close' +// +// If host_context_handle is nullptr and an active host context exists, this function will get the +// properties for the active host context. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_properties_fn)( + const hostfxr_handle host_context_handle, + /*inout*/ size_t * count, + /*out*/ const char_t **keys, + /*out*/ const char_t **values); + +// +// Load CoreCLR and run the application for an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// If the app was successfully run, the exit code of the application. Otherwise, the error code result. +// +// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line. +// +// This function will not return until the managed application exits. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_run_app_fn)(const hostfxr_handle host_context_handle); + +// +// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one. +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// type +// Type of runtime delegate requested +// delegate +// An out parameter that will be assigned the delegate. +// +// Return value: +// The error code result. +// +// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config, +// then all delegate types are supported. +// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line, +// then only the following delegate types are currently supported: +// hdt_load_assembly_and_get_function_pointer +// hdt_get_function_pointer +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_get_runtime_delegate_fn)( + const hostfxr_handle host_context_handle, + enum hostfxr_delegate_type type, + /*out*/ void **delegate); + +// +// Closes an initialized host context +// +// Parameters: +// host_context_handle +// Handle to the initialized host context +// +// Return value: +// The error code result. +// +typedef int32_t(HOSTFXR_CALLTYPE *hostfxr_close_fn)(const hostfxr_handle host_context_handle); + +struct hostfxr_dotnet_environment_sdk_info +{ + size_t size; + const char_t* version; + const char_t* path; +}; + +struct hostfxr_dotnet_environment_framework_info +{ + size_t size; + const char_t* name; + const char_t* version; + const char_t* path; +}; + +struct hostfxr_dotnet_environment_info +{ + size_t size; + + const char_t* hostfxr_version; + const char_t* hostfxr_commit_hash; + + size_t sdk_count; + const struct hostfxr_dotnet_environment_sdk_info* sdks; + + size_t framework_count; + const struct hostfxr_dotnet_environment_framework_info* frameworks; +}; + +typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)( + const struct hostfxr_dotnet_environment_info* info, + void* result_context); + +// +// Returns available SDKs and frameworks. +// +// Resolves the existing SDKs and frameworks from a dotnet root directory (if +// any), or the global default location. If multi-level lookup is enabled and +// the dotnet root location is different than the global location, the SDKs and +// frameworks will be enumerated from both locations. +// +// The SDKs are sorted in ascending order by version, multi-level lookup +// locations are put before private ones. +// +// The frameworks are sorted in ascending order by name followed by version, +// multi-level lookup locations are put before private ones. +// +// Parameters: +// dotnet_root +// The path to a directory containing a dotnet executable. +// +// reserved +// Reserved for future parameters. +// +// result +// Callback invoke to return the list of SDKs and frameworks. +// Structs and their elements are valid for the duration of the call. +// +// result_context +// Additional context passed to the result callback. +// +// Return value: +// 0 on success, otherwise failure. +// +// String encoding: +// Windows - UTF-16 (pal::char_t is 2 byte wchar_t) +// Non-Windows - UTF-8 (pal::char_t is 1 byte char) +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_fn)( + const char_t* dotnet_root, + void* reserved, + hostfxr_get_dotnet_environment_info_result_fn result, + void* result_context); + +struct hostfxr_framework_result +{ + size_t size; + const char_t* name; + const char_t* requested_version; + const char_t* resolved_version; + const char_t* resolved_path; +}; + +struct hostfxr_resolve_frameworks_result +{ + size_t size; + + size_t resolved_count; + const struct hostfxr_framework_result* resolved_frameworks; + + size_t unresolved_count; + const struct hostfxr_framework_result* unresolved_frameworks; +}; + +typedef void (HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_result_fn)( + const struct hostfxr_resolve_frameworks_result* result, + void* result_context); + +// +// Resolves frameworks for a runtime config +// +// Parameters: +// runtime_config_path +// Path to the .runtimeconfig.json file +// parameters +// Optional. Additional parameters for initialization. +// If null or dotnet_root is null, the root corresponding to the running hostfx is used. +// callback +// Optional. Result callback invoked with result of the resolution. +// Structs and their elements are valid for the duration of the call. +// result_context +// Optional. Additional context passed to the result callback. +// +// Return value: +// 0 on success, otherwise failure. +// +// String encoding: +// Windows - UTF-16 (pal::char_t is 2-byte wchar_t) +// Non-Windows - UTF-8 (pal::char_t is 1-byte char) +// +typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_resolve_frameworks_for_runtime_config_fn)( + const char_t* runtime_config_path, + /*opt*/ const struct hostfxr_initialize_parameters* parameters, + /*opt*/ hostfxr_resolve_frameworks_result_fn callback, + /*opt*/ void* result_context); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // HAVE_HOSTFXR_H diff --git a/external/dotnet/libnethost.a b/external/dotnet/libnethost.a new file mode 100644 index 00000000..4b6bd3b4 Binary files /dev/null and b/external/dotnet/libnethost.a differ diff --git a/external/dotnet/libnethost.lib b/external/dotnet/libnethost.lib new file mode 100644 index 00000000..a86ced08 Binary files /dev/null and b/external/dotnet/libnethost.lib differ diff --git a/external/dotnet/libnethost.pdb b/external/dotnet/libnethost.pdb new file mode 100644 index 00000000..6c3ad74a Binary files /dev/null and b/external/dotnet/libnethost.pdb differ diff --git a/external/dotnet/libnethost.so b/external/dotnet/libnethost.so new file mode 100644 index 00000000..d0559bf9 Binary files /dev/null and b/external/dotnet/libnethost.so differ diff --git a/external/dotnet/nethost.dll b/external/dotnet/nethost.dll new file mode 100644 index 00000000..9d7592be Binary files /dev/null and b/external/dotnet/nethost.dll differ diff --git a/external/dotnet/nethost.h b/external/dotnet/nethost.h new file mode 100644 index 00000000..ef420e81 --- /dev/null +++ b/external/dotnet/nethost.h @@ -0,0 +1,100 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef HAVE_NETHOST_H +#define HAVE_NETHOST_H + +#include + +#ifdef _WIN32 + #ifdef NETHOST_EXPORT + #define NETHOST_API __declspec(dllexport) + #else + // Consuming the nethost as a static library + // Shouldn't export attempt to dllimport. + #ifdef NETHOST_USE_AS_STATIC + #define NETHOST_API + #else + #define NETHOST_API __declspec(dllimport) + #endif + #endif + + #define NETHOST_CALLTYPE __stdcall + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + #ifdef NETHOST_EXPORT + #define NETHOST_API __attribute__((__visibility__("default"))) + #else + #define NETHOST_API + #endif + + #define NETHOST_CALLTYPE + typedef char char_t; +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + +// Parameters for get_hostfxr_path +// +// Fields: +// size +// Size of the struct. This is used for versioning. +// +// assembly_path +// Path to the component's assembly. +// If specified, hostfxr is located as if the assembly_path is the apphost +// +// dotnet_root +// Path to directory containing the dotnet executable. +// If specified, hostfxr is located as if an application is started using +// 'dotnet app.dll', which means it will be searched for under the dotnet_root +// path and the assembly_path is ignored. +// +struct get_hostfxr_parameters { + size_t size; + const char_t *assembly_path; + const char_t *dotnet_root; +}; + +// +// Get the path to the hostfxr library +// +// Parameters: +// buffer +// Buffer that will be populated with the hostfxr path, including a null terminator. +// +// buffer_size +// [in] Size of buffer in char_t units. +// [out] Size of buffer used in char_t units. If the input value is too small +// or buffer is nullptr, this is populated with the minimum required size +// in char_t units for a buffer to hold the hostfxr path +// +// get_hostfxr_parameters +// Optional. Parameters that modify the behaviour for locating the hostfxr library. +// If nullptr, hostfxr is located using the environment variable or global registration +// +// Return value: +// 0 on success, otherwise failure +// 0x80008098 - buffer is too small (HostApiBufferTooSmall) +// +// Remarks: +// The full search for the hostfxr library is done on every call. To minimize the need +// to call this function multiple times, pass a large buffer (e.g. PATH_MAX). +// +NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path( + char_t * buffer, + size_t * buffer_size, + const struct get_hostfxr_parameters *parameters); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // HAVE_NETHOST_H diff --git a/external/dotnet/nethost.lib b/external/dotnet/nethost.lib new file mode 100644 index 00000000..e9923c79 Binary files /dev/null and b/external/dotnet/nethost.lib differ diff --git a/external/jetbrains/Annotations.cs b/external/jetbrains/Annotations.cs new file mode 100644 index 00000000..9b8b37bc --- /dev/null +++ b/external/jetbrains/Annotations.cs @@ -0,0 +1,1998 @@ +/* MIT License + +Copyright (c) 2016 JetBrains http://www.jetbrains.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. */ + +#nullable disable + +// ReSharper disable once RedundantUsingDirective +using System; +#pragma warning disable IDE0079 // Remove unnecessary suppression +#pragma warning disable CA1069 +#pragma warning disable IDE0290 // Use primary constructor +// ReSharper disable UnusedType.Global +// ReSharper disable UnusedMember.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable IntroduceOptionalParameters.Global +// ReSharper disable MemberCanBeProtected.Global +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable RedundantTypeDeclarationBody +// ReSharper disable ArrangeNamespaceBody +// ReSharper disable InconsistentNaming + +namespace JetBrains.Annotations +{ + /// + /// Indicates that the value of the marked element could be sometimes, + /// so checking for is required before its usage. + /// + /// + /// [CanBeNull] object Test() => null; + /// + /// void UseTest() { + /// var p = Test(); + /// var s = p.ToString(); // Warning: Possible 'System.NullReferenceException' + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] +internal sealed class CanBeNullAttribute : Attribute { } + + /// + /// Indicates that the value of the marked element can never be . + /// + /// + /// [NotNull] object Foo() { + /// return null; // Warning: Possible 'null' assignment + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field | AttributeTargets.Event | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.GenericParameter)] +internal sealed class NotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property, + /// or of the Lazy.Value property can never be null. + /// + /// + /// public void Foo([ItemNotNull]List<string> books) + /// { + /// foreach (var book in books) { + /// if (book != null) // Warning: Expression is always true + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] +internal sealed class ItemNotNullAttribute : Attribute { } + + /// + /// Can be applied to symbols of types derived from IEnumerable as well as to symbols of Task + /// and Lazy classes to indicate that the value of a collection item, of the Task.Result property, + /// or of the Lazy.Value property can be null. + /// + /// + /// public void Foo([ItemCanBeNull]List<string> books) + /// { + /// foreach (var book in books) + /// { + /// // Warning: Possible 'System.NullReferenceException' + /// Console.WriteLine(book.ToUpper()); + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Property | + AttributeTargets.Delegate | AttributeTargets.Field)] +internal sealed class ItemCanBeNullAttribute : Attribute { } + + /// + /// Indicates that the marked method builds a string by the format pattern and (optional) arguments. + /// The parameter that accepts the format string should be specified in the constructor. The format string + /// should be in the -like form. + /// + /// + /// [StringFormatMethod("message")] + /// void ShowError(string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// ShowError("Failed: {0}"); // Warning: Non-existing argument in format string + /// } + /// + /// + [AttributeUsage( + AttributeTargets.Constructor | AttributeTargets.Method | + AttributeTargets.Property | AttributeTargets.Delegate)] +internal sealed class StringFormatMethodAttribute : Attribute + { + /// + /// Specifies which parameter of an annotated method should be treated as the format string. + /// + public StringFormatMethodAttribute([NotNull] string formatParameterName) + { + FormatParameterName = formatParameterName; + } + + [NotNull] public string FormatParameterName { get; } + } + + /// + /// Indicates that the marked parameter is a message template where placeholders are to be replaced by + /// the following arguments in the order in which they appear. + /// + /// + /// void LogInfo([StructuredMessageTemplate]string message, params object[] args) { /* do something */ } + /// + /// void Foo() { + /// LogInfo("User created: {username}"); // Warning: Non-existing argument in format string + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class StructuredMessageTemplateAttribute : Attribute {} + + /// + /// Use this annotation to specify a type that contains static or const fields + /// with values for the annotated property/field/parameter. + /// The specified type will be used to improve completion suggestions. + /// + /// + /// namespace TestNamespace + /// { + /// public class Constants + /// { + /// public static int INT_CONST = 1; + /// public const string STRING_CONST = "1"; + /// } + /// + /// public class Class1 + /// { + /// [ValueProvider("TestNamespace.Constants")] public int myField; + /// public void Foo([ValueProvider("TestNamespace.Constants")] string str) { } + /// + /// public void Test() + /// { + /// Foo(/*try completion here*/);// + /// myField = /*try completion here*/ + /// } + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field, + AllowMultiple = true)] +internal sealed class ValueProviderAttribute : Attribute + { + public ValueProviderAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Indicates that the integral value falls into the specified interval. + /// It is allowed to specify multiple non-intersecting intervals. + /// Values of interval boundaries are included in the interval. + /// + /// + /// void Foo([ValueRange(0, 100)] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate, + AllowMultiple = true)] +internal sealed class ValueRangeAttribute : Attribute + { + public object From { get; } + public object To { get; } + + public ValueRangeAttribute(long from, long to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(ulong from, ulong to) + { + From = from; + To = to; + } + + public ValueRangeAttribute(long value) + { + From = To = value; + } + + public ValueRangeAttribute(ulong value) + { + From = To = value; + } + } + + /// + /// Indicates that the integral value never falls below zero. + /// + /// + /// void Foo([NonNegativeValue] int value) { + /// if (value == -1) { // Warning: Expression is always 'false' + /// ... + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | + AttributeTargets.Method | AttributeTargets.Delegate)] +internal sealed class NonNegativeValueAttribute : Attribute { } + + /// + /// Indicates that the function argument should be a string literal and match + /// one of the parameters of the caller function. This annotation is used for parameters + /// like string paramName parameter of the constructor. + /// + /// + /// void Foo(string param) { + /// if (param == null) + /// throw new ArgumentNullException("par"); // Warning: Cannot resolve symbol + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class InvokerParameterNameAttribute : Attribute { } + + /// + /// Indicates that the method is contained in a type that implements the + /// System.ComponentModel.INotifyPropertyChanged interface and this method + /// is used to notify that some property value changed. + /// + /// + /// The method should be non-static and conform to one of the supported signatures: + /// + /// NotifyChanged(string) + /// NotifyChanged(params string[]) + /// NotifyChanged{T}(Expression{Func{T}}) + /// NotifyChanged{T,U}(Expression{Func{T,U}}) + /// SetProperty{T}(ref T, T, string) + /// + /// + /// + /// public class Foo : INotifyPropertyChanged { + /// public event PropertyChangedEventHandler PropertyChanged; + /// + /// [NotifyPropertyChangedInvocator] + /// protected virtual void NotifyChanged(string propertyName) { ... } + /// + /// string _name; + /// + /// public string Name { + /// get { return _name; } + /// set { _name = value; NotifyChanged("LastName"); /* Warning */ } + /// } + /// } + /// + /// Examples of generated notifications: + /// + /// NotifyChanged("Property") + /// NotifyChanged(() => Property) + /// NotifyChanged((VM x) => x.Property) + /// SetProperty(ref myField, value, "Property") + /// + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class NotifyPropertyChangedInvocatorAttribute : Attribute + { + public NotifyPropertyChangedInvocatorAttribute() { } + public NotifyPropertyChangedInvocatorAttribute([NotNull] string parameterName) + { + ParameterName = parameterName; + } + + [CanBeNull] public string ParameterName { get; } + } + + /// + /// Describes dependence between the input and output of a method. + /// + /// + ///

Function Definition Table syntax:

+ /// + /// FDT ::= FDTRow [;FDTRow]* + /// FDTRow ::= Input => Output | Output <= Input + /// Input ::= ParameterName: Value [, Input]* + /// Output ::= [ParameterName: Value]* {halt|stop|void|nothing|Value} + /// Value ::= true | false | null | notnull | canbenull + /// + /// If the method has a single input parameter, its name could be omitted.
+ /// Using halt (or void/nothing, which is the same) for the method output + /// means that the method doesn't return normally (throws or terminates the process).
+ /// The canbenull value is only applicable to output parameters.
+ /// You can use multiple [ContractAnnotation] for each FDT row, or use a single attribute + /// with rows separated by the semicolon. The order of rows doesn't matter, all rows are checked + /// for applicability and applied per each program state tracked by the analysis engine.
+ ///
+ /// + /// + /// [ContractAnnotation("=> halt")] + /// public void TerminationMethod() + /// + /// + /// [ContractAnnotation("null <= param:null")] // reverse condition syntax + /// public string GetName(string surname) + /// + /// + /// [ContractAnnotation("s:null => true")] + /// public bool IsNullOrEmpty(string s) // string.IsNullOrEmpty() + /// + /// + /// // A method that returns null if the parameter is null, + /// // and not null if the parameter is not null + /// [ContractAnnotation("null => null; notnull => notnull")] + /// public object Transform(object data) + /// + /// + /// [ContractAnnotation("=> true, result: notnull; => false, result: null")] + /// public bool TryParse(string s, out Person result) + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +internal sealed class ContractAnnotationAttribute : Attribute + { + public ContractAnnotationAttribute([NotNull] string contract) + : this(contract, false) { } + + public ContractAnnotationAttribute([NotNull] string contract, bool forceFullStates) + { + Contract = contract; + ForceFullStates = forceFullStates; + } + + [NotNull] public string Contract { get; } + + public bool ForceFullStates { get; } + } + + /// + /// Indicates whether the marked element should be localized. + /// + /// + /// [LocalizationRequiredAttribute(true)] + /// class Foo { + /// string str = "my string"; // Warning: Localizable string + /// } + /// + [AttributeUsage(AttributeTargets.All)] +internal sealed class LocalizationRequiredAttribute : Attribute + { + public LocalizationRequiredAttribute() : this(true) { } + + public LocalizationRequiredAttribute(bool required) + { + Required = required; + } + + public bool Required { get; } + } + + /// + /// Indicates that the value of the marked type (or its derivatives) + /// cannot be compared using == or != operators, and Equals() + /// should be used instead. However, using == or != for comparison + /// with is always permitted. + /// + /// + /// [CannotApplyEqualityOperator] + /// class NoEquality { } + /// + /// class UsesNoEquality { + /// void Test() { + /// var ca1 = new NoEquality(); + /// var ca2 = new NoEquality(); + /// if (ca1 != null) { // OK + /// bool condition = ca1 == ca2; // Warning + /// } + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Struct)] +internal sealed class CannotApplyEqualityOperatorAttribute : Attribute { } + + /// + /// Indicates that the method or type uses equality members of the annotated element. + /// + /// + /// When applied to the method's generic parameter, indicates that the equality of the annotated type is used, + /// unless a custom equality comparer is passed when calling this method. The attribute can also be applied + /// directly to the method's parameter or return type to specify equality usage for it. + /// When applied to the type's generic parameter, indicates that type equality usage can happen anywhere + /// inside this type, so the instantiation of this type is treated as equality usage, unless a custom + /// equality comparer is passed to the constructor. + /// + /// + /// struct StructWithDefaultEquality { } + /// + /// class MySet<[DefaultEqualityUsage] T> { } + /// + /// static class Extensions { + /// public static MySet<T> ToMySet<[DefaultEqualityUsage] T>(this IEnumerable<T> items) => new(); + /// } + /// + /// class MyList<T> { public int IndexOf([DefaultEqualityUsage] T item) => 0; } + /// + /// class UsesDefaultEquality { + /// void Test() { + /// var list = new MyList<StructWithDefaultEquality>(); + /// list.IndexOf(new StructWithDefaultEquality()); // Warning: Default equality of struct 'StructWithDefaultEquality' is used + /// + /// var set = new MySet<StructWithDefaultEquality>(); // Warning: Default equality of struct 'StructWithDefaultEquality' is used + /// var set2 = new StructWithDefaultEquality[1].ToMySet(); // Warning: Default equality of struct 'StructWithDefaultEquality' is used + /// } + /// } + /// + [AttributeUsage(AttributeTargets.GenericParameter | AttributeTargets.Parameter | AttributeTargets.ReturnValue)] +internal sealed class DefaultEqualityUsageAttribute : Attribute { } + + /// + /// When applied to a target attribute, specifies a requirement for any type marked + /// with the target attribute to implement or inherit the specific type or types. + /// + /// + /// [BaseTypeRequired(typeof(IComponent)] // Specify requirement + /// class ComponentAttribute : Attribute { } + /// + /// [Component] // ComponentAttribute requires implementing IComponent interface + /// class MyComponent : IComponent { } + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + [BaseTypeRequired(typeof(Attribute))] +internal sealed class BaseTypeRequiredAttribute : Attribute + { + public BaseTypeRequiredAttribute([NotNull] Type baseType) + { + BaseType = baseType; + } + + [NotNull] public Type BaseType { get; } + } + + /// + /// Indicates that the marked symbol is used implicitly (via reflection, in an external library, and so on), + /// so this symbol will be ignored by usage-checking inspections.
+ /// You can use and + /// to configure how this attribute is applied. + ///
+ /// + /// [UsedImplicitly] + /// public class TypeConverter {} + /// + /// public class SummaryData + /// { + /// [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] + /// public SummaryData() {} + /// } + /// + /// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors | ImplicitUseTargetFlags.Default)] + /// public interface IService {} + /// + [AttributeUsage(AttributeTargets.All)] +internal sealed class UsedImplicitlyAttribute : Attribute + { + public UsedImplicitlyAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public UsedImplicitlyAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public UsedImplicitlyAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + public ImplicitUseKindFlags UseKindFlags { get; } + + public ImplicitUseTargetFlags TargetFlags { get; } + + public string Reason { get; set; } + } + + /// + /// Can be applied to attributes, type parameters, and parameters of a type assignable from . + /// When applied to an attribute, the decorated attribute behaves the same as . + /// When applied to a type parameter or to a parameter of type , + /// indicates that the corresponding type is used implicitly. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.GenericParameter | AttributeTargets.Parameter)] +internal sealed class MeansImplicitUseAttribute : Attribute + { + public MeansImplicitUseAttribute() + : this(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags) + : this(useKindFlags, ImplicitUseTargetFlags.Default) { } + + public MeansImplicitUseAttribute(ImplicitUseTargetFlags targetFlags) + : this(ImplicitUseKindFlags.Default, targetFlags) { } + + public MeansImplicitUseAttribute(ImplicitUseKindFlags useKindFlags, ImplicitUseTargetFlags targetFlags) + { + UseKindFlags = useKindFlags; + TargetFlags = targetFlags; + } + + [UsedImplicitly] public ImplicitUseKindFlags UseKindFlags { get; } + + [UsedImplicitly] public ImplicitUseTargetFlags TargetFlags { get; } + } + + /// + /// Specifies the details of an implicitly used symbol when it is marked + /// with or . + /// + [Flags] +internal enum ImplicitUseKindFlags + { + Default = Access | Assign | InstantiatedWithFixedConstructorSignature, + /// Only entity marked with attribute considered used. + Access = 1, + /// Indicates implicit assignment to a member. + Assign = 2, + /// + /// Indicates implicit instantiation of a type with fixed constructor signature. + /// That means any unused constructor parameters will not be reported as such. + /// + InstantiatedWithFixedConstructorSignature = 4, + /// Indicates implicit instantiation of a type. + InstantiatedNoFixedConstructorSignature = 8, + } + + /// + /// Specifies what is considered to be used implicitly when marked + /// with or . + /// + [Flags] +internal enum ImplicitUseTargetFlags + { + Default = Itself, + Itself = 1, + /// Members of the type marked with the attribute are considered used. + Members = 2, + /// Inherited entities are considered used. + WithInheritors = 4, + /// Entity marked with the attribute and all its members considered used. + WithMembers = Itself | Members + } + + /// + /// This attribute is intended to mark publicly available APIs + /// that should not be removed and therefore should never be reported as unused. + /// + [MeansImplicitUse(ImplicitUseTargetFlags.WithMembers)] + [AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class PublicAPIAttribute : Attribute + { + public PublicAPIAttribute() { } + + public PublicAPIAttribute([NotNull] string comment) + { + Comment = comment; + } + + [CanBeNull] public string Comment { get; } + } + + /// + /// Tells the code analysis engine if the parameter is completely handled when the invoked method is on stack. + /// If the parameter is a delegate, indicates that the delegate can only be invoked during method execution + /// (the delegate can be invoked zero or multiple times, but not stored to some field and invoked later, + /// when the containing method is no longer on the execution stack). + /// If the parameter is an enumerable, indicates that it is enumerated while the method is executed. + /// If is true, the attribute will only take effect + /// if the method invocation is located under the await expression. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class InstantHandleAttribute : Attribute + { + /// + /// Requires the method invocation to be used under the await expression for this attribute to take effect. + /// Can be used for delegate/enumerable parameters of async methods. + /// + public bool RequireAwait { get; set; } + } + + /// + /// Indicates that the method does not make any observable state changes. + /// The same as . + /// + /// + /// [Pure] int Multiply(int x, int y) => x * y; + /// + /// void M() { + /// Multiply(123, 42); // Warning: Return value of pure method is not used + /// } + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class PureAttribute : Attribute { } + + /// + /// Indicates that the return value of the method invocation must be used. + /// + /// + /// Methods decorated with this attribute (in contrast to pure methods) might change state, + /// but make no sense without using their return value.
+ /// Similarly to , this attribute + /// will help detect usages of the method when the return value is not used. + /// Optionally, you can specify a message to use when showing warnings, e.g. + /// [MustUseReturnValue("Use the return value to...")]. + ///
+ [AttributeUsage(AttributeTargets.Method)] +internal sealed class MustUseReturnValueAttribute : Attribute + { + public MustUseReturnValueAttribute() { } + + public MustUseReturnValueAttribute([NotNull] string justification) + { + Justification = justification; + } + + [CanBeNull] public string Justification { get; } + + /// + /// Enables the special handling of the "fluent" APIs that perform mutations and return 'this' object. + /// In this case the analysis checks the fluent invocations chain and only warns if the initial receiver value + /// is probably a temporary value - in this case the very last fluent method return assumed to be temporary as well, + /// therefore is a subject of warning if unused. If the initial receiver is a local variable or 'this' reference + /// the analysis assumes that fluent invocations were used to mutate the existing value and warning will not be shown. + /// + /// + /// This property must only be used for methods with the return type matching the receiver type. + /// + public bool IsFluentBuilderMethod { get; set; } + } + + /// + /// Indicates that the resource disposal must be handled at the use site, + /// meaning that the resource ownership is transferred to the caller. + /// This annotation can be used to annotate disposable types or their constructors individually to enable + /// the IDE code analysis for resource disposal in every context where the new instance of this type is created. + /// Factory methods and out parameters can also be annotated to indicate that the return value + /// of the disposable type needs handling. + /// + /// + /// Annotation of input parameters with this attribute is meaningless.
+ /// Constructors inherit this attribute from their type if it is annotated, + /// but not from the base constructors they delegate to (if any).
+ /// Resource disposal is expected via using (resource) statement, + /// using var declaration, explicit Dispose() call, or passing the resource as an argument + /// to a parameter annotated with the attribute. + ///
+ [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Struct | + AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Parameter)] +internal sealed class MustDisposeResourceAttribute : Attribute + { + public MustDisposeResourceAttribute() + { + Value = true; + } + + public MustDisposeResourceAttribute(bool value) + { + Value = value; + } + + /// + /// When set to , disposing of the resource is not obligatory. + /// The main use-case for explicit [MustDisposeResource(false)] annotation + /// is to loosen the annotation for inheritors. + /// + public bool Value { get; } + } + + /// + /// Indicates that method or class instance acquires resource ownership and will dispose it after use. + /// + /// + /// Annotation of out parameters with this attribute is meaningless.
+ /// When an instance method is annotated with this attribute, + /// it means that it is handling the resource disposal of the corresponding resource instance.
+ /// When a field or a property is annotated with this attribute, it means that this type owns the resource + /// and will handle the resource disposal properly (e.g. in own IDisposable implementation). + ///
+ [AttributeUsage( + AttributeTargets.Method | AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class HandlesResourceDisposalAttribute : Attribute { } + + /// + /// This annotation allows enforcing allocation-less usage patterns of delegates for performance-critical APIs. + /// When this annotation is applied to the parameter of a delegate type, + /// the IDE checks the input argument of this parameter: + /// * When a lambda expression or anonymous method is passed as an argument, the IDE verifies that the passed closure + /// has no captures of the containing local variables and the compiler is able to cache the delegate instance + /// to avoid heap allocations. Otherwise, a warning is produced. + /// * The IDE warns when the method name or local function name is passed as an argument because this always results + /// in heap allocation of the delegate instance. + /// + /// + /// In C# 9.0+ code, the IDE will also suggest annotating the anonymous functions with the static modifier + /// to make use of the similar analysis provided by the language/compiler. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class RequireStaticDelegateAttribute : Attribute + { + public bool IsError { get; set; } + } + + /// + /// Indicates the type member or parameter of some type that should be used instead of all other ways + /// to get the value of that type. This annotation is useful when you have some 'context' value evaluated + /// and stored somewhere, meaning that all other ways to get this value must be consolidated with the existing one. + /// + /// + /// class Foo { + /// [ProvidesContext] IBarService _barService = ...; + /// + /// void ProcessNode(INode node) { + /// DoSomething(node, node.GetGlobalServices().Bar); + /// // ^ Warning: use value of '_barService' field + /// } + /// } + /// + [AttributeUsage( + AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter | AttributeTargets.Method | + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.GenericParameter)] +internal sealed class ProvidesContextAttribute : Attribute { } + + /// + /// Indicates that a parameter is a path to a file or a folder within a web project. + /// Path can be relative or absolute, starting from web root (~). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class PathReferenceAttribute : Attribute + { + public PathReferenceAttribute() { } + + public PathReferenceAttribute([NotNull, PathReference] string basePath) + { + BasePath = basePath; + } + + [CanBeNull] public string BasePath { get; } + } + + /// + /// An extension method marked with this attribute is processed by code completion + /// as a 'Source Template'. When the extension method is completed over some expression, its source code + /// is automatically expanded like a template at the call site. + /// + /// + /// Template method bodies can contain valid source code and/or special comments starting with '$'. + /// Text inside these comments is added as source code when the template is applied. Template parameters + /// can be used either as additional method parameters or as identifiers wrapped in two '$' signs. + /// Use the attribute to specify macros for parameters. + /// The expression to be used in the expansion can be adjusted + /// by the parameter. + /// + /// + /// In this example, the forEach method is a source template available over all values + /// of enumerable types, producing an ordinary C# foreach statement and placing the caret inside the block: + /// + /// [SourceTemplate] + /// public static void forEach<T>(this IEnumerable<T> xs) { + /// foreach (var x in xs) { + /// //$ $END$ + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class SourceTemplateAttribute : Attribute + { + /// + /// Allows specifying the expression to capture for template execution if more than one expression + /// is available at the expansion point. + /// If not specified, is assumed. + /// + public SourceTemplateTargetExpression Target { get; set; } + } + + /// + /// Provides a value for the to define how to capture + /// the expression at the point of expansion + /// +internal enum SourceTemplateTargetExpression + { + /// Selects inner expression + /// value > 42.{caret} captures 42 + /// _args = args.{caret} captures args + Inner = 0, + + /// Selects outer expression + /// value > 42.{caret} captures value > 42 + /// _args = args.{caret} captures whole assignment + Outer = 1 + } + + /// + /// Allows specifying a macro for a parameter of a source template. + /// + /// + /// You can apply the attribute to the whole method or to any of its additional parameters. The macro expression + /// is defined in the property. When applied to a method, the target + /// template parameter is defined in the property. To apply the macro silently + /// for the parameter, set the property value to -1. + /// + /// + /// Applying the attribute to a source template method: + /// + /// [SourceTemplate, Macro(Target = "item", Expression = "suggestVariableName()")] + /// public static void forEach<T>(this IEnumerable<T> collection) { + /// foreach (var item in collection) { + /// //$ $END$ + /// } + /// } + /// + /// Applying the attribute to a template method parameter: + /// + /// [SourceTemplate] + /// public static void something(this Entity x, [Macro(Expression = "guid()", Editable = -1)] string newguid) { + /// /*$ var $x$Id = "$newguid$" + x.ToString(); + /// x.DoSomething($x$Id); */ + /// } + /// + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method, AllowMultiple = true)] +internal sealed class MacroAttribute : Attribute + { + /// + /// Allows specifying a macro that will be executed for a source template + /// parameter when the template is expanded. + /// + [CanBeNull] public string Expression { get; set; } + + /// + /// Allows specifying the occurrence of the target parameter that becomes editable when the template is deployed. + /// + /// + /// If the target parameter is used several times in the template, only one occurrence becomes editable; + /// other occurrences are changed synchronously. To specify the zero-based index of the editable occurrence, + /// use values >= 0. To make the parameter non-editable when the template is expanded, use -1. + /// + public int Editable { get; set; } + + /// + /// Identifies the target parameter of a source template if the + /// is applied to a template method. + /// + [CanBeNull] public string Target { get; set; } + } + + /// + /// Indicates how a method, constructor invocation, or property access + /// over a collection type affects the contents of the collection. + /// When applied to a return value of a method, indicates whether the returned collection + /// is created exclusively for the caller (CollectionAccessType.UpdatedContent) or + /// can be read/updated from outside (CollectionAccessType.Read/CollectionAccessType.UpdatedContent). + /// Use to specify the access type. + /// + /// + /// Using this attribute only makes sense if all collection methods are marked with this attribute. + /// + /// + /// public class MyStringCollection : List<string> + /// { + /// [CollectionAccess(CollectionAccessType.Read)] + /// public string GetFirstString() + /// { + /// return this.ElementAt(0); + /// } + /// } + /// class Test + /// { + /// public void Foo() + /// { + /// // Warning: Contents of the collection is never updated + /// var col = new MyStringCollection(); + /// string x = col.GetFirstString(); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.ReturnValue)] +internal sealed class CollectionAccessAttribute : Attribute + { + public CollectionAccessAttribute(CollectionAccessType collectionAccessType) + { + CollectionAccessType = collectionAccessType; + } + + public CollectionAccessType CollectionAccessType { get; } + } + + /// + /// Provides a value for the to define + /// how the collection method invocation affects the contents of the collection. + /// + [Flags] +internal enum CollectionAccessType + { + /// Method does not use or modify content of the collection. + None = 0, + /// Method only reads content of the collection but does not modify it. + Read = 1, + /// Method can change content of the collection but does not add new elements. + ModifyExistingContent = 2, + /// Method can add new elements to the collection. + UpdatedContent = ModifyExistingContent | 4 + } + + /// + /// Indicates that the marked method is an assertion method, i.e. it halts the control flow if + /// one of the conditions is satisfied. To set the condition, mark one of the parameters with + /// attribute. + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class AssertionMethodAttribute : Attribute { } + + /// + /// Indicates the condition parameter of the assertion method. The method itself should be + /// marked by the attribute. The mandatory argument of + /// the attribute is the assertion type. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class AssertionConditionAttribute : Attribute + { + public AssertionConditionAttribute(AssertionConditionType conditionType) + { + ConditionType = conditionType; + } + + public AssertionConditionType ConditionType { get; } + } + + /// + /// Specifies the assertion type. If the assertion method argument satisfies the condition, + /// then the execution continues. Otherwise, execution is assumed to be halted. + /// +internal enum AssertionConditionType + { + /// Marked parameter should be evaluated to true. + IS_TRUE = 0, + /// Marked parameter should be evaluated to false. + IS_FALSE = 1, + /// Marked parameter should be evaluated to null value. + IS_NULL = 2, + /// Marked parameter should be evaluated to not null value. + IS_NOT_NULL = 3, + } + + /// + /// Indicates that the marked method unconditionally terminates control flow execution. + /// For example, it could unconditionally throw an exception. + /// + [Obsolete("Use [ContractAnnotation('=> halt')] instead")] + [AttributeUsage(AttributeTargets.Method)] +internal sealed class TerminatesProgramAttribute : Attribute { } + + /// + /// Indicates that the method is a pure LINQ method with postponed enumeration (like Enumerable.Select or + /// Enumerable.Where). This annotation allows inference of the [InstantHandle] annotation for parameters + /// of delegate type by analyzing LINQ method chains. + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class LinqTunnelAttribute : Attribute { } + + /// + /// Indicates that an IEnumerable passed as a parameter is not enumerated. + /// Use this annotation to suppress the 'Possible multiple enumeration of IEnumerable' inspection. + /// + /// + /// static void ThrowIfNull<T>([NoEnumeration] T v, string n) where T : class + /// { + /// // custom check for null but no enumeration + /// } + /// + /// void Foo(IEnumerable<string> values) + /// { + /// ThrowIfNull(values, nameof(values)); + /// var x = values.ToList(); // No warnings about multiple enumeration + /// } + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class NoEnumerationAttribute : Attribute { } + + /// + /// Indicates that the marked parameter, field, or property is a regular expression pattern. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class RegexPatternAttribute : Attribute { } + + /// + /// Language of the injected code fragment inside a string literal marked by the . + /// +internal enum InjectedLanguage + { + CSS = 0, + HTML = 1, + JAVASCRIPT = 2, + JSON = 3, + XML = 4 + } + + /// + /// Indicates that the marked parameter, field, or property accepts string literals + /// containing code fragments in a specified language. + /// + /// + /// void Foo([LanguageInjection(InjectedLanguage.CSS, Prefix = "body{", Suffix = "}")] string cssProps) + /// { + /// // cssProps should only contain a list of CSS properties + /// } + /// + /// + /// void Bar([LanguageInjection("json")] string json) + /// { + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.ReturnValue)] +internal sealed class LanguageInjectionAttribute : Attribute + { + public LanguageInjectionAttribute(InjectedLanguage injectedLanguage) + { + InjectedLanguage = injectedLanguage; + } + + public LanguageInjectionAttribute([NotNull] string injectedLanguage) + { + InjectedLanguageName = injectedLanguage; + } + + /// Specifies a language of the injected code fragment. + public InjectedLanguage InjectedLanguage { get; } + + /// Specifies a language name of the injected code fragment. + [CanBeNull] public string InjectedLanguageName { get; } + + /// Specifies a string that 'precedes' the injected string literal. + [CanBeNull] public string Prefix { get; set; } + + /// Specifies a string that 'follows' the injected string literal. + [CanBeNull] public string Suffix { get; set; } + } + + /// + /// Prevents the Member Reordering feature in the IDE from tossing members of the marked class. + /// + /// + /// The attribute must be mentioned in your member reordering patterns. + /// + [AttributeUsage( + AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct | AttributeTargets.Enum, AllowMultiple = true)] +internal sealed class NoReorderAttribute : Attribute { } + + /// + /// Defines a code search pattern using the Structural Search and Replace syntax. + /// It allows you to find and, if necessary, replace blocks of code that match a specific pattern. + /// + /// + /// Search and replace patterns consist of a textual part and placeholders. + /// Textural part must contain only identifiers allowed in the target language and will be matched exactly + /// (whitespaces, tabulation characters, and line breaks are ignored). + /// Placeholders allow matching variable parts of the target code blocks. + ///
+ /// A placeholder has the following format: + /// $placeholder_name$ - where placeholder_name is an arbitrary identifier. + /// Predefined placeholders: + /// + /// $this$ - expression of containing type + /// $thisType$ - containing type + /// $member$ - current member placeholder + /// $qualifier$ - this placeholder is available in the replace pattern and can be used + /// to insert a qualifier expression matched by the $member$ placeholder. + /// (Note that if $qualifier$ placeholder is used, + /// then $member$ placeholder will match only qualified references) + /// $expression$ - expression of any type + /// $identifier$ - identifier placeholder + /// $args$ - any number of arguments + /// $arg$ - single argument + /// $arg1$ ... $arg10$ - single argument + /// $stmts$ - any number of statements + /// $stmt$ - single statement + /// $stmt1$ ... $stmt10$ - single statement + /// $name{Expression, 'Namespace.FooType'}$ - expression with the Namespace.FooType type + /// $expression{'Namespace.FooType'}$ - expression with the Namespace.FooType type + /// $name{Type, 'Namespace.FooType'}$ - Namespace.FooType type + /// $type{'Namespace.FooType'}$ - Namespace.FooType type + /// $statement{1,2}$ - 1 or 2 statements + /// + /// You can also define your own placeholders of the supported types and specify arguments for each placeholder type. + /// This can be done using the following format: $name{type, arguments}$. Where + /// name - is the name of your placeholder, + /// type - is the type of your placeholder + /// (one of the following: Expression, Type, Identifier, Statement, Argument, Member), + /// arguments - a list of arguments for your placeholder. Each placeholder type supports its own arguments. + /// Check the examples below for more details. + /// The placeholder type may be omitted and determined from the placeholder name, + /// if the name has one of the following prefixes: + /// + /// expr, expression - expression placeholder, e.g. $exprPlaceholder{}$, $expressionFoo{}$ + /// arg, argument - argument placeholder, e.g. $argPlaceholder{}$, $argumentFoo{}$ + /// ident, identifier - identifier placeholder, e.g. $identPlaceholder{}$, $identifierFoo{}$ + /// stmt, statement - statement placeholder, e.g. $stmtPlaceholder{}$, $statementFoo{}$ + /// type - type placeholder, e.g. $typePlaceholder{}$, $typeFoo{}$ + /// member - member placeholder, e.g. $memberPlaceholder{}$, $memberFoo{}$ + /// + ///
+ /// + /// Expression placeholder arguments: + /// + /// expressionType - string value in single quotes, specifies full type name to match + /// (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myExpr{Expression, 'Namespace.FooType', true}$ - defines an expression placeholder + /// matching expressions of the Namespace.FooType type with exact matching. + /// $myExpr{Expression, 'Namespace.FooType'}$ - defines an expression placeholder + /// matching expressions of the Namespace.FooType type or expressions that can be + /// implicitly converted to Namespace.FooType. + /// $myExpr{Expression}$ - defines an expression placeholder matching expressions of any type. + /// $exprFoo{'Namespace.FooType', true}$ - defines an expression placeholder + /// matching expressions of the Namespace.FooType type with exact matching. + /// + /// + /// + /// Type placeholder arguments: + /// + /// type - string value in single quotes, specifies the full type name to match (empty string by default) + /// exactType - boolean value, specifies whether the expression should have the exact type match + /// (false by default) + /// + /// Examples: + /// + /// $myType{Type, 'Namespace.FooType', true}$ - defines a type placeholder + /// matching Namespace.FooType types with exact matching. + /// $myType{Type, 'Namespace.FooType'}$ - defines a type placeholder matching Namespace.FooType + /// types or types that can be implicitly converted to Namespace.FooType. + /// $myType{Type}$ - defines a type placeholder matching any type. + /// $typeFoo{'Namespace.FooType', true}$ - defines a type placeholder matching Namespace.FooType + /// types with exact matching. + /// + /// + /// + /// Identifier placeholder arguments: + /// + /// nameRegex - string value in single quotes, specifies regex to use for matching (empty string by default) + /// nameRegexCaseSensitive - boolean value, specifies if name regex is case-sensitive (true by default) + /// type - string value in single quotes, specifies full type name to match (empty string by default) + /// exactType - boolean value, specifies if expression should have exact type match (false by default) + /// + /// Examples: + /// + /// $myIdentifier{Identifier, 'my.*', false, 'Namespace.FooType', true}$ - + /// defines an identifier placeholder matching identifiers (ignoring case) starting with my prefix with + /// Namespace.FooType type. + /// $myIdentifier{Identifier, 'my.*', true, 'Namespace.FooType', true}$ - + /// defines an identifier placeholder matching identifiers (case sensitively) starting with my prefix with + /// Namespace.FooType type. + /// $identFoo{'my.*'}$ - defines an identifier placeholder matching identifiers (case sensitively) + /// starting with my prefix. + /// + /// + /// + /// Statement placeholder arguments: + /// + /// minimalOccurrences - minimal number of statements to match (-1 by default) + /// maximalOccurrences - maximal number of statements to match (-1 by default) + /// + /// Examples: + /// + /// $myStmt{Statement, 1, 2}$ - defines a statement placeholder matching 1 or 2 statements. + /// $myStmt{Statement}$ - defines a statement placeholder matching any number of statements. + /// $stmtFoo{1, 2}$ - defines a statement placeholder matching 1 or 2 statements. + /// + /// + /// + /// Argument placeholder arguments: + /// + /// minimalOccurrences - minimal number of arguments to match (-1 by default) + /// maximalOccurrences - maximal number of arguments to match (-1 by default) + /// + /// Examples: + /// + /// $myArg{Argument, 1, 2}$ - defines an argument placeholder matching 1 or 2 arguments. + /// $myArg{Argument}$ - defines an argument placeholder matching any number of arguments. + /// $argFoo{1, 2}$ - defines an argument placeholder matching 1 or 2 arguments. + /// + /// + /// + /// Member placeholder arguments: + /// + /// docId - string value in single quotes, specifies XML documentation ID of the member to match (empty by default) + /// + /// Examples: + /// + /// $myMember{Member, 'M:System.String.IsNullOrEmpty(System.String)'}$ - + /// defines a member placeholder matching IsNullOrEmpty member of the System.String type. + /// $memberFoo{'M:System.String.IsNullOrEmpty(System.String)'}$ - + /// defines a member placeholder matching IsNullOrEmpty member of the System.String type. + /// + /// + /// + /// Structural Search and Replace + /// + /// Find and update deprecated APIs + [AttributeUsage( + AttributeTargets.Method + | AttributeTargets.Constructor + | AttributeTargets.Property + | AttributeTargets.Field + | AttributeTargets.Event + | AttributeTargets.Interface + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum, + AllowMultiple = true, + Inherited = false)] +internal sealed class CodeTemplateAttribute : Attribute + { + public CodeTemplateAttribute(string searchTemplate) + { + SearchTemplate = searchTemplate; + } + + /// + /// Structural search pattern. + /// + /// + /// The pattern includes a textual part, which must only contain identifiers allowed in the target language + /// and placeholders to match variable parts of the target code blocks. + /// + public string SearchTemplate { get; } + + /// + /// Message to show when a code block matching the search pattern was found. + /// + /// + /// You can also prepend the message text with 'Error:', 'Warning:', 'Suggestion:' or 'Hint:' prefix + /// to specify the pattern severity. + /// Code patterns with replace templates have the 'Suggestion' severity by default. + /// If a replace pattern is not provided, the pattern will have the 'Warning' severity. + /// + public string Message { get; set; } + + /// + /// Replace pattern to use for replacing a matched pattern. + /// + public string ReplaceTemplate { get; set; } + + /// + /// Replace message to show in the light bulb. + /// + public string ReplaceMessage { get; set; } + + /// + /// Apply code formatting after code replacement. + /// + public bool FormatAfterReplace { get; set; } = true; + + /// + /// Whether similar code blocks should be matched. + /// + public bool MatchSimilarConstructs { get; set; } + + /// + /// Automatically insert namespace import directives or remove qualifiers + /// that become redundant after the template is applied. + /// + public bool ShortenReferences { get; set; } + + /// + /// The string to use as a suppression key. + /// By default, the following suppression key is used: CodeTemplate_SomeType_SomeMember, + /// where 'SomeType' and 'SomeMember' are names of the associated containing type and member, + /// to which this attribute is applied. + /// + public string SuppressionKey { get; set; } + } + + /// + /// Indicates that the string literal passed as an argument to this parameter + /// should not be checked for spelling or grammar errors. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class IgnoreSpellingAndGrammarErrorsAttribute : Attribute + { + } + + #region ASP.NET + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +internal sealed class AspChildControlTypeAttribute : Attribute + { + public AspChildControlTypeAttribute([NotNull] string tagName, [NotNull] Type controlType) + { + TagName = tagName; + ControlType = controlType; + } + + [NotNull] public string TagName { get; } + + [NotNull] public Type ControlType { get; } + } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +internal sealed class AspDataFieldAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] +internal sealed class AspDataFieldsAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] +internal sealed class AspMethodPropertyAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +internal sealed class AspRequiredAttributeAttribute : Attribute + { + public AspRequiredAttributeAttribute([NotNull] string attribute) + { + Attribute = attribute; + } + + [NotNull] public string Attribute { get; } + } + + [AttributeUsage(AttributeTargets.Property)] +internal sealed class AspTypePropertyAttribute : Attribute + { + public bool CreateConstructorReferences { get; } + + public AspTypePropertyAttribute(bool createConstructorReferences) + { + CreateConstructorReferences = createConstructorReferences; + } + } + + #endregion + + #region ASP.NET MVC + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcAreaMasterLocationFormatAttribute : Attribute + { + public AspMvcAreaMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcAreaPartialViewLocationFormatAttribute : Attribute + { + public AspMvcAreaPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcAreaViewComponentViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewComponentViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcAreaViewLocationFormatAttribute : Attribute + { + public AspMvcAreaViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcMasterLocationFormatAttribute : Attribute + { + public AspMvcMasterLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcPartialViewLocationFormatAttribute : Attribute + { + public AspMvcPartialViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcViewComponentViewLocationFormatAttribute : Attribute + { + public AspMvcViewComponentViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] +internal sealed class AspMvcViewLocationFormatAttribute : Attribute + { + public AspMvcViewLocationFormatAttribute([NotNull] string format) + { + Format = format; + } + + [NotNull] public string Format { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC action. If applied to a method, the MVC action name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcActionAttribute : Attribute + { + public AspMvcActionAttribute() { } + + public AspMvcActionAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC area. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcAreaAttribute : Attribute + { + public AspMvcAreaAttribute() { } + + public AspMvcAreaAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is + /// an MVC controller. If applied to a method, the MVC controller name is calculated + /// implicitly from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.ChildActionExtensions.RenderAction(HtmlHelper, String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcControllerAttribute : Attribute + { + public AspMvcControllerAttribute() { } + + public AspMvcControllerAttribute([NotNull] string anonymousProperty) + { + AnonymousProperty = anonymousProperty; + } + + [CanBeNull] public string AnonymousProperty { get; } + } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC Master. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcMasterAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC model type. Use this attribute + /// for custom wrappers similar to System.Web.Mvc.Controller.View(String, Object). + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class AspMvcModelTypeAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter is an MVC + /// partial view. If applied to a method, the MVC partial view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.RenderPartialExtensions.RenderPartial(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcPartialViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Allows disabling inspections for MVC views within a class or a method. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +internal sealed class AspMvcSuppressViewErrorAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that a parameter is an MVC display template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.DisplayExtensions.DisplayForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcDisplayTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC editor template. + /// Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Html.EditorExtensions.EditorForModel(HtmlHelper, String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcEditorTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. Indicates that the marked parameter is an MVC template. + /// Use this attribute for custom wrappers similar to + /// System.ComponentModel.DataAnnotations.UIHintAttribute(System.String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcTemplateAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component. If applied to a method, the MVC view name is calculated implicitly + /// from the context. Use this attribute for custom wrappers similar to + /// System.Web.Mvc.Controller.View(Object). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component name. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcViewComponentAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. If applied to a parameter, indicates that the parameter + /// is an MVC view component view. If applied to a method, the MVC view component view name is default. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class AspMvcViewComponentViewAttribute : Attribute { } + + /// + /// ASP.NET MVC attribute. When applied to a parameter of an attribute, + /// indicates that this parameter is an MVC action name. + /// + /// + /// [ActionName("Foo")] + /// public ActionResult Login(string returnUrl) { + /// ViewBag.ReturnUrl = Url.Action("Foo"); // OK + /// return RedirectToAction("Bar"); // Error: Cannot resolve action + /// } + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)] +internal sealed class AspMvcActionSelectorAttribute : Attribute { } + + #endregion + + #region ASP.NET Routing + + /// + /// Indicates that the marked parameter, field, or property is a route template. + /// + /// + /// This attribute allows IDE to recognize the use of web frameworks' route templates + /// to enable syntax highlighting, code completion, navigation, rename and other features in string literals. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class RouteTemplateAttribute : Attribute { } + + /// + /// Indicates that the marked type is custom route parameter constraint, + /// which is registered in the application's Startup with the name ConstraintName. + /// + /// + /// You can specify ProposedType if target constraint matches only route parameters of specific type, + /// it will allow IDE to create method's parameter from usage in route template + /// with specified type instead of default System.String + /// and check if constraint's proposed type conflicts with matched parameter's type. + /// + [AttributeUsage(AttributeTargets.Class)] +internal sealed class RouteParameterConstraintAttribute : Attribute + { + [NotNull] public string ConstraintName { get; } + [CanBeNull] public Type ProposedType { get; set; } + + public RouteParameterConstraintAttribute([NotNull] string constraintName) + { + ConstraintName = constraintName; + } + } + + /// + /// Indicates that the marked parameter, field, or property is a URI string. + /// + /// + /// This attribute enables code completion, navigation, renaming, and other features + /// in URI string literals assigned to annotated parameters, fields, or properties. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class UriStringAttribute : Attribute + { + public UriStringAttribute() { } + + public UriStringAttribute(string httpVerb) + { + HttpVerb = httpVerb; + } + + [CanBeNull] public string HttpVerb { get; } + } + + /// + /// Indicates that the marked method declares routing convention for ASP.NET. + /// + /// + /// The IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation, and other features over URI strings. + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class AspRouteConventionAttribute : Attribute + { + public AspRouteConventionAttribute() { } + + public AspRouteConventionAttribute(string predefinedPattern) + { + PredefinedPattern = predefinedPattern; + } + + [CanBeNull] public string PredefinedPattern { get; } + } + + /// + /// Indicates that the marked method parameter contains default route values of routing convention for ASP.NET. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class AspDefaultRouteValuesAttribute : Attribute { } + + /// + /// Indicates that the marked method parameter contains constraints on route values of routing convention for ASP.NET. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class AspRouteValuesConstraintsAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains routing order provided by ASP.NET routing attribute. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +internal sealed class AspRouteOrderAttribute : Attribute { } + + /// + /// Indicates that the marked parameter or property contains HTTP verbs provided by ASP.NET routing attribute. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] +internal sealed class AspRouteVerbsAttribute : Attribute { } + + /// + /// Indicates that the marked attribute is used for attribute routing in ASP.NET. + /// + /// + /// The IDE will analyze all usages of attributes marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings. + /// + [AttributeUsage(AttributeTargets.Class)] +internal sealed class AspAttributeRoutingAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked method declares an ASP.NET Minimal API endpoint. + /// + /// + /// The IDE will analyze all usages of methods marked with this attribute, + /// and will add all routes to completion, navigation and other features over URI strings. + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class AspMinimalApiDeclarationAttribute : Attribute + { + public string HttpVerb { get; set; } + } + + /// + /// Indicates that the marked method declares an ASP.NET Minimal API endpoints group. + /// + [AttributeUsage(AttributeTargets.Method)] +internal sealed class AspMinimalApiGroupAttribute : Attribute { } + + /// + /// Indicates that the marked parameter contains an ASP.NET Minimal API endpoint handler. + /// + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class AspMinimalApiHandlerAttribute : Attribute { } + + /// + /// Indicates that the marked method contains Minimal API endpoint declaration. + /// + /// + /// The IDE will analyze all usages of methods marked with this attribute, + /// and will add all declared in attributes routes to completion, navigation and other features over URI strings. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +internal sealed class AspMinimalApiImplicitEndpointDeclarationAttribute : Attribute + { + public string HttpVerb { get; set; } + + public string RouteTemplate { get; set; } + + public Type BodyType { get; set; } + + /// + /// Comma-separated list of query parameters defined for endpoint + /// + public string QueryParameters { get; set; } + } + + #endregion + + #region Razor + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.Field)] +internal sealed class HtmlElementAttributesAttribute : Attribute + { + public HtmlElementAttributesAttribute() { } + + public HtmlElementAttributesAttribute([NotNull] string name) + { + Name = name; + } + + [CanBeNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class HtmlAttributeValueAttribute : Attribute + { + public HtmlAttributeValueAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + /// + /// Razor attribute. Indicates that the marked parameter or method is a Razor section. + /// Use this attribute for custom wrappers similar to + /// System.Web.WebPages.WebPageBase.RenderSection(String). + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Method)] +internal sealed class RazorSectionAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal sealed class RazorImportNamespaceAttribute : Attribute + { + public RazorImportNamespaceAttribute([NotNull] string name) + { + Name = name; + } + + [NotNull] public string Name { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal sealed class RazorInjectionAttribute : Attribute + { + public RazorInjectionAttribute([NotNull] string type, [NotNull] string fieldName) + { + Type = type; + FieldName = fieldName; + } + + [NotNull] public string Type { get; } + + [NotNull] public string FieldName { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal sealed class RazorDirectiveAttribute : Attribute + { + public RazorDirectiveAttribute([NotNull] string directive) + { + Directive = directive; + } + + [NotNull] public string Directive { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal sealed class RazorPageBaseTypeAttribute : Attribute + { + public RazorPageBaseTypeAttribute([NotNull] string baseType) + { + BaseType = baseType; + } + public RazorPageBaseTypeAttribute([NotNull] string baseType, string pageName) + { + BaseType = baseType; + PageName = pageName; + } + + [NotNull] public string BaseType { get; } + [CanBeNull] public string PageName { get; } + } + + [AttributeUsage(AttributeTargets.Method)] +internal sealed class RazorHelperCommonAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Property)] +internal sealed class RazorLayoutAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] +internal sealed class RazorWriteLiteralMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Method)] +internal sealed class RazorWriteMethodAttribute : Attribute { } + + [AttributeUsage(AttributeTargets.Parameter)] +internal sealed class RazorWriteMethodParameterAttribute : Attribute { } + + #endregion + + #region XAML + + /// + /// XAML attribute. Indicates the type that has an ItemsSource property and should be treated + /// as an ItemsControl-derived type, to enable inner items DataContext type resolution. + /// + [AttributeUsage(AttributeTargets.Class)] +internal sealed class XamlItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some BindingBase-derived type, that + /// is used to bind some item of an ItemsControl-derived type. This annotation will + /// enable the DataContext type resolution for XAML bindings for such properties. + /// + /// + /// The property should have a tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] +internal sealed class XamlItemBindingOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates the property of some Style-derived type that + /// is used to style items of an ItemsControl-derived type. This annotation will + /// enable the DataContext type resolution in XAML bindings for such properties. + /// + /// + /// Property should have a tree ancestor of the ItemsControl type or + /// marked with the attribute. + /// + [AttributeUsage(AttributeTargets.Property)] +internal sealed class XamlItemStyleOfItemsControlAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has OneWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, or to a DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class XamlOneWayBindingModeByDefaultAttribute : Attribute { } + + /// + /// XAML attribute. Indicates that DependencyProperty has TwoWay binding mode by default. + /// + /// + /// This attribute must be applied to DependencyProperty's CLR accessor property if it is present, or to a DependencyProperty descriptor field otherwise. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +internal sealed class XamlTwoWayBindingModeByDefaultAttribute : Attribute { } + + #endregion + + #region Unit Testing + + /// + /// Specifies a type being tested by a test class or a test method. + /// + /// + /// This information can be used by the IDE to navigate between tests and tested types, + /// or by test runners to group tests by subject and to provide better test reports. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] +internal sealed class TestSubjectAttribute : Attribute + { + /// + /// Gets the type being tested. + /// + [NotNull] public Type Subject { get; } + + /// + /// Initializes a new instance of the class with the specified tested type. + /// + /// The type being tested. + public TestSubjectAttribute([NotNull] Type subject) + { + Subject = subject; + } + } + + /// + /// Marks a generic argument as the test subject for a test class. + /// + /// + /// Can be applied to a generic parameter of a base test class to indicate that + /// the type passed as the argument is the class being tested. This information can be used by the IDE + /// to navigate between tests and tested types, + /// or by test runners to group tests by subject and to provide better test reports. + /// + /// + /// public class BaseTestClass<[MeansTestSubject] T> + /// { + /// protected T Component { get; } + /// } + /// + /// public class CalculatorAdditionTests : BaseTestClass<Calculator> + /// { + /// [Test] + /// public void Should_add_two_numbers() + /// { + /// Assert.That(Component.Add(2, 3), Is.EqualTo(5)); + /// } + /// } + /// + [AttributeUsage(AttributeTargets.GenericParameter)] +internal sealed class MeansTestSubjectAttribute : Attribute { } + + #endregion +} diff --git a/external/sdk b/external/sdk new file mode 160000 index 00000000..473b732b --- /dev/null +++ b/external/sdk @@ -0,0 +1 @@ +Subproject commit 473b732b2de159773b2e2908fd11aa99f07db284 diff --git a/src/SampSharp.Analyzer/AnalyzerIds.cs b/src/SampSharp.Analyzer/AnalyzerIds.cs new file mode 100644 index 00000000..aa59f72b --- /dev/null +++ b/src/SampSharp.Analyzer/AnalyzerIds.cs @@ -0,0 +1,79 @@ +using Microsoft.CodeAnalysis; + +namespace SampSharp.Analyzer; + +public static class AnalyzerIds +{ + public static readonly DiagnosticDescriptor Sash0001MissingExtensionAttribute = new( + "SASH0001", + "Missing 'ExtensionAttribute'", + "Type '{0} 'is missing the 'ExtensionAttribute'", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "Type {0} must have the 'ExtensionAttribute'."); + + public static readonly DiagnosticDescriptor Sash0002GenericEventHandlerUnsupported = new( + "SASH0002", + "Unsupported generic parameters in open.mp event handler", + "Unsupported generic parameters in open.mp event handler type '{0}'", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp event handler type '{0}' must not contain generic parameters."); + + public static readonly DiagnosticDescriptor Sash0003ApiStructMustBeReadonlyPartial = new( + "SASH0003", + "open.mp api struct must be readonly partial", + "open.mp api struct '{0}' must be marked as readonly and partial", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp api struct type '{0}' must be marked as readonly and partial."); + + public static readonly DiagnosticDescriptor Sash0004ApiStructMarshalRefReturnUnsupported = new( + "SASH0004", + "'ref return' marshalling not supported for open.mp api structs", + "open.mp api function '{0}' use of 'ref return' in combination with marshalling not supported", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp api function '{0}' returns a value by ref while also using a marshaller which is not supported."); + + public static readonly DiagnosticDescriptor Sash0005ApiStructRequiresAllowUnsafeBlocks = new( + "SASH0005", + "open.mp api struct requires 'AllowUnsafeBlocks'", + "open.mp api struct require the 'AllowUnsafeBlocks' option to be set to true", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp api struct require the 'AllowUnsafeBlocks' option to be set to true."); + + public static readonly DiagnosticDescriptor Sash0006ApiStructBaseTypeMustBeApiStruct = new( + "SASH0006", + "open.mp api struct base types must be open.mp api structs", + "base type '{0}' in base type list of open.mp api struct '{1}' is not an open.mp api struct", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "base type '{0}' in base type list of open.mp api struct '{1}' is not an open.mp api struct."); + + public static readonly DiagnosticDescriptor Sash0007ApiStructMustNotContainFields = new( + "SASH0007", + "open.mp api struct may not contain fields", + "open.mp api struct '{0}' may not contain fields", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp api struct '{0}' may not contain fields or properties with backing fields."); + + public static readonly DiagnosticDescriptor Sash0008EventHandlerMarshalRefReturnUnsupported = new( + "SASH0008", + "'ref return' marshalling not supported for open.mp event handler", + "open.mp event handler function '{0}' use of 'ref return' in combination with marshalling not supported", + DiagnosticCategories.Correctness, + DiagnosticSeverity.Error, + true, + "open.mp event handler function '{0}' returns a value by ref while also using a marshaller which is not supported."); + +} diff --git a/src/SampSharp.Analyzer/AnalyzerReleases.Shipped.md b/src/SampSharp.Analyzer/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..60b59dd9 --- /dev/null +++ b/src/SampSharp.Analyzer/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/SampSharp.Analyzer/AnalyzerReleases.Unshipped.md b/src/SampSharp.Analyzer/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..7a553659 --- /dev/null +++ b/src/SampSharp.Analyzer/AnalyzerReleases.Unshipped.md @@ -0,0 +1,15 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +SASH0001 | Correctness | Error | AnalyzerIds +SASH0002 | Correctness | Error | AnalyzerIds +SASH0003 | Correctness | Error | AnalyzerIds +SASH0004 | Correctness | Error | AnalyzerIds +SASH0005 | Correctness | Error | AnalyzerIds +SASH0006 | Correctness | Error | AnalyzerIds +SASH0007 | Correctness | Error | AnalyzerIds +SASH0008 | Correctness | Error | AnalyzerIds \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0001ExtensionAttributeAnalyzer.cs b/src/SampSharp.Analyzer/Analyzers/Sash0001ExtensionAttributeAnalyzer.cs new file mode 100644 index 00000000..a981f12c --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0001ExtensionAttributeAnalyzer.cs @@ -0,0 +1,49 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0001ExtensionAttributeAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0001MissingExtensionAttribute]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.ClassDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var extensionType = context.Compilation.GetTypeByMetadataName(Constants.ExtensionFQN); + var extensionAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ExtensionAttributeFQN); + + if (extensionType == null || extensionAttributeType == null) + { + return; + } + + var classDeclaration = (ClassDeclarationSyntax)context.Node; + + if (!context.SemanticModel.IsBaseType(classDeclaration, extensionType)) + { + return; + } + + if (!context.SemanticModel.HasAttribute(classDeclaration, extensionAttributeType)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0001MissingExtensionAttribute, + classDeclaration.Identifier.GetLocation(), + classDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0002EventHandlerWithGenericParametersAnalyzer.cs b/src/SampSharp.Analyzer/Analyzers/Sash0002EventHandlerWithGenericParametersAnalyzer.cs new file mode 100644 index 00000000..852cbda4 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0002EventHandlerWithGenericParametersAnalyzer.cs @@ -0,0 +1,50 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0002EventHandlerWithGenericParametersAnalyzer : DiagnosticAnalyzer +{ + public const string EventHandlerAttributeTypeFQN = "SampSharp.OpenMp.Core.OpenMpEventHandlerAttribute"; + + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0002GenericEventHandlerUnsupported]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InterfaceDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var eventHandlerAttributeType = context.Compilation.GetTypeByMetadataName(EventHandlerAttributeTypeFQN); + + if (eventHandlerAttributeType == null) + { + return; + } + + var ifaceDeclaration = (InterfaceDeclarationSyntax)context.Node; + + if (ifaceDeclaration.TypeParameterList == null || ifaceDeclaration.TypeParameterList.Parameters.Count == 0) + { + return; + } + + if (context.SemanticModel.HasAttribute(ifaceDeclaration, eventHandlerAttributeType)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0002GenericEventHandlerUnsupported, + ifaceDeclaration.Identifier.GetLocation(), + ifaceDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0003ApiStructMustBePartialAnalyzer.cs b/src/SampSharp.Analyzer/Analyzers/Sash0003ApiStructMustBePartialAnalyzer.cs new file mode 100644 index 00000000..0250c8f4 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0003ApiStructMustBePartialAnalyzer.cs @@ -0,0 +1,48 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0003ApiStructMustBeReadonlyPartialAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.StructDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var apiAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ApiAttributeFQN); + + if (apiAttributeType == null) + { + return; + } + + var structDeclaration = (StructDeclarationSyntax)context.Node; + + if (structDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword) && structDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) + { + return; + } + + if (context.SemanticModel.HasAttribute(structDeclaration, apiAttributeType)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial, + structDeclaration.Identifier.GetLocation(), + structDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer.cs b/src/SampSharp.Analyzer/Analyzers/Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer.cs new file mode 100644 index 00000000..5d587668 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0004ApiStructMethodMarshalRefReturnNotSupportedAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.StructDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var apiAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ApiAttributeFQN); + var marshalUsingAttribute = context.Compilation.GetTypeByMetadataName(Constants.MarshalUsingAttributeFQN); + var nativeMarshallingAttribute = context.Compilation.GetTypeByMetadataName(Constants.NativeMarshallingAttributeFQN); + + if (apiAttributeType == null || marshalUsingAttribute == null || nativeMarshallingAttribute == null) + { + return; + } + + var structDeclaration = (StructDeclarationSyntax)context.Node; + + if (!context.SemanticModel.HasAttribute(structDeclaration, apiAttributeType)) + { + return; + } + + foreach (var method in structDeclaration.Members.OfType()) + { + if (!method.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + return; + } + + if (method.ReturnType is RefTypeSyntax refType) + { + if (method.AttributeLists.Any(attribList => attribList.Target?.Identifier.IsKind(SyntaxKind.ReturnKeyword) == true && + context.SemanticModel.HasAttribute(attribList, marshalUsingAttribute)) || + context.SemanticModel.HasAttribute(refType.Type, nativeMarshallingAttribute)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0004ApiStructMarshalRefReturnUnsupported, + method.Identifier.GetLocation(), + method.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0005ApiStructRequiresAllowUnsafeBlocks.cs b/src/SampSharp.Analyzer/Analyzers/Sash0005ApiStructRequiresAllowUnsafeBlocks.cs new file mode 100644 index 00000000..5184cd15 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0005ApiStructRequiresAllowUnsafeBlocks.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0005ApiStructRequiresAllowUnsafeBlocks : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.Attribute); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var apiAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ApiAttributeFQN); + + if (apiAttributeType == null) + { + return; + } + + var attribute = (AttributeSyntax)context.Node; + + if (context.SemanticModel.IsAttribute(attribute, apiAttributeType)) + { + var compilationOptions = context.Compilation.Options as CSharpCompilationOptions; + if (compilationOptions == null || !compilationOptions.AllowUnsafe) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks, + attribute.GetLocation()); + + context.ReportDiagnostic(diagnostic); + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0006ApiStructBaseTypeMustBeApiStruct.cs b/src/SampSharp.Analyzer/Analyzers/Sash0006ApiStructBaseTypeMustBeApiStruct.cs new file mode 100644 index 00000000..0bf671e6 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0006ApiStructBaseTypeMustBeApiStruct.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0006ApiStructBaseTypeMustBeApiStruct : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.Attribute); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var apiAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ApiAttributeFQN); + + if (apiAttributeType == null) + { + return; + } + + var attribute = (AttributeSyntax)context.Node; + + if (!context.SemanticModel.IsAttribute(attribute, apiAttributeType) || attribute.ArgumentList == null) + { + return; + } + + var structDeclaration = attribute.FirstAncestorOrSelf(); + if (structDeclaration == null) + { + return; + } + + var structName = structDeclaration.Identifier.Text; + + foreach (var arg in attribute.ArgumentList.Arguments) + { + if (arg.Expression is TypeOfExpressionSyntax typeOfExpression) + { + if (!context.SemanticModel.HasAttribute(typeOfExpression.Type, apiAttributeType)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0006ApiStructBaseTypeMustBeApiStruct, + typeOfExpression.GetLocation(), typeOfExpression.Type.ToString(), structName); + + context.ReportDiagnostic(diagnostic); + } + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0007ApiStructMustNotContainFields.cs b/src/SampSharp.Analyzer/Analyzers/Sash0007ApiStructMustNotContainFields.cs new file mode 100644 index 00000000..65d99229 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0007ApiStructMustNotContainFields.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0007ApiStructMustNotContainFields : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0007ApiStructMustNotContainFields]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.StructDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var apiAttributeType = context.Compilation.GetTypeByMetadataName(Constants.ApiAttributeFQN); + + if (apiAttributeType == null) + { + return; + } + + var structDeclaration = (StructDeclarationSyntax)context.Node; + + if (!context.SemanticModel.HasAttribute(structDeclaration, apiAttributeType)) + { + return; + } + + var fieldMembers = structDeclaration.Members.OfType().ToList(); + + var propertyMembersWithBackingFields = structDeclaration.Members + .OfType() + .Where(property => property.AccessorList?.Accessors.Any(accessor => + accessor.Body?.Statements.OfType() + .Any(s => s.Expression is AssignmentExpressionSyntax { Left: IdentifierNameSyntax { Identifier.Text: "field" } }) == true || + accessor.Body == null && accessor.ExpressionBody == null + ) == true) + .ToList(); + + foreach (var fieldMember in fieldMembers) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0007ApiStructMustNotContainFields, + fieldMember.GetLocation(), + structDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + + foreach (var propertyMember in propertyMembersWithBackingFields) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0007ApiStructMustNotContainFields, + propertyMember.GetLocation(), + structDeclaration.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Analyzers/Sash0008EventHandlerMarshalRefReturnUnsupported.cs b/src/SampSharp.Analyzer/Analyzers/Sash0008EventHandlerMarshalRefReturnUnsupported.cs new file mode 100644 index 00000000..75853b47 --- /dev/null +++ b/src/SampSharp.Analyzer/Analyzers/Sash0008EventHandlerMarshalRefReturnUnsupported.cs @@ -0,0 +1,59 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using SampSharp.Analyzer.Helpers; + +namespace SampSharp.Analyzer.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class Sash0008EventHandlerMarshalRefReturnUnsupported : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics => [AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeSyntaxNode, SyntaxKind.InterfaceDeclaration); + } + + private static void AnalyzeSyntaxNode(SyntaxNodeAnalysisContext context) + { + var eventHandlerAttributeType = context.Compilation.GetTypeByMetadataName(Constants.EventHandlerAttributeFQN); + var marshalUsingAttribute = context.Compilation.GetTypeByMetadataName(Constants.MarshalUsingAttributeFQN); + var nativeMarshallingAttribute = context.Compilation.GetTypeByMetadataName(Constants.NativeMarshallingAttributeFQN); + + if (eventHandlerAttributeType == null || marshalUsingAttribute == null || nativeMarshallingAttribute == null) + { + return; + } + + var structDeclaration = (InterfaceDeclarationSyntax)context.Node; + + if (!context.SemanticModel.HasAttribute(structDeclaration, eventHandlerAttributeType)) + { + return; + } + + foreach (var method in structDeclaration.Members.OfType()) + { + if (method.ReturnType is RefTypeSyntax refType) + { + if (method.AttributeLists.Any(attribList => attribList.Target?.Identifier.IsKind(SyntaxKind.ReturnKeyword) == true && + context.SemanticModel.HasAttribute(attribList, marshalUsingAttribute)) || + context.SemanticModel.HasAttribute(refType.Type, nativeMarshallingAttribute)) + { + var diagnostic = Diagnostic.Create( + AnalyzerIds.Sash0008EventHandlerMarshalRefReturnUnsupported, + method.Identifier.GetLocation(), + method.Identifier.ToString()); + + context.ReportDiagnostic(diagnostic); + } + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Constants.cs b/src/SampSharp.Analyzer/Constants.cs new file mode 100644 index 00000000..e5092ee4 --- /dev/null +++ b/src/SampSharp.Analyzer/Constants.cs @@ -0,0 +1,13 @@ +namespace SampSharp.Analyzer; + +public static class Constants +{ + public const string ApiAttributeFQN = "SampSharp.OpenMp.Core.OpenMpApiAttribute"; + public const string EventHandlerAttributeFQN = "SampSharp.OpenMp.Core.OpenMpEventHandlerAttribute"; + public const string ExtensionFQN = "SampSharp.OpenMp.Core.Extension"; + public const string ExtensionAttributeFQN = "SampSharp.OpenMp.Core.ExtensionAttribute"; + + public const string MarshalUsingAttributeFQN = "System.Runtime.InteropServices.Marshalling.MarshalUsingAttribute"; + public const string NativeMarshallingAttributeFQN = "System.Runtime.InteropServices.Marshalling.NativeMarshallingAttribute"; + +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/DiagnosticCategories.cs b/src/SampSharp.Analyzer/DiagnosticCategories.cs new file mode 100644 index 00000000..b47aba4a --- /dev/null +++ b/src/SampSharp.Analyzer/DiagnosticCategories.cs @@ -0,0 +1,15 @@ +namespace SampSharp.Analyzer; + +public static class DiagnosticCategories +{ + public const string Style = "Style"; + public const string Usage = "Usage"; + public const string Naming = "Naming"; + public const string Performance = "Performance"; + public const string Security = "Security"; + public const string Design = "Design"; + public const string Maintainability = "Maintainability"; + public const string Correctness = "Correctness"; + public const string Documentation = "Documentation"; + public const string Reliability = "Reliability"; +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/Helpers/SemanticModelExtensions.cs b/src/SampSharp.Analyzer/Helpers/SemanticModelExtensions.cs new file mode 100644 index 00000000..4d80266e --- /dev/null +++ b/src/SampSharp.Analyzer/Helpers/SemanticModelExtensions.cs @@ -0,0 +1,56 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.Analyzer.Helpers; + +public static class SemanticModelExtensions +{ + public static bool HasAttribute(this SemanticModel semanticModel, BaseTypeDeclarationSyntax declaration, INamedTypeSymbol attributeType) + { + foreach (var attributeList in declaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + + var symbol = semanticModel.GetTypeInfo(attribute).Type; + if (symbol == null) + { + continue; + } + + if (SymbolEqualityComparer.Default.Equals(symbol, attributeType)) + { + return true; + } + } + } + + return false; + } + + public static bool IsAttribute(this SemanticModel semanticModel, AttributeSyntax attribute, INamedTypeSymbol attributeType) + { + var symbol = semanticModel.GetTypeInfo(attribute).Type; + return symbol != null && SymbolEqualityComparer.Default.Equals(symbol, attributeType); + } + + public static bool HasAttribute(this SemanticModel semanticModel, TypeSyntax type, INamedTypeSymbol attributeType) + { + return semanticModel.GetSymbolInfo(type).Symbol is INamedTypeSymbol sym && + sym.GetAttributes().Any(x => SymbolEqualityComparer.Default.Equals(x.AttributeClass, attributeType)); + } + + public static bool HasAttribute(this SemanticModel semanticModel, AttributeListSyntax attributeList, INamedTypeSymbol attributeType) + { + return attributeList.Attributes.Any(x => SymbolEqualityComparer.Default.Equals(semanticModel.GetTypeInfo(x).Type, attributeType)); + } + + public static bool IsBaseType(this SemanticModel semanticModel, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol baseType) + { + return classDeclaration.BaseList?.Types + .Select(baseTypeSyntax => semanticModel.GetTypeInfo(baseTypeSyntax.Type).Type) + .Any(baseTypeSymbol => SymbolEqualityComparer.Default.Equals(baseTypeSymbol, baseType)) ?? false; + } +} \ No newline at end of file diff --git a/src/SampSharp.Analyzer/SampSharp.Analyzer.csproj b/src/SampSharp.Analyzer/SampSharp.Analyzer.csproj new file mode 100644 index 00000000..3e0bf8bb --- /dev/null +++ b/src/SampSharp.Analyzer/SampSharp.Analyzer.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + true + latest + enable + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/src/SampSharp.CodeFixes/SampSharp.CodeFixes.csproj b/src/SampSharp.CodeFixes/SampSharp.CodeFixes.csproj new file mode 100644 index 00000000..70eb22b3 --- /dev/null +++ b/src/SampSharp.CodeFixes/SampSharp.CodeFixes.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + true + latest + enable + + + + + + + + + + + + + + + + diff --git a/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs b/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs new file mode 100644 index 00000000..26e1ae4e --- /dev/null +++ b/src/SampSharp.CodeFixes/Sash0001ExtensionAttributeCodeFixProvider.cs @@ -0,0 +1,95 @@ +using System; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using SampSharp.Analyzer; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Sash0001ExtensionAttributeCodeFixProvider)), Shared] +public class Sash0001ExtensionAttributeCodeFixProvider : CodeFixProvider +{ + private const string Title = "Add 'ExtensionAttribute'"; + + public sealed override ImmutableArray FixableDiagnosticIds => [AnalyzerIds.Sash0001MissingExtensionAttribute.Id]; + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics[0]; + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var classDeclaration = root + ?.FindToken(diagnosticSpan.Start).Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + + if (classDeclaration == null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => Fix(context.Document, classDeclaration, c), + equivalenceKey: Title), + diagnostic); + } + + private static async Task Fix(Document document, ClassDeclarationSyntax classDeclaration, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (root == null) + { + return document; + } + +#pragma warning disable RS1035 // Do not use APIs banned for analyzers + var random = new Random(); + + var bytes = new byte[8]; + random.NextBytes(bytes); +#pragma warning restore RS1035 // Do not use APIs banned for analyzers + var id = BitConverter.ToUInt64(bytes, 0); + + var newClassDeclaration = classDeclaration.AddAttributeLists( + AttributeList( + SingletonSeparatedList( + Attribute( + ParseName("Extension")) + .WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + LiteralExpression( + SyntaxKind.NumericLiteralExpression, + Literal( + $"0x{id:x16}", + id))))))))); + + var newRoot = root.ReplaceNode(classDeclaration, newClassDeclaration); + + if (root is CompilationUnitSyntax compilationUnit && compilationUnit.Usings.All(u => u.Name?.ToString() != "SampSharp.OpenMp.Core")) + { + newRoot = compilationUnit.AddUsings(UsingDirective(ParseName("SampSharp.OpenMp.Core"))); + } + + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs b/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs new file mode 100644 index 00000000..162c7f90 --- /dev/null +++ b/src/SampSharp.CodeFixes/Sash0003MakeStructPartialCodeFixProvider.cs @@ -0,0 +1,84 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using SampSharp.Analyzer; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; +using Microsoft.CodeAnalysis.CSharp; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Sash0003MakeStructPartialCodeFixProvider)), Shared] +public class Sash0003MakeStructPartialCodeFixProvider : CodeFixProvider +{ + private const string Title = "Make struct partial"; + + public sealed override ImmutableArray FixableDiagnosticIds => [AnalyzerIds.Sash0003ApiStructMustBeReadonlyPartial.Id]; + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var diagnostic = context.Diagnostics[0]; + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var structDeclaration = root + ?.FindToken(diagnosticSpan.Start).Parent + ?.AncestorsAndSelf() + .OfType() + .First(); + + if (structDeclaration == null) + { + return; + } + + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedDocument: c => Fix(context.Document, structDeclaration, c), + equivalenceKey: Title), + diagnostic); + } + + private static async Task Fix(Document document, StructDeclarationSyntax structDeclaration, CancellationToken cancellationToken) + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (root == null) + { + return document; + } + + var newStructDeclaration = structDeclaration; + + if (!newStructDeclaration.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + newStructDeclaration = newStructDeclaration + .WithModifiers( + structDeclaration.Modifiers.Add( + Token(SyntaxKind.PartialKeyword))); + } + + if (!newStructDeclaration.Modifiers.Any(SyntaxKind.ReadOnlyKeyword)) + { + newStructDeclaration = newStructDeclaration + .WithModifiers( + structDeclaration.Modifiers.Insert(0, + Token(SyntaxKind.ReadOnlyKeyword))); + } + + var newRoot = root.ReplaceNode(structDeclaration, newStructDeclaration); + + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/SampSharp.CodeFixes/Sash0005AllowUnsafeBlocksCodeFixProvider.cs b/src/SampSharp.CodeFixes/Sash0005AllowUnsafeBlocksCodeFixProvider.cs new file mode 100644 index 00000000..ffeec8e4 --- /dev/null +++ b/src/SampSharp.CodeFixes/Sash0005AllowUnsafeBlocksCodeFixProvider.cs @@ -0,0 +1,50 @@ +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis; +using SampSharp.Analyzer; +using System.Collections.Immutable; +using System.Composition; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; + +namespace SampSharp.CodeFixes; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(Sash0005AllowUnsafeBlocksCodeFixProvider)), Shared] +public class Sash0005AllowUnsafeBlocksCodeFixProvider : CodeFixProvider +{ + private const string Title = "Set 'AllowUnsafeBlocks' to true"; + + public sealed override ImmutableArray FixableDiagnosticIds => [AnalyzerIds.Sash0005ApiStructRequiresAllowUnsafeBlocks.Id]; + + public sealed override FixAllProvider GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics[0]; + + context.RegisterCodeFix( + CodeAction.Create( + title: Title, + createChangedSolution: _ => Fix(context.Document.Project.Solution, context.Document.Project), + equivalenceKey: Title), + diagnostic); + + return Task.CompletedTask; + } + + private static Task Fix(Solution solution, Project project) + { + var compilationOptions = project.CompilationOptions as CSharpCompilationOptions; + + if (compilationOptions != null && !compilationOptions.AllowUnsafe) + { + var newCompilationOptions = compilationOptions.WithAllowUnsafe(true); + solution = solution.WithProjectCompilationOptions(project.Id, newCompilationOptions); + } + + return Task.FromResult(solution); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Anim/Animation.cs b/src/SampSharp.OpenMp.Core/Api/Anim/Animation.cs new file mode 100644 index 00000000..99e1a009 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Anim/Animation.cs @@ -0,0 +1,2014 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides methods related to animation names. +/// +public static class Animation +{ + private static readonly HashSet _animationNames = + [ + "", + "AIRPORT:THRW_BARL_THRW", + "ATTRACTORS:STEPSIT_IN", + "ATTRACTORS:STEPSIT_LOOP", + "ATTRACTORS:STEPSIT_OUT", + "BAR:BARCUSTOM_GET", + "BAR:BARCUSTOM_LOOP", + "BAR:BARCUSTOM_ORDER", + "BAR:BARMAN_IDLE", + "BAR:BARSERVE_BOTTLE", + "BAR:BARSERVE_GIVE", + "BAR:BARSERVE_GLASS", + "BAR:BARSERVE_IN", + "BAR:BARSERVE_LOOP", + "BAR:BARSERVE_ORDER", + "BAR:DNK_STNDF_LOOP", + "BAR:DNK_STNDM_LOOP", + "BASEBALL:BAT_1", + "BASEBALL:BAT_2", + "BASEBALL:BAT_3", + "BASEBALL:BAT_4", + "BASEBALL:BAT_BLOCK", + "BASEBALL:BAT_HIT_1", + "BASEBALL:BAT_HIT_2", + "BASEBALL:BAT_HIT_3", + "BASEBALL:BAT_IDLE", + "BASEBALL:BAT_M", + "BASEBALL:BAT_PART", + "BD_FIRE:BD_FIRE1", + "BD_FIRE:BD_FIRE2", + "BD_FIRE:BD_FIRE3", + "BD_FIRE:BD_GF_WAVE", + "BD_FIRE:BD_PANIC_01", + "BD_FIRE:BD_PANIC_02", + "BD_FIRE:BD_PANIC_03", + "BD_FIRE:BD_PANIC_04", + "BD_FIRE:BD_PANIC_LOOP", + "BD_FIRE:GRLFRD_KISS_03", + "BD_FIRE:M_SMKLEAN_LOOP", + "BD_FIRE:PLAYA_KISS_03", + "BD_FIRE:WASH_UP", + "BEACH:BATHER", + "BEACH:LAY_BAC_LOOP", + "BEACH:PARKSIT_M_LOOP", + "BEACH:PARKSIT_W_LOOP", + "BEACH:SITNWAIT_LOOP_W", + "BENCHPRESS:GYM_BP_CELEBRATE", + "BENCHPRESS:GYM_BP_DOWN", + "BENCHPRESS:GYM_BP_GETOFF", + "BENCHPRESS:GYM_BP_GETON", + "BENCHPRESS:GYM_BP_UP_A", + "BENCHPRESS:GYM_BP_UP_B", + "BENCHPRESS:GYM_BP_UP_SMOOTH", + "BF_INJECTION:BF_GETIN_LHS", + "BF_INJECTION:BF_GETIN_RHS", + "BF_INJECTION:BF_GETOUT_LHS", + "BF_INJECTION:BF_GETOUT_RHS", + "BIKED:BIKED_BACK", + "BIKED:BIKED_DRIVEBYFT", + "BIKED:BIKED_DRIVEBYLHS", + "BIKED:BIKED_DRIVEBYRHS", + "BIKED:BIKED_FWD", + "BIKED:BIKED_GETOFFBACK", + "BIKED:BIKED_GETOFFLHS", + "BIKED:BIKED_GETOFFRHS", + "BIKED:BIKED_HIT", + "BIKED:BIKED_JUMPONL", + "BIKED:BIKED_JUMPONR", + "BIKED:BIKED_KICK", + "BIKED:BIKED_LEFT", + "BIKED:BIKED_PASSENGER", + "BIKED:BIKED_PUSHES", + "BIKED:BIKED_RIDE", + "BIKED:BIKED_RIGHT", + "BIKED:BIKED_SHUFFLE", + "BIKED:BIKED_STILL", + "BIKEH:BIKEH_BACK", + "BIKEH:BIKEH_DRIVEBYFT", + "BIKEH:BIKEH_DRIVEBYLHS", + "BIKEH:BIKEH_DRIVEBYRHS", + "BIKEH:BIKEH_FWD", + "BIKEH:BIKEH_GETOFFBACK", + "BIKEH:BIKEH_GETOFFLHS", + "BIKEH:BIKEH_GETOFFRHS", + "BIKEH:BIKEH_HIT", + "BIKEH:BIKEH_JUMPONL", + "BIKEH:BIKEH_JUMPONR", + "BIKEH:BIKEH_KICK", + "BIKEH:BIKEH_LEFT", + "BIKEH:BIKEH_PASSENGER", + "BIKEH:BIKEH_PUSHES", + "BIKEH:BIKEH_RIDE", + "BIKEH:BIKEH_RIGHT", + "BIKEH:BIKEH_STILL", + "BIKELEAP:BK_BLNCE_IN", + "BIKELEAP:BK_BLNCE_OUT", + "BIKELEAP:BK_JMP", + "BIKELEAP:BK_RDY_IN", + "BIKELEAP:BK_RDY_OUT", + "BIKELEAP:STRUGGLE_CESAR", + "BIKELEAP:STRUGGLE_DRIVER", + "BIKELEAP:TRUCK_DRIVER", + "BIKELEAP:TRUCK_GETIN", + "BIKES:BIKES_BACK", + "BIKES:BIKES_DRIVEBYFT", + "BIKES:BIKES_DRIVEBYLHS", + "BIKES:BIKES_DRIVEBYRHS", + "BIKES:BIKES_FWD", + "BIKES:BIKES_GETOFFBACK", + "BIKES:BIKES_GETOFFLHS", + "BIKES:BIKES_GETOFFRHS", + "BIKES:BIKES_HIT", + "BIKES:BIKES_JUMPONL", + "BIKES:BIKES_JUMPONR", + "BIKES:BIKES_KICK", + "BIKES:BIKES_LEFT", + "BIKES:BIKES_PASSENGER", + "BIKES:BIKES_PUSHES", + "BIKES:BIKES_RIDE", + "BIKES:BIKES_RIGHT", + "BIKES:BIKES_SNATCH_L", + "BIKES:BIKES_SNATCH_R", + "BIKES:BIKES_STILL", + "BIKEV:BIKEV_BACK", + "BIKEV:BIKEV_DRIVEBYFT", + "BIKEV:BIKEV_DRIVEBYLHS", + "BIKEV:BIKEV_DRIVEBYRHS", + "BIKEV:BIKEV_FWD", + "BIKEV:BIKEV_GETOFFBACK", + "BIKEV:BIKEV_GETOFFLHS", + "BIKEV:BIKEV_GETOFFRHS", + "BIKEV:BIKEV_HIT", + "BIKEV:BIKEV_JUMPONL", + "BIKEV:BIKEV_JUMPONR", + "BIKEV:BIKEV_KICK", + "BIKEV:BIKEV_LEFT", + "BIKEV:BIKEV_PASSENGER", + "BIKEV:BIKEV_PUSHES", + "BIKEV:BIKEV_RIDE", + "BIKEV:BIKEV_RIGHT", + "BIKEV:BIKEV_STILL", + "BIKE_DBZ:PASS_DRIVEBY_BWD", + "BIKE_DBZ:PASS_DRIVEBY_FWD", + "BIKE_DBZ:PASS_DRIVEBY_LHS", + "BIKE_DBZ:PASS_DRIVEBY_RHS", + "BMX:BMX_BACK", + "BMX:BMX_BUNNYHOP", + "BMX:BMX_DRIVEBYFT", + "BMX:BMX_DRIVEBY_LHS", + "BMX:BMX_DRIVEBY_RHS", + "BMX:BMX_FWD", + "BMX:BMX_GETOFFBACK", + "BMX:BMX_GETOFFLHS", + "BMX:BMX_GETOFFRHS", + "BMX:BMX_JUMPONL", + "BMX:BMX_JUMPONR", + "BMX:BMX_LEFT", + "BMX:BMX_PEDAL", + "BMX:BMX_PUSHES", + "BMX:BMX_RIDE", + "BMX:BMX_RIGHT", + "BMX:BMX_SPRINT", + "BMX:BMX_STILL", + "BOMBER:BOM_PLANT", + "BOMBER:BOM_PLANT_2IDLE", + "BOMBER:BOM_PLANT_CROUCH_IN", + "BOMBER:BOM_PLANT_CROUCH_OUT", + "BOMBER:BOM_PLANT_IN", + "BOMBER:BOM_PLANT_LOOP", + "BOX:BOXHIPIN", + "BOX:BOXHIPUP", + "BOX:BOXSHDWN", + "BOX:BOXSHUP", + "BOX:BXHIPWLK", + "BOX:BXHWLKI", + "BOX:BXSHWLK", + "BOX:BXSHWLKI", + "BOX:BXWLKO", + "BOX:CATCH_BOX", + "BSKTBALL:BBALL_DEF_JUMP_SHOT", + "BSKTBALL:BBALL_DEF_LOOP", + "BSKTBALL:BBALL_DEF_STEPL", + "BSKTBALL:BBALL_DEF_STEPR", + "BSKTBALL:BBALL_DNK", + "BSKTBALL:BBALL_DNK_GLI", + "BSKTBALL:BBALL_DNK_GLI_O", + "BSKTBALL:BBALL_DNK_LNCH", + "BSKTBALL:BBALL_DNK_LNCH_O", + "BSKTBALL:BBALL_DNK_LND", + "BSKTBALL:BBALL_DNK_O", + "BSKTBALL:BBALL_IDLE", + "BSKTBALL:BBALL_IDLE2", + "BSKTBALL:BBALL_IDLE2_O", + "BSKTBALL:BBALL_IDLELOOP", + "BSKTBALL:BBALL_IDLELOOP_O", + "BSKTBALL:BBALL_IDLE_O", + "BSKTBALL:BBALL_JUMP_CANCEL", + "BSKTBALL:BBALL_JUMP_CANCEL_O", + "BSKTBALL:BBALL_JUMP_END", + "BSKTBALL:BBALL_JUMP_SHOT", + "BSKTBALL:BBALL_JUMP_SHOT_O", + "BSKTBALL:BBALL_NET_DNK_O", + "BSKTBALL:BBALL_PICKUP", + "BSKTBALL:BBALL_PICKUP_O", + "BSKTBALL:BBALL_REACT_MISS", + "BSKTBALL:BBALL_REACT_SCORE", + "BSKTBALL:BBALL_RUN", + "BSKTBALL:BBALL_RUN_O", + "BSKTBALL:BBALL_SKIDSTOP_L", + "BSKTBALL:BBALL_SKIDSTOP_L_O", + "BSKTBALL:BBALL_SKIDSTOP_R", + "BSKTBALL:BBALL_SKIDSTOP_R_O", + "BSKTBALL:BBALL_WALK", + "BSKTBALL:BBALL_WALKSTOP_L", + "BSKTBALL:BBALL_WALKSTOP_L_O", + "BSKTBALL:BBALL_WALKSTOP_R", + "BSKTBALL:BBALL_WALKSTOP_R_O", + "BSKTBALL:BBALL_WALK_O", + "BSKTBALL:BBALL_WALK_START", + "BSKTBALL:BBALL_WALK_START_O", + "BUDDY:BUDDY_CROUCHFIRE", + "BUDDY:BUDDY_CROUCHRELOAD", + "BUDDY:BUDDY_FIRE", + "BUDDY:BUDDY_FIRE_POOR", + "BUDDY:BUDDY_RELOAD", + "BUS:BUS_CLOSE", + "BUS:BUS_GETIN_LHS", + "BUS:BUS_GETIN_RHS", + "BUS:BUS_GETOUT_LHS", + "BUS:BUS_GETOUT_RHS", + "BUS:BUS_JACKED_LHS", + "BUS:BUS_OPEN", + "BUS:BUS_OPEN_RHS", + "BUS:BUS_PULLOUT_LHS", + "CAMERA:CAMCRCH_CMON", + "CAMERA:CAMCRCH_IDLELOOP", + "CAMERA:CAMCRCH_STAY", + "CAMERA:CAMCRCH_TO_CAMSTND", + "CAMERA:CAMSTND_CMON", + "CAMERA:CAMSTND_IDLELOOP", + "CAMERA:CAMSTND_LKABT", + "CAMERA:CAMSTND_TO_CAMCRCH", + "CAMERA:PICCRCH_IN", + "CAMERA:PICCRCH_OUT", + "CAMERA:PICCRCH_TAKE", + "CAMERA:PICSTND_IN", + "CAMERA:PICSTND_OUT", + "CAMERA:PICSTND_TAKE", + "CAR:FIXN_CAR_LOOP", + "CAR:FIXN_CAR_OUT", + "CAR:FLAG_DROP", + "CAR:SIT_RELAXED", + "CAR:TAP_HAND", + "CAR:TYD2CAR_BUMP", + "CAR:TYD2CAR_HIGH", + "CAR:TYD2CAR_LOW", + "CAR:TYD2CAR_MED", + "CAR:TYD2CAR_TURNL", + "CAR:TYD2CAR_TURNR", + "CARRY:CRRY_PRTIAL", + "CARRY:LIFTUP", + "CARRY:LIFTUP05", + "CARRY:LIFTUP105", + "CARRY:PUTDWN", + "CARRY:PUTDWN05", + "CARRY:PUTDWN105", + "CAR_CHAT:CARFONE_IN", + "CAR_CHAT:CARFONE_LOOPA", + "CAR_CHAT:CARFONE_LOOPA_TO_B", + "CAR_CHAT:CARFONE_LOOPB", + "CAR_CHAT:CARFONE_LOOPB_TO_A", + "CAR_CHAT:CARFONE_OUT", + "CAR_CHAT:CAR_SC1_BL", + "CAR_CHAT:CAR_SC1_BR", + "CAR_CHAT:CAR_SC1_FL", + "CAR_CHAT:CAR_SC1_FR", + "CAR_CHAT:CAR_SC2_FL", + "CAR_CHAT:CAR_SC3_BR", + "CAR_CHAT:CAR_SC3_FL", + "CAR_CHAT:CAR_SC3_FR", + "CAR_CHAT:CAR_SC4_BL", + "CAR_CHAT:CAR_SC4_BR", + "CAR_CHAT:CAR_SC4_FL", + "CAR_CHAT:CAR_SC4_FR", + "CAR_CHAT:CAR_TALKM_IN", + "CAR_CHAT:CAR_TALKM_LOOP", + "CAR_CHAT:CAR_TALKM_OUT", + "CASINO:CARDS_IN", + "CASINO:CARDS_LOOP", + "CASINO:CARDS_LOSE", + "CASINO:CARDS_OUT", + "CASINO:CARDS_PICK_01", + "CASINO:CARDS_PICK_02", + "CASINO:CARDS_RAISE", + "CASINO:CARDS_WIN", + "CASINO:DEALONE", + "CASINO:MANWINB", + "CASINO:MANWIND", + "CASINO:ROULETTE_BET", + "CASINO:ROULETTE_IN", + "CASINO:ROULETTE_LOOP", + "CASINO:ROULETTE_LOSE", + "CASINO:ROULETTE_OUT", + "CASINO:ROULETTE_WIN", + "CASINO:SLOT_BET_01", + "CASINO:SLOT_BET_02", + "CASINO:SLOT_IN", + "CASINO:SLOT_LOSE_OUT", + "CASINO:SLOT_PLYR", + "CASINO:SLOT_WAIT", + "CASINO:SLOT_WIN_OUT", + "CASINO:WOF", + "CHAINSAW:CSAW_1", + "CHAINSAW:CSAW_2", + "CHAINSAW:CSAW_3", + "CHAINSAW:CSAW_G", + "CHAINSAW:CSAW_HIT_1", + "CHAINSAW:CSAW_HIT_2", + "CHAINSAW:CSAW_HIT_3", + "CHAINSAW:CSAW_PART", + "CHAINSAW:IDLE_CSAW", + "CHAINSAW:WEAPON_CSAW", + "CHAINSAW:WEAPON_CSAWLO", + "CHOPPA:CHOPPA_BACK", + "CHOPPA:CHOPPA_BUNNYHOP", + "CHOPPA:CHOPPA_DRIVEBYFT", + "CHOPPA:CHOPPA_DRIVEBY_LHS", + "CHOPPA:CHOPPA_DRIVEBY_RHS", + "CHOPPA:CHOPPA_FWD", + "CHOPPA:CHOPPA_GETOFFBACK", + "CHOPPA:CHOPPA_GETOFFLHS", + "CHOPPA:CHOPPA_GETOFFRHS", + "CHOPPA:CHOPPA_JUMPONL", + "CHOPPA:CHOPPA_JUMPONR", + "CHOPPA:CHOPPA_LEFT", + "CHOPPA:CHOPPA_PEDAL", + "CHOPPA:CHOPPA_PUSHES", + "CHOPPA:CHOPPA_RIDE", + "CHOPPA:CHOPPA_RIGHT", + "CHOPPA:CHOPPA_SPRINT", + "CHOPPA:CHOPPA_STILL", + "CLOTHES:CLO_BUY", + "CLOTHES:CLO_IN", + "CLOTHES:CLO_OUT", + "CLOTHES:CLO_POSE_HAT", + "CLOTHES:CLO_POSE_IN", + "CLOTHES:CLO_POSE_IN_O", + "CLOTHES:CLO_POSE_LEGS", + "CLOTHES:CLO_POSE_LOOP", + "CLOTHES:CLO_POSE_OUT", + "CLOTHES:CLO_POSE_OUT_O", + "CLOTHES:CLO_POSE_SHOES", + "CLOTHES:CLO_POSE_TORSO", + "CLOTHES:CLO_POSE_WATCH", + "COACH:COACH_INL", + "COACH:COACH_INR", + "COACH:COACH_OPNL", + "COACH:COACH_OPNR", + "COACH:COACH_OUTL", + "COACH:COACH_OUTR", + "COLT45:2GUNS_CROUCHFIRE", + "COLT45:COLT45_CROUCHFIRE", + "COLT45:COLT45_CROUCHRELOAD", + "COLT45:COLT45_FIRE", + "COLT45:COLT45_FIRE_2HANDS", + "COLT45:COLT45_RELOAD", + "COLT45:SAWNOFF_RELOAD", + "COP_AMBIENT:COPBROWSE_IN", + "COP_AMBIENT:COPBROWSE_LOOP", + "COP_AMBIENT:COPBROWSE_NOD", + "COP_AMBIENT:COPBROWSE_OUT", + "COP_AMBIENT:COPBROWSE_SHAKE", + "COP_AMBIENT:COPLOOK_IN", + "COP_AMBIENT:COPLOOK_LOOP", + "COP_AMBIENT:COPLOOK_NOD", + "COP_AMBIENT:COPLOOK_OUT", + "COP_AMBIENT:COPLOOK_SHAKE", + "COP_AMBIENT:COPLOOK_THINK", + "COP_AMBIENT:COPLOOK_WATCH", + "COP_DVBYZ:COP_DVBY_B", + "COP_DVBYZ:COP_DVBY_FT", + "COP_DVBYZ:COP_DVBY_L", + "COP_DVBYZ:COP_DVBY_R", + "CRACK:BBALBAT_IDLE_01", + "CRACK:BBALBAT_IDLE_02", + "CRACK:CRCKDETH1", + "CRACK:CRCKDETH2", + "CRACK:CRCKDETH3", + "CRACK:CRCKDETH4", + "CRACK:CRCKIDLE1", + "CRACK:CRCKIDLE2", + "CRACK:CRCKIDLE3", + "CRACK:CRCKIDLE4", + "CRIB:CRIB_CONSOLE_LOOP", + "CRIB:CRIB_USE_SWITCH", + "CRIB:PED_CONSOLE_LOOP", + "CRIB:PED_CONSOLE_LOOSE", + "CRIB:PED_CONSOLE_WIN", + "DAM_JUMP:DAM_DIVE_LOOP", + "DAM_JUMP:DAM_LAND", + "DAM_JUMP:DAM_LAUNCH", + "DAM_JUMP:JUMP_ROLL", + "DAM_JUMP:SF_JUMPWALL", + "DANCING:BD_CLAP", + "DANCING:BD_CLAP1", + "DANCING:DANCE_LOOP", + "DANCING:DAN_DOWN_A", + "DANCING:DAN_LEFT_A", + "DANCING:DAN_LOOP_A", + "DANCING:DAN_RIGHT_A", + "DANCING:DAN_UP_A", + "DANCING:DNCE_M_A", + "DANCING:DNCE_M_B", + "DANCING:DNCE_M_C", + "DANCING:DNCE_M_D", + "DANCING:DNCE_M_E", + "DEALER:DEALER_DEAL", + "DEALER:DEALER_IDLE", + "DEALER:DEALER_IDLE_01", + "DEALER:DEALER_IDLE_02", + "DEALER:DEALER_IDLE_03", + "DEALER:DRUGS_BUY", + "DEALER:SHOP_PAY", + "DILDO:DILDO_1", + "DILDO:DILDO_2", + "DILDO:DILDO_3", + "DILDO:DILDO_BLOCK", + "DILDO:DILDO_G", + "DILDO:DILDO_HIT_1", + "DILDO:DILDO_HIT_2", + "DILDO:DILDO_HIT_3", + "DILDO:DILDO_IDLE", + "DODGE:COVER_DIVE_01", + "DODGE:COVER_DIVE_02", + "DODGE:CRUSHED", + "DODGE:CRUSH_JUMP", + "DOZER:DOZER_ALIGN_LHS", + "DOZER:DOZER_ALIGN_RHS", + "DOZER:DOZER_GETIN_LHS", + "DOZER:DOZER_GETIN_RHS", + "DOZER:DOZER_GETOUT_LHS", + "DOZER:DOZER_GETOUT_RHS", + "DOZER:DOZER_JACKED_LHS", + "DOZER:DOZER_JACKED_RHS", + "DOZER:DOZER_PULLOUT_LHS", + "DOZER:DOZER_PULLOUT_RHS", + "DRIVEBYS:GANG_DRIVEBYLHS", + "DRIVEBYS:GANG_DRIVEBYLHS_BWD", + "DRIVEBYS:GANG_DRIVEBYLHS_FWD", + "DRIVEBYS:GANG_DRIVEBYRHS", + "DRIVEBYS:GANG_DRIVEBYRHS_BWD", + "DRIVEBYS:GANG_DRIVEBYRHS_FWD", + "DRIVEBYS:GANG_DRIVEBYTOP_LHS", + "DRIVEBYS:GANG_DRIVEBYTOP_RHS", + "FAT:FATIDLE", + "FAT:FATIDLE_ARMED", + "FAT:FATIDLE_CSAW", + "FAT:FATIDLE_ROCKET", + "FAT:FATRUN", + "FAT:FATRUN_ARMED", + "FAT:FATRUN_CSAW", + "FAT:FATRUN_ROCKET", + "FAT:FATSPRINT", + "FAT:FATWALK", + "FAT:FATWALKSTART", + "FAT:FATWALKSTART_CSAW", + "FAT:FATWALKST_ARMED", + "FAT:FATWALKST_ROCKET", + "FAT:FATWALK_ARMED", + "FAT:FATWALK_CSAW", + "FAT:FATWALK_ROCKET", + "FAT:IDLE_TIRED", + "FIGHT_B:FIGHTB_1", + "FIGHT_B:FIGHTB_2", + "FIGHT_B:FIGHTB_3", + "FIGHT_B:FIGHTB_BLOCK", + "FIGHT_B:FIGHTB_G", + "FIGHT_B:FIGHTB_IDLE", + "FIGHT_B:FIGHTB_M", + "FIGHT_B:HITB_1", + "FIGHT_B:HITB_2", + "FIGHT_B:HITB_3", + "FIGHT_C:FIGHTC_1", + "FIGHT_C:FIGHTC_2", + "FIGHT_C:FIGHTC_3", + "FIGHT_C:FIGHTC_BLOCK", + "FIGHT_C:FIGHTC_BLOCKING", + "FIGHT_C:FIGHTC_G", + "FIGHT_C:FIGHTC_IDLE", + "FIGHT_C:FIGHTC_M", + "FIGHT_C:FIGHTC_SPAR", + "FIGHT_C:HITC_1", + "FIGHT_C:HITC_2", + "FIGHT_C:HITC_3", + "FIGHT_D:FIGHTD_1", + "FIGHT_D:FIGHTD_2", + "FIGHT_D:FIGHTD_3", + "FIGHT_D:FIGHTD_BLOCK", + "FIGHT_D:FIGHTD_G", + "FIGHT_D:FIGHTD_IDLE", + "FIGHT_D:FIGHTD_M", + "FIGHT_D:HITD_1", + "FIGHT_D:HITD_2", + "FIGHT_D:HITD_3", + "FIGHT_E:FIGHTKICK", + "FIGHT_E:FIGHTKICK_B", + "FIGHT_E:HIT_FIGHTKICK", + "FIGHT_E:HIT_FIGHTKICK_B", + "FINALE:FIN_CLIMB_IN", + "FINALE:FIN_COP1_CLIMBOUT2", + "FINALE:FIN_COP1_LOOP", + "FINALE:FIN_COP1_STOMP", + "FINALE:FIN_HANG_L", + "FINALE:FIN_HANG_LOOP", + "FINALE:FIN_HANG_R", + "FINALE:FIN_HANG_SLIP", + "FINALE:FIN_JUMP_ON", + "FINALE:FIN_LAND_CAR", + "FINALE:FIN_LAND_DIE", + "FINALE:FIN_LEGSUP", + "FINALE:FIN_LEGSUP_L", + "FINALE:FIN_LEGSUP_LOOP", + "FINALE:FIN_LEGSUP_R", + "FINALE:FIN_LET_GO", + "FINALE2:FIN_COP1_CLIMBOUT", + "FINALE2:FIN_COP1_FALL", + "FINALE2:FIN_COP1_LOOP", + "FINALE2:FIN_COP1_SHOT", + "FINALE2:FIN_COP1_SWING", + "FINALE2:FIN_COP2_CLIMBOUT", + "FINALE2:FIN_SWITCH_P", + "FINALE2:FIN_SWITCH_S", + "FLAME:FLAME_FIRE", + "FLOWERS:FLOWER_ATTACK", + "FLOWERS:FLOWER_ATTACK_M", + "FLOWERS:FLOWER_HIT", + "FOOD:EAT_BURGER", + "FOOD:EAT_CHICKEN", + "FOOD:EAT_PIZZA", + "FOOD:EAT_VOMIT_P", + "FOOD:EAT_VOMIT_SK", + "FOOD:FF_DAM_BKW", + "FOOD:FF_DAM_FWD", + "FOOD:FF_DAM_LEFT", + "FOOD:FF_DAM_RIGHT", + "FOOD:FF_DIE_BKW", + "FOOD:FF_DIE_FWD", + "FOOD:FF_DIE_LEFT", + "FOOD:FF_DIE_RIGHT", + "FOOD:FF_SIT_EAT1", + "FOOD:FF_SIT_EAT2", + "FOOD:FF_SIT_EAT3", + "FOOD:FF_SIT_IN", + "FOOD:FF_SIT_IN_L", + "FOOD:FF_SIT_IN_R", + "FOOD:FF_SIT_LOOK", + "FOOD:FF_SIT_LOOP", + "FOOD:FF_SIT_OUT_180", + "FOOD:FF_SIT_OUT_L_180", + "FOOD:FF_SIT_OUT_R_180", + "FOOD:SHP_THANK", + "FOOD:SHP_TRAY_IN", + "FOOD:SHP_TRAY_LIFT", + "FOOD:SHP_TRAY_LIFT_IN", + "FOOD:SHP_TRAY_LIFT_LOOP", + "FOOD:SHP_TRAY_LIFT_OUT", + "FOOD:SHP_TRAY_OUT", + "FOOD:SHP_TRAY_POSE", + "FOOD:SHP_TRAY_RETURN", + "FREEWEIGHTS:GYM_BARBELL", + "FREEWEIGHTS:GYM_FREE_A", + "FREEWEIGHTS:GYM_FREE_B", + "FREEWEIGHTS:GYM_FREE_CELEBRATE", + "FREEWEIGHTS:GYM_FREE_DOWN", + "FREEWEIGHTS:GYM_FREE_LOOP", + "FREEWEIGHTS:GYM_FREE_PICKUP", + "FREEWEIGHTS:GYM_FREE_PUTDOWN", + "FREEWEIGHTS:GYM_FREE_UP_SMOOTH", + "GANGS:DEALER_DEAL", + "GANGS:DEALER_IDLE", + "GANGS:DRNKBR_PRTL", + "GANGS:DRNKBR_PRTL_F", + "GANGS:DRUGS_BUY", + "GANGS:HNDSHKAA", + "GANGS:HNDSHKBA", + "GANGS:HNDSHKCA", + "GANGS:HNDSHKCB", + "GANGS:HNDSHKDA", + "GANGS:HNDSHKEA", + "GANGS:HNDSHKFA", + "GANGS:HNDSHKFA_SWT", + "GANGS:INVITE_NO", + "GANGS:INVITE_YES", + "GANGS:LEANIDLE", + "GANGS:LEANIN", + "GANGS:LEANOUT", + "GANGS:PRTIAL_GNGTLKA", + "GANGS:PRTIAL_GNGTLKB", + "GANGS:PRTIAL_GNGTLKC", + "GANGS:PRTIAL_GNGTLKD", + "GANGS:PRTIAL_GNGTLKE", + "GANGS:PRTIAL_GNGTLKF", + "GANGS:PRTIAL_GNGTLKG", + "GANGS:PRTIAL_GNGTLKH", + "GANGS:PRTIAL_HNDSHK_01", + "GANGS:PRTIAL_HNDSHK_BIZ_01", + "GANGS:SHAKE_CARA", + "GANGS:SHAKE_CARK", + "GANGS:SHAKE_CARSH", + "GANGS:SMKCIG_PRTL", + "GANGS:SMKCIG_PRTL_F", + "GHANDS:GSIGN1", + "GHANDS:GSIGN1LH", + "GHANDS:GSIGN2", + "GHANDS:GSIGN2LH", + "GHANDS:GSIGN3", + "GHANDS:GSIGN3LH", + "GHANDS:GSIGN4", + "GHANDS:GSIGN4LH", + "GHANDS:GSIGN5", + "GHANDS:GSIGN5LH", + "GHANDS:LHGSIGN1", + "GHANDS:LHGSIGN2", + "GHANDS:LHGSIGN3", + "GHANDS:LHGSIGN4", + "GHANDS:LHGSIGN5", + "GHANDS:RHGSIGN1", + "GHANDS:RHGSIGN2", + "GHANDS:RHGSIGN3", + "GHANDS:RHGSIGN4", + "GHANDS:RHGSIGN5", + "GHETTO_DB:GDB_CAR2_PLY", + "GHETTO_DB:GDB_CAR2_SMO", + "GHETTO_DB:GDB_CAR2_SWE", + "GHETTO_DB:GDB_CAR_PLY", + "GHETTO_DB:GDB_CAR_RYD", + "GHETTO_DB:GDB_CAR_SMO", + "GHETTO_DB:GDB_CAR_SWE", + "GOGGLES:GOGGLES_PUT_ON", + "GRAFFITI:GRAFFITI_CHKOUT", + "GRAFFITI:SPRAYCAN_FIRE", + "GRAVEYARD:MRNF_LOOP", + "GRAVEYARD:MRNM_LOOP", + "GRAVEYARD:PRST_LOOPA", + "GRENADE:WEAPON_START_THROW", + "GRENADE:WEAPON_THROW", + "GRENADE:WEAPON_THROWU", + "GYMNASIUM:GYMSHADOWBOX", + "GYMNASIUM:GYM_BIKE_CELEBRATE", + "GYMNASIUM:GYM_BIKE_FAST", + "GYMNASIUM:GYM_BIKE_FASTER", + "GYMNASIUM:GYM_BIKE_GETOFF", + "GYMNASIUM:GYM_BIKE_GETON", + "GYMNASIUM:GYM_BIKE_PEDAL", + "GYMNASIUM:GYM_BIKE_SLOW", + "GYMNASIUM:GYM_BIKE_STILL", + "GYMNASIUM:GYM_JOG_FALLOFF", + "GYMNASIUM:GYM_SHADOWBOX", + "GYMNASIUM:GYM_TREAD_CELEBRATE", + "GYMNASIUM:GYM_TREAD_FALLOFF", + "GYMNASIUM:GYM_TREAD_GETOFF", + "GYMNASIUM:GYM_TREAD_GETON", + "GYMNASIUM:GYM_TREAD_JOG", + "GYMNASIUM:GYM_TREAD_SPRINT", + "GYMNASIUM:GYM_TREAD_TIRED", + "GYMNASIUM:GYM_TREAD_WALK", + "GYMNASIUM:GYM_WALK_FALLOFF", + "GYMNASIUM:PEDALS_FAST", + "GYMNASIUM:PEDALS_MED", + "GYMNASIUM:PEDALS_SLOW", + "GYMNASIUM:PEDALS_STILL", + "HAIRCUTS:BRB_BEARD_01", + "HAIRCUTS:BRB_BUY", + "HAIRCUTS:BRB_CUT", + "HAIRCUTS:BRB_CUT_IN", + "HAIRCUTS:BRB_CUT_OUT", + "HAIRCUTS:BRB_HAIR_01", + "HAIRCUTS:BRB_HAIR_02", + "HAIRCUTS:BRB_IN", + "HAIRCUTS:BRB_LOOP", + "HAIRCUTS:BRB_OUT", + "HAIRCUTS:BRB_SIT_IN", + "HAIRCUTS:BRB_SIT_LOOP", + "HAIRCUTS:BRB_SIT_OUT", + "HEIST9:CAS_G2_GASKO", + "HEIST9:SWT_WLLPK_L", + "HEIST9:SWT_WLLPK_L_BACK", + "HEIST9:SWT_WLLPK_R", + "HEIST9:SWT_WLLPK_R_BACK", + "HEIST9:SWT_WLLSHOOT_IN_L", + "HEIST9:SWT_WLLSHOOT_IN_R", + "HEIST9:SWT_WLLSHOOT_OUT_L", + "HEIST9:SWT_WLLSHOOT_OUT_R", + "HEIST9:USE_SWIPECARD", + "INT_HOUSE:BED_IN_L", + "INT_HOUSE:BED_IN_R", + "INT_HOUSE:BED_LOOP_L", + "INT_HOUSE:BED_LOOP_R", + "INT_HOUSE:BED_OUT_L", + "INT_HOUSE:BED_OUT_R", + "INT_HOUSE:LOU_IN", + "INT_HOUSE:LOU_LOOP", + "INT_HOUSE:LOU_OUT", + "INT_HOUSE:WASH_UP", + "INT_OFFICE:FF_DAM_FWD", + "INT_OFFICE:OFF_SIT_2IDLE_180", + "INT_OFFICE:OFF_SIT_BORED_LOOP", + "INT_OFFICE:OFF_SIT_CRASH", + "INT_OFFICE:OFF_SIT_DRINK", + "INT_OFFICE:OFF_SIT_IDLE_LOOP", + "INT_OFFICE:OFF_SIT_IN", + "INT_OFFICE:OFF_SIT_READ", + "INT_OFFICE:OFF_SIT_TYPE_LOOP", + "INT_OFFICE:OFF_SIT_WATCH", + "INT_SHOP:SHOP_CASHIER", + "INT_SHOP:SHOP_IN", + "INT_SHOP:SHOP_LOOKA", + "INT_SHOP:SHOP_LOOKB", + "INT_SHOP:SHOP_LOOP", + "INT_SHOP:SHOP_OUT", + "INT_SHOP:SHOP_PAY", + "INT_SHOP:SHOP_SHELF", + "JST_BUISNESS:GIRL_01", + "JST_BUISNESS:GIRL_02", + "JST_BUISNESS:PLAYER_01", + "JST_BUISNESS:SMOKE_01", + "KART:KART_GETIN_LHS", + "KART:KART_GETIN_RHS", + "KART:KART_GETOUT_LHS", + "KART:KART_GETOUT_RHS", + "KISSING:BD_GF_WAVE", + "KISSING:GFWAVE2", + "KISSING:GF_CARARGUE_01", + "KISSING:GF_CARARGUE_02", + "KISSING:GF_CARSPOT", + "KISSING:GF_STREETARGUE_01", + "KISSING:GF_STREETARGUE_02", + "KISSING:GIFT_GET", + "KISSING:GIFT_GIVE", + "KISSING:GRLFRD_KISS_01", + "KISSING:GRLFRD_KISS_02", + "KISSING:GRLFRD_KISS_03", + "KISSING:PLAYA_KISS_01", + "KISSING:PLAYA_KISS_02", + "KISSING:PLAYA_KISS_03", + "KNIFE:KILL_KNIFE_PED_DAMAGE", + "KNIFE:KILL_KNIFE_PED_DIE", + "KNIFE:KILL_KNIFE_PLAYER", + "KNIFE:KILL_PARTIAL", + "KNIFE:KNIFE_1", + "KNIFE:KNIFE_2", + "KNIFE:KNIFE_3", + "KNIFE:KNIFE_4", + "KNIFE:KNIFE_BLOCK", + "KNIFE:KNIFE_G", + "KNIFE:KNIFE_HIT_1", + "KNIFE:KNIFE_HIT_2", + "KNIFE:KNIFE_HIT_3", + "KNIFE:KNIFE_IDLE", + "KNIFE:KNIFE_PART", + "KNIFE:WEAPON_KNIFEIDLE", + "LAPDAN1:LAPDAN_D", + "LAPDAN1:LAPDAN_P", + "LAPDAN2:LAPDAN_D", + "LAPDAN2:LAPDAN_P", + "LAPDAN3:LAPDAN_D", + "LAPDAN3:LAPDAN_P", + "LOWRIDER:F_SMKLEAN_LOOP", + "LOWRIDER:LRGIRL_BDBNCE", + "LOWRIDER:LRGIRL_HAIR", + "LOWRIDER:LRGIRL_HURRY", + "LOWRIDER:LRGIRL_IDLELOOP", + "LOWRIDER:LRGIRL_IDLE_TO_L0", + "LOWRIDER:LRGIRL_L0_BNCE", + "LOWRIDER:LRGIRL_L0_LOOP", + "LOWRIDER:LRGIRL_L0_TO_L1", + "LOWRIDER:LRGIRL_L12_TO_L0", + "LOWRIDER:LRGIRL_L1_BNCE", + "LOWRIDER:LRGIRL_L1_LOOP", + "LOWRIDER:LRGIRL_L1_TO_L2", + "LOWRIDER:LRGIRL_L2_BNCE", + "LOWRIDER:LRGIRL_L2_LOOP", + "LOWRIDER:LRGIRL_L2_TO_L3", + "LOWRIDER:LRGIRL_L345_TO_L1", + "LOWRIDER:LRGIRL_L3_BNCE", + "LOWRIDER:LRGIRL_L3_LOOP", + "LOWRIDER:LRGIRL_L3_TO_L4", + "LOWRIDER:LRGIRL_L4_BNCE", + "LOWRIDER:LRGIRL_L4_LOOP", + "LOWRIDER:LRGIRL_L4_TO_L5", + "LOWRIDER:LRGIRL_L5_BNCE", + "LOWRIDER:LRGIRL_L5_LOOP", + "LOWRIDER:M_SMKLEAN_LOOP", + "LOWRIDER:M_SMKSTND_LOOP", + "LOWRIDER:PRTIAL_GNGTLKB", + "LOWRIDER:PRTIAL_GNGTLKC", + "LOWRIDER:PRTIAL_GNGTLKD", + "LOWRIDER:PRTIAL_GNGTLKE", + "LOWRIDER:PRTIAL_GNGTLKF", + "LOWRIDER:PRTIAL_GNGTLKG", + "LOWRIDER:PRTIAL_GNGTLKH", + "LOWRIDER:RAP_A_LOOP", + "LOWRIDER:RAP_B_LOOP", + "LOWRIDER:RAP_C_LOOP", + "LOWRIDER:SIT_RELAXED", + "LOWRIDER:TAP_HAND", + "MD_CHASE:CARHIT_HANGON", + "MD_CHASE:CARHIT_TUMBLE", + "MD_CHASE:DONUTDROP", + "MD_CHASE:FEN_CHOPPA_L1", + "MD_CHASE:FEN_CHOPPA_L2", + "MD_CHASE:FEN_CHOPPA_L3", + "MD_CHASE:FEN_CHOPPA_R1", + "MD_CHASE:FEN_CHOPPA_R2", + "MD_CHASE:FEN_CHOPPA_R3", + "MD_CHASE:HANGON_STUN_LOOP", + "MD_CHASE:HANGON_STUN_TURN", + "MD_CHASE:MD_BIKE_2_HANG", + "MD_CHASE:MD_BIKE_JMP_BL", + "MD_CHASE:MD_BIKE_JMP_F", + "MD_CHASE:MD_BIKE_LND_BL", + "MD_CHASE:MD_BIKE_LND_DIE_BL", + "MD_CHASE:MD_BIKE_LND_DIE_F", + "MD_CHASE:MD_BIKE_LND_F", + "MD_CHASE:MD_BIKE_LND_ROLL", + "MD_CHASE:MD_BIKE_LND_ROLL_F", + "MD_CHASE:MD_BIKE_PUNCH", + "MD_CHASE:MD_BIKE_PUNCH_F", + "MD_CHASE:MD_BIKE_SHOT_F", + "MD_CHASE:MD_HANG_LND_ROLL", + "MD_CHASE:MD_HANG_LOOP", + "MD_END:END_SC1_PLY", + "MD_END:END_SC1_RYD", + "MD_END:END_SC1_SMO", + "MD_END:END_SC1_SWE", + "MD_END:END_SC2_PLY", + "MD_END:END_SC2_RYD", + "MD_END:END_SC2_SMO", + "MD_END:END_SC2_SWE", + "MEDIC:CPR", + "MISC:BITCHSLAP", + "MISC:BMX_CELEBRATE", + "MISC:BMX_COMEON", + "MISC:BMX_IDLELOOP_01", + "MISC:BMX_IDLELOOP_02", + "MISC:BMX_TALKLEFT_IN", + "MISC:BMX_TALKLEFT_LOOP", + "MISC:BMX_TALKLEFT_OUT", + "MISC:BMX_TALKRIGHT_IN", + "MISC:BMX_TALKRIGHT_LOOP", + "MISC:BMX_TALKRIGHT_OUT", + "MISC:BNG_WNDW", + "MISC:BNG_WNDW_02", + "MISC:CASE_PICKUP", + "MISC:DOOR_JET", + "MISC:GRAB_L", + "MISC:GRAB_R", + "MISC:HIKER_POSE", + "MISC:HIKER_POSE_L", + "MISC:IDLE_CHAT_02", + "MISC:KAT_THROW_K", + "MISC:KAT_THROW_O", + "MISC:KAT_THROW_P", + "MISC:PASS_RIFLE_O", + "MISC:PASS_RIFLE_PED", + "MISC:PASS_RIFLE_PLY", + "MISC:PICKUP_BOX", + "MISC:PLANE_DOOR", + "MISC:PLANE_EXIT", + "MISC:PLANE_HIJACK", + "MISC:PLUNGER_01", + "MISC:PLYRLEAN_LOOP", + "MISC:PLYR_SHKHEAD", + "MISC:RUN_DIVE", + "MISC:SCRATCHBALLS_01", + "MISC:SEAT_LR", + "MISC:SEAT_TALK_01", + "MISC:SEAT_TALK_02", + "MISC:SEAT_WATCH", + "MISC:SMALPLANE_DOOR", + "MISC:SMLPLANE_DOOR", + "MTB:MTB_BACK", + "MTB:MTB_BUNNYHOP", + "MTB:MTB_DRIVEBYFT", + "MTB:MTB_DRIVEBY_LHS", + "MTB:MTB_DRIVEBY_RHS", + "MTB:MTB_FWD", + "MTB:MTB_GETOFFBACK", + "MTB:MTB_GETOFFLHS", + "MTB:MTB_GETOFFRHS", + "MTB:MTB_JUMPONL", + "MTB:MTB_JUMPONR", + "MTB:MTB_LEFT", + "MTB:MTB_PEDAL", + "MTB:MTB_PUSHES", + "MTB:MTB_RIDE", + "MTB:MTB_RIGHT", + "MTB:MTB_SPRINT", + "MTB:MTB_STILL", + "MUSCULAR:MSCLEWALKST_ARMED", + "MUSCULAR:MSCLEWALKST_CSAW", + "MUSCULAR:MSCLE_RCKT_RUN", + "MUSCULAR:MSCLE_RCKT_WALKST", + "MUSCULAR:MSCLE_RUN_CSAW", + "MUSCULAR:MUSCLEIDLE", + "MUSCULAR:MUSCLEIDLE_ARMED", + "MUSCULAR:MUSCLEIDLE_CSAW", + "MUSCULAR:MUSCLEIDLE_ROCKET", + "MUSCULAR:MUSCLERUN", + "MUSCULAR:MUSCLERUN_ARMED", + "MUSCULAR:MUSCLESPRINT", + "MUSCULAR:MUSCLEWALK", + "MUSCULAR:MUSCLEWALKSTART", + "MUSCULAR:MUSCLEWALK_ARMED", + "MUSCULAR:MUSCLEWALK_CSAW", + "MUSCULAR:MUSCLEWALK_ROCKET", + "NEVADA:NEVADA_GETIN", + "NEVADA:NEVADA_GETOUT", + "ON_LOOKERS:LKAROUND_IN", + "ON_LOOKERS:LKAROUND_LOOP", + "ON_LOOKERS:LKAROUND_OUT", + "ON_LOOKERS:LKUP_IN", + "ON_LOOKERS:LKUP_LOOP", + "ON_LOOKERS:LKUP_OUT", + "ON_LOOKERS:LKUP_POINT", + "ON_LOOKERS:PANIC_COWER", + "ON_LOOKERS:PANIC_HIDE", + "ON_LOOKERS:PANIC_IN", + "ON_LOOKERS:PANIC_LOOP", + "ON_LOOKERS:PANIC_OUT", + "ON_LOOKERS:PANIC_POINT", + "ON_LOOKERS:PANIC_SHOUT", + "ON_LOOKERS:POINTUP_IN", + "ON_LOOKERS:POINTUP_LOOP", + "ON_LOOKERS:POINTUP_OUT", + "ON_LOOKERS:POINTUP_SHOUT", + "ON_LOOKERS:POINT_IN", + "ON_LOOKERS:POINT_LOOP", + "ON_LOOKERS:POINT_OUT", + "ON_LOOKERS:SHOUT_01", + "ON_LOOKERS:SHOUT_02", + "ON_LOOKERS:SHOUT_IN", + "ON_LOOKERS:SHOUT_LOOP", + "ON_LOOKERS:SHOUT_OUT", + "ON_LOOKERS:WAVE_IN", + "ON_LOOKERS:WAVE_LOOP", + "ON_LOOKERS:WAVE_OUT", + "OTB:BETSLP_IN", + "OTB:BETSLP_LKABT", + "OTB:BETSLP_LOOP", + "OTB:BETSLP_OUT", + "OTB:BETSLP_TNK", + "OTB:WTCHRACE_CMON", + "OTB:WTCHRACE_IN", + "OTB:WTCHRACE_LOOP", + "OTB:WTCHRACE_LOSE", + "OTB:WTCHRACE_OUT", + "OTB:WTCHRACE_WIN", + "PARACHUTE:FALL_SKYDIVE", + "PARACHUTE:FALL_SKYDIVE_ACCEL", + "PARACHUTE:FALL_SKYDIVE_DIE", + "PARACHUTE:FALL_SKYDIVE_L", + "PARACHUTE:FALL_SKYDIVE_R", + "PARACHUTE:PARA_DECEL", + "PARACHUTE:PARA_DECEL_O", + "PARACHUTE:PARA_FLOAT", + "PARACHUTE:PARA_FLOAT_O", + "PARACHUTE:PARA_LAND", + "PARACHUTE:PARA_LAND_O", + "PARACHUTE:PARA_LAND_WATER", + "PARACHUTE:PARA_LAND_WATER_O", + "PARACHUTE:PARA_OPEN", + "PARACHUTE:PARA_OPEN_O", + "PARACHUTE:PARA_RIP_LAND_O", + "PARACHUTE:PARA_RIP_LOOP_O", + "PARACHUTE:PARA_RIP_O", + "PARACHUTE:PARA_STEERL", + "PARACHUTE:PARA_STEERL_O", + "PARACHUTE:PARA_STEERR", + "PARACHUTE:PARA_STEERR_O", + "PARK:TAI_CHI_IN", + "PARK:TAI_CHI_LOOP", + "PARK:TAI_CHI_OUT", + "PAULNMAC:PISS_IN", + "PAULNMAC:PISS_LOOP", + "PAULNMAC:PISS_OUT", + "PAULNMAC:PNM_ARGUE1_A", + "PAULNMAC:PNM_ARGUE1_B", + "PAULNMAC:PNM_ARGUE2_A", + "PAULNMAC:PNM_ARGUE2_B", + "PAULNMAC:PNM_LOOP_A", + "PAULNMAC:PNM_LOOP_B", + "PAULNMAC:WANK_IN", + "PAULNMAC:WANK_LOOP", + "PAULNMAC:WANK_OUT", + "PED:ABSEIL", + "PED:ARRESTGUN", + "PED:ATM", + "PED:BIKE_ELBOWL", + "PED:BIKE_ELBOWR", + "PED:BIKE_FALLR", + "PED:BIKE_FALL_OFF", + "PED:BIKE_PICKUPL", + "PED:BIKE_PICKUPR", + "PED:BIKE_PULLUPL", + "PED:BIKE_PULLUPR", + "PED:BOMBER", + "PED:CAR_ALIGNHI_LHS", + "PED:CAR_ALIGNHI_RHS", + "PED:CAR_ALIGN_LHS", + "PED:CAR_ALIGN_RHS", + "PED:CAR_CLOSEDOORL_LHS", + "PED:CAR_CLOSEDOORL_RHS", + "PED:CAR_CLOSEDOOR_LHS", + "PED:CAR_CLOSEDOOR_RHS", + "PED:CAR_CLOSE_LHS", + "PED:CAR_CLOSE_RHS", + "PED:CAR_CRAWLOUTRHS", + "PED:CAR_DEAD_LHS", + "PED:CAR_DEAD_RHS", + "PED:CAR_DOORLOCKED_LHS", + "PED:CAR_DOORLOCKED_RHS", + "PED:CAR_FALLOUT_LHS", + "PED:CAR_FALLOUT_RHS", + "PED:CAR_GETINL_LHS", + "PED:CAR_GETINL_RHS", + "PED:CAR_GETIN_LHS", + "PED:CAR_GETIN_RHS", + "PED:CAR_GETOUTL_LHS", + "PED:CAR_GETOUTL_RHS", + "PED:CAR_GETOUT_LHS", + "PED:CAR_GETOUT_RHS", + "PED:CAR_HOOKERTALK", + "PED:CAR_JACKEDLHS", + "PED:CAR_JACKEDRHS", + "PED:CAR_JUMPIN_LHS", + "PED:CAR_LB", + "PED:CAR_LB_PRO", + "PED:CAR_LB_WEAK", + "PED:CAR_LJACKEDLHS", + "PED:CAR_LJACKEDRHS", + "PED:CAR_LSHUFFLE_RHS", + "PED:CAR_LSIT", + "PED:CAR_OPEN_LHS", + "PED:CAR_OPEN_RHS", + "PED:CAR_PULLOUTL_LHS", + "PED:CAR_PULLOUTL_RHS", + "PED:CAR_PULLOUT_LHS", + "PED:CAR_PULLOUT_RHS", + "PED:CAR_QJACKED", + "PED:CAR_ROLLDOOR", + "PED:CAR_ROLLDOORLO", + "PED:CAR_ROLLOUT_LHS", + "PED:CAR_ROLLOUT_RHS", + "PED:CAR_SHUFFLE_RHS", + "PED:CAR_SIT", + "PED:CAR_SITP", + "PED:CAR_SITPLO", + "PED:CAR_SIT_PRO", + "PED:CAR_SIT_WEAK", + "PED:CAR_TUNE_RADIO", + "PED:CLIMB_IDLE", + "PED:CLIMB_JUMP", + "PED:CLIMB_JUMP2FALL", + "PED:CLIMB_JUMP_B", + "PED:CLIMB_PULL", + "PED:CLIMB_STAND", + "PED:CLIMB_STAND_FINISH", + "PED:COWER", + "PED:CROUCH_ROLL_L", + "PED:CROUCH_ROLL_R", + "PED:DAM_ARML_FRMBK", + "PED:DAM_ARML_FRMFT", + "PED:DAM_ARML_FRMLT", + "PED:DAM_ARMR_FRMBK", + "PED:DAM_ARMR_FRMFT", + "PED:DAM_ARMR_FRMRT", + "PED:DAM_LEGL_FRMBK", + "PED:DAM_LEGL_FRMFT", + "PED:DAM_LEGL_FRMLT", + "PED:DAM_LEGR_FRMBK", + "PED:DAM_LEGR_FRMFT", + "PED:DAM_LEGR_FRMRT", + "PED:DAM_STOMACH_FRMBK", + "PED:DAM_STOMACH_FRMFT", + "PED:DAM_STOMACH_FRMLT", + "PED:DAM_STOMACH_FRMRT", + "PED:DOOR_LHINGE_O", + "PED:DOOR_RHINGE_O", + "PED:DRIVEBYL_L", + "PED:DRIVEBYL_R", + "PED:DRIVEBY_L", + "PED:DRIVEBY_R", + "PED:DRIVE_BOAT", + "PED:DRIVE_BOAT_BACK", + "PED:DRIVE_BOAT_L", + "PED:DRIVE_BOAT_R", + "PED:DRIVE_L", + "PED:DRIVE_LO_L", + "PED:DRIVE_LO_R", + "PED:DRIVE_L_PRO", + "PED:DRIVE_L_PRO_SLOW", + "PED:DRIVE_L_SLOW", + "PED:DRIVE_L_WEAK", + "PED:DRIVE_L_WEAK_SLOW", + "PED:DRIVE_R", + "PED:DRIVE_R_PRO", + "PED:DRIVE_R_PRO_SLOW", + "PED:DRIVE_R_SLOW", + "PED:DRIVE_R_WEAK", + "PED:DRIVE_R_WEAK_SLOW", + "PED:DRIVE_TRUCK", + "PED:DRIVE_TRUCK_BACK", + "PED:DRIVE_TRUCK_L", + "PED:DRIVE_TRUCK_R", + "PED:DROWN", + "PED:DUCK_COWER", + "PED:ENDCHAT_01", + "PED:ENDCHAT_02", + "PED:ENDCHAT_03", + "PED:EV_DIVE", + "PED:EV_STEP", + "PED:FACANGER", + "PED:FACGUM", + "PED:FACSURP", + "PED:FACSURPM", + "PED:FACTALK", + "PED:FACURIOS", + "PED:FALL_BACK", + "PED:FALL_COLLAPSE", + "PED:FALL_FALL", + "PED:FALL_FRONT", + "PED:FALL_GLIDE", + "PED:FALL_LAND", + "PED:FALL_SKYDIVE", + "PED:FIGHT2IDLE", + "PED:FIGHTA_1", + "PED:FIGHTA_2", + "PED:FIGHTA_3", + "PED:FIGHTA_BLOCK", + "PED:FIGHTA_G", + "PED:FIGHTA_M", + "PED:FIGHTIDLE", + "PED:FIGHTSHB", + "PED:FIGHTSHF", + "PED:FIGHTSH_BWD", + "PED:FIGHTSH_FWD", + "PED:FIGHTSH_LEFT", + "PED:FIGHTSH_RIGHT", + "PED:FLEE_LKAROUND_01", + "PED:FLOOR_HIT", + "PED:FLOOR_HIT_F", + "PED:FUCKU", + "PED:GANG_GUNSTAND", + "PED:GAS_CWR", + "PED:GETUP", + "PED:GETUP_FRONT", + "PED:GUM_EAT", + "PED:GUNCROUCHBWD", + "PED:GUNCROUCHFWD", + "PED:GUNMOVE_BWD", + "PED:GUNMOVE_FWD", + "PED:GUNMOVE_L", + "PED:GUNMOVE_R", + "PED:GUN_2_IDLE", + "PED:GUN_BUTT", + "PED:GUN_BUTT_CROUCH", + "PED:GUN_STAND", + "PED:HANDSCOWER", + "PED:HANDSUP", + "PED:HITA_1", + "PED:HITA_2", + "PED:HITA_3", + "PED:HIT_BACK", + "PED:HIT_BEHIND", + "PED:HIT_FRONT", + "PED:HIT_GUN_BUTT", + "PED:HIT_L", + "PED:HIT_R", + "PED:HIT_WALK", + "PED:HIT_WALL", + "PED:IDLESTANCE_FAT", + "PED:IDLESTANCE_OLD", + "PED:IDLE_ARMED", + "PED:IDLE_CHAT", + "PED:IDLE_CSAW", + "PED:IDLE_GANG1", + "PED:IDLE_HBHB", + "PED:IDLE_ROCKET", + "PED:IDLE_STANCE", + "PED:IDLE_TAXI", + "PED:IDLE_TIRED", + "PED:JETPACK_IDLE", + "PED:JOG_FEMALEA", + "PED:JOG_MALEA", + "PED:JUMP_GLIDE", + "PED:JUMP_LAND", + "PED:JUMP_LAUNCH", + "PED:JUMP_LAUNCH_R", + "PED:KART_DRIVE", + "PED:KART_L", + "PED:KART_LB", + "PED:KART_R", + "PED:KD_LEFT", + "PED:KD_RIGHT", + "PED:KO_SHOT_FACE", + "PED:KO_SHOT_FRONT", + "PED:KO_SHOT_STOM", + "PED:KO_SKID_BACK", + "PED:KO_SKID_FRONT", + "PED:KO_SPIN_L", + "PED:KO_SPIN_R", + "PED:PASS_SMOKE_IN_CAR", + "PED:PHONE_IN", + "PED:PHONE_OUT", + "PED:PHONE_TALK", + "PED:PLAYER_SNEAK", + "PED:PLAYER_SNEAK_WALKSTART", + "PED:ROADCROSS", + "PED:ROADCROSS_FEMALE", + "PED:ROADCROSS_GANG", + "PED:ROADCROSS_OLD", + "PED:RUN_1ARMED", + "PED:RUN_ARMED", + "PED:RUN_CIVI", + "PED:RUN_CSAW", + "PED:RUN_FAT", + "PED:RUN_FATOLD", + "PED:RUN_GANG1", + "PED:RUN_LEFT", + "PED:RUN_OLD", + "PED:RUN_PLAYER", + "PED:RUN_RIGHT", + "PED:RUN_ROCKET", + "PED:RUN_STOP", + "PED:RUN_STOPR", + "PED:RUN_WUZI", + "PED:SEAT_DOWN", + "PED:SEAT_IDLE", + "PED:SEAT_UP", + "PED:SHOT_LEFTP", + "PED:SHOT_PARTIAL", + "PED:SHOT_PARTIAL_B", + "PED:SHOT_RIGHTP", + "PED:SHOVE_PARTIAL", + "PED:SMOKE_IN_CAR", + "PED:SPRINT_CIVI", + "PED:SPRINT_PANIC", + "PED:SPRINT_WUZI", + "PED:SWAT_RUN", + "PED:SWIM_TREAD", + "PED:TAP_HAND", + "PED:TAP_HANDP", + "PED:TURN_180", + "PED:TURN_L", + "PED:TURN_R", + "PED:WALK_ARMED", + "PED:WALK_CIVI", + "PED:WALK_CSAW", + "PED:WALK_DOORPARTIAL", + "PED:WALK_DRUNK", + "PED:WALK_FAT", + "PED:WALK_FATOLD", + "PED:WALK_GANG1", + "PED:WALK_GANG2", + "PED:WALK_OLD", + "PED:WALK_PLAYER", + "PED:WALK_ROCKET", + "PED:WALK_SHUFFLE", + "PED:WALK_START", + "PED:WALK_START_ARMED", + "PED:WALK_START_CSAW", + "PED:WALK_START_ROCKET", + "PED:WALK_WUZI", + "PED:WEAPON_CROUCH", + "PED:WOMAN_IDLESTANCE", + "PED:WOMAN_RUN", + "PED:WOMAN_RUNBUSY", + "PED:WOMAN_RUNFATOLD", + "PED:WOMAN_RUNPANIC", + "PED:WOMAN_RUNSEXY", + "PED:WOMAN_WALKBUSY", + "PED:WOMAN_WALKFATOLD", + "PED:WOMAN_WALKNORM", + "PED:WOMAN_WALKOLD", + "PED:WOMAN_WALKPRO", + "PED:WOMAN_WALKSEXY", + "PED:WOMAN_WALKSHOP", + "PED:XPRESSSCRATCH", + "PLAYER_DVBYS:PLYR_DRIVEBYBWD", + "PLAYER_DVBYS:PLYR_DRIVEBYFWD", + "PLAYER_DVBYS:PLYR_DRIVEBYLHS", + "PLAYER_DVBYS:PLYR_DRIVEBYRHS", + "PLAYIDLES:SHIFT", + "PLAYIDLES:SHLDR", + "PLAYIDLES:STRETCH", + "PLAYIDLES:STRLEG", + "PLAYIDLES:TIME", + "POLICE:COPTRAF_AWAY", + "POLICE:COPTRAF_COME", + "POLICE:COPTRAF_LEFT", + "POLICE:COPTRAF_STOP", + "POLICE:COP_GETOUTCAR_LHS", + "POLICE:COP_MOVE_FWD", + "POLICE:CRM_DRGBST_01", + "POLICE:DOOR_KICK", + "POLICE:PLC_DRGBST_01", + "POLICE:PLC_DRGBST_02", + "POOL:POOL_CHALKCUE", + "POOL:POOL_IDLE_STANCE", + "POOL:POOL_LONG_SHOT", + "POOL:POOL_LONG_SHOT_O", + "POOL:POOL_LONG_START", + "POOL:POOL_LONG_START_O", + "POOL:POOL_MED_SHOT", + "POOL:POOL_MED_SHOT_O", + "POOL:POOL_MED_START", + "POOL:POOL_MED_START_O", + "POOL:POOL_PLACE_WHITE", + "POOL:POOL_SHORT_SHOT", + "POOL:POOL_SHORT_SHOT_O", + "POOL:POOL_SHORT_START", + "POOL:POOL_SHORT_START_O", + "POOL:POOL_WALK", + "POOL:POOL_WALK_START", + "POOL:POOL_XLONG_SHOT", + "POOL:POOL_XLONG_SHOT_O", + "POOL:POOL_XLONG_START", + "POOL:POOL_XLONG_START_O", + "POOR:WINWASH_START", + "POOR:WINWASH_WASH2BEG", + "PYTHON:PYTHON_CROUCHFIRE", + "PYTHON:PYTHON_CROUCHRELOAD", + "PYTHON:PYTHON_FIRE", + "PYTHON:PYTHON_FIRE_POOR", + "PYTHON:PYTHON_RELOAD", + "QUAD:QUAD_BACK", + "QUAD:QUAD_DRIVEBY_FT", + "QUAD:QUAD_DRIVEBY_LHS", + "QUAD:QUAD_DRIVEBY_RHS", + "QUAD:QUAD_FWD", + "QUAD:QUAD_GETOFF_B", + "QUAD:QUAD_GETOFF_LHS", + "QUAD:QUAD_GETOFF_RHS", + "QUAD:QUAD_GETON_LHS", + "QUAD:QUAD_GETON_RHS", + "QUAD:QUAD_HIT", + "QUAD:QUAD_KICK", + "QUAD:QUAD_LEFT", + "QUAD:QUAD_PASSENGER", + "QUAD:QUAD_REVERSE", + "QUAD:QUAD_RIDE", + "QUAD:QUAD_RIGHT", + "QUAD_DBZ:PASS_DRIVEBY_BWD", + "QUAD_DBZ:PASS_DRIVEBY_FWD", + "QUAD_DBZ:PASS_DRIVEBY_LHS", + "QUAD_DBZ:PASS_DRIVEBY_RHS", + "RAPPING:LAUGH_01", + "RAPPING:RAP_A_IN", + "RAPPING:RAP_A_LOOP", + "RAPPING:RAP_A_OUT", + "RAPPING:RAP_B_IN", + "RAPPING:RAP_B_LOOP", + "RAPPING:RAP_B_OUT", + "RAPPING:RAP_C_LOOP", + "RIFLE:RIFLE_CROUCHFIRE", + "RIFLE:RIFLE_CROUCHLOAD", + "RIFLE:RIFLE_FIRE", + "RIFLE:RIFLE_FIRE_POOR", + "RIFLE:RIFLE_LOAD", + "RIOT:RIOT_ANGRY", + "RIOT:RIOT_ANGRY_B", + "RIOT:RIOT_CHALLENGE", + "RIOT:RIOT_CHANT", + "RIOT:RIOT_FUKU", + "RIOT:RIOT_PUNCHES", + "RIOT:RIOT_SHOUT", + "ROB_BANK:CAT_SAFE_END", + "ROB_BANK:CAT_SAFE_OPEN", + "ROB_BANK:CAT_SAFE_OPEN_O", + "ROB_BANK:CAT_SAFE_ROB", + "ROB_BANK:SHP_HANDSUP_SCR", + "ROCKET:IDLE_ROCKET", + "ROCKET:ROCKETFIRE", + "ROCKET:RUN_ROCKET", + "ROCKET:WALK_ROCKET", + "ROCKET:WALK_START_ROCKET", + "RUSTLER:PLANE_ALIGN_LHS", + "RUSTLER:PLANE_CLOSE", + "RUSTLER:PLANE_GETIN", + "RUSTLER:PLANE_GETOUT", + "RUSTLER:PLANE_OPEN", + "RYDER:RYD_BECKON_01", + "RYDER:RYD_BECKON_02", + "RYDER:RYD_BECKON_03", + "RYDER:RYD_DIE_PT1", + "RYDER:RYD_DIE_PT2", + "RYDER:VAN_CRATE_L", + "RYDER:VAN_CRATE_R", + "RYDER:VAN_FALL_L", + "RYDER:VAN_FALL_R", + "RYDER:VAN_LEAN_L", + "RYDER:VAN_LEAN_R", + "RYDER:VAN_PICKUP_E", + "RYDER:VAN_PICKUP_S", + "RYDER:VAN_STAND", + "RYDER:VAN_STAND_CRATE", + "RYDER:VAN_THROW", + "SCRATCHING:SCDLDLP", + "SCRATCHING:SCDLULP", + "SCRATCHING:SCDRDLP", + "SCRATCHING:SCDRULP", + "SCRATCHING:SCLNG_L", + "SCRATCHING:SCLNG_R", + "SCRATCHING:SCMID_L", + "SCRATCHING:SCMID_R", + "SCRATCHING:SCSHRTL", + "SCRATCHING:SCSHRTR", + "SCRATCHING:SC_LTOR", + "SCRATCHING:SC_RTOL", + "SHAMAL:SHAMAL_ALIGN", + "SHAMAL:SHAMAL_GETIN_LHS", + "SHAMAL:SHAMAL_GETOUT_LHS", + "SHAMAL:SHAMAL_OPEN", + "SHOP:ROB_2IDLE", + "SHOP:ROB_LOOP", + "SHOP:ROB_LOOP_THREAT", + "SHOP:ROB_SHIFTY", + "SHOP:ROB_STICKUP_IN", + "SHOP:SHP_DUCK", + "SHOP:SHP_DUCK_AIM", + "SHOP:SHP_DUCK_FIRE", + "SHOP:SHP_GUN_AIM", + "SHOP:SHP_GUN_DUCK", + "SHOP:SHP_GUN_FIRE", + "SHOP:SHP_GUN_GRAB", + "SHOP:SHP_GUN_THREAT", + "SHOP:SHP_HANDSUP_SCR", + "SHOP:SHP_JUMP_GLIDE", + "SHOP:SHP_JUMP_LAND", + "SHOP:SHP_JUMP_LAUNCH", + "SHOP:SHP_ROB_GIVECASH", + "SHOP:SHP_ROB_HANDSUP", + "SHOP:SHP_ROB_REACT", + "SHOP:SHP_SERVE_END", + "SHOP:SHP_SERVE_IDLE", + "SHOP:SHP_SERVE_LOOP", + "SHOP:SHP_SERVE_START", + "SHOP:SMOKE_RYD", + "SHOTGUN:SHOTGUN_CROUCHFIRE", + "SHOTGUN:SHOTGUN_FIRE", + "SHOTGUN:SHOTGUN_FIRE_POOR", + "SILENCED:CROUCHRELOAD", + "SILENCED:SILENCECROUCHFIRE", + "SILENCED:SILENCE_FIRE", + "SILENCED:SILENCE_RELOAD", + "SKATE:SKATE_IDLE", + "SKATE:SKATE_RUN", + "SKATE:SKATE_SPRINT", + "SMOKING:F_SMKLEAN_LOOP", + "SMOKING:M_SMKLEAN_LOOP", + "SMOKING:M_SMKSTND_LOOP", + "SMOKING:M_SMK_DRAG", + "SMOKING:M_SMK_IN", + "SMOKING:M_SMK_LOOP", + "SMOKING:M_SMK_OUT", + "SMOKING:M_SMK_TAP", + "SNIPER:WEAPON_SNIPER", + "SPRAYCAN:SPRAYCAN_FIRE", + "SPRAYCAN:SPRAYCAN_FULL", + "STRIP:PLY_CASH", + "STRIP:PUN_CASH", + "STRIP:PUN_HOLLER", + "STRIP:PUN_LOOP", + "STRIP:STRIP_A", + "STRIP:STRIP_B", + "STRIP:STRIP_C", + "STRIP:STRIP_D", + "STRIP:STRIP_E", + "STRIP:STRIP_F", + "STRIP:STRIP_G", + "STRIP:STR_A2B", + "STRIP:STR_B2A", + "STRIP:STR_B2C", + "STRIP:STR_C1", + "STRIP:STR_C2", + "STRIP:STR_C2B", + "STRIP:STR_LOOP_A", + "STRIP:STR_LOOP_B", + "STRIP:STR_LOOP_C", + "SUNBATHE:BATHERDOWN", + "SUNBATHE:BATHERUP", + "SUNBATHE:LAY_BAC_IN", + "SUNBATHE:LAY_BAC_OUT", + "SUNBATHE:PARKSIT_M_IDLEA", + "SUNBATHE:PARKSIT_M_IDLEB", + "SUNBATHE:PARKSIT_M_IDLEC", + "SUNBATHE:PARKSIT_M_IN", + "SUNBATHE:PARKSIT_M_OUT", + "SUNBATHE:PARKSIT_W_IDLEA", + "SUNBATHE:PARKSIT_W_IDLEB", + "SUNBATHE:PARKSIT_W_IDLEC", + "SUNBATHE:PARKSIT_W_IN", + "SUNBATHE:PARKSIT_W_OUT", + "SUNBATHE:SBATHE_F_LIEB2SIT", + "SUNBATHE:SBATHE_F_OUT", + "SUNBATHE:SITNWAIT_IN_W", + "SUNBATHE:SITNWAIT_OUT_W", + "SWAT:GNSTWALL_INJURD", + "SWAT:JMP_WALL1M_180", + "SWAT:RAIL_FALL", + "SWAT:RAIL_FALL_CRAWL", + "SWAT:SWT_BREACH_01", + "SWAT:SWT_BREACH_02", + "SWAT:SWT_BREACH_03", + "SWAT:SWT_GO", + "SWAT:SWT_LKT", + "SWAT:SWT_STY", + "SWAT:SWT_VENT_01", + "SWAT:SWT_VENT_02", + "SWAT:SWT_VNT_SHT_DIE", + "SWAT:SWT_VNT_SHT_IN", + "SWAT:SWT_VNT_SHT_LOOP", + "SWAT:SWT_WLLPK_L", + "SWAT:SWT_WLLPK_L_BACK", + "SWAT:SWT_WLLPK_R", + "SWAT:SWT_WLLPK_R_BACK", + "SWAT:SWT_WLLSHOOT_IN_L", + "SWAT:SWT_WLLSHOOT_IN_R", + "SWAT:SWT_WLLSHOOT_OUT_L", + "SWAT:SWT_WLLSHOOT_OUT_R", + "SWEET:HO_ASS_SLAPPED", + "SWEET:LAFIN_PLAYER", + "SWEET:LAFIN_SWEET", + "SWEET:PLYR_HNDSHLDR_01", + "SWEET:SWEET_ASS_SLAP", + "SWEET:SWEET_HNDSHLDR_01", + "SWEET:SWEET_INJUREDLOOP", + "SWIM:SWIM_BREAST", + "SWIM:SWIM_CRAWL", + "SWIM:SWIM_DIVE_UNDER", + "SWIM:SWIM_GLIDE", + "SWIM:SWIM_JUMPOUT", + "SWIM:SWIM_TREAD", + "SWIM:SWIM_UNDER", + "SWORD:SWORD_1", + "SWORD:SWORD_2", + "SWORD:SWORD_3", + "SWORD:SWORD_4", + "SWORD:SWORD_BLOCK", + "SWORD:SWORD_HIT_1", + "SWORD:SWORD_HIT_2", + "SWORD:SWORD_HIT_3", + "SWORD:SWORD_IDLE", + "SWORD:SWORD_PART", + "TANK:TANK_ALIGN_LHS", + "TANK:TANK_CLOSE_LHS", + "TANK:TANK_DOORLOCKED", + "TANK:TANK_GETIN_LHS", + "TANK:TANK_GETOUT_LHS", + "TANK:TANK_OPEN_LHS", + "TATTOOS:TAT_ARML_IN_O", + "TATTOOS:TAT_ARML_IN_P", + "TATTOOS:TAT_ARML_IN_T", + "TATTOOS:TAT_ARML_OUT_O", + "TATTOOS:TAT_ARML_OUT_P", + "TATTOOS:TAT_ARML_OUT_T", + "TATTOOS:TAT_ARML_POSE_O", + "TATTOOS:TAT_ARML_POSE_P", + "TATTOOS:TAT_ARML_POSE_T", + "TATTOOS:TAT_ARMR_IN_O", + "TATTOOS:TAT_ARMR_IN_P", + "TATTOOS:TAT_ARMR_IN_T", + "TATTOOS:TAT_ARMR_OUT_O", + "TATTOOS:TAT_ARMR_OUT_P", + "TATTOOS:TAT_ARMR_OUT_T", + "TATTOOS:TAT_ARMR_POSE_O", + "TATTOOS:TAT_ARMR_POSE_P", + "TATTOOS:TAT_ARMR_POSE_T", + "TATTOOS:TAT_BACK_IN_O", + "TATTOOS:TAT_BACK_IN_P", + "TATTOOS:TAT_BACK_IN_T", + "TATTOOS:TAT_BACK_OUT_O", + "TATTOOS:TAT_BACK_OUT_P", + "TATTOOS:TAT_BACK_OUT_T", + "TATTOOS:TAT_BACK_POSE_O", + "TATTOOS:TAT_BACK_POSE_P", + "TATTOOS:TAT_BACK_POSE_T", + "TATTOOS:TAT_BACK_SIT_IN_P", + "TATTOOS:TAT_BACK_SIT_LOOP_P", + "TATTOOS:TAT_BACK_SIT_OUT_P", + "TATTOOS:TAT_BEL_IN_O", + "TATTOOS:TAT_BEL_IN_T", + "TATTOOS:TAT_BEL_OUT_O", + "TATTOOS:TAT_BEL_OUT_T", + "TATTOOS:TAT_BEL_POSE_O", + "TATTOOS:TAT_BEL_POSE_T", + "TATTOOS:TAT_CHE_IN_O", + "TATTOOS:TAT_CHE_IN_P", + "TATTOOS:TAT_CHE_IN_T", + "TATTOOS:TAT_CHE_OUT_O", + "TATTOOS:TAT_CHE_OUT_P", + "TATTOOS:TAT_CHE_OUT_T", + "TATTOOS:TAT_CHE_POSE_O", + "TATTOOS:TAT_CHE_POSE_P", + "TATTOOS:TAT_CHE_POSE_T", + "TATTOOS:TAT_DROP_O", + "TATTOOS:TAT_IDLE_LOOP_O", + "TATTOOS:TAT_IDLE_LOOP_T", + "TATTOOS:TAT_SIT_IN_O", + "TATTOOS:TAT_SIT_IN_P", + "TATTOOS:TAT_SIT_IN_T", + "TATTOOS:TAT_SIT_LOOP_O", + "TATTOOS:TAT_SIT_LOOP_P", + "TATTOOS:TAT_SIT_LOOP_T", + "TATTOOS:TAT_SIT_OUT_O", + "TATTOOS:TAT_SIT_OUT_P", + "TATTOOS:TAT_SIT_OUT_T", + "TEC:TEC_CROUCHFIRE", + "TEC:TEC_CROUCHRELOAD", + "TEC:TEC_FIRE", + "TEC:TEC_RELOAD", + "TRAIN:TRAN_GTUP", + "TRAIN:TRAN_HNG", + "TRAIN:TRAN_OUCH", + "TRAIN:TRAN_STMB", + "TRUCK:TRUCK_ALIGN_LHS", + "TRUCK:TRUCK_ALIGN_RHS", + "TRUCK:TRUCK_CLOSEDOOR_LHS", + "TRUCK:TRUCK_CLOSEDOOR_RHS", + "TRUCK:TRUCK_CLOSE_LHS", + "TRUCK:TRUCK_CLOSE_RHS", + "TRUCK:TRUCK_GETIN_LHS", + "TRUCK:TRUCK_GETIN_RHS", + "TRUCK:TRUCK_GETOUT_LHS", + "TRUCK:TRUCK_GETOUT_RHS", + "TRUCK:TRUCK_JACKEDLHS", + "TRUCK:TRUCK_JACKEDRHS", + "TRUCK:TRUCK_OPEN_LHS", + "TRUCK:TRUCK_OPEN_RHS", + "TRUCK:TRUCK_PULLOUT_LHS", + "TRUCK:TRUCK_PULLOUT_RHS", + "TRUCK:TRUCK_SHUFFLE", + "UZI:UZI_CROUCHFIRE", + "UZI:UZI_CROUCHRELOAD", + "UZI:UZI_FIRE", + "UZI:UZI_FIRE_POOR", + "UZI:UZI_RELOAD", + "VAN:VAN_CLOSE_BACK_LHS", + "VAN:VAN_CLOSE_BACK_RHS", + "VAN:VAN_GETIN_BACK_LHS", + "VAN:VAN_GETIN_BACK_RHS", + "VAN:VAN_GETOUT_BACK_LHS", + "VAN:VAN_GETOUT_BACK_RHS", + "VAN:VAN_OPEN_BACK_LHS", + "VAN:VAN_OPEN_BACK_RHS", + "VENDING:VEND_DRINK2_P", + "VENDING:VEND_DRINK_P", + "VENDING:VEND_EAT1_P", + "VENDING:VEND_EAT_P", + "VENDING:VEND_USE", + "VENDING:VEND_USE_PT2", + "VORTEX:CAR_JUMPIN_LHS", + "VORTEX:CAR_JUMPIN_RHS", + "VORTEX:VORTEX_GETOUT_LHS", + "VORTEX:VORTEX_GETOUT_RHS", + "WAYFARER:WF_BACK", + "WAYFARER:WF_DRIVEBYFT", + "WAYFARER:WF_DRIVEBYLHS", + "WAYFARER:WF_DRIVEBYRHS", + "WAYFARER:WF_FWD", + "WAYFARER:WF_GETOFFBACK", + "WAYFARER:WF_GETOFFLHS", + "WAYFARER:WF_GETOFFRHS", + "WAYFARER:WF_HIT", + "WAYFARER:WF_JUMPONL", + "WAYFARER:WF_JUMPONR", + "WAYFARER:WF_KICK", + "WAYFARER:WF_LEFT", + "WAYFARER:WF_PASSENGER", + "WAYFARER:WF_PUSHES", + "WAYFARER:WF_RIDE", + "WAYFARER:WF_RIGHT", + "WAYFARER:WF_STILL", + "WEAPONS:SHP_1H_LIFT", + "WEAPONS:SHP_1H_LIFT_END", + "WEAPONS:SHP_1H_RET", + "WEAPONS:SHP_1H_RET_S", + "WEAPONS:SHP_2H_LIFT", + "WEAPONS:SHP_2H_LIFT_END", + "WEAPONS:SHP_2H_RET", + "WEAPONS:SHP_2H_RET_S", + "WEAPONS:SHP_AR_LIFT", + "WEAPONS:SHP_AR_LIFT_END", + "WEAPONS:SHP_AR_RET", + "WEAPONS:SHP_AR_RET_S", + "WEAPONS:SHP_G_LIFT_IN", + "WEAPONS:SHP_G_LIFT_OUT", + "WEAPONS:SHP_TRAY_IN", + "WEAPONS:SHP_TRAY_OUT", + "WEAPONS:SHP_TRAY_POSE", + "WUZI:CS_DEAD_GUY", + "WUZI:CS_PLYR_PT1", + "WUZI:CS_PLYR_PT2", + "WUZI:CS_WUZI_PT1", + "WUZI:CS_WUZI_PT2", + "WUZI:WALKSTART_IDLE_01", + "WUZI:WUZI_FOLLOW", + "WUZI:WUZI_GREET_PLYR", + "WUZI:WUZI_GREET_WUZI", + "WUZI:WUZI_GRND_CHK", + "WUZI:WUZI_STAND_LOOP", + "WUZI:WUZI_WALK", + "WOP:DANCE_G1", + "WOP:DANCE_G2", + "WOP:DANCE_G3", + "WOP:DANCE_G4", + "WOP:DANCE_G5", + "WOP:DANCE_G6", + "WOP:DANCE_G7", + "WOP:DANCE_G8", + "WOP:DANCE_G9", + "WOP:DANCE_G10", + "WOP:DANCE_G11", + "WOP:DANCE_G12", + "WOP:DANCE_G13", + "WOP:DANCE_G14", + "WOP:DANCE_G15", + "WOP:DANCE_G16", + "WOP:DANCE_B1", + "WOP:DANCE_B2", + "WOP:DANCE_B3", + "WOP:DANCE_B4", + "WOP:DANCE_B5", + "WOP:DANCE_B6", + "WOP:DANCE_B7", + "WOP:DANCE_B8", + "WOP:DANCE_B9", + "WOP:DANCE_B10", + "WOP:DANCE_B11", + "WOP:DANCE_B12", + "WOP:DANCE_B13", + "WOP:DANCE_B14", + "WOP:DANCE_B15", + "WOP:DANCE_B16", + "WOP:DANCE_LOOP", + "GFUNK:DANCE_G1", + "GFUNK:DANCE_G2", + "GFUNK:DANCE_G3", + "GFUNK:DANCE_G4", + "GFUNK:DANCE_G5", + "GFUNK:DANCE_G6", + "GFUNK:DANCE_G7", + "GFUNK:DANCE_G8", + "GFUNK:DANCE_G9", + "GFUNK:DANCE_G10", + "GFUNK:DANCE_G11", + "GFUNK:DANCE_G12", + "GFUNK:DANCE_G13", + "GFUNK:DANCE_G14", + "GFUNK:DANCE_G15", + "GFUNK:DANCE_G16", + "GFUNK:DANCE_B1", + "GFUNK:DANCE_B2", + "GFUNK:DANCE_B3", + "GFUNK:DANCE_B4", + "GFUNK:DANCE_B5", + "GFUNK:DANCE_B6", + "GFUNK:DANCE_B7", + "GFUNK:DANCE_B8", + "GFUNK:DANCE_B9", + "GFUNK:DANCE_B10", + "GFUNK:DANCE_B11", + "GFUNK:DANCE_B12", + "GFUNK:DANCE_B13", + "GFUNK:DANCE_B14", + "GFUNK:DANCE_B15", + "GFUNK:DANCE_B16", + "GFUNK:DANCE_LOOP", + "RUNNINGMAN:DANCE_G1", + "RUNNINGMAN:DANCE_G2", + "RUNNINGMAN:DANCE_G3", + "RUNNINGMAN:DANCE_G4", + "RUNNINGMAN:DANCE_G5", + "RUNNINGMAN:DANCE_G6", + "RUNNINGMAN:DANCE_G7", + "RUNNINGMAN:DANCE_G8", + "RUNNINGMAN:DANCE_G9", + "RUNNINGMAN:DANCE_G10", + "RUNNINGMAN:DANCE_G11", + "RUNNINGMAN:DANCE_G12", + "RUNNINGMAN:DANCE_G13", + "RUNNINGMAN:DANCE_G14", + "RUNNINGMAN:DANCE_G15", + "RUNNINGMAN:DANCE_G16", + "RUNNINGMAN:DANCE_B1", + "RUNNINGMAN:DANCE_B2", + "RUNNINGMAN:DANCE_B3", + "RUNNINGMAN:DANCE_B4", + "RUNNINGMAN:DANCE_B5", + "RUNNINGMAN:DANCE_B6", + "RUNNINGMAN:DANCE_B7", + "RUNNINGMAN:DANCE_B8", + "RUNNINGMAN:DANCE_B9", + "RUNNINGMAN:DANCE_B10", + "RUNNINGMAN:DANCE_B11", + "RUNNINGMAN:DANCE_B12", + "RUNNINGMAN:DANCE_B13", + "RUNNINGMAN:DANCE_B14", + "RUNNINGMAN:DANCE_B15", + "RUNNINGMAN:DANCE_B16", + "RUNNINGMAN:DANCE_LOOP", + "SAMP:FISHINGIDLE", + ]; + + private static readonly HashSet _animLibs = + [ + "AIRPORT", + "ATTRACTORS", + "BAR", + "BASEBALL", + "BD_FIRE", + "BEACH", + "BENCHPRESS", + "BF_INJECTION", + "BIKE_DBZ", + "BIKED", + "BIKEH", + "BIKELEAP", + "BIKES", + "BIKEV", + "BMX", + "BOMBER", + "BOX", + "BSKTBALL", + "BUDDY", + "BUS", + "CAMERA", + "CAR", + "CAR_CHAT", + "CARRY", + "CASINO", + "CHAINSAW", + "CHOPPA", + "CLOTHES", + "COACH", + "COLT45", + "COP_AMBIENT", + "COP_DVBYZ", + "CRACK", + "CRIB", + "DAM_JUMP", + "DANCING", + "DEALER", + "DILDO", + "DODGE", + "DOZER", + "DRIVEBYS", + "FAT", + "FIGHT_B", + "FIGHT_C", + "FIGHT_D", + "FIGHT_E", + "FINALE", + "FINALE2", + "FLAME", + "FLOWERS", + "FOOD", + "FREEWEIGHTS", + "GANGS", + "GFUNK", + "GHANDS", + "GHETTO_DB", + "GOGGLES", + "GRAFFITI", + "GRAVEYARD", + "GRENADE", + "GYMNASIUM", + "HAIRCUTS", + "HEIST9", + "INT_HOUSE", + "INT_OFFICE", + "INT_SHOP", + "JST_BUISNESS", + "KART", + "KISSING", + "KNIFE", + "LAPDAN1", + "LAPDAN2", + "LAPDAN3", + "LOWRIDER", + "MD_CHASE", + "MD_END", + "MEDIC", + "MISC", + "MTB", + "MUSCULAR", + "NEVADA", + "ON_LOOKERS", + "OTB", + "PARACHUTE", + "PARK", + "PAULNMAC", + "PED", + "PLAYER_DVBYS", + "PLAYIDLES", + "POLICE", + "POOL", + "POOR", + "PYTHON", + "QUAD", + "QUAD_DBZ", + "RAPPING", + "RIFLE", + "RIOT", + "ROB_BANK", + "ROCKET", + "RUNNINGMAN", + "RUSTLER", + "RYDER", + "SAMP", + "SCRATCHING", + "SHAMAL", + "SHOP", + "SHOTGUN", + "SILENCED", + "SKATE", + "SMOKING", + "SNIPER", + "SPRAYCAN", + "STRIP", + "SUNBATHE", + "SWAT", + "SWEET", + "SWIM", + "SWORD", + "TANK", + "TATTOOS", + "TEC", + "TRAIN", + "TRUCK", + "UZI", + "VAN", + "VENDING", + "VORTEX", + "WAYFARER", + "WEAPONS", + "WUZI", + "WOP" + ]; + + /// + /// Returns a value indicating whether the animation with the specified and is valid. + /// + /// The name of the animation library. + /// The name of the animation. + /// if the specified animation is valid; otherwise. + public static bool IsNameValid(string library, string name) + { + var fullName = $"{library}:{name}".ToUpperInvariant(); + return _animationNames.Contains(fullName); + } + + /// + /// returns the library and name of the animation with the specified . + /// + /// The identifier of the animation. + /// The library and name of the animation. values are returned if the specified identifier is invalid. + public static (string? library, string? name) GetAnmiation(int id) + { + if (id <= 0 || id >= _animationNames.Count) + { + return (null, null); + } + + var spl = _animationNames.ElementAt(id); + var idx = spl.IndexOf(':'); + + return (spl[..idx], spl[(idx + 1)..]); + } + + /// + /// Returns a value indicating whether the specified animation is valid. + /// + /// The name of the animation library. + /// Whether to consider GTA:SA v1.0 animation libraries as valid. + /// if the specified animation library is valid; otherwise. + public static bool IsLibraryValid(string library, bool v1_0 = true) + { + library = library.ToUpperInvariant(); + + if (_animLibs.Contains(library)) + { + return true; + } + + if (v1_0) + { + // Check three more libraries, removed in version 1.1 + return library == "BLOWJOBZ" || library == "SEX" || library == "SNM"; + } + + return false; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Anim/AnimationData.cs b/src/SampSharp.OpenMp.Core/Api/Anim/AnimationData.cs new file mode 100644 index 00000000..f8ccabc4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Anim/AnimationData.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides animation data for the ApplyAnimation function. +/// +/// The speed to play the animation. +/// If set to 1, the animation will loop. If set to 0, the animation will play once. +/// If set to 0, the player is returned to their old X coordinate once the animation is complete (for animations that move the player such as walking). 1 will not return them to their old position. +/// Same as above but for the Y axis. Should be kept the same as the previous parameter. +/// Setting this to 1 will freeze the player at the end of the animation. 0 will not. +/// Timer in milliseconds. For a never-ending loop it should be 0. +/// The animation library of the animation to apply. +/// The name of the animation to apply. +[NativeMarshalling(typeof(AnimationDataMarshaller))] +public record AnimationData(float Delta, bool Loop, bool LockX, bool LockY, bool Freeze, uint Time, string Library, string Name); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Anim/AnimationDataMarshaller.cs b/src/SampSharp.OpenMp.Core/Api/Anim/AnimationDataMarshaller.cs new file mode 100644 index 00000000..2191b9b4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Anim/AnimationDataMarshaller.cs @@ -0,0 +1,90 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a marshaller entrypoint for marshalling to its unmanaged counterpart. +/// +[CustomMarshaller(typeof(AnimationData), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(AnimationData), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +public static unsafe class AnimationDataMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static int BufferSize { get; } = Marshal.SizeOf(); + + public static BlittableStructRef ConvertToUnmanaged(AnimationData managed, Span callerAllocatedBuffer) + { + var native = ToNative(managed); + + + var ptr = (nint)Unsafe.AsPointer(ref callerAllocatedBuffer.GetPinnableReference()); + Marshal.StructureToPtr(native, ptr, false); + + return new BlittableStructRef(ptr); + } + + private static Native ToNative(AnimationData managed) + { + return new Native(managed.Delta, managed.Loop, managed.LockX, managed.LockY, managed.Freeze, managed.Time, new HybridString16(managed.Library), + new HybridString24(managed.Name)); + } + } + + public static class NativeToManaged + { + public static AnimationData? ConvertToManaged(BlittableStructRef unmanaged) + { + if (!unmanaged.HasValue) + { + return null; + } + + var native = unmanaged.GetValueOrDefault(); + + return FromNative(native); + } + + private static AnimationData FromNative(Native native) + { + return new AnimationData(native.Delta, native.Loop, native.LockX, native.LockY, native.Freeze, native.Time, native.Lib.ToString(), native.Name.ToString()); + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct Native + { + [FieldOffset(0)] + public float Delta; + [FieldOffset(4)] + public bool Loop; + [FieldOffset(5)] + public bool LockX; + [FieldOffset(6)] + public bool LockY; + [FieldOffset(7)] + public bool Freeze; + [FieldOffset(8)] + public uint Time; + [FieldOffset(16)] + public HybridString16 Lib; + [FieldOffset(40)] + public HybridString24 Name; + + public Native(float delta, bool loop, bool lockX, bool lockY, bool freeze, uint time, HybridString16 lib, HybridString24 name) + { + Delta = delta; + Loop = loop; + LockX = lockX; + LockY = lockY; + Freeze = freeze; + Time = time; + Lib = lib; + Name = name; + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Colour.cs b/src/SampSharp.OpenMp.Core/Api/Colour.cs new file mode 100644 index 00000000..70fe136c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Colour.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a colour. +/// +/// The red component of the colour. +/// The green component of the colour. +/// The blue component of the colour. +/// The alpha component of the colour. +[StructLayout(LayoutKind.Sequential)] +public readonly struct Colour(byte r, byte g, byte b, byte a) +{ + /// + /// The red component of the colour. + /// + public readonly byte R = r; + + /// + /// The green component of the colour. + /// + public readonly byte G = g; + + /// + /// The blue component of the colour. + /// + public readonly byte B = b; + + /// + /// The alpha component of the colour. + /// + public readonly byte A = a; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Component/ComponentType.cs b/src/SampSharp.OpenMp.Core/Api/Component/ComponentType.cs new file mode 100644 index 00000000..74ac29cb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Component/ComponentType.cs @@ -0,0 +1,20 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the type of a component. +/// +public enum ComponentType +{ + /// + /// The component is of an unknown type. + /// + Other, + /// + /// The component is a network component. + /// + Network, + /// + /// The component is a pool component. + /// + Pool, +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Component/IComponent.cs b/src/SampSharp.OpenMp.Core/Api/Component/IComponent.cs new file mode 100644 index 00000000..2a9c9587 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Component/IComponent.cs @@ -0,0 +1,55 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +[OpenMpApiPartial] +public readonly partial struct IComponent +{ + /// + /// Gets the type of the component. + /// + /// The component type. + public partial ComponentType GetComponentType(); + + /// + /// Gets the supported version of the component. + /// + /// + /// The idea is for the SDK to be totally forward compatible, so code built at any time will always work, thanks to + /// ABI compatibility. This method is an emergency trap door, just in case that's ever not the problem. Check + /// which major version this component was built for, if it isn't the current major version, fail to load it. + /// Always just returns a constant, recompiling will often be enough to upgrade. `virtual` and `final` to be the + /// vtable, but it can't be overridden because it is a constant. + /// + /// The supported version of the component. + public partial int SupportedVersion(); + + /// + /// Gets the name of the component. + /// + /// The component name. + public partial string ComponentName(); + + /// + /// Gets the version of the component. + /// + /// The component version. + public partial SemanticVersion ComponentVersion(); + + public partial interface IManagedInterface + { + /// + /// Gets the identifier of the component type. + /// + static abstract UID ComponentId { get; } + + /// + /// Casts a handle from a IComponent handle to a handle of this type. + /// + /// The IComponent handle. + /// The handle of this type. + static abstract nint FromComponentHandle(nint handle); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Component/IComponentList.cs b/src/SampSharp.OpenMp.Core/Api/Component/IComponentList.cs new file mode 100644 index 00000000..295f0448 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Component/IComponentList.cs @@ -0,0 +1,34 @@ +using System.Diagnostics.Contracts; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct IComponentList +{ + /// + /// Gets a component by its identifier. + /// + /// The identifier of the component. + /// The component or if not found. + [Pure] + public partial IComponent QueryComponent(UID id); + + /// + /// Gets a component by its type. + /// + /// The type of the component. + /// The component or if not found. + [Pure] + public T QueryComponent() where T : unmanaged, IComponent.IManagedInterface + { + var component = QueryComponent(T.ComponentId); + + var componentHandle = component.Handle; + + componentHandle = T.FromComponentHandle(componentHandle); + return StructPointer.AsStruct(componentHandle); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Component/IExtensible.cs b/src/SampSharp.OpenMp.Core/Api/Component/IExtensible.cs new file mode 100644 index 00000000..7a6cc4d8 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Component/IExtensible.cs @@ -0,0 +1,164 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct IExtensible +{ + /// + /// Gets the extension with the specified . + /// + /// The identifier of the extension type. + /// The extension or if the extension could not be found. + // workaround for the fact that the SDK doesn't expose the miscExtensions field + // ref: https://github.com/openmultiplayer/open.mp-sdk/issues/44 + [OpenMpApiOverload("_workaround")] + public partial IExtension GetExtension(UID id); + + private partial bool AddExtension(IExtension extension, bool autoDeleteExt); + + [OpenMpApiOverload("_uid")] + [OpenMpApiFunction("removeExtension")] + private partial bool RemoveExtensionInternal(UID id); + + [OpenMpApiFunction("removeExtension")] + private partial bool RemoveExtensionInternal(IExtension extension); + + /// + /// Removes the extension with the specified from this extensible. + /// + /// The identifier of the extension. + /// Thrown if the extension could not be found. + public void RemoveExtension(UID id) + { + if (!RemoveExtensionInternal(id)) + { + throw new ArgumentException($"Failed to remove extension with id '{id}'.", nameof(id)); + } + } + + /// + /// Removes the specified from this extensible. + /// + /// The extension to remove. + /// Thrown if the extension could not be found. + public void RemoveExtension(IExtension extension) + { + if (extension == null) + { + // Can't use ThrowIfNull - extension is not a reference type. + throw new ArgumentNullException(nameof(extension)); + } + + if (!RemoveExtensionInternal(extension)) + { + throw new ArgumentException("Failed to remove extension", nameof(extension)); + } + } + + /// + /// Removes the specified managed from this extensible. + /// + /// The type of the managed extension. + /// The managed extension to remove. + /// Thrown if the extension could not be found. + public void RemoveExtension(T extension) where T : Extension + { + ArgumentNullException.ThrowIfNull(extension); + + RemoveExtension(extension.GetUnmanaged()); + } + + /// + /// Adds the specified managed to this extensible. + /// + /// The type of the managed extension. + /// An instance of the extension to add. The extension will be disposed if the extension could not be added to this extensible. + /// A managed extension can only be added to one extensible. + /// Throw when an instance of the extension type was already added to this extensible. + public void AddExtension(T extension) where T : Extension + { + ArgumentNullException.ThrowIfNull(extension); + + var unmanaged = extension.GetUnmanaged(); + + try + { + if (!AddExtension(unmanaged, true)) + { + throw new ArgumentException("Failed to add extension", nameof(extension)); + } + } + catch + { + extension.Dispose(); + throw; + } + } + + /// + /// Gets the specified managed extension from this extensible. + /// + /// The type of the managed extension. + /// An instance of the extension with type . + /// Thrown if the extension could not be found. + public T GetExtension() where T : Extension + { + var result = TryGetExtension(); + + return result ?? throw new ArgumentException($"Extension of type '{typeof(T).Name}' not found.", nameof(T)); + } + + /// + /// Tries to get the specified managed extension from this extensible. + /// + /// The type of the managed extension. + /// An instance of the extension with type or if no extension with the specified type could be found. + public T? TryGetExtension() where T : Extension + { + if (!HasValue) + { + return null; + } + + var ext = GetExtension(ExtensionIdProvider.Id); + + if (ext == null) + { + return null; + } + + + return Extension.Get(ext) as T; + } + + /// + /// Gets the specified unmanaged extension from this extensible. + /// + /// The type of the unmanaged extension. + /// The unmanaged extension or if the extension could not be found. + public T QueryExtension() where T : unmanaged, IExtension.IManagedInterface + { + if (!HasValue) + { + return default; + } + + var extension = GetExtension(T.ExtensionId).Handle; + + return StructPointer.AsStruct(extension); + } + + /// + /// Tries to get the specified unmanaged extension from this extensible. + /// + /// The type of the unmanaged extension. + /// The extension if found, otherwise . + /// if the extension was found; otherwise. + public bool TryQueryExtension(out T extension) where T : unmanaged, IExtension.IManagedInterface + { + extension = QueryExtension(); + return extension.HasValue; + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Component/IExtension.cs b/src/SampSharp.OpenMp.Core/Api/Component/IExtension.cs new file mode 100644 index 00000000..ce83abf2 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Component/IExtension.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +[OpenMpApiPartial] +public readonly partial struct IExtension +{ + public partial interface IManagedInterface + { + /// + /// Gets the identifier of the extension type. + /// + static abstract UID ExtensionId { get; } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Actors/ActorSpawnData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Actors/ActorSpawnData.cs new file mode 100644 index 00000000..e109bc61 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Actors/ActorSpawnData.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the data required to spawn an actor in the game. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct ActorSpawnData +{ + /// + /// Gets the position where the actor will be spawned. + /// + public readonly Vector3 Position; + + /// + /// Gets the angle at which the actor will be facing upon spawn. + /// + public readonly float FacingAngle; + + /// + /// Gets the skin ID of the actor. + /// + public readonly int Skin; +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActor.cs b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActor.cs new file mode 100644 index 00000000..837a0178 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActor.cs @@ -0,0 +1,86 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +public readonly partial struct IActor +{ + /// + /// Sets the actor's skin. + /// + /// The ID of the skin to set. + public partial void SetSkin(int id); + + /// + /// Gets the actor's skin ID. + /// + /// The ID of the actor's skin. + public partial int GetSkin(); + + /// + /// Applies an animation to the actor. + /// + /// The animation data to apply to the actor. + public partial void ApplyAnimation(AnimationData animation); + + /// + /// Gets the animation currently applied to the actor. + /// + /// The animation data currently applied to the actor, or null if no animation is applied. + public partial AnimationData? GetAnimation(); + + /// + /// Clears all animations applied to the actor. + /// + public partial void ClearAnimations(); + + /// + /// Sets the actor's health. + /// + /// The health value to set for the actor. + public partial void SetHealth(float health); + + /// + /// Gets the actor's current health. + /// + /// The current health of the actor. + public partial float GetHealth(); + + /// + /// Sets whether the actor is invulnerable. + /// + /// true to make the actor invulnerable; otherwise, false. + public partial void SetInvulnerable(bool invuln); + + /// + /// Checks whether the actor is invulnerable. + /// + /// true if the actor is invulnerable; otherwise, false. + public partial bool IsInvulnerable(); + + /// + /// Checks if the actor is streamed in for a specific player. + /// + /// The player to check streaming status for. + /// true if the actor is streamed in for the player; otherwise, false. + public partial bool IsStreamedInForPlayer(IPlayer player); + + /// + /// Streams the actor in for a specific player. + /// + /// The player for whom the actor should be streamed in. + public partial void StreamInForPlayer(IPlayer player); + + /// + /// Streams the actor out for a specific player. + /// + /// The player for whom the actor should be streamed out. + public partial void StreamOutForPlayer(IPlayer player); + + /// + /// Gets the spawn data of the actor. + /// + /// The spawn data of the actor, including position, facing angle, and skin. + public partial ref ActorSpawnData GetSpawnData(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorEventHandler.cs new file mode 100644 index 00000000..207646b3 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorEventHandler.cs @@ -0,0 +1,32 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IActorEventHandler +{ + /// + /// Called when a player gives damage to an actor. + /// + /// The player who gave the damage. + /// The actor that received the damage. + /// The amount of damage dealt. + /// The weapon used to deal the damage. + /// The body part of the actor that was hit. + void OnPlayerGiveDamageActor(IPlayer player, IActor actor, float amount, uint weapon, BodyPart part); + + /// + /// Called when an actor streams out for a player. + /// + /// The actor that streamed out. + /// The player for whom the actor streamed out. + void OnActorStreamOut(IActor actor, IPlayer forPlayer); + + /// + /// Called when an actor streams in for a player. + /// + /// The actor that streamed in. + /// The player for whom the actor streamed in. + void OnActorStreamIn(IActor actor, IPlayer forPlayer); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorsComponent.cs new file mode 100644 index 00000000..8706a235 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Actors/IActorsComponent.cs @@ -0,0 +1,28 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IActorsComponent +{ + /// + public static UID ComponentId => new(0xc81ca021eae2ad5c); + + /// + /// Gets the event dispatcher for actor-related events. + /// + /// An event dispatcher for actor-related events. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new actor in the game world. + /// + /// The skin ID of the actor. + /// The position where the actor will be created. + /// The facing angle of the actor. + /// The created actor instance. + public partial IActor Create(int skin, Vector3 pos, float angle); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointData.cs new file mode 100644 index 00000000..291880cc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointData.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ICheckpointDataBase))] +public readonly partial struct ICheckpointData; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointDataBase.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointDataBase.cs new file mode 100644 index 00000000..7670b1eb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointDataBase.cs @@ -0,0 +1,62 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct ICheckpointDataBase +{ + /// + /// Gets the position of this checkpoint. + /// + /// The checkpoint position. + public partial Vector3 GetPosition(); + + /// + /// Sets the position of this checkpoint. + /// + /// The checkpoint position to set. + public partial void SetPosition(ref Vector3 position); + + /// + /// Gets the radius of this checkpoint. + /// + /// The checkpoint radius. + public partial float EtRadius(); + + /// + /// Sets the radius of this checkpoint. + /// + /// The checkpoint radius to set. + public partial void SetRadius(float radius); + + /// + /// Checks whether the player is inside this checkpoint. + /// + /// true if the player is inside the checkpoint; otherwise, false. + public partial bool IsPlayerInside(); + + /// + /// Sets whether the player is inside this checkpoint. + /// + /// true to set the player as inside; false to set them as outside. + public partial void SetPlayerInside(bool inside); + + /// + /// Enables this checkpoint. + /// + public partial void Enable(); + + /// + /// Disables this checkpoint. + /// + public partial void Disable(); + + /// + /// Checks whether this checkpoint is enabled. + /// + /// true if the checkpoint is enabled; otherwise, false. + public partial bool IsEnabled(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointsComponent.cs new file mode 100644 index 00000000..ab42d363 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/ICheckpointsComponent.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface +/// (the source of events). +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct ICheckpointsComponent +{ + /// + public static UID ComponentId => new(0x44a937350d611dde); + + /// + /// Gets the event dispatcher for checkpoint enter/leave events. + /// + public partial IEventDispatcher GetEventDispatcher(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointData.cs new file mode 100644 index 00000000..f80567c7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointData.cs @@ -0,0 +1,23 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerCheckpointData +{ + /// + public static UID ExtensionId => new(0xbc07576aa3591a66); + + /// + /// Gets the player's current race checkpoint data. + /// + /// A reference to the race checkpoint data. + public partial IRaceCheckpointData GetRaceCheckpoint(); + + /// + /// Gets the player's current checkpoint data. + /// + /// A reference to the checkpoint data. + public partial ICheckpointData GetCheckpoint(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointEventHandler.cs new file mode 100644 index 00000000..61b0c374 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IPlayerCheckpointEventHandler.cs @@ -0,0 +1,20 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerCheckpointEventHandler +{ + /// Fired when the player enters their currently-set checkpoint. + void OnPlayerEnterCheckpoint(IPlayer player); + + /// Fired when the player leaves their currently-set checkpoint. + void OnPlayerLeaveCheckpoint(IPlayer player); + + /// Fired when the player enters their currently-set race checkpoint. + void OnPlayerEnterRaceCheckpoint(IPlayer player); + + /// Fired when the player leaves their currently-set race checkpoint. + void OnPlayerLeaveRaceCheckpoint(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IRaceCheckpointData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IRaceCheckpointData.cs new file mode 100644 index 00000000..46767c7b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/IRaceCheckpointData.cs @@ -0,0 +1,35 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ICheckpointDataBase))] +public readonly partial struct IRaceCheckpointData +{ + /// + /// Gets the type of this race checkpoint. + /// + /// The checkpoint type. + [OpenMpApiFunction("getType")] + public partial RaceCheckpointType GetCheckpointType(); + + /// + /// Sets the type of this race checkpoint. + /// + /// The checkpoint type to set. + public partial void SetType(RaceCheckpointType type); + + /// + /// Gets the next checkpoint position in the race sequence. + /// + /// The position of the next checkpoint. + public partial Vector3 GetNextPosition(); + + /// + /// Sets the next checkpoint position in the race sequence. + /// + /// The next checkpoint position to set. + public partial void SetNextPosition(ref Vector3 nextPosition); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/RaceCheckpointType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/RaceCheckpointType.cs new file mode 100644 index 00000000..5b34ef48 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Checkpoints/RaceCheckpointType.cs @@ -0,0 +1,57 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of race checkpoint. +/// +public enum RaceCheckpointType +{ + /// + /// Normal checkpoint. Must have a next position, otherwise it displays as a finish checkpoint. + /// + NORMAL = 0, + + /// + /// Finish checkpoint. Must not have a next position, otherwise it displays as a normal checkpoint. + /// + FINISH, + + /// + /// Invisible checkpoint with no visual representation. + /// + NOTHING, + + /// + /// Floating/aerial normal checkpoint. + /// + AIR_NORMAL, + + /// + /// Floating/aerial finish checkpoint. + /// + AIR_FINISH, + + /// + /// Floating/aerial checkpoint variant 1. + /// + AIR_ONE, + + /// + /// Floating/aerial checkpoint variant 2. + /// + AIR_TWO, + + /// + /// Floating/aerial checkpoint variant 3. + /// + AIR_THREE, + + /// + /// Floating/aerial checkpoint variant 4. + /// + AIR_FOUR, + + /// + /// No checkpoint type. + /// + NONE +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClass.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClass.cs new file mode 100644 index 00000000..7229f07b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClass.cs @@ -0,0 +1,20 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IIDProvider))] +public readonly partial struct IClass +{ + /// + /// Gets the player class data. + /// + /// A reference to the player class information. + public partial ref PlayerClass GetClass(); + + /// + /// Sets the player class data. + /// + /// The player class data to set. + public partial void SetClass(ref PlayerClass data); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassEventHandler.cs new file mode 100644 index 00000000..e7a4cefc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassEventHandler.cs @@ -0,0 +1,16 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IClassEventHandler +{ + /// + /// Called when a player requests to spawn with a class. + /// + /// The player who requested the class. + /// The ID of the class being requested. + /// true to allow the class selection; otherwise, false to deny it. + bool OnPlayerRequestClass(IPlayer player, uint classId); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassesComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassesComponent.cs new file mode 100644 index 00000000..75b75611 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IClassesComponent.cs @@ -0,0 +1,30 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IClassesComponent +{ + /// + public static UID ComponentId => new(0x8cfb3183976da208); + + /// + /// Gets the event dispatcher for class-related events. + /// + /// The event dispatcher instance. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new player class with the specified properties. + /// + /// The skin model ID for the class. + /// The team ID for the class. + /// The spawn position for the class. + /// The spawn angle (rotation) for the class. + /// The weapons assigned to the class. + /// The newly created class object. + public partial IClass Create(int skin, int team, Vector3 spawn, float angle, ref WeaponSlots weapons); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs new file mode 100644 index 00000000..dcdfa7c2 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/IPlayerClassData.cs @@ -0,0 +1,28 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerClassData +{ + /// + public static UID ExtensionId => new(0x185655ded843788b); + + /// + /// Gets the player's current class information. + /// + /// A reference to the player's class data. + public partial ref PlayerClass GetClass(); + + /// + /// Sets the spawn information for the player's class. + /// + /// The class spawn information to set. + public partial void SetSpawnInfo(ref PlayerClass info); + + /// + /// Spawns the player with their current class information. + /// + public partial void SpawnPlayer(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Classes/PlayerClass.cs b/src/SampSharp.OpenMp.Core/Api/Components/Classes/PlayerClass.cs new file mode 100644 index 00000000..774d8fb5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Classes/PlayerClass.cs @@ -0,0 +1,53 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Defines player spawn class information. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerClass +{ + /// + /// The team ID for this class. + /// + public readonly int Team; + + /// + /// The skin model ID for this class. + /// + public readonly int Skin; + + /// + /// The spawn position for this class. + /// + public readonly Vector3 Spawn; + + /// + /// The spawn angle (rotation) for this class. + /// + public readonly float Angle; + + /// + /// The weapon slots assigned to this class. + /// + public readonly WeaponSlots Weapons; + + /// + /// Initializes a new instance of the struct. + /// + /// The team ID. + /// The skin model ID. + /// The spawn position. + /// The spawn angle in degrees. + /// The weapon slots for the class. + public PlayerClass(int team, int skin, Vector3 spawn, float angle, WeaponSlots weapons) + { + Team = team; + Skin = skin; + Spawn = spawn; + Angle = angle; + Weapons = weapons; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSender.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSender.cs new file mode 100644 index 00000000..85ad3be6 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSender.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the source of a console command. +/// +public enum ConsoleCommandSender +{ + /// + /// Command sent from the server console. + /// + Console, + + /// + /// Command sent by a player via rcon. + /// + Player, + + /// + /// Command sent from a custom source. + /// + Custom +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSenderData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSenderData.cs new file mode 100644 index 00000000..75fb5f4d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleCommandSenderData.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Contains information about the source of a console command. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct ConsoleCommandSenderData +{ + /// + /// Initializes a new instance of the struct. + /// + /// The type of sender. + /// The unmanaged handle to the sender object. + public ConsoleCommandSenderData(ConsoleCommandSender sender, IntPtr handle) + { + Sender = sender; + _handle = handle; + } + + /// + /// The type of command sender. + /// + public readonly ConsoleCommandSender Sender; + private readonly nint _handle; + + /// + /// Gets the player if the sender is a player; otherwise, null. + /// + public IPlayer? Player => Sender == ConsoleCommandSender.Player ? new IPlayer(_handle) : null; + + /// + /// Gets the message handler if the sender is custom; otherwise, null. + /// + public ConsoleMessageHandler? Handler => Sender == ConsoleCommandSender.Custom ? new ConsoleMessageHandler(_handle) : null; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleMessageHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleMessageHandler.cs new file mode 100644 index 00000000..99af4ab5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/ConsoleMessageHandler.cs @@ -0,0 +1,14 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct ConsoleMessageHandler +{ + /// + /// Handles a console message. + /// + /// The console message to handle. + public partial void HandleConsoleMessage(string message); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleComponent.cs new file mode 100644 index 00000000..5d69885c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleComponent.cs @@ -0,0 +1,31 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct IConsoleComponent +{ + /// + public static UID ComponentId => new(0xbfa24e49d0c95ee4); + + /// + /// Gets the event dispatcher for console-related events. + /// + /// The event dispatcher instance. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Sends a command from a console source. + /// + /// The command to send. + /// Information about the command sender. + public partial void Send(string command, ref ConsoleCommandSenderData sender); + + /// + /// Sends a message to a console recipient. + /// + /// Information about the message recipient. + /// The message to send. + public partial void SendMessage(ref ConsoleCommandSenderData recipient, string message); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleEventHandler.cs new file mode 100644 index 00000000..7fd8baed --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/IConsoleEventHandler.cs @@ -0,0 +1,33 @@ +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IConsoleEventHandler +{ + /// + /// Called when console text/command is entered. + /// + /// The command name. + /// The command parameters. + /// Information about the command sender. + /// true to prevent further processing of the command; otherwise, false. + bool OnConsoleText(string command, string parameters, ref ConsoleCommandSenderData sender); + + /// + /// Called when an RCON login attempt is made. + /// + /// The player attempting to login. + /// The password used in the login attempt. + /// Whether the login was successful. + void OnRconLoginAttempt(IPlayer player, string password, bool success); + + /// + /// Called when a list of console commands is requested. + /// + /// The set of available commands. + void OnConsoleCommandListRequest(FlatHashSetStringView commands); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Console/IPlayerConsoleData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Console/IPlayerConsoleData.cs new file mode 100644 index 00000000..d47ea24b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Console/IPlayerConsoleData.cs @@ -0,0 +1,23 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerConsoleData +{ + /// + public static UID ExtensionId => new(0x9f8d20f2f471cbae); + + /// + /// Checks whether the player has console (RCON) access. + /// + /// true if the player has console access; otherwise, false. + public partial bool HasConsoleAccess(); + + /// + /// Sets whether the player has console (RCON) access. + /// + /// true to grant console access; false to revoke it. + public partial void SetConsoleAccessibility(bool set); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ICustomModelsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ICustomModelsComponent.cs new file mode 100644 index 00000000..ee1a4a15 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ICustomModelsComponent.cs @@ -0,0 +1,62 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct ICustomModelsComponent +{ + /// + public static UID ComponentId => new(0x15E3CB1E7C77FFFF); + + /// + /// Adds a custom model to the server. + /// + /// The type of model being added. + /// The custom model ID to assign. + /// The base model ID to replace. + /// The name of the DFF file. + /// The name of the TXD file. + /// The virtual world for the model (-1 for all). + /// Time when the model is active (hour). + /// Time when the model is inactive (hour). + /// true if the model was added successfully; otherwise, false. + public partial bool AddCustomModel(ModelType type, int id, int baseId, string dffName, string txdName, int virtualWorld = -1, byte timeOn = 0, byte timeOff = 0); + + /// + /// Gets the relationship between a base model and custom model. + /// + /// Input/output parameter for model IDs. + /// The custom model ID. + /// true if the mapping exists; otherwise, false. + public partial bool GetBaseModel(ref uint baseModelIdOrInput, ref uint customModel); + + /// + /// Gets the event dispatcher for custom model events. + /// + /// The event dispatcher instance. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Gets the model name from its checksum. + /// + /// The model checksum. + /// The model name, or null if not found. + public partial string? GetModelNameFromChecksum(uint checksum); + + /// + /// Checks whether a model ID is a valid custom model. + /// + /// The model ID to check. + /// true if the model is a valid custom model; otherwise, false. + public partial bool IsValidCustomModel(int modelId); + + /// + /// Gets the file paths for a custom model. + /// + /// The custom model ID. + /// When the method returns, contains the DFF file path. + /// When the method returns, contains the TXD file path. + /// true if the paths were retrieved successfully; otherwise, false. + public partial bool GetCustomModelPath(int modelId, out string? dffPath, out string? txdPath); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerCustomModelsData.cs b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerCustomModelsData.cs new file mode 100644 index 00000000..c91c35bb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerCustomModelsData.cs @@ -0,0 +1,30 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerCustomModelsData +{ + /// + public static UID ExtensionId => new(0xD3E2F572B38FB3F2); + + /// + /// Gets the custom skin model ID for the player. + /// + /// The custom skin model ID, or 0 if no custom skin is set. + public partial uint GetCustomSkin(); + + /// + /// Sets the custom skin model for the player. + /// + /// The custom skin model ID to set. + public partial void SetCustomSkin(uint skinModel); + + /// + /// Sends a download URL to the player for custom models. + /// + /// The URL to send to the player. + /// true if the URL was sent successfully; otherwise, false. + public partial bool SendDownloadUrl(string url); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerModelsEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerModelsEventHandler.cs new file mode 100644 index 00000000..b8df044e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/IPlayerModelsEventHandler.cs @@ -0,0 +1,23 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerModelsEventHandler +{ + /// + /// Called when a player has finished downloading all custom models. + /// + /// The player who finished downloading. + void OnPlayerFinishedDownloading(IPlayer player); + + /// + /// Called when a player requests to download a custom model. + /// + /// The player requesting the download. + /// The type of model being requested. + /// The checksum of the model. + /// true to allow the download; false to deny it. + bool OnPlayerRequestDownload(IPlayer player, ModelDownloadType type, uint checksum); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelDownloadType.cs b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelDownloadType.cs new file mode 100644 index 00000000..817e43c0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelDownloadType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of custom model file being downloaded. +/// +public enum ModelDownloadType : byte +{ + /// + /// No model download type. + /// + NONE = 0, + + /// + /// DFF (Draw File Format) - model/collision data file. + /// + DFF = 1, + + /// + /// TXD (Texture Dictionary) - texture file. + /// + TXD = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelType.cs b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelType.cs new file mode 100644 index 00000000..34a1ef44 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/CustomModels/ModelType.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of custom model. +/// +public enum ModelType : byte +{ + /// + /// Custom player skin model. + /// + Skin = 1, + + /// + /// Custom object model. + /// + Object +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogResponse.cs b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogResponse.cs new file mode 100644 index 00000000..064d9527 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogResponse.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies which button was clicked in a dialog response. +/// +public enum DialogResponse +{ + /// + /// The right button was clicked. + /// + DialogResponse_Right = 0, + + /// + /// The left button was clicked. + /// + DialogResponse_Left +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogStyle.cs b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogStyle.cs new file mode 100644 index 00000000..b1a79f3a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/DialogStyle.cs @@ -0,0 +1,37 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the style/type of dialog to display to a player. +/// +public enum DialogStyle +{ + /// + /// Message box dialog with two buttons. + /// + DialogStyle_MSGBOX = 0, + + /// + /// Input dialog with a text input field. + /// + DialogStyle_INPUT, + + /// + /// List dialog with selectable items. + /// + DialogStyle_LIST, + + /// + /// Password input dialog (text is masked). + /// + DialogStyle_PASSWORD, + + /// + /// Tabular list dialog with columns. + /// + DialogStyle_TABLIST, + + /// + /// Tabular list dialog with headers. + /// + DialogStyle_TABLIST_HEADERS +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IDialogsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IDialogsComponent.cs new file mode 100644 index 00000000..2d5b4dfe --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IDialogsComponent.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct IDialogsComponent +{ + /// + public static UID ComponentId => new(0x44a111350d611dde); + + /// + /// Gets the event dispatcher for dialog events. + /// + /// An event dispatcher for events. + public partial IEventDispatcher GetEventDispatcher(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogData.cs new file mode 100644 index 00000000..976f1930 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogData.cs @@ -0,0 +1,46 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerDialogData +{ + /// + public static UID ExtensionId => new(0xbc03376aa3591a11); + + /// + /// Hides any dialog currently shown to the player. + /// + /// The player to hide the dialog for. + public partial void Hide(IPlayer player); + + /// + /// Shows a dialog to the player. + /// + /// The player to show the dialog to. + /// The ID of the dialog. + /// The style/type of dialog. + /// The title of the dialog. + /// The body/content text of the dialog. + /// The text for the first (right) button. + /// The text for the second (left) button. + public partial void Show(IPlayer player, int id, DialogStyle style, string title, string body, string button1, string button2); + + /// + /// Gets the information of the current dialog shown to the player. + /// + /// When the method returns, contains the ID of the dialog. + /// When the method returns, contains the style of the dialog. + /// When the method returns, contains the title of the dialog. + /// When the method returns, contains the body text of the dialog. + /// When the method returns, contains the text of the first button. + /// When the method returns, contains the text of the second button. + public partial void Get(out int id, out DialogStyle style, out string? title, out string? body, out string? button1, out string? button2); + + /// + /// Gets the ID of the currently active dialog for the player. + /// + /// The ID of the active dialog, or -1 if no dialog is active. + public partial int GetActiveID(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogEventHandler.cs new file mode 100644 index 00000000..ddb6e7ef --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Dialogs/IPlayerDialogEventHandler.cs @@ -0,0 +1,18 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerDialogEventHandler +{ + /// + /// Called when a player responds to a dialog. + /// + /// The player who responded to the dialog. + /// The ID of the dialog. + /// The button that was clicked. + /// The selected list item (for list dialogs), or -1 for other types. + /// The text entered (for input dialogs), or empty string for other types. + void OnDialogResponse(IPlayer player, int dialogId, DialogResponse response, int listItem, string inputText); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IFixesComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IFixesComponent.cs new file mode 100644 index 00000000..064dc607 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IFixesComponent.cs @@ -0,0 +1,37 @@ +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct IFixesComponent +{ + /// + public static UID ComponentId => new(0xb5c615eff0329ff7); + + /// + /// Sends game text to all players. + /// + /// The message to display. + /// How long to display the message. + /// The display style/slot for the game text. + /// true if successful; otherwise, false. + public partial bool SendGameTextToAll(string message, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan time, int style); + + /// + /// Hides game text for all players. + /// + /// The display style/slot to hide. + /// true if successful; otherwise, false. + public partial bool HideGameTextForAll(int style); + + /// + /// Clears the current animation of a player or actor. + /// + /// The player to clear animation for. + /// The actor to clear animation for. + public partial void ClearAnimation(IPlayer player, IActor actor); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IPlayerFixesData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IPlayerFixesData.cs new file mode 100644 index 00000000..fa3fc5bd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Fixes/IPlayerFixesData.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerFixesData +{ + /// + public static UID ExtensionId => new(0x672d5d6fbb094ef7); + + /// + /// Sends game text to the player. + /// + /// The message to display. + /// How long to display the message. + /// The display style/slot for the game text. + /// true if successful; otherwise, false. + public partial bool SendGameText(string message, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan time, int style); + + /// + /// Hides game text for the player. + /// + /// The display style/slot to hide. + /// true if successful; otherwise, false. + public partial bool HideGameText(int style); + + /// + /// Checks if the player has active game text at the specified style/slot. + /// + /// The display style/slot to check. + /// true if there is active game text at that slot; otherwise, false. + public partial bool HasGameText(int style); + + /// + /// Gets information about the game text at the specified style/slot. + /// + /// The display style/slot to query. + /// When the method returns, contains the message text. + /// When the method returns, contains the total display time. + /// When the method returns, contains the remaining display time. + /// true if information was retrieved successfully; otherwise, false. + public partial bool GetGameText(int style, out string? message, [MarshalUsing(typeof(MillisecondsMarshaller))] out TimeSpan time, [MarshalUsing(typeof(MillisecondsMarshaller))] out TimeSpan remaining); + + /// + /// Applies an animation to the player or actor. + /// + /// The player to apply the animation to. + /// The actor to apply the animation to. + /// The animation data to apply. + public partial void ApplyAnimation(IPlayer player, IActor actor, AnimationData animation); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/GangZonePos.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/GangZonePos.cs new file mode 100644 index 00000000..c0fcdbd0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/GangZonePos.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the position boundaries of a gang zone. +/// +public readonly struct GangZonePos(Vector2 min, Vector2 max) +{ + /// + /// The minimum corner coordinates of the gang zone. + /// + public readonly Vector2 Min = min; + + /// + /// The maximum corner coordinates of the gang zone. + /// + public readonly Vector2 Max = max; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IBaseGangZone.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IBaseGangZone.cs new file mode 100644 index 00000000..d024c1b6 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IBaseGangZone.cs @@ -0,0 +1,102 @@ +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IIDProvider))] +public readonly partial struct IBaseGangZone +{ + /// + /// Checks if the gangzone is shown for the specified player. + /// + /// The player to check for. + /// true if the gangzone is shown for the player; otherwise, false. + public partial bool IsShownForPlayer(IPlayer player); + + /// + /// Checks if the gangzone is flashing for the specified player. + /// + /// The player to check for. + /// true if the gangzone is flashing for the player; otherwise, false. + public partial bool IsFlashingForPlayer(IPlayer player); + + /// + /// Shows the gangzone to the specified player with the given colour. + /// + /// The player to show the gangzone to. + /// The colour to display the gangzone with. + public partial void ShowForPlayer(IPlayer player, ref Colour colour); + + /// + /// Hides the gangzone for the specified player. + /// + /// The player to hide the gangzone for. + public partial void HideForPlayer(IPlayer player); + + /// + /// Starts flashing the gangzone for the specified player with the given colour. + /// + /// The player to flash the gangzone for. + /// The colour to flash the gangzone with. + public partial void FlashForPlayer(IPlayer player, ref Colour colour); + + /// + /// Stops flashing the gangzone for the specified player. + /// + /// The player to stop flashing the gangzone for. + public partial void StopFlashForPlayer(IPlayer player); + + /// + /// Gets the position of the gangzone. + /// + /// A structure containing the minimum and maximum coordinates. + public partial GangZonePos GetPosition(); + + /// + /// Sets the position of the gangzone. + /// + /// A structure containing the minimum and maximum coordinates. + public partial void SetPosition(ref GangZonePos position); + + /// + /// Checks if the specified player is within the gangzone bounds. + /// + /// This only works if the gangzone has been added to the checking list via IGangZonesComponent.UseGangZoneCheck. + /// The player to check. + /// true if the player is inside the gangzone; otherwise, false. + public partial bool IsPlayerInside(IPlayer player); + + /// + /// Gets a list of players the gangzone is shown for. + /// + /// A collection of players the gangzone is visible to. + public partial FlatPtrHashSet GetShownFor(); + + /// + /// Gets the flashing colour of the gangzone for the specified player. + /// + /// The player to get the flashing colour for. + /// The flashing colour of the gangzone for the player. + public partial Colour GetFlashingColourForPlayer(IPlayer player); + + /// + /// Gets the colour of the gangzone for the specified player. + /// + /// The player to get the colour for. + /// The colour of the gangzone for the player. + public partial Colour GetColourForPlayer(IPlayer player); + + /// + /// Sets the legacy player for this gangzone, used for ID mapping in per-player gangzones. + /// + /// The player to associate with this gangzone, or null. + public partial void SetLegacyPlayer(IPlayer player); + + /// + /// Gets the legacy player associated with this gangzone, used for ID mapping in per-player gangzones. + /// + /// The associated legacy player, or null if not set. + public partial IPlayer GetLegacyPlayer(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZone.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZone.cs new file mode 100644 index 00000000..44a538ec --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZone.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBaseGangZone))] +public readonly partial struct IGangZone; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZoneEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZoneEventHandler.cs new file mode 100644 index 00000000..6bd016b9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZoneEventHandler.cs @@ -0,0 +1,29 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IGangZoneEventHandler +{ + /// + /// Called when a player enters a gang zone. + /// + /// The player who entered the zone. + /// The gang zone that was entered. + void OnPlayerEnterGangZone(IPlayer player, IGangZone zone); + + /// + /// Called when a player leaves a gang zone. + /// + /// The player who left the zone. + /// The gang zone that was left. + void OnPlayerLeaveGangZone(IPlayer player, IGangZone zone); + + /// + /// Called when a player clicks a gang zone. + /// + /// The player who clicked the zone. + /// The gang zone that was clicked. + void OnPlayerClickGangZone(IPlayer player, IGangZone zone); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZonesComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZonesComponent.cs new file mode 100644 index 00000000..4aeca27f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IGangZonesComponent.cs @@ -0,0 +1,72 @@ +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IGangZonesComponent +{ + /// + public static UID ComponentId => new(0xb3351d11ee8d8056); + + /// + /// Gets the event dispatcher for gang zone events. + /// + /// An event dispatcher for events. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new gang zone. + /// + /// The position/boundaries of the gang zone. + /// The created gang zone, or null if creation failed. + public partial IGangZone Create(GangZonePos pos); + + /// + /// Gets the set of gang zones that have enter/leave checking enabled. + /// + /// A collection of gang zones with checking enabled. + public partial FlatPtrHashSet GetCheckingGangZones(); + + /// + /// Enables or disables enter/leave checking for a gang zone. + /// + /// The gang zone to configure. + /// true to enable checking; false to disable it. + public partial void UseGangZoneCheck(IGangZone zone, bool enable); + + /// + /// Converts a real gang zone ID to its legacy ID equivalent. + /// + /// The real gang zone ID. + /// The legacy gang zone ID. + public partial int ToLegacyID(int real); + + /// + /// Converts a legacy gang zone ID to its real ID equivalent. + /// + /// The legacy gang zone ID. + /// The real gang zone ID. + public partial int FromLegacyID(int legacy); + + /// + /// Releases a legacy gang zone ID that was previously reserved. + /// + /// The legacy gang zone ID to release. + public partial void ReleaseLegacyID(int legacy); + + /// + /// Reserves an unused legacy gang zone ID. + /// + /// A reserved legacy gang zone ID. + public partial int ReserveLegacyID(); + + /// + /// Assigns a real gang zone ID to a previously reserved legacy ID. + /// + /// The legacy gang zone ID. + /// The real gang zone ID to assign. + public partial void SetLegacyID(int legacy, int real); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZone.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZone.cs new file mode 100644 index 00000000..3fc5b830 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZone.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBaseGangZone))] +public readonly partial struct IPlayerGangZone; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZoneData.cs b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZoneData.cs new file mode 100644 index 00000000..76cede4f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/GangZones/IPlayerGangZoneData.cs @@ -0,0 +1,77 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerGangZoneData +{ + /// + public static UID ExtensionId => new(0xee8d8056b3351d11); + + /// + /// Converts a real gang zone ID to its legacy ID equivalent. + /// + /// The real gang zone ID. + /// The legacy gang zone ID. + public partial int ToLegacyID(int real); + + /// + /// Converts a legacy gang zone ID to its real ID equivalent. + /// + /// The legacy gang zone ID. + /// The real gang zone ID. + public partial int FromLegacyID(int legacy); + + /// + /// Releases a legacy gang zone ID that was previously reserved. + /// + /// The legacy gang zone ID to release. + public partial void ReleaseLegacyID(int legacy); + + /// + /// Reserves an unused legacy gang zone ID. + /// + /// A reserved legacy gang zone ID. + public partial int ReserveLegacyID(); + + /// + /// Assigns a real gang zone ID to a previously reserved legacy ID. + /// + /// The legacy gang zone ID. + /// The real gang zone ID to assign. + public partial void SetLegacyID(int legacy, int real); + + /// + /// Converts a real gang zone ID to its client ID equivalent. + /// + /// The real gang zone ID. + /// The client gang zone ID. + public partial int ToClientID(int real); + + /// + /// Converts a client gang zone ID to its real ID equivalent. + /// + /// The client gang zone ID. + /// The real gang zone ID. + public partial int FromClientID(int legacy); + + /// + /// Releases a client gang zone ID that was previously reserved. + /// + /// The client gang zone ID to release. + public partial void ReleaseClientID(int legacy); + + /// + /// Reserves an unused client gang zone ID. + /// + /// A reserved client gang zone ID. + public partial int ReserveClientID(); + + /// + /// Assigns a real gang zone ID to a previously reserved client ID. + /// + /// The client gang zone ID. + /// The real gang zone ID to assign. + public partial void SetClientID(int legacy, int real); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenu.cs b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenu.cs new file mode 100644 index 00000000..ae2b6367 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenu.cs @@ -0,0 +1,117 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IIDProvider))] +public readonly partial struct IMenu +{ + /// + /// Sets the header text for a menu column. + /// + /// The header text to display. + /// The column index (0 or 1). + public partial void SetColumnHeader(string header, byte column); + + /// + /// Adds a cell with text to a menu column. + /// + /// The text for the cell. + /// The column index (0 or 1). + /// The row index of the added cell. + public partial int AddCell(string itemText, byte column); + + /// + /// Disables a menu row, making it unselectable. + /// + /// The row index to disable. + public partial void DisableRow(byte row); + + /// + /// Checks if a menu row is enabled. + /// + /// The row index to check. + /// true if the row is enabled; otherwise, false. + public partial bool IsRowEnabled(byte row); + + /// + /// Disables the entire menu. + /// + public partial void Disable(); + + /// + /// Checks if the menu is enabled. + /// + /// true if the menu is enabled; otherwise, false. + public partial bool IsEnabled(); + + /// + /// Gets the position of the menu. + /// + /// A reference to the menu's position as a . + public partial ref Vector2 GetPosition(); + + /// + /// Gets the number of rows in a menu column. + /// + /// The column index. + /// The number of rows in the column. + public partial int GetRowCount(byte column); + + /// + /// Gets the number of columns in the menu. + /// + /// The number of columns (typically 1 or 2). + public partial int GetColumnCount(); + + /// + /// Gets the widths of menu columns. + /// + /// When the method returns, contains the column widths. + private partial void GetColumnWidths(out Vector2 widths); + + /// + /// Gets the widths of the menu columns. + /// + /// A containing the widths of the columns. + public Vector2 GetColumnWidths() + { + GetColumnWidths(out var result); + return result; + } + + /// + /// Gets the header text of a menu column. + /// + /// The column index. + /// The header text, or null if not set. + public partial string? GetColumnHeader(byte column); + + /// + /// Gets the text of a menu cell. + /// + /// The column index. + /// The row index. + /// The cell text, or null if empty. + public partial string? GetCell(byte column, byte row); + + /// + /// Initializes the menu for a player (called before showing). + /// + /// The player to initialize the menu for. + public partial void InitForPlayer(IPlayer player); + + /// + /// Shows the menu to a player. + /// + /// The player to show the menu to. + public partial void ShowForPlayer(IPlayer player); + + /// + /// Hides the menu from a player. + /// + /// The player to hide the menu from. + public partial void HideForPlayer(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenuEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenuEventHandler.cs new file mode 100644 index 00000000..28b48b09 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenuEventHandler.cs @@ -0,0 +1,21 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IMenuEventHandler +{ + /// + /// Called when a player selects a row in a menu. + /// + /// The player who selected the row. + /// The row index that was selected. + void OnPlayerSelectedMenuRow(IPlayer player, byte row); + + /// + /// Called when a player exits a menu (usually by pressing Escape). + /// + /// The player who exited the menu. + void OnPlayerExitedMenu(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenusComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenusComponent.cs new file mode 100644 index 00000000..fc79b8fd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IMenusComponent.cs @@ -0,0 +1,30 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IMenusComponent +{ + /// + public static UID ComponentId => new(0x621e219eb97ee0b2); + + /// + /// Gets the event dispatcher for menu events. + /// + /// An event dispatcher for events. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new menu with the specified properties. + /// + /// The title of the menu. + /// The position to display the menu. + /// The number of columns (1 or 2). + /// The width of the first column. + /// The width of the second column (if applicable). + /// The created menu, or null if creation failed. + public partial IMenu Create(string title, Vector2 position, byte columns, float col1Width, float col2Width); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Menus/IPlayerMenuData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IPlayerMenuData.cs new file mode 100644 index 00000000..c83969ae --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Menus/IPlayerMenuData.cs @@ -0,0 +1,23 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerMenuData +{ + /// + public static UID ExtensionId => new(0x01d8e934e9791b99); + + /// + /// Gets the ID of the menu currently shown to the player. + /// + /// The menu ID, or 0xFF if no menu is currently shown. + public partial byte GetMenuID(); + + /// + /// Sets the menu ID for the player. + /// + /// The menu ID to set. + public partial void SetMenuID(byte id); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/NPCs/EntityCheckType.cs b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/EntityCheckType.cs new file mode 100644 index 00000000..9a9233e7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/EntityCheckType.cs @@ -0,0 +1,51 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Bitfield flags for NPC AimAt / Shoot 'in-between' entity checks. +/// +[Flags] +public enum EntityCheckType : byte +{ + // TODO validate documentation. + + /// + /// No checks. + /// + None = 0, + /// + /// Check for players. + /// + Player = 1, + /// + /// Check for NPCs. + /// + NPC = 2, + /// + /// Check for actors. + /// + Actor = 4, + /// + /// Check for vehicles. + /// + Vehicle = 8, + /// + /// Check for objects. + /// + Object = 16, + /// + /// Projection origins check. + /// + ProjectOrig = 32, + /// + /// Projection target check. + /// + ProjectTarg = 64, + /// + /// Check for world geometry. + /// + Map = 128, + /// + /// Run all checks. + /// + All = 255 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPC.cs b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPC.cs new file mode 100644 index 00000000..afb5093e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPC.cs @@ -0,0 +1,107 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface +/// (a server-controlled bot built on top of an ). +/// +[OpenMpApi(typeof(IExtensible), typeof(IIDProvider))] +public readonly partial struct INPC +{ + /// Gets the underlying instance for this NPC. + public partial IPlayer GetPlayer(); + + /// Gets the NPC's world position. + public partial Vector3 GetPosition(); + + /// + /// Sets the NPC's position. =true broadcasts a sync + /// to streamed-in players right away instead of waiting for the next tick. + /// + public partial void SetPosition(Vector3 position, bool immediateUpdate); + + /// Gets the NPC's rotation as a quaternion. + public partial GTAQuat GetRotation(); + + /// Sets the NPC's rotation. See for the immediate-update flag. + public partial void SetRotation(GTAQuat rotation, bool immediateUpdate); + + /// Gets the virtual world this NPC is in. + public partial int GetVirtualWorld(); + + /// Sets the virtual world this NPC is in. + public partial void SetVirtualWorld(int vw); + + /// Gets the interior id this NPC is recorded in (server-side bookkeeping only). + public partial uint GetInterior(); + + /// Sets the interior id (server-side bookkeeping only — does not relocate the NPC). + public partial void SetInterior(uint interior); + + /// Gets the current velocity vector. + public partial Vector3 GetVelocity(); + + /// Sets the velocity vector. =true forces a sync this tick. + public partial void SetVelocity(Vector3 velocity, bool update); + + /// Spawns the NPC at its currently configured position/rotation. + public partial void Spawn(); + + /// Respawns the NPC keeping its current state. + public partial void Respawn(); + + /// True if the NPC has been killed and not yet respawned. + public partial bool IsDead(); + + /// Sets the NPC's skin model id. + public partial void SetSkin(int model); + + /// Sets the current weapon id. + public partial void SetWeapon(byte weapon); + + /// Gets the current weapon id. + public partial byte GetWeapon(); + + /// Sets ammo for the current weapon. + public partial void SetAmmo(int ammo); + + /// Gets ammo for the current weapon. + public partial int GetAmmo(); + + /// Gets the NPC's health. + public partial float GetHealth(); + + /// Sets the NPC's health. + public partial void SetHealth(float health); + + /// Gets the NPC's armour. + public partial float GetArmour(); + + /// Sets the NPC's armour. + public partial void SetArmour(float armour); + + /// True iff the NPC is invulnerable. + public partial bool IsInvulnerable(); + + /// Sets invulnerability. + public partial void SetInvulnerable(bool toggle); + + /// True if the NPC is currently moving (path or direct). + public partial bool IsMoving(); + + /// Tells the NPC to walk/jog/sprint/drive to . + public partial bool Move(Vector3 position, NPCMoveType moveType, float moveSpeed, float stopRange); + + /// Stops any active movement. + public partial void StopMove(); + + /// Clears any applied animation. + public partial void ClearAnimations(); + + /// Applies an animation. bundles lib + name + flags + duration. + public partial void ApplyAnimation(AnimationData animationData); + + /// Checks whether this NPC is streamed in for the given player. + public partial bool IsStreamedInForPlayer(IPlayer other); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCComponent.cs new file mode 100644 index 00000000..7fb49fc0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCComponent.cs @@ -0,0 +1,49 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPool), typeof(INetworkComponent))] +public readonly partial struct INPCComponent +{ + /// + public static UID ComponentId => new(0x3D0E59E87F4E90BC); + + /// + /// Gets the event dispatcher for NPC-related events. + /// + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a controllable NPC with the given name. The NPC must be spawned via + /// separately before it appears in the world. + /// + /// The NPC name (must follow the same rules as normal player names). + public partial INPC Create(string name); + + /// + /// Destroys the given NPC. Required because NPC removal is more than a pool release. + /// + public partial void Destroy(INPC npc); + + /// Creates a new (empty) path container. + public partial int CreatePath(); + + /// Destroys a previously created path container. + public partial bool DestroyPath(int pathId); + + /// Adds a point with stop range to a path container. + public partial bool AddPointToPath(int pathId, Vector3 position, float stopRange); + + /// Checks if a path id is valid. + public partial bool IsValidPath(int pathId); + + /// Loads a record file for playback. Returns the record id (or -1 on failure). + public partial int LoadRecord(string filePath); + + /// Unloads a previously loaded record. + public partial bool UnloadRecord(int recordId); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCEventHandler.cs new file mode 100644 index 00000000..622927b7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/INPCEventHandler.cs @@ -0,0 +1,26 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface INPCEventHandler +{ + /// Fired when an NPC finishes a Move() command (reaches the target position). + void OnNPCFinishMove(INPC npc); + + /// Fired right after an NPC is created via . + void OnNPCCreate(INPC npc); + + /// Fired when the NPC is destroyed. + void OnNPCDestroy(INPC npc); + + /// Fired after the NPC spawns into the world. + void OnNPCSpawn(INPC npc); + + /// Fired after the NPC respawns. + void OnNPCRespawn(INPC npc); + + /// Fired when the NPC dies. + void OnNPCDeath(INPC npc, IPlayer killer, int reason); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/NPCs/NPCMoveType.cs b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/NPCMoveType.cs new file mode 100644 index 00000000..ef7f3339 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/NPCs/NPCMoveType.cs @@ -0,0 +1,25 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Movement type for an NPC. +/// +public enum NPCMoveType +{ + /// Not moving. + None = 0, + + /// Walk speed. + Walk = 1, + + /// Jog speed. + Jog = 2, + + /// Sprint speed. + Sprint = 3, + + /// Drive (in vehicle). + Drive = 4, + + /// Pick a sensible default automatically. + Auto = 5 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/AttachmentType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/AttachmentType.cs new file mode 100644 index 00000000..4ed7f35f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/AttachmentType.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of attachment point for an object. +/// +public enum AttachmentType : byte +{ + /// + /// The object is not attached to anything. + /// + None, + + /// + /// The object is attached to a vehicle. + /// + Vehicle, + + /// + /// The object is attached to another object. + /// + Object, + + /// + /// The object is attached to a player. + /// + Player +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IBaseObject.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IBaseObject.cs new file mode 100644 index 00000000..ec919fad --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IBaseObject.cs @@ -0,0 +1,121 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +public readonly partial struct IBaseObject +{ + /// + /// Sets the draw distance for the object. + /// + /// The draw distance in units. + public partial void SetDrawDistance(float drawDistance); + + /// + /// Gets the draw distance of the object. + /// + /// The draw distance in units. + public partial float GetDrawDistance(); + + /// + /// Sets the model ID of the object. + /// + /// The model ID of the object. + public partial void SetModel(int model); + + /// + /// Gets the model ID of the object. + /// + /// The model ID of the object. + public partial int GetModel(); + + /// + /// Sets whether the object has camera collision enabled. + /// + /// true to enable camera collision; otherwise, false. + public partial void SetCameraCollision(bool collision); + + /// + /// Gets whether the object has camera collision enabled. + /// + /// true if the object has camera collision enabled; otherwise, false. + public partial bool GetCameraCollision(); + + /// + /// Moves the object to a new position and rotation over a specified time. + /// + /// The movement data containing the target position, rotation, and time. + public partial void Move(ref ObjectMoveData data); + + /// + /// Checks if the object is currently moving. + /// + /// true if the object is moving; otherwise, false. + public partial bool IsMoving(); + + /// + /// Stops the object's current movement. + /// + public partial void Stop(); + + /// + /// Gets the current movement data of the object. + /// + /// A reference to the structure containing the movement information. + public partial ref ObjectMoveData GetMovingData(); + + /// + /// Attaches the object to a vehicle at the specified offset and rotation. + /// + /// The vehicle to attach the object to. + /// The offset from the vehicle's center. + /// The rotation of the object relative to the vehicle. + public partial void AttachToVehicle(IVehicle vehicle, Vector3 offset, Vector3 rotation); + + /// + /// Resets the object's attachment, removing it from any vehicle or object it was attached to. + /// + public partial void ResetAttachment(); + + /// + /// Gets the attachment data of the object. + /// + /// A reference to the structure containing the attachment information. + public partial ref ObjectAttachmentData GetAttachmentData(); + + /// + /// Gets the material data for a specific material index on the object. + /// + /// The index of the material to retrieve. + /// When the method returns, contains the material data if successful; otherwise, null. + /// true if the material data was retrieved successfully; otherwise, false. + public partial bool GetMaterialData(uint materialIndex, out ObjectMaterialData? output); + + /// + /// Sets the material for a specific material index on the object. + /// + /// The index of the material to set. + /// The model ID to use for the texture library. + /// The texture library (TXD) name. + /// The texture name from the library. + /// The colour to apply to the material. + public partial void SetMaterial(uint materialIndex, int model, string textureLibrary, string textureName, Colour colour); + + /// + /// Sets text material on a specific material index of the object. + /// + /// The index of the material to set text on. + /// The text to display on the material. + /// The size of the material. + /// The font face to use for the text. + /// The font size for the text. + /// true to make the text bold; otherwise, false. + /// The colour of the text. + /// The background colour of the material. + /// The text alignment on the material. + public partial void SetMaterialText(uint materialIndex, string text, ObjectMaterialSize materialSize, string fontFace, int fontSize, bool bold, Colour fontColour, + Colour backgroundColour, ObjectMaterialTextAlign align); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObject.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObject.cs new file mode 100644 index 00000000..b2dad27f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObject.cs @@ -0,0 +1,27 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBaseObject))] +public readonly partial struct IObject +{ + /// + /// Attaches this object to a player with the specified offset and rotation. + /// + /// The player to attach to. + /// The offset of this object relative to the player. + /// The rotation of this object relative to the player. + public partial void AttachToPlayer(IPlayer player, Vector3 offset, Vector3 rotation); + + /// + /// Attaches this object to another object with the specified offset, rotation, and synchronization option. + /// + /// The object to attach to. + /// The offset of this object relative to the target object. + /// The rotation of this object relative to the target object. + /// A value indicating whether the rotation should be synchronized with the target object. + public partial void AttachToObject(IObject objekt, Vector3 offset, Vector3 rotation, bool syncRotation); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectEventHandler.cs new file mode 100644 index 00000000..c8b27e8f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectEventHandler.cs @@ -0,0 +1,70 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IObjectEventHandler +{ + /// + /// Called when an object has finished moving. + /// + /// The object that moved. + void OnMoved(IObject objekt); + + /// + /// Called when a player object has finished moving. + /// + /// The player associated with the object. + /// The player object that moved. + void OnPlayerObjectMoved(IPlayer player, IPlayerObject objekt); + + /// + /// Called when an object is selected by a player. + /// + /// The player who selected the object. + /// The object that was selected. + /// The model ID of the selected object. + /// The position of the selected object. + void OnObjectSelected(IPlayer player, IObject objekt, int model, Vector3 position); + + /// + /// Called when a player object is selected by a player. + /// + /// The player who selected the object. + /// The player object that was selected. + /// The model ID of the selected object. + /// The position of the selected object. + void OnPlayerObjectSelected(IPlayer player, IPlayerObject objekt, int model, Vector3 position); + + /// + /// Called when an object is edited by a player. + /// + /// The player who edited the object. + /// The object that was edited. + /// The response of the edit operation. + /// The offset of the edited object. + /// The rotation of the edited object. + void OnObjectEdited(IPlayer player, IObject objekt, ObjectEditResponse response, Vector3 offset, Vector3 rotation); + + /// + /// Called when a player object is edited by a player. + /// + /// The player who edited the object. + /// The player object that was edited. + /// The response of the edit operation. + /// The offset of the edited object. + /// The rotation of the edited object. + void OnPlayerObjectEdited(IPlayer player, IPlayerObject objekt, ObjectEditResponse response, Vector3 offset, Vector3 rotation); + + /// + /// Called when a player edits an attached object. + /// + /// The player who edited the attached object. + /// The index of the attachment slot. + /// A value indicating whether the changes were saved. + /// The data of the edited attachment slot. + void OnPlayerAttachedObjectEdited(IPlayer player, int index, bool saved, ref ObjectAttachmentSlotData data); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectsComponent.cs new file mode 100644 index 00000000..6e87b1dd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IObjectsComponent.cs @@ -0,0 +1,41 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IObjectsComponent +{ + /// + public static UID ComponentId => new(0x59f8415f72da6160); + + /// + /// Gets the event dispatcher for handling object events. + /// + /// An event dispatcher for . + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Sets the default camera collision state. + /// + /// A value indicating whether camera collision is enabled by default. + public partial void SetDefaultCameraCollision(bool collision); + + /// + /// Gets the default camera collision state. + /// + /// if camera collision is enabled by default; otherwise, . + public partial bool GetDefaultCameraCollision(); + + /// + /// Creates a new object with the specified model ID, position, and rotation. + /// + /// The model ID of the object. + /// The position of the object. + /// The rotation of the object. + /// The draw distance of the object. Defaults to 0. + /// A new instance of . + public partial IObject Create(int modelID, Vector3 position, Vector3 rotation, float drawDist = 0); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObject.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObject.cs new file mode 100644 index 00000000..c1b45f0e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObject.cs @@ -0,0 +1,26 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBaseObject))] +public readonly partial struct IPlayerObject +{ + /// + /// Attaches this player object to another player object with the specified offset and rotation. + /// + /// The player object to attach to. + /// The offset of this object relative to the target object. + /// The rotation of this object relative to the target object. + public partial void AttachToObject(IPlayerObject objekt, Vector3 offset, Vector3 rotation); + + /// + /// Attaches this player object to a player with the specified offset and rotation. + /// + /// The player to attach to. + /// The offset of this object relative to the player. + /// The rotation of this object relative to the player. + public partial void AttachToPlayer(IPlayer player, Vector3 offset, Vector3 rotation); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObjectData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObjectData.cs new file mode 100644 index 00000000..9554db25 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/IPlayerObjectData.cs @@ -0,0 +1,99 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension), typeof(IPool))] +public readonly partial struct IPlayerObjectData +{ + /// + public static UID ExtensionId => new(0x93d4ed2344b07456); + + /// + /// Creates a new player object with the specified model ID, position, and rotation. + /// + /// The model ID of the object. + /// The position of the object. + /// The rotation of the object. + /// The draw distance of the object. Defaults to 0. + /// A new instance of . + public partial IPlayerObject Create(int modelID, Vector3 position, Vector3 rotation, float drawDist = 0); + + /// + /// Sets the attached object data for the specified index. + /// + /// The index of the attachment slot. + /// The attachment data to set. + public partial void SetAttachedObject(int index, ref ObjectAttachmentSlotData data); + + /// + /// Removes the attached object at the specified index. + /// + /// The index of the attachment slot to remove. + public partial void RemoveAttachedObject(int index); + + /// + /// Checks if an object is attached at the specified index. + /// + /// The index to check. + /// if an object is attached; otherwise, . + public partial bool HasAttachedObject(int index); + + /// + /// Gets the attached object data for the specified index. + /// + /// The index of the attachment slot. + /// A reference to the for the specified index. + public partial ref ObjectAttachmentSlotData GetAttachedObject(int index); + + /// + /// Begins the object selection process. + /// + public partial void BeginSelecting(); + + /// + /// Checks if an object is currently being selected. + /// + /// if an object is being selected; otherwise, . + public partial bool SelectingObject(); + + /// + /// Ends the object editing process. + /// + public partial void EndEditing(); + + /// + /// Begins editing the specified object. + /// + /// The object to edit. + public partial void BeginEditing(IObject objekt); + + /// + /// Begins editing the specified player object. + /// + /// The player object to edit. + public partial void BeginEditing(IPlayerObject objekt); + + /// + /// Checks if an object is currently being edited. + /// + /// if an object is being edited; otherwise, . + public partial bool EditingObject(); + + /// + /// Edits the attached object at the specified index. + /// + /// The index of the attachment slot to edit. + public partial void EditAttachedObject(int index); + + /// + /// Gets the pool interface for managing player objects. + /// + /// A pool interface for player objects. + public IPool AsPool() + { + return (IPool)this; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/MaterialType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/MaterialType.cs new file mode 100644 index 00000000..12f23bb9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/MaterialType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the type of a material. +/// +public enum MaterialType : byte +{ + /// + /// Indicates that no material is applied. + /// + None, + + /// + /// Indicates that the default material is applied. + /// + Default, + + /// + /// Indicates that a text material is applied. + /// + Text +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentData.cs new file mode 100644 index 00000000..7647eb66 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentData.cs @@ -0,0 +1,36 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the data required to attach an object or entity to another object or entity. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct ObjectAttachmentData +{ + /// + /// Gets the type of attachment. + /// + public readonly AttachmentType Type; + + /// + /// Gets a value indicating whether the rotation of the attachment is synchronized. + /// + public readonly byte SyncRotation; + + /// + /// Gets the identifier of the attached object or entity. + /// + public readonly int Id; + + /// + /// Gets the offset of the attachment relative to its parent. + /// + public readonly Vector3 Offset; + + /// + /// Gets the rotation of the attachment relative to its parent. + /// + public readonly Vector3 Rotation; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentSlotData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentSlotData.cs new file mode 100644 index 00000000..be0f2982 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectAttachmentSlotData.cs @@ -0,0 +1,67 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents attachment data for an object attached to a player bone. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct ObjectAttachmentSlotData +{ + /// + /// Initializes a new instance of the struct. + /// + /// The model ID of the attached object. + /// The bone ID to which the object is attached. + /// The offset of the object relative to the bone. + /// The rotation of the object relative to the bone. + /// The scale of the attached object. + /// The primary color of the attached object. + /// The secondary color of the attached object. + public ObjectAttachmentSlotData(int model, int bone, Vector3 offset, Vector3 rotation, Vector3 scale, Colour colour1, Colour colour2) + { + Model = model; + Bone = bone; + Offset = offset; + Rotation = rotation; + Scale = scale; + Colour1 = colour1; + Colour2 = colour2; + } + + /// + /// Gets the model ID of the attached object. + /// + public readonly int Model; + + /// + /// Gets the bone ID to which the object is attached. + /// + public readonly int Bone; + + /// + /// Gets the offset of the object relative to the bone. + /// + public readonly Vector3 Offset; + + /// + /// Gets the rotation of the object relative to the bone. + /// + public readonly Vector3 Rotation; + + /// + /// Gets the scale of the attached object. + /// + public readonly Vector3 Scale; + + /// + /// Gets the primary color of the attached object. + /// + public readonly Colour Colour1; + + /// + /// Gets the secondary color of the attached object. + /// + public readonly Colour Colour2; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectEditResponse.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectEditResponse.cs new file mode 100644 index 00000000..93fc26e7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectEditResponse.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the possible responses to an object editing request. +/// +public enum ObjectEditResponse +{ + /// + /// Indicates that the object editing process was canceled. + /// + Cancel, + + /// + /// Indicates that the object editing process was completed and finalized. + /// + Final, + + /// + /// Indicates that the object editing process is in progress and has been updated. + /// + Update +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialData.cs new file mode 100644 index 00000000..9f22c1e2 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialData.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents material data applied to an object. +/// +[NativeMarshalling(typeof(ObjectMaterialDataMarshaller))] +public record ObjectMaterialData( + int Model, + byte MaterialSize, + byte FontSize, + byte Alignment, + bool Bold, + Colour MaterialColour, + Colour BackgroundColour, + string Text, + string Font, + MaterialType Type, + bool Used) +{ + /// + /// Gets the color of the font used in the material. + /// + public Colour FontColour => MaterialColour; + + /// + /// Gets the texture dictionary (TXD) name used in the material. + /// + public string Txd => Text; + + /// + /// Gets the texture name used in the material. + /// + public string Texture => Font; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialDataMarshaller.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialDataMarshaller.cs new file mode 100644 index 00000000..6f00bf76 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialDataMarshaller.cs @@ -0,0 +1,50 @@ +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a marshaller entrypoint for marshalling to its unmanaged counterpart. +/// +[CustomMarshaller(typeof(ObjectMaterialData), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +public static class ObjectMaterialDataMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class NativeToManaged + { + public static ObjectMaterialData? ConvertToManaged(BlittableStructRef unmanaged) + { + if (!unmanaged.HasValue) + { + return null; + } + + var native = unmanaged.GetValueOrDefault(); + + return FromNative(native); + } + + private static ObjectMaterialData FromNative(NativeObjMat native) + { + return new ObjectMaterialData(native.Model, native.MaterialSize, native.FontSize, native.Alignment, native.Bold, native.MaterialColour, native.BackgroundColour, + native.TextOrTXD.ToString(), native.FontOrTexture.ToString(), native.Type, native.Used); + } + } + + [StructLayout(LayoutKind.Explicit)] + public readonly struct NativeObjMat + { + [FieldOffset(0)] public readonly int Model; + [FieldOffset(0)] public readonly byte MaterialSize; + [FieldOffset(1)] public readonly byte FontSize; + [FieldOffset(2)] public readonly byte Alignment; + [FieldOffset(3)] public readonly BlittableBoolean Bold; // len = 1 + [FieldOffset(4)] public readonly Colour MaterialColour; // len = 4 + [FieldOffset(8)] public readonly Colour BackgroundColour; // len = 4 + [FieldOffset(16)] public readonly HybridString32 TextOrTXD; // len = 40 + [FieldOffset(56)] public readonly HybridString32 FontOrTexture; // len = 40 + [FieldOffset(96)] public readonly MaterialType Type; // len = 1 + [FieldOffset(97)] public readonly BlittableBoolean Used; // len = 1 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialSize.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialSize.cs new file mode 100644 index 00000000..34991149 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialSize.cs @@ -0,0 +1,77 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the size of an object material for text rendering. +/// +public enum ObjectMaterialSize +{ + /// + /// Represents the size of an object material as 32x32 pixels. + /// + _32x32 = 10, + + /// + /// Represents the size of an object material as 64x32 pixels. + /// + _64x32 = 20, + + /// + /// Represents the size of an object material as 64x64 pixels. + /// + _64x64 = 30, + + /// + /// Represents the size of an object material as 128x32 pixels. + /// + _128x32 = 40, + + /// + /// Represents the size of an object material as 128x64 pixels. + /// + _128x64 = 50, + + /// + /// Represents the size of an object material as 128x128 pixels. + /// + _128x128 = 60, + + /// + /// Represents the size of an object material as 256x32 pixels. + /// + _256x32 = 70, + + /// + /// Represents the size of an object material as 256x64 pixels. + /// + _256x64 = 80, + + /// + /// Represents the size of an object material as 256x128 pixels. + /// + _256x128 = 90, + + /// + /// Represents the size of an object material as 256x256 pixels. + /// + _256x256 = 100, + + /// + /// Represents the size of an object material as 512x64 pixels. + /// + _512x64 = 110, + + /// + /// Represents the size of an object material as 512x128 pixels. + /// + _512x128 = 120, + + /// + /// Represents the size of an object material as 512x256 pixels. + /// + _512x256 = 130, + + /// + /// Represents the size of an object material as 512x512 pixels. + /// + _512x512 = 140 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialTextAlign.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialTextAlign.cs new file mode 100644 index 00000000..ab6a9088 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMaterialTextAlign.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the alignment of text on an object material. +/// +public enum ObjectMaterialTextAlign +{ + /// + /// Aligns the text to the left side of the material. + /// + Left, + + /// + /// Centers the text on the material. + /// + Center, + + /// + /// Aligns the text to the right side of the material. + /// + Right +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMoveData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMoveData.cs new file mode 100644 index 00000000..b13d0d9b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectMoveData.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the movement data of an object, including its target position, target rotation, and speed. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct ObjectMoveData +{ + /// + /// Initializes a new instance of the struct. + /// + /// The target position of the object. + /// The target rotation of the object. + /// The speed at which the object moves. + public ObjectMoveData(Vector3 targetPos, Vector3 targetRot, float speed) + { + TargetPos = targetPos; + TargetRot = targetRot; + Speed = speed; + } + + /// + /// Gets the target position of the object. + /// + public readonly Vector3 TargetPos; + + /// + /// Gets the target rotation of the object. + /// + public readonly Vector3 TargetRot; + + /// + /// Gets the speed at which the object moves. + /// + public readonly float Speed; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectSelectType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectSelectType.cs new file mode 100644 index 00000000..51d84011 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/ObjectSelectType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of object selection. +/// +public enum ObjectSelectType +{ + /// + /// Indicates that no object selection is active. + /// + None, + + /// + /// Indicates that a global object is being selected. + /// + Global, + + /// + /// Indicates that a player-specific object is being selected. + /// + Player +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Objects/PlayerBone.cs b/src/SampSharp.OpenMp.Core/Api/Components/Objects/PlayerBone.cs new file mode 100644 index 00000000..9bdabba0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Objects/PlayerBone.cs @@ -0,0 +1,102 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the bones of a player model that objects can be attached to. +/// +public enum PlayerBone +{ + /// + /// Represents no specific bone. + /// + None, + + /// + /// Represents the spine bone. + /// + Spine, + + /// + /// Represents the head bone. + /// + Head, + + /// + /// Represents the left upper arm bone. + /// + LeftUpperArm, + + /// + /// Represents the right upper arm bone. + /// + RightUpperArm, + + /// + /// Represents the left hand bone. + /// + LeftHand, + + /// + /// Represents the right hand bone. + /// + RightHand, + + /// + /// Represents the left thigh bone. + /// + LeftThigh, + + /// + /// Represents the right thigh bone. + /// + RightThigh, + + /// + /// Represents the left foot bone. + /// + LeftFoot, + + /// + /// Represents the right foot bone. + /// + RightFoot, + + /// + /// Represents the right calf bone. + /// + RightCalf, + + /// + /// Represents the left calf bone. + /// + LeftCalf, + + /// + /// Represents the left forearm bone. + /// + LeftForearm, + + /// + /// Represents the right forearm bone. + /// + RightForearm, + + /// + /// Represents the left shoulder bone. + /// + LeftShoulder, + + /// + /// Represents the right shoulder bone. + /// + RightShoulder, + + /// + /// Represents the neck bone. + /// + Neck, + + /// + /// Represents the jaw bone. + /// + Jaw +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IBasePickup.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IBasePickup.cs new file mode 100644 index 00000000..1a8ca815 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IBasePickup.cs @@ -0,0 +1,88 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +public readonly partial struct IBasePickup +{ + /// + /// Sets the type of the pickup. + /// + /// The type ID of the pickup. + /// Whether to update the pickup visually for all players; defaults to true. + public partial void SetType(byte type, bool update = true); + + /// + /// Gets the type of the pickup. + /// + /// The type ID of the pickup. + [OpenMpApiFunction("getType")] + public partial byte GetPickupType(); + + /// + /// Sets the position of the pickup without updating the visual representation. + /// + /// The new position for the pickup. + public partial void SetPositionNoUpdate(Vector3 position); + + /// + /// Sets the model ID of the pickup. + /// + /// The model ID to set. + /// Whether to update the pickup visually for all players; defaults to true. + public partial void SetModel(int id, bool update = true); + + /// + /// Gets the model ID of the pickup. + /// + /// The model ID of the pickup. + public partial int GetModel(); + + /// + /// Checks if the pickup is streamed in for the specified player. + /// + /// The player to check for. + /// true if the pickup is streamed in for the player; otherwise, false. + public partial bool IsStreamedInForPlayer(IPlayer player); + + /// + /// Streams the pickup in for the specified player. + /// + /// The player to stream the pickup in for. + public partial void StreamInForPlayer(IPlayer player); + + /// + /// Streams the pickup out for the specified player. + /// + /// The player to stream the pickup out for. + public partial void StreamOutForPlayer(IPlayer player); + + /// + /// Sets whether the pickup is hidden for the specified player. + /// + /// The player to hide or show the pickup for. + /// true to hide the pickup; false to show it. + public partial void SetPickupHiddenForPlayer(IPlayer player, bool hidden); + + /// + /// Checks if the pickup is hidden for the specified player. + /// + /// The player to check for. + /// true if the pickup is hidden for the player; otherwise, false. + public partial bool IsPickupHiddenForPlayer(IPlayer player); + + /// + /// Sets the legacy player for this pickup, used for ID mapping in per-player pickups. + /// + /// The player to associate with this pickup, or null. + public partial void SetLegacyPlayer(IPlayer player); + + /// + /// Gets the legacy player associated with this pickup, used for ID mapping in per-player pickups. + /// + /// The associated legacy player, or null if not set. + public partial IPlayer GetLegacyPlayer(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickup.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickup.cs new file mode 100644 index 00000000..de0a20ac --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickup.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBasePickup))] +public readonly partial struct IPickup; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupEventHandler.cs new file mode 100644 index 00000000..dc69983c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupEventHandler.cs @@ -0,0 +1,15 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPickupEventHandler +{ + /// + /// Called when a player picks up a pickup. + /// + /// The player who picked up the pickup. + /// The pickup that was picked up. + void OnPlayerPickUpPickup(IPlayer player, IPickup pickup); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupsComponent.cs new file mode 100644 index 00000000..e5bf6662 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPickupsComponent.cs @@ -0,0 +1,63 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IPickupsComponent +{ + /// + public static UID ComponentId => new(0xcf304faa363dd971); + + /// + /// Gets the event dispatcher for pickup events. + /// + /// An event dispatcher for events. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new pickup. + /// + /// The model ID of the pickup. + /// The type of the pickup. + /// The position where the pickup will be created. + /// The virtual world ID for the pickup. + /// Whether the pickup is static and cannot be moved. + /// The created pickup, or null if creation failed. + public partial IPickup Create(int modelId, byte type, Vector3 pos, uint virtualWorld, bool isStatic); + + /// + /// Converts a real pickup ID to its legacy ID equivalent. + /// + /// The real pickup ID. + /// The legacy pickup ID. + public partial int ToLegacyID(int real); + + /// + /// Converts a legacy pickup ID to its real ID equivalent. + /// + /// The legacy pickup ID. + /// The real pickup ID. + public partial int FromLegacyID(int legacy); + + /// + /// Releases a legacy pickup ID that was previously reserved. + /// + /// The legacy pickup ID to release. + public partial void ReleaseLegacyID(int legacy); + + /// + /// Reserves an unused legacy pickup ID. + /// + /// A reserved legacy pickup ID. + public partial int ReserveLegacyID(); + + /// + /// Assigns a real pickup ID to a previously reserved legacy ID. + /// + /// The legacy pickup ID. + /// The real pickup ID to assign. + public partial void SetLegacyID(int legacy, int real); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickup.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickup.cs new file mode 100644 index 00000000..a91b925e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickup.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IBasePickup))] +public readonly partial struct IPlayerPickup; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickupData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickupData.cs new file mode 100644 index 00000000..28889244 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pickups/IPlayerPickupData.cs @@ -0,0 +1,77 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerPickupData +{ + /// + public static UID ExtensionId => new(0x98376F4428D7B70B); + + /// + /// Converts a real pickup ID to its legacy ID equivalent. + /// + /// The real pickup ID. + /// The legacy pickup ID. + public partial int ToLegacyID(int real); + + /// + /// Converts a legacy pickup ID to its real ID equivalent. + /// + /// The legacy pickup ID. + /// The real pickup ID. + public partial int FromLegacyID(int legacy); + + /// + /// Releases a legacy pickup ID that was previously reserved. + /// + /// The legacy pickup ID to release. + public partial void ReleaseLegacyID(int legacy); + + /// + /// Reserves an unused legacy pickup ID. + /// + /// A reserved legacy pickup ID. + public partial int ReserveLegacyID(); + + /// + /// Assigns a real pickup ID to a previously reserved legacy ID. + /// + /// The legacy pickup ID. + /// The real pickup ID to assign. + public partial void SetLegacyID(int legacy, int real); + + /// + /// Converts a real pickup ID to its client ID equivalent. + /// + /// The real pickup ID. + /// The client pickup ID. + public partial int ToClientID(int real); + + /// + /// Converts a client pickup ID to its real ID equivalent. + /// + /// The client pickup ID. + /// The real pickup ID. + public partial int FromClientID(int legacy); + + /// + /// Releases a client pickup ID that was previously reserved. + /// + /// The client pickup ID to release. + public partial void ReleaseClientID(int legacy); + + /// + /// Reserves an unused client pickup ID. + /// + /// A reserved client pickup ID. + public partial int ReserveClientID(); + + /// + /// Assigns a real pickup ID to a previously reserved client ID. + /// + /// The client pickup ID. + /// The real pickup ID to assign. + public partial void SetClientID(int legacy, int real); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPool.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPool.cs new file mode 100644 index 00000000..06abb1f5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPool.cs @@ -0,0 +1,192 @@ +using System.Collections; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct IPool : IEnumerable where T : unmanaged, IIDProvider.IManagedInterface +{ + /// + /// Gets the element at the specified index. + /// + /// The index of the element to retrieve. + /// The element at the specified index. + public T Get(int index) + { + return ((IReadOnlyPool)this).Get(index); + } + + /// + /// Gets the bounds of the pool. + /// + /// A tuple containing the lower and upper bounds of the pool. + public (Size, Size) Bounds() + { + ((IReadOnlyPool)this).Bounds(out var bounds); + return bounds; + } + + /// + /// Releases the element at the specified index. + /// + /// The index of the element to release. + public void Release(int index) + { + IPoolInterop.IPool_release(_handle, index); + } + + /// + /// Locks the element at the specified index. + /// + /// The index of the element to lock. + public void Lock(int index) + { + IPoolInterop.IPool_lock(_handle, index); + } + + /// + /// Unlocks the element at the specified index. + /// + /// The index of the element to unlock. + /// if the element was successfully unlocked; otherwise, . + public bool Unlock(int index) + { + return IPoolInterop.IPool_unlock(_handle, index); + } + + /// + /// Gets the number of elements in the pool. + /// + /// The number of elements in the pool. + public Size Count() + { + return IPoolInterop.IPool_count(_handle); + } + + /// + /// Gets the event dispatcher for the pool. + /// + /// An for handling pool events. + public IEventDispatcher> GetPoolEventDispatcher() + { + var data = IPoolInterop.IPool_getPoolEventDispatcher(_handle); + return new IEventDispatcher>(data); + } + + private FlatPtrHashSet Entries() + { + return new FlatPtrHashSet(IPoolInterop.IPool_entries(_handle)); + } + + private MarkedPoolIterator Begin() + { + var entries = Entries(); + return new MarkedPoolIterator(this, entries, entries.Begin()); + } + + private MarkedPoolIterator End() + { + var entries = Entries(); + return new MarkedPoolIterator(this, entries, entries.End()); + } + + /// + /// Gets an enumerator that iterates through the pool. + /// + /// An enumerator for the pool. + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Converts the pool to a read-only pool. + /// + /// The pool to convert. + public static explicit operator IReadOnlyPool(IPool value) + { + return new IReadOnlyPool(value._handle); + } + + /// + /// Represents an enumerator for the . + /// + public struct Enumerator : IEnumerator + { + private readonly IPool _pool; + private MarkedPoolIterator? _iterator; + + /// + /// Initializes a new instance of the struct. + /// + /// The pool to enumerate. + internal Enumerator(IPool pool) + { + _pool = pool; + _iterator = null; + } + + /// + /// Advances the enumerator to the next element of the pool. + /// + /// if the enumerator was successfully advanced; otherwise, . + public bool MoveNext() + { + if (!_iterator.HasValue) + { + _iterator = _pool.Begin(); + return _iterator != _pool.End(); + } + + var iter = _iterator.Value; + iter.Advance(); + + if (iter == _pool.End()) + { + return false; + } + + _iterator = iter; + return true; + } + + /// + /// Resets the enumerator to its initial position. + /// + /// Always thrown as resetting is not supported. + public void Reset() + { + throw new InvalidOperationException(); + } + + /// + /// Gets the element in the pool at the current position of the enumerator. + /// + public readonly T Current => _iterator?.Current ?? throw new InvalidOperationException(); + + readonly object IEnumerator.Current => Current; + + /// + /// Releases all resources used by the enumerator. + /// + public void Dispose() + { + _iterator?.Dispose(); + _iterator = null; + } + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponent.cs new file mode 100644 index 00000000..04ce9e26 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponent.cs @@ -0,0 +1,18 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +[OpenMpApiPartial] +public readonly partial struct IPoolComponent where T : unmanaged, IIDProvider.IManagedInterface +{ + /// + /// Converts this component to a pool. + /// + /// A pool. + public IPool AsPool() + { + return new IPool(IPoolComponentInterop.cast_IPoolComponent_to_IPool(_handle)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponentInterop.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponentInterop.cs new file mode 100644 index 00000000..60b50680 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolComponentInterop.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +internal static class IPoolComponentInterop +{ + [DllImport("SampSharp", EntryPoint = "cast_IPoolComponent_to_IPool")] + public static extern nint cast_IPoolComponent_to_IPool(nint handle); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolEventHandler.cs new file mode 100644 index 00000000..26db9285 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolEventHandler.cs @@ -0,0 +1,58 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +public interface IPoolEventHandler : IEventHandler> where T : unmanaged, IUnmanagedInterface +{ + /// + /// Called when a pool entry is created. + /// + /// The created pool entry. + void OnPoolEntryCreated(T entry); + + /// + /// Called when a pool entry is destroyed. + /// + /// The destroyed pool entry. + void OnPoolEntryDestroyed(T entry); + + // [OpenMpEventHandler] does not support generic types - implement it manually + + /// + /// Gets the marshaller for the event handler. + /// + static IEventHandlerMarshaller> IEventHandler>.Marshaller => NativeEventHandlerManager.Instance; + + /// + /// Manages the marshalling of native event handlers for . + /// + public class NativeEventHandlerManager : EventHandlerMarshaller> + { + /// + /// Gets the singleton instance of the . + /// + public static NativeEventHandlerManager Instance { get; } = new(); + + /// + protected override (nint, object) Create(IPoolEventHandler handler) + { + Delegate onPoolEntryCreatedDelegate = (PoolDelegate)(h => handler.OnPoolEntryCreated(StructPointer.AsStruct(h))), + onPoolEntryDestroyedDelegate = (PoolDelegate)(h => handler.OnPoolEntryDestroyed(StructPointer.AsStruct(h))); + + nint onPoolEntryCreatedPtr = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(onPoolEntryCreatedDelegate), + onPoolEntryDestroyedPtr = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(onPoolEntryDestroyedDelegate); + + object[] data = [onPoolEntryCreatedDelegate, onPoolEntryDestroyedDelegate]; + + var handle = PoolEventHandlerInterop.PoolEventHandlerImpl_create(onPoolEntryCreatedPtr, onPoolEntryDestroyedPtr); + return (handle, data); + } + + /// + protected override void Free(nint handle) + { + PoolEventHandlerInterop.PoolEventHandlerImpl_delete(handle); + } + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolInterop.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolInterop.cs new file mode 100644 index 00000000..703e94ed --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IPoolInterop.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +internal static partial class IPoolInterop +{ + [LibraryImport("SampSharp", EntryPoint = "IPool_release")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial void IPool_release(nint handle, int index); + + [LibraryImport("SampSharp", EntryPoint = "IPool_lock")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial void IPool_lock(nint handle, int index); + + [LibraryImport("SampSharp", EntryPoint = "IPool_unlock")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool IPool_unlock(nint handle, int index); + + [LibraryImport("SampSharp", EntryPoint = "IPool_getPoolEventDispatcher")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial nint IPool_getPoolEventDispatcher(nint handle); + + [LibraryImport("SampSharp", EntryPoint = "IPool_entries")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial nint IPool_entries(nint handle); + + [LibraryImport("SampSharp", EntryPoint = "IPool_count")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial Size IPool_count(nint handle); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPool.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPool.cs new file mode 100644 index 00000000..17f48960 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPool.cs @@ -0,0 +1,30 @@ +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct IReadOnlyPool where T : unmanaged, IUnmanagedInterface +{ + /// + /// Gets the element at the specified index from the pool. + /// + /// The index of the element to retrieve. + /// The element at the specified index. + public T Get(int index) + { + var data = IReadOnlyPoolInterop.IReadOnlyPool_get(_handle, index); + return StructPointer.AsStruct(data); + } + + /// + /// Retrieves the bounds of the pool. + /// + /// The output parameter that will contain the bounds of the pool as a pair of sizes. + public void Bounds(out Pair bounds) + { + IReadOnlyPoolInterop.IReadOnlyPool_bounds(_handle, out bounds); + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPoolInterop.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPoolInterop.cs new file mode 100644 index 00000000..1584ce4b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/IReadOnlyPoolInterop.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +internal static class IReadOnlyPoolInterop +{ + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "IReadOnlyPool_get", ExactSpelling = true)] + public static extern nint IReadOnlyPool_get(nint handle_, int index); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl, EntryPoint = "IReadOnlyPool_bounds", ExactSpelling = true)] + public static extern void IReadOnlyPool_bounds(nint handle_, out Pair bounds); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIterator.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIterator.cs new file mode 100644 index 00000000..caac0366 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIterator.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Full copy of the unmanaged implementation of the MarkedPoolIterator struct. We need to implement this ourselves +/// because the original implementation uses a template for the type of the pool entries and calls getID on the entries. +/// In the proxy wrapper code we assume all IPools to be of type IDProvider, which would cause mismatching vtable refs. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct MarkedPoolIterator : IDisposable, IEquatable> where T : unmanaged, IIDProvider.IManagedInterface +{ + private readonly IPool _pool; + private int _lockedId; + private readonly FlatPtrHashSet _entries; + private FlatPtrHashSetIterator _iter; + + internal MarkedPoolIterator(IPool pool, FlatPtrHashSet entries, FlatPtrHashSetIterator iter) + { + _pool = pool; + _lockedId = -1; + _entries = entries; + _iter = iter; + + Lock(); + } + + public void Dispose() + { + Unlock(); + } + + private void Lock() + { + Debug.Assert(_lockedId == -1); + if (_iter != _entries.End()) + { + _lockedId = _iter.Get().GetID(); + _pool.Lock(_lockedId); + } + } + + private void Unlock() + { + if (_lockedId != -1) + { + _pool.Unlock(_lockedId); + _lockedId = -1; + } + } + + public readonly T Current => _iter.Get(); + + public readonly bool Equals(MarkedPoolIterator other) + { + return _iter.Equals(other._iter); + } + + public override readonly bool Equals(object? obj) + { + return obj is MarkedPoolIterator other && Equals(other); + } + + public override readonly int GetHashCode() + { + return _iter.GetHashCode(); + } + + public void Advance() + { + _iter.Advance(); + Unlock(); + Lock(); + } + + public static bool operator ==(MarkedPoolIterator a, MarkedPoolIterator b) + { + return a.Equals(b); + } + + public static bool operator !=(MarkedPoolIterator a, MarkedPoolIterator b) + { + return !a.Equals(b); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIteratorData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIteratorData.cs new file mode 100644 index 00000000..84207f24 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/MarkedPoolIteratorData.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.OpenMp.Core.Api; + +[StructLayout(LayoutKind.Sequential)] +internal struct MarkedPoolIteratorData +{ + public nint Pool; + public int LockedId; + public nint Entries; + public FlatPtrHashSetIterator Iter; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolDelegate.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolDelegate.cs new file mode 100644 index 00000000..4ac3e9e2 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolDelegate.cs @@ -0,0 +1,3 @@ +namespace SampSharp.OpenMp.Core.Api; + +internal delegate void PoolDelegate(nint entry); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolEventHandlerInterop.cs b/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolEventHandlerInterop.cs new file mode 100644 index 00000000..ac5b7cd8 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Pool/PoolEventHandlerInterop.cs @@ -0,0 +1,14 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +internal static partial class PoolEventHandlerInterop +{ + [LibraryImport("SampSharp", EntryPoint = "PoolEventHandlerImpl_create")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial nint PoolEventHandlerImpl_create(nint onPoolEntryCreated, nint onPoolEntryDestroyed); + + [LibraryImport("SampSharp", EntryPoint = "PoolEventHandlerImpl_delete")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial void PoolEventHandlerImpl_delete(nint ptr); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IPlayerRecordingData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IPlayerRecordingData.cs new file mode 100644 index 00000000..e9fa08e4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IPlayerRecordingData.cs @@ -0,0 +1,23 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerRecordingData +{ + /// + public static UID ExtensionId => new(0x34DB532857286482); + + /// + /// Starts recording the player's movements and actions. + /// + /// The type of recording to start (e.g., driver, on foot). + /// The file path where the recording will be saved. + public partial void Start(PlayerRecordingType type, string file); + + /// + /// Stops the current player recording. + /// + public partial void Stop(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IRecordingsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IRecordingsComponent.cs new file mode 100644 index 00000000..90058a69 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/IRecordingsComponent.cs @@ -0,0 +1,11 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct IRecordingsComponent +{ + /// + public static UID ComponentId => new(0x871144D399F5F613); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Recordings/PlayerRecordingType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/PlayerRecordingType.cs new file mode 100644 index 00000000..d60c94ca --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Recordings/PlayerRecordingType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of recording for a player. +/// +public enum PlayerRecordingType +{ + /// + /// No recording is active. + /// + None, + + /// + /// Recording player movement while in a vehicle (driver recording). + /// + Driver, + + /// + /// Recording player movement while on foot. + /// + OnFoot +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDraw.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDraw.cs new file mode 100644 index 00000000..b7c48fd2 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDraw.cs @@ -0,0 +1,24 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ITextDrawBase))] +public readonly partial struct IPlayerTextDraw +{ + /// + /// Shows the player-specific text draw. + /// + public partial void Show(); + + /// + /// Hides the player-specific text draw. + /// + public partial void Hide(); + + /// + /// Determines whether the player-specific text draw is currently shown. + /// + /// if the text draw is shown; otherwise, . + public partial bool IsShown(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDrawData.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDrawData.cs new file mode 100644 index 00000000..d620e05b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/IPlayerTextDrawData.cs @@ -0,0 +1,56 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension), typeof(IPool))] +public readonly partial struct IPlayerTextDrawData +{ + /// + public static UID ExtensionId => new(0xbf08495682312400); + + /// + /// Begins the selection of text draws for the player. + /// + /// The highlight color to use during selection. + public partial void BeginSelection(Colour highlight); + + /// + /// Determines whether the player is currently selecting text draws. + /// + /// if the player is selecting text draws; otherwise, . + public partial bool IsSelecting(); + + /// + /// Ends the selection of text draws for the player. + /// + public partial void EndSelection(); + + /// + /// Creates a new player-specific text draw with the specified position and text. + /// + /// The position of the text draw. + /// The text of the text draw. + /// The created player-specific text draw. + public partial IPlayerTextDraw Create(Vector2 position, string text); + + /// + /// Creates a new player-specific text draw with the specified position and preview model. + /// + /// The position of the text draw. + /// The preview model of the text draw. + /// The created player-specific text draw. + [OpenMpApiOverload("_model")] + public partial IPlayerTextDraw Create(Vector2 position, int model); + + /// + /// Gets the pool interface for managing player-specific text draws. + /// + /// A pool interface for player-specific text draws. + public IPool AsPool() + { + return (IPool)this; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDraw.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDraw.cs new file mode 100644 index 00000000..aaa1b26e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDraw.cs @@ -0,0 +1,34 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ITextDrawBase))] +public readonly partial struct ITextDraw +{ + /// + /// Shows the text draw for the specified player. + /// + /// The player for whom the text draw will be shown. + public partial void ShowForPlayer(IPlayer player); + + /// + /// Hides the text draw for the specified player. + /// + /// The player for whom the text draw will be hidden. + public partial void HideForPlayer(IPlayer player); + + /// + /// Determines whether the text draw is shown for the specified player. + /// + /// The player to check. + /// if the text draw is shown for the player; otherwise, . + public partial bool IsShownForPlayer(IPlayer player); + + /// + /// Sets the text of the text draw for the specified player. + /// + /// The player for whom the text will be set. + /// The new text of the text draw. + public partial void SetTextForPlayer(IPlayer player, string text); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawBase.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawBase.cs new file mode 100644 index 00000000..a318d9fb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawBase.cs @@ -0,0 +1,291 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IIDProvider))] +public readonly partial struct ITextDrawBase +{ + private partial void GetPosition(out Vector2 position); + + /// + /// Gets the position of the textdraw. + /// + /// The position of the textdraw. + public Vector2 GetPosition() + { + GetPosition(out var result); + return result; + } + + /// + /// Sets the position of the textdraw. + /// + /// The new position of the textdraw. + /// This instance for chaining. + public partial ref ITextDrawBase SetPosition(Vector2 position); + + /// + /// Sets the text of the textdraw. + /// + /// The new text to display. + public partial void SetText(string text); + + /// + /// Gets the text of the textdraw. + /// + /// The text currently displayed by the textdraw. + public partial string GetText(); + + /// + /// Sets the size of the textdraw's letters. + /// + /// The new letter size. + /// This instance for chaining. + public partial ref ITextDrawBase SetLetterSize(Vector2 size); + private partial void GetLetterSize(out Vector2 size); + + /// + /// Gets the size of the textdraw's letters. + /// + /// The size of the textdraw's letters. + public Vector2 GetLetterSize() + { + GetLetterSize(out var result); + return result; + } + + /// + /// Sets the size of the textdraw area. + /// + /// The new textdraw area size. + /// This instance for chaining. + public partial ref ITextDrawBase SetTextSize(Vector2 size); + private partial void GetTextSize(out Vector2 size); + + /// + /// Gets the size of the textdraw area. + /// + /// The size of the textdraw area. + public Vector2 GetTextSize() + { + GetTextSize(out var result); + return result; + } + + /// + /// Sets the alignment of the textdraw's text. + /// + /// The alignment to set. + /// This instance for chaining. + public partial ref ITextDrawBase SetAlignment(TextDrawAlignmentTypes alignment); + + /// + /// Gets the alignment of the textdraw's text. + /// + /// The current text alignment. + public partial TextDrawAlignmentTypes GetAlignment(); + + /// + /// Sets the colour of the textdraw's letters. + /// + /// The new letter colour. + /// This instance for chaining. + public partial ref ITextDrawBase SetColour(Colour colour); + private partial void GetLetterColour(out Colour colour); + + /// + /// Gets the colour of the textdraw's letters. + /// + /// The colour of the textdraw's letters. + public Colour GetLetterColour() + { + GetLetterColour(out var result); + return result; + } + + /// + /// Sets whether the textdraw uses a box. + /// + /// Whether to use a box. to use a box, otherwise. + /// This instance for chaining. + public partial ref ITextDrawBase UseBox(bool use); + + /// + /// Gets whether the textdraw uses a box. + /// + /// if the textdraw uses a box; otherwise, . + public partial bool HasBox(); + + /// + /// Sets the colour of the textdraw's box. + /// + /// The new box colour. + /// This instance for chaining. + public partial ref ITextDrawBase SetBoxColour(Colour colour); + + /// + /// Gets the colour of the textdraw's box. + /// + /// The box colour. + public partial void GetBoxColour(out Colour colour); + + /// + /// Gets the colour of the textdraw's box. + /// + /// The colour of the textdraw's box. + public Colour GetBoxColour() + { + GetBoxColour(out var result); + return result; + } + + /// + /// Sets the shadow strength of the textdraw. + /// + /// The shadow strength. + /// This instance for chaining. + public partial ref ITextDrawBase SetShadow(int shadow); + + /// + /// Gets the shadow strength of the textdraw. + /// + /// The shadow strength. + public partial int GetShadow(); + + /// + /// Sets the outline of the textdraw. + /// + /// The outline thickness. + /// This instance for chaining. + public partial ref ITextDrawBase SetOutline(int outline); + + /// + /// Gets the outline thickness of the textdraw. + /// + /// The outline thickness. + public partial int GetOutline(); + + /// + /// Sets the background colour of the textdraw. + /// + /// The new background colour. + /// This instance for chaining. + public partial ref ITextDrawBase SetBackgroundColour(Colour colour); + private partial void GetBackgroundColour(out Colour colour); + + /// + /// Gets the background colour of the textdraw. + /// + /// The background colour. + public Colour GetBackgroundColour() + { + GetBackgroundColour(out var result); + return result; + } + + /// + /// Sets the drawing style of the textdraw. + /// + /// The drawing style. + /// This instance for chaining. + public partial ref ITextDrawBase SetStyle(TextDrawStyle style); + + /// + /// Gets the drawing style of the textdraw. + /// + /// The drawing style. + public partial TextDrawStyle GetStyle(); + + /// + /// Sets whether the textdraw is proportional. + /// + /// Whether the textdraw is proportional. for proportional, otherwise. + /// This instance for chaining. + public partial ref ITextDrawBase SetProportional(bool proportional); + + /// + /// Gets whether the textdraw is proportional. + /// + /// if the textdraw is proportional; otherwise, . + public partial bool IsProportional(); + + /// + /// Sets whether the textdraw is selectable. + /// + /// Whether the textdraw is selectable. for selectable, otherwise. + /// This instance for chaining. + public partial ref ITextDrawBase SetSelectable(bool selectable); + + /// + /// Gets whether the textdraw is selectable. + /// + /// if the textdraw is selectable; otherwise, . + public partial bool IsSelectable(); + + /// + /// Sets the preview model for the textdraw. + /// + /// The model ID to preview. + /// This instance for chaining. + public partial ref ITextDrawBase SetPreviewModel(int model); + + /// + /// Gets the preview model for the textdraw. + /// + /// The model ID being previewed. + public partial int GetPreviewModel(); + + /// + /// Sets the preview rotation for the textdraw. + /// + /// The rotation vector. + /// This instance for chaining. + public partial ref ITextDrawBase SetPreviewRotation(Vector3 rotation); + + /// + /// Gets the preview rotation for the textdraw. + /// + /// The rotation vector. + public partial Vector3 GetPreviewRotation(); + + /// + /// Sets the preview vehicle colours for the textdraw. + /// + /// The first vehicle colour. + /// The second vehicle colour. + /// This instance for chaining. + public partial ref ITextDrawBase SetPreviewVehicleColour(int colour1, int colour2); + private partial void GetPreviewVehicleColour(out Pair result); + + /// + /// Gets the preview vehicle colours for the textdraw. + /// + /// A tuple containing the two vehicle colours. + public (int, int) GetPreviewVehicleColour() + { + GetPreviewVehicleColour(out var result); + return result; + } + + /// + /// Sets the preview zoom factor for the textdraw. + /// + /// The zoom factor. + /// This instance for chaining. + public partial ref ITextDrawBase SetPreviewZoom(float zoom); + + /// + /// Gets the preview zoom factor for the textdraw. + /// + /// The preview zoom factor. + public partial float GetPreviewZoom(); + + /// + /// Restreams the textdraw. TODO: Add more details. + /// + public partial void Restream(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawEventHandler.cs new file mode 100644 index 00000000..9393bb2b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawEventHandler.cs @@ -0,0 +1,36 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface ITextDrawEventHandler +{ + /// + /// Called when a player clicks on a text draw. + /// + /// The player who clicked the text draw. + /// The text draw that was clicked. + void OnPlayerClickTextDraw(IPlayer player, ITextDraw td); + + /// + /// Called when a player clicks on a player-specific text draw. + /// + /// The player who clicked the text draw. + /// The player-specific text draw that was clicked. + void OnPlayerClickPlayerTextDraw(IPlayer player, IPlayerTextDraw td); + + /// + /// Called when a player cancels text draw selection. + /// + /// The player who canceled the selection. + /// if the cancellation was handled; otherwise, . + bool OnPlayerCancelTextDrawSelection(IPlayer player); + + /// + /// Called when a player cancels player-specific text draw selection. + /// + /// The player who canceled the selection. + /// if the cancellation was handled; otherwise, . + bool OnPlayerCancelPlayerTextDrawSelection(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawsComponent.cs new file mode 100644 index 00000000..4ef0336f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/ITextDrawsComponent.cs @@ -0,0 +1,35 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct ITextDrawsComponent +{ + /// + public static UID ComponentId => new(0x9b5dc2b1d15c992a); + + /// + /// Retrieves the event dispatcher for text draw events. + /// + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Creates a new text draw with the specified position and text. + /// + /// The position of the text draw. + /// The text of the text draw. + /// The created text draw. + public partial ITextDraw Create(Vector2 position, string text); + + /// + /// Creates a new text draw with the specified position and preview model. + /// + /// The position of the text draw. + /// The preview model of the text draw. + /// The created text draw. + [OpenMpApiOverload("_model")] + public partial ITextDraw Create(Vector2 position, int model); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawAlignmentTypes.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawAlignmentTypes.cs new file mode 100644 index 00000000..577ad5ca --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawAlignmentTypes.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the alignment of a text draw. +/// +public enum TextDrawAlignmentTypes +{ + /// + /// Default alignment. + /// + TextDrawAlignment_Default, + + /// + /// Aligns the text to the left. + /// + TextDrawAlignment_Left, + + /// + /// Centers the text. + /// + TextDrawAlignment_Center, + + /// + /// Aligns the text to the right. + /// + TextDrawAlignment_Right +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawStyle.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawStyle.cs new file mode 100644 index 00000000..790ef812 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextDraws/TextDrawStyle.cs @@ -0,0 +1,37 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the style of a text draw. +/// +public enum TextDrawStyle +{ + /// + /// A font type. + /// + FontBeckettRegular = 0, + + /// + /// A font type. + /// + FontAharoniBold, + + /// + /// A font type. + /// + FontBankGothic, + + /// + /// A font type. + /// + FontPricedown, + + /// + /// A TXD sprite. + /// + Sprite, + + /// + /// A model preview. + /// + Preview +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabel.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabel.cs new file mode 100644 index 00000000..ffefeff0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabel.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ITextLabelBase))] +public readonly partial struct IPlayerTextLabel; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabelData.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabelData.cs new file mode 100644 index 00000000..3fcb8b24 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/IPlayerTextLabelData.cs @@ -0,0 +1,59 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension), typeof(IPool))] +public readonly partial struct IPlayerTextLabelData +{ + /// + public static UID ExtensionId => new(0xb9e2bd0dc5148c3c); + + /// + /// Creates a new per-player text label. + /// + /// The text to display on the label. + /// The colour of the text label. + /// The position where the text label will be created. + /// The draw distance of the text label. + /// Whether line-of-sight testing is enabled. + /// The created per-player text label, or null if creation failed. + public partial IPlayerTextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, bool los); + + /// + /// Creates a new per-player text label attached to a player. + /// + /// The text to display on the label. + /// The colour of the text label. + /// The position offset from the player. + /// The draw distance of the text label. + /// Whether line-of-sight testing is enabled. + /// The player to attach the text label to. + /// The created per-player text label, or null if creation failed. + [OpenMpApiOverload("_player")] + public partial IPlayerTextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, bool los, IPlayer attach); + + /// + /// Creates a new per-player text label attached to a vehicle. + /// + /// The text to display on the label. + /// The colour of the text label. + /// The position offset from the vehicle. + /// The draw distance of the text label. + /// Whether line-of-sight testing is enabled. + /// The vehicle to attach the text label to. + /// The created per-player text label, or null if creation failed. + [OpenMpApiOverload("_vehicle")] + public partial IPlayerTextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, bool los, IVehicle attach); + + /// + /// Gets the pool interface for managing per-player text labels. + /// + /// A pool interface for per-player text labels. + public IPool AsPool() + { + return (IPool)this; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabel.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabel.cs new file mode 100644 index 00000000..5389201c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabel.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(ITextLabelBase))] +public readonly partial struct ITextLabel +{ + /// + /// Determines whether the text label is streamed in for the specified player. + /// + /// The player to check. + /// if the text label is streamed in for the player; otherwise, . + public partial bool IsStreamedInForPlayer(IPlayer player); + + /// + /// Streams the text label in for the specified player. + /// + /// The player for whom the text label will be streamed in. + public partial void StreamInForPlayer(IPlayer player); + + /// + /// Streams the text label out for the specified player. + /// + /// The player for whom the text label will be streamed out. + public partial void StreamOutForPlayer(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelBase.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelBase.cs new file mode 100644 index 00000000..4f1a824e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelBase.cs @@ -0,0 +1,109 @@ +using System.Diagnostics.CodeAnalysis; +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +[SuppressMessage("ReSharper", "InconsistentNaming")] +public readonly partial struct ITextLabelBase +{ + /// + /// Sets the text content of the text label. + /// + /// The new text to display. + public partial void SetText(string text); + + /// + /// Gets the text content of the text label. + /// + /// The text currently displayed on the text label. + public partial string GetText(); + + /// + /// Sets the colour of the text label. + /// + /// The colour to set. + public partial void SetColour(Colour colour); + + /// + /// Gets the colour of the text label. + /// + /// When the method returns, contains the colour of the text label. + public partial void GetColour(out Colour colour); + + /// + /// Gets the colour of the text label. + /// + /// The colour of the text label. + public Colour GetColour() + { + GetColour(out var result); + return result; + } + + /// + /// Sets the draw distance for the text label. + /// + /// The draw distance in units. + public partial void SetDrawDistance(float dist); + + /// + /// Gets the draw distance of the text label. + /// + /// The draw distance in units. + public partial float GetDrawDistance(); + + /// + /// Attaches the text label to a player. + /// + /// The player to attach the text label to. + /// The offset from the player's position. + public partial void AttachToPlayer(IPlayer player, Vector3 offset); + + /// + /// Attaches the text label to a vehicle. + /// + /// The vehicle to attach the text label to. + /// The offset from the vehicle's position. + public partial void AttachToVehicle(IVehicle vehicle, Vector3 offset); + + /// + /// Gets the attachment data of the text label. + /// + /// A reference to the structure containing the attachment information. + public partial ref TextLabelAttachmentData GetAttachmentData(); + + /// + /// Detaches the text label from the player it was attached to. + /// + /// The new position for the text label after detachment. + public partial void DetachFromPlayer(Vector3 position); + + /// + /// Detaches the text label from the vehicle it was attached to. + /// + /// The new position for the text label after detachment. + public partial void DetachFromVehicle(Vector3 position); + + /// + /// Sets whether line-of-sight testing is enabled for the text label. + /// + /// true to enable line-of-sight testing; false to disable it. + public partial void SetTestLOS(bool status); + + /// + /// Gets whether line-of-sight testing is enabled for the text label. + /// + /// true if line-of-sight testing is enabled; otherwise, false. + public partial bool GetTestLOS(); + + /// + /// Sets both the colour and text of the text label in a single operation. + /// + /// The colour to set. + /// The text to set. + public partial void SetColourAndText(Colour colour, string text); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelsComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelsComponent.cs new file mode 100644 index 00000000..73b21e86 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/ITextLabelsComponent.cs @@ -0,0 +1,53 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct ITextLabelsComponent +{ + /// + public static UID ComponentId => new(0xa0c57ea80a009742); + + /// + /// Creates a new text label with the specified properties. + /// + /// The text to display in the label. + /// The color of the text label. + /// The position of the text label. + /// The draw distance for the text label. + /// The virtual world in which the text label is visible. + /// A value indicating whether the text label requires line of sight to be visible. + /// The created text label. + public partial ITextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, int vw, bool los); + + /// + /// Creates a new text label attached to a player with the specified properties. + /// + /// The text to display in the label. + /// The color of the text label. + /// The position of the text label. + /// The draw distance for the text label. + /// The virtual world in which the text label is visible. + /// A value indicating whether the text label requires line of sight to be visible. + /// The player to which the text label is attached. + /// The created text label. + [OpenMpApiOverload("_player")] + public partial ITextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, int vw, bool los, IPlayer attach); + + /// + /// Creates a new text label attached to a vehicle with the specified properties. + /// + /// The text to display in the label. + /// The color of the text label. + /// The position of the text label. + /// The draw distance for the text label. + /// The virtual world in which the text label is visible. + /// A value indicating whether the text label requires line of sight to be visible. + /// The vehicle to which the text label is attached. + /// The created text label. + [OpenMpApiOverload("_vehicle")] + public partial ITextLabel Create(string text, Colour colour, Vector3 pos, float drawDist, int vw, bool los, IVehicle attach); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/TextLabelAttachmentData.cs b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/TextLabelAttachmentData.cs new file mode 100644 index 00000000..6be6e8e5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/TextLabels/TextLabelAttachmentData.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents data for attaching a text label to a player or vehicle. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct TextLabelAttachmentData +{ + /// + /// Gets the ID of the player to which the text label is attached. + /// Default value is INVALID_PLAYER_ID. + /// + public readonly int PlayerId; + + /// + /// Gets the ID of the vehicle to which the text label is attached. + /// Default value is INVALID_VEHICLE_ID. + /// + public readonly int VehicleId; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/CarriagesArray.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/CarriagesArray.cs new file mode 100644 index 00000000..eadedc64 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/CarriagesArray.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents an array of carriages linked to a vehicle. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct CarriagesArray +{ + /// + /// Gets the array of vehicles representing the carriages. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = OpenMpConstants.MAX_VEHICLE_CARRIAGES)] + public readonly IVehicle[] Values; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IPlayerVehicleData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IPlayerVehicleData.cs new file mode 100644 index 00000000..8ba4ff61 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IPlayerVehicleData.cs @@ -0,0 +1,49 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct IPlayerVehicleData +{ + /// + public static UID ExtensionId => new(0xa960485be6c70fb2); + + /// + /// Gets the vehicle the player is currently in. + /// + /// The vehicle the player is in, or null if not in a vehicle. + public partial IVehicle GetVehicle(); + + /// + /// Resets the player's vehicle data internally. + /// + /// + /// TODO: Clarify the exact purpose and usage of this method. + /// + public partial void ResetVehicle(); + + /// + /// Gets the seat the player is currently occupying in a vehicle. + /// + /// The seat index, or -1 if the player is not in a vehicle. + public partial int GetSeat(); + + /// + /// Checks if the player is in a mod shop. + /// + /// if the player is in a mod shop; otherwise, . + public partial bool IsInModShop(); + + /// + /// Checks if the player is in drive-by mode. + /// + /// if the player is in drive-by mode; otherwise, . + public partial bool IsInDriveByMode(); + + /// + /// Checks if the player is cuffed. + /// + /// if the player is cuffed; otherwise, . + public partial bool IsCuffed(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs new file mode 100644 index 00000000..55ee19e1 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicle.cs @@ -0,0 +1,432 @@ +using System.Numerics; +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +public readonly partial struct IVehicle +{ + /// + /// Sets the spawn data for the vehicle. + /// + /// The spawn data to set. + public partial void SetSpawnData(ref VehicleSpawnData data); + + /// + /// Retrieves the spawn data for the vehicle. + /// + /// The output parameter to store the spawn data. + private partial void GetSpawnData(out VehicleSpawnData data); + + /// + /// Gets the spawn data for the vehicle. + /// + /// The spawn data of the vehicle. + public VehicleSpawnData GetSpawnData() + { + GetSpawnData(out var data); + return data; + } + + /// + /// Checks if the vehicle is streamed in for a specific player. + /// + /// The player to check. + /// True if the vehicle is streamed in for the player; otherwise, false. + public partial bool IsStreamedInForPlayer(IPlayer player); + + /// + /// Streams the vehicle in for a specific player. + /// + /// The player for whom to stream the vehicle. + public partial void StreamInForPlayer(IPlayer player); + + /// + /// Streams the vehicle out for a specific player. + /// + /// The player for whom to stream the vehicle out. + public partial void StreamOutForPlayer(IPlayer player); + + /// + /// Sets the primary and secondary colors of the vehicle. + /// + /// The primary color. + /// The secondary color. + public partial void SetColour(int col1, int col2); + + /// + /// Gets the primary and secondary colors of the vehicle. + /// + /// The output parameter to store the colors. + public partial void GetColour(out Pair result); + + /// + /// Gets the primary and secondary colors of the vehicle. + /// + /// A tuple containing the primary and secondary colors. + public (int, int) GetColour() + { + GetColour(out var result); + return result; + } + + /// + /// Sets the health of the vehicle. + /// + /// The health value to set. + public partial void SetHealth(float health); + + /// + /// Gets the health of the vehicle. + /// + /// The health value of the vehicle. + public partial float GetHealth(); + + /// + /// Updates the vehicle state based on driver synchronization data. + /// + /// The driver synchronization data. + /// The player associated with the synchronization. + /// True if the update was successful; otherwise, false. + public partial bool UpdateFromDriverSync(ref VehicleDriverSyncPacket vehicleSync, IPlayer player); + + /// + /// Updates the vehicle state based on passenger synchronization data. + /// + /// The passenger synchronization data. + /// The player associated with the synchronization. + /// True if the update was successful; otherwise, false. + public partial bool UpdateFromPassengerSync(ref VehiclePassengerSyncPacket passengerSync, IPlayer player); + + /// + /// Updates the vehicle state based on unoccupied synchronization data. + /// + /// The unoccupied synchronization data. + /// The player associated with the synchronization. + /// True if the update was successful; otherwise, false. + public partial bool UpdateFromUnoccupied(ref VehicleUnoccupiedSyncPacket unoccupiedSync, IPlayer player); + + /// + /// Updates the vehicle state based on trailer synchronization data. + /// + /// The trailer synchronization data. + /// The player associated with the synchronization. + /// True if the update was successful; otherwise, false. + public partial bool UpdateFromTrailerSync(ref VehicleTrailerSyncPacket unoccupiedSync, IPlayer player); + + /// + /// Gets the players for whom the vehicle is streamed in. + /// + /// A set of players for whom the vehicle is streamed in. + public partial FlatPtrHashSet StreamedForPlayers(); + + /// + /// Gets the driver of the vehicle. + /// + /// The player who is the driver of the vehicle. + public partial IPlayer GetDriver(); + + /// + /// Gets the passengers of the vehicle. + /// + /// A set of players who are passengers in the vehicle. + public partial FlatPtrHashSet GetPassengers(); + + /// + /// Sets the license plate text for the vehicle. + /// + /// The license plate text to set. + public partial void SetPlate(string plate); + + /// + /// Gets the license plate text of the vehicle. + /// + /// The license plate text of the vehicle. + public partial string GetPlate(); + + /// + /// Sets the damage status of the vehicle. + /// + /// The status of the vehicle's panels. + /// The status of the vehicle's doors. + /// The status of the vehicle's lights. + /// The status of the vehicle's tyres. + /// The player updating the vehicle's damage status. Optional. + public partial void SetDamageStatus(int panelStatus, int doorStatus, byte lightStatus, byte tyreStatus, IPlayer vehicleUpdater = default); + + /// + /// Gets the damage status of the vehicle. + /// + /// The output parameter for the status of the vehicle's panels. + /// The output parameter for the status of the vehicle's doors. + /// The output parameter for the status of the vehicle's lights. + /// The output parameter for the status of the vehicle's tyres. + public partial void GetDamageStatus(out int panelStatus, out int doorStatus, out int lightStatus, out int tyreStatus); + + /// + /// Sets the paint job of the vehicle. + /// + /// The paint job ID to set. + public partial void SetPaintJob(int paintjob); + + /// + /// Gets the paint job of the vehicle. + /// + /// The paint job ID of the vehicle. + public partial int GetPaintJob(); + + /// + /// Adds a component to the vehicle. + /// + /// The component ID to add. + public partial void AddComponent(int component); + + /// + /// Gets the component in a specific slot of the vehicle. + /// + /// The slot ID to check. + /// The component ID in the specified slot. + public partial int GetComponentInSlot(int slot); + + /// + /// Removes a component from the vehicle. + /// + /// The component ID to remove. + public partial void RemoveComponent(int component); + + /// + /// Places a player in a specific seat of the vehicle. + /// + /// The player to place in the vehicle. + /// The seat ID to place the player in. + public partial void PutPlayer(IPlayer player, int seatID); + + /// + /// Sets the Z angle (rotation) of the vehicle. + /// + /// The Z angle to set. + public partial void SetZAngle(float angle); + + /// + /// Gets the Z angle (rotation) of the vehicle. + /// + /// The Z angle of the vehicle. + public partial float GetZAngle(); + + /// + /// Sets the parameters of the vehicle. + /// + /// The parameters to set. + public partial void SetParams(ref VehicleParams parameters); + + /// + /// Sets the parameters of the vehicle for a specific player. + /// + /// The player for whom to set the parameters. + /// The parameters to set. + public partial void SetParamsForPlayer(IPlayer player, ref VehicleParams parameters); + + private partial void GetParams(out VehicleParams parameters); + + /// + /// Gets the parameters of the vehicle. + /// + /// The parameters of the vehicle. + public VehicleParams GetParams() + { + GetParams(out var parameters); + return parameters; + } + + /// + /// Checks if the vehicle is dead. + /// + /// True if the vehicle is dead; otherwise, false. + public partial bool IsDead(); + + /// + /// Respawns the vehicle. + /// + public partial void Respawn(); + + /// + /// Gets the respawn delay of the vehicle. + /// + /// The respawn delay of the vehicle. + [return: MarshalUsing(typeof(SecondsMarshaller))] + public partial TimeSpan GetRespawnDelay(); + + /// + /// Sets the respawn delay of the vehicle. + /// + /// The respawn delay to set. + public partial void SetRespawnDelay([MarshalUsing(typeof(SecondsMarshaller))]TimeSpan delay); + + /// + /// Checks if the vehicle is respawning. + /// + /// True if the vehicle is respawning; otherwise, false. + public partial bool IsRespawning(); + + /// + /// Sets the interior ID of the vehicle. + /// + /// The interior ID to set. + public partial void SetInterior(int interiorID); + + /// + /// Gets the interior ID of the vehicle. + /// + /// The interior ID of the vehicle. + public partial int GetInterior(); + + /// + /// Attaches a trailer to the vehicle. + /// + /// The trailer to attach. + public partial void AttachTrailer(IVehicle trailer); + + /// + /// Detaches the trailer from the vehicle. + /// + public partial void DetachTrailer(); + + /// + /// Checks if the vehicle has a trailer attached. + /// + /// True if the vehicle has a trailer attached; otherwise, false. + public partial bool IsTrailer(); + + /// + /// Gets the trailer attached to the vehicle. + /// + /// The trailer attached to the vehicle. + public partial IVehicle GetTrailer(); + + /// + /// Gets the cab of the vehicle. + /// + /// The cab of the vehicle. + public partial IVehicle GetCab(); + + /// + /// Repairs the vehicle. + /// + public partial void Repair(); + + /// + /// Adds a carriage to the vehicle. + /// + /// The carriage to add. + /// The position of the carriage. + public partial void AddCarriage(IVehicle carriage, int pos); + + /// + /// Updates the position and velocity of a carriage. + /// + /// The position of the carriage. + /// The velocity of the carriage. + public partial void UpdateCarriage(Vector3 pos, Vector3 veloc); + + /// + /// Gets the carriages attached to the vehicle. + /// + /// The carriages attached to the vehicle. + public partial ref CarriagesArray GetCarriages(); + + /// + /// Sets the velocity of the vehicle. + /// + /// The velocity to set. + public partial void SetVelocity(Vector3 velocity); + + /// + /// Gets the velocity of the vehicle. + /// + /// The velocity of the vehicle. + public partial Vector3 GetVelocity(); + + /// + /// Sets the angular velocity of the vehicle. + /// + /// The angular velocity to set. + public partial void SetAngularVelocity(Vector3 velocity); + + /// + /// Gets the angular velocity of the vehicle. + /// + /// The angular velocity of the vehicle. + public partial Vector3 GetAngularVelocity(); + + /// + /// Gets the model ID of the vehicle. + /// + /// The model ID of the vehicle. + public partial int GetModel(); + + /// + /// Gets the state of the vehicle's landing gear. + /// + /// The state of the landing gear. + public partial byte GetLandingGearState(); + + /// + /// Checks if the vehicle has been occupied. + /// + /// True if the vehicle has been occupied; otherwise, false. + public partial bool HasBeenOccupied(); + + /// + /// Gets the last time the vehicle was occupied. + /// + /// The last occupied time of the vehicle. + public partial ref TimePoint GetLastOccupiedTime(); + + /// + /// Gets the last time the vehicle was spawned. + /// + /// The last spawn time of the vehicle. + public partial ref TimePoint GetLastSpawnTime(); + + /// + /// Checks if the vehicle is currently occupied. + /// + /// True if the vehicle is occupied; otherwise, false. + public partial bool IsOccupied(); + + /// + /// Sets the siren status of the vehicle. + /// + /// The siren status to set. + public partial void SetSiren(bool status); + + /// + /// Gets the siren state of the vehicle. + /// + /// The siren state of the vehicle. + public partial byte GetSirenState(); + + /// + /// Gets the hydra thrust angle of the vehicle. + /// + /// The hydra thrust angle of the vehicle. + public partial uint GetHydraThrustAngle(); + + /// + /// Gets the train speed of the vehicle. + /// + /// The train speed of the vehicle. + public partial float GetTrainSpeed(); + + /// + /// Gets the pool ID of the last driver of the vehicle. + /// + /// The pool ID of the last driver. + public partial int GetLastDriverPoolID(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicleEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicleEventHandler.cs new file mode 100644 index 00000000..21f4497f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehicleEventHandler.cs @@ -0,0 +1,119 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IVehicleEventHandler +{ + /// + /// Called when a vehicle is streamed in for a player. + /// + /// The vehicle being streamed in. + /// The player for whom the vehicle is streamed in. + void OnVehicleStreamIn(IVehicle vehicle, IPlayer player); + + /// + /// Called when a vehicle is streamed out for a player. + /// + /// The vehicle being streamed out. + /// The player for whom the vehicle is streamed out. + void OnVehicleStreamOut(IVehicle vehicle, IPlayer player); + + /// + /// Called when a vehicle is destroyed. + /// + /// The vehicle that was destroyed. + /// The player associated with the destruction, if any. + void OnVehicleDeath(IVehicle vehicle, IPlayer player); + + /// + /// Called when a player enters a vehicle. + /// + /// The player entering the vehicle. + /// The vehicle being entered. + /// if the player is entering as a passenger; otherwise, . + void OnPlayerEnterVehicle(IPlayer player, IVehicle vehicle, bool passenger); + + /// + /// Called when a player exits a vehicle. + /// + /// The player exiting the vehicle. + /// The vehicle being exited. + void OnPlayerExitVehicle(IPlayer player, IVehicle vehicle); + + /// + /// Called when a vehicle's damage status is updated. + /// + /// The vehicle whose damage status was updated. + /// The player associated with the update, if any. + void OnVehicleDamageStatusUpdate(IVehicle vehicle, IPlayer player); + + /// + /// Called when a player applies a paint job to a vehicle. + /// + /// The player applying the paint job. + /// The vehicle being painted. + /// The paint job ID being applied. + /// if the paint job is allowed; otherwise, . + bool OnVehiclePaintJob(IPlayer player, IVehicle vehicle, int paintJob); + + /// + /// Called when a player modifies a vehicle. + /// + /// The player modifying the vehicle. + /// The vehicle being modified. + /// The component ID being added. + /// if the modification is allowed; otherwise, . + bool OnVehicleMod(IPlayer player, IVehicle vehicle, int component); + + /// + /// Called when a player resprays a vehicle. + /// + /// The player respraying the vehicle. + /// The vehicle being resprayed. + /// The primary color being applied. + /// The secondary color being applied. + /// if the respray is allowed; otherwise, . + bool OnVehicleRespray(IPlayer player, IVehicle vehicle, int colour1, int colour2); + + /// + /// Called when a player enters or exits a mod shop. + /// + /// The player entering or exiting the mod shop. + /// if the player is entering; if exiting. + /// The interior ID of the mod shop. + void OnEnterExitModShop(IPlayer player, bool enterexit, int interiorId); + + /// + /// Called when a vehicle spawns. + /// + /// The vehicle that spawned. + void OnVehicleSpawn(IVehicle vehicle); + + /// + /// Called when an unoccupied vehicle is updated. + /// + /// The vehicle being updated. + /// The player associated with the update. + /// The update data for the vehicle. + /// if the update is allowed; otherwise, . + bool OnUnoccupiedVehicleUpdate(IVehicle vehicle, IPlayer player, UnoccupiedVehicleUpdate updateData); + + /// + /// Called when a trailer is updated. + /// + /// The player associated with the update. + /// The trailer being updated. + /// if the update is allowed; otherwise, . + bool OnTrailerUpdate(IPlayer player, IVehicle trailer); + + /// + /// Called when a vehicle's siren state changes. + /// + /// The player associated with the siren state change. + /// The vehicle whose siren state changed. + /// The new siren state. + /// if the siren state change is allowed; otherwise, . + bool OnVehicleSirenStateChange(IPlayer player, IVehicle vehicle, byte sirenState); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehiclesComponent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehiclesComponent.cs new file mode 100644 index 00000000..de2858dc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/IVehiclesComponent.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IPoolComponent))] +public readonly partial struct IVehiclesComponent +{ + /// + public static UID ComponentId => new(0x3f1f62ee9e22ab19); + + /// + /// Retrieves the array of vehicle models. + /// + /// A reference to the array of vehicle models. + public partial ref VehicleModelsArray Models(); + + /// + /// Creates a new vehicle. + /// + /// if the vehicle is static; otherwise, . + /// The model ID of the vehicle. + /// The position where the vehicle will be created. + /// The Z angle (rotation) of the vehicle. Default is 0.0f. + /// The primary color of the vehicle. Default is -1. + /// The secondary color of the vehicle. Default is -1. + /// The respawn delay of the vehicle. Use or a negative value to disable respawn. + /// if the vehicle should have a siren; otherwise, . Default is . + /// The created vehicle. + public partial IVehicle Create(bool isStatic, int modelID, Vector3 position, float Z = 0.0f, int colour1 = -1, int colour2 = -1, [MarshalUsing(typeof(SecondsMarshaller))] TimeSpan respawnDelay = default, bool addSiren = false); + + /// + /// Gets the event dispatcher for vehicle events. + /// + /// The event dispatcher for vehicle events. + public partial IEventDispatcher GetEventDispatcher(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/UnoccupiedVehicleUpdate.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/UnoccupiedVehicleUpdate.cs new file mode 100644 index 00000000..71724cfb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/UnoccupiedVehicleUpdate.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents an update for an unoccupied vehicle. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct UnoccupiedVehicleUpdate +{ + /// + /// Gets the seat ID in the vehicle. + /// + public readonly byte seat; + + /// + /// Gets the position of the vehicle. + /// + public readonly Vector3 position; + + /// + /// Gets the velocity of the vehicle. + /// + public readonly Vector3 velocity; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleComponentSlot.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleComponentSlot.cs new file mode 100644 index 00000000..3c661c17 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleComponentSlot.cs @@ -0,0 +1,92 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the slots available for vehicle components. +/// +public enum VehicleComponentSlot +{ + /// + /// No component slot. + /// + None = -1, + + /// + /// Slot for a spoiler component. + /// + Spoiler = 0, + + /// + /// Slot for a hood component. + /// + Hood = 1, + + /// + /// Slot for a roof component. + /// + Roof = 2, + + /// + /// Slot for a side skirt component. + /// + SideSkirt = 3, + + /// + /// Slot for lamps. + /// + Lamps = 4, + + /// + /// Slot for nitro. + /// + Nitro = 5, + + /// + /// Slot for an exhaust component. + /// + Exhaust = 6, + + /// + /// Slot for wheels. + /// + Wheels = 7, + + /// + /// Slot for a stereo component. + /// + Stereo = 8, + + /// + /// Slot for hydraulics. + /// + Hydraulics = 9, + + /// + /// Slot for a front bumper component. + /// + FrontBumper = 10, + + /// + /// Slot for a rear bumper component. + /// + RearBumper = 11, + + /// + /// Slot for a right vent component. + /// + VentRight = 12, + + /// + /// Slot for a left vent component. + /// + VentLeft = 13, + + /// + /// Slot for a front bullbar component. + /// + FrontBullbar = 14, + + /// + /// Slot for a rear bullbar component. + /// + RearBullbar = 15 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleData.cs new file mode 100644 index 00000000..50a17dfb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleData.cs @@ -0,0 +1,68 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +#pragma warning disable CA1401 + +/// +/// Provides utility functions and data for vehicle operations. +/// +public static class VehicleData +{ + /// + /// Checks if a component is valid for a specific vehicle model. + /// + /// The vehicle model ID. + /// The component ID to check. + /// if the component is valid for the vehicle model; otherwise, . + [DllImport("SampSharp", EntryPoint = "vehicles_isValidComponentForVehicleModel")] + public static extern bool IsValidComponentForVehicleModel(int vehicleModel, int componentId); + + /// + /// Gets the slot of a specific vehicle component. + /// + /// The component ID. + /// The slot ID of the component. + [DllImport("SampSharp", EntryPoint = "vehicles_getVehicleComponentSlot")] + public static extern int GetVehicleComponentSlot(int component); + + /// + /// Retrieves information about a vehicle model. + /// + /// The vehicle model ID. + /// The type of information to retrieve. + /// The output parameter to store the retrieved information. + /// if the information was successfully retrieved; otherwise, . + [DllImport("SampSharp", EntryPoint = "vehicles_getVehicleModelInfo")] + public static extern bool GetVehicleModelInfo(int model, SampSharp.OpenMp.Core.Api.VehicleModelInfoType type, out Vector3 outInfo); + + /// + /// Gets random colors for a vehicle. + /// + /// The vehicle model ID. + /// The output parameter for the first color. + /// The output parameter for the second color. + /// The output parameter for the third color. + /// The output parameter for the fourth color. + [DllImport("SampSharp", EntryPoint = "vehicles_getRandomVehicleColour")] + public static extern void GetRandomVehicleColour(int modelId, out int colour1, out int colour2, out int colour3, out int colour4); + + /// + /// Converts a car color index to a color value. + /// + /// The color index. + /// The alpha value of the color. Default is 0xFF. + /// The color value corresponding to the index. + [DllImport("SampSharp", EntryPoint = "vehicles_carColourIndexToColour")] + public static extern Colour CarColourIndexToColour(int index, uint alpha = 0xFF); + + /// + /// Gets the number of passenger seats for a vehicle model. + /// + /// The vehicle model ID. + /// The number of passenger seats in the vehicle model. + [DllImport("SampSharp", EntryPoint = "vehicles_getVehiclePassengerSeats")] + public static extern byte GetVehiclePassengerSeats(int model); +} +#pragma warning restore CA1401 \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleDriverSyncPacket.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleDriverSyncPacket.cs new file mode 100644 index 00000000..00e98770 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleDriverSyncPacket.cs @@ -0,0 +1,112 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents synchronization data for a vehicle driver. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleDriverSyncPacket +{ + /// + /// Gets the ID of the player driving the vehicle. + /// + public readonly int PlayerID; + + /// + /// Gets the ID of the vehicle being driven. + /// + public readonly ushort VehicleID; + + /// + /// Gets the left-right movement of the vehicle. + /// + public readonly ushort LeftRight; + + /// + /// Gets the up-down movement of the vehicle. + /// + public readonly ushort UpDown; + + /// + /// Gets the keys pressed by the driver. + /// + public readonly ushort Keys; + + /// + /// Gets the rotation quaternion of the vehicle. + /// + public readonly GTAQuat Rotation; + + /// + /// Gets the position of the vehicle. + /// + public readonly Vector3 Position; + + /// + /// Gets the velocity of the vehicle. + /// + public readonly Vector3 Velocity; + + /// + /// Gets the health of the vehicle. + /// + public readonly float Health; + + /// + /// Gets the health and armor of the driver. + /// + public readonly Vector2 PlayerHealthArmour; + + /// + /// Gets the siren state of the vehicle. + /// + public readonly byte Siren; + + /// + /// Gets the landing gear state of the vehicle. + /// + public readonly byte LandingGear; + + /// + /// Gets the ID of the trailer attached to the vehicle. + /// + public readonly ushort TrailerID; + + /// + /// Gets a value indicating whether the vehicle has a trailer attached. + /// + public readonly BlittableBoolean HasTrailer; + + /// + /// Gets the combined data for additional key and weapon. + /// + public readonly byte AdditionalKeyWeapon; + + /// + /// Gets the thrust angle for Hydra vehicles. + /// + public readonly uint HydraThrustAngle; + + /// + /// Gets the weapon ID of the driver. + /// + public byte WeaponID => (byte)(AdditionalKeyWeapon & 0b111111); + + /// + /// Gets the additional key pressed by the driver. + /// + public byte AdditionalKey => (byte)(AdditionalKeyWeapon >> 6); + + /// + /// Gets the speed of the train if the vehicle is a train. + /// + public float TrainSpeed => GetTrainSpeed(); + + private unsafe float GetTrainSpeed() + { + var value = HydraThrustAngle; + return *(float*)&value; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfo.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfo.cs new file mode 100644 index 00000000..cc9551cc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfo.cs @@ -0,0 +1,56 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents detailed information about a vehicle model. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleModelInfo +{ + /// + /// Gets the size of the vehicle model. + /// + public readonly Vector3 Size; + + /// + /// Gets the position of the front seat. + /// + public readonly Vector3 FrontSeat; + + /// + /// Gets the position of the rear seat. + /// + public readonly Vector3 RearSeat; + + /// + /// Gets the position of the petrol cap. + /// + public readonly Vector3 PetrolCap; + + /// + /// Gets the position of the front wheel. + /// + public readonly Vector3 FrontWheel; + + /// + /// Gets the position of the rear wheel. + /// + public readonly Vector3 RearWheel; + + /// + /// Gets the position of the mid wheel. + /// + public readonly Vector3 MidWheel; + + /// + /// Gets the Z position of the front bumper. + /// + public readonly float FrontBumperZ; + + /// + /// Gets the Z position of the rear bumper. + /// + public readonly float RearBumperZ; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfoType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfoType.cs new file mode 100644 index 00000000..b830be71 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelInfoType.cs @@ -0,0 +1,52 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of information available for a vehicle model. +/// +public enum VehicleModelInfoType +{ + /// + /// Represents the size of the vehicle model. + /// + Size = 1, + + /// + /// Represents the position of the front seat. + /// + FrontSeat, + + /// + /// Represents the position of the rear seat. + /// + RearSeat, + + /// + /// Represents the position of the petrol cap. + /// + PetrolCap, + + /// + /// Represents the position of the front wheels. + /// + WheelsFront, + + /// + /// Represents the position of the rear wheels. + /// + WheelsRear, + + /// + /// Represents the position of the mid wheels. + /// + WheelsMid, + + /// + /// Represents the Z position of the front bumper. + /// + FrontBumperZ, + + /// + /// Represents the Z position of the rear bumper. + /// + RearBumperZ +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelsArray.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelsArray.cs new file mode 100644 index 00000000..671ea825 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleModelsArray.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents an array of vehicle models. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleModelsArray +{ + /// + /// Gets the array of vehicle model values. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = OpenMpConstants.MAX_VEHICLE_MODELS)] + public readonly byte[] Values; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleParams.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleParams.cs new file mode 100644 index 00000000..b151c9ab --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleParams.cs @@ -0,0 +1,146 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the parameters of a vehicle in the game. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly record struct VehicleParams +{ + /// + /// Gets the state of the vehicle's engine. + /// + public readonly sbyte engine = -1; + + /// + /// Gets the state of the vehicle's lights. + /// + public readonly sbyte lights = -1; + + /// + /// Gets the state of the vehicle's alarm. + /// + public readonly sbyte alarm = -1; + + /// + /// Gets the state of the vehicle's doors. + /// + public readonly sbyte doors = -1; + + /// + /// Gets the state of the vehicle's bonnet. + /// + public readonly sbyte bonnet = -1; + + /// + /// Gets the state of the vehicle's boot. + /// + public readonly sbyte boot = -1; + + /// + /// Gets the state of the vehicle's objective. + /// + public readonly sbyte objective = -1; + + /// + /// Gets the state of the vehicle's siren. + /// + public readonly sbyte siren = -1; + + /// + /// Gets the state of the driver's door. + /// + public readonly sbyte doorDriver = -1; + + /// + /// Gets the state of the passenger's door. + /// + public readonly sbyte doorPassenger = -1; + + /// + /// Gets the state of the back-left door. + /// + public readonly sbyte doorBackLeft = -1; + + /// + /// Gets the state of the back-right door. + /// + public readonly sbyte doorBackRight = -1; + + /// + /// Gets the state of the driver's window. + /// + public readonly sbyte windowDriver = -1; + + /// + /// Gets the state of the passenger's window. + /// + public readonly sbyte windowPassenger = -1; + + /// + /// Gets the state of the back-left window. + /// + public readonly sbyte windowBackLeft = -1; + + /// + /// Gets the state of the back-right window. + /// + public readonly sbyte windowBackRight = -1; + + /// + /// Initializes a new instance of the struct. + /// + /// The state of the engine. + /// The state of the lights. + /// The state of the alarm. + /// The state of the doors. + /// The state of the bonnet. + /// The state of the boot. + /// The state of the objective. + /// The state of the siren. + /// The state of the driver's door. + /// The state of the passenger's door. + /// The state of the back-left door. + /// The state of the back-right door. + /// The state of the driver's window. + /// The state of the passenger's window. + /// The state of the back-left window. + /// The state of the back-right window. + public VehicleParams(sbyte engine, sbyte lights, sbyte alarm, sbyte doors, sbyte bonnet, sbyte boot, sbyte objective, sbyte siren, sbyte doorDriver, sbyte doorPassenger, sbyte doorBackLeft, sbyte doorBackRight, sbyte windowDriver, sbyte windowPassenger, sbyte windowBackLeft, sbyte windowBackRight) + { + this.engine = engine; + this.lights = lights; + this.alarm = alarm; + this.doors = doors; + this.bonnet = bonnet; + this.boot = boot; + this.objective = objective; + this.siren = siren; + this.doorDriver = doorDriver; + this.doorPassenger = doorPassenger; + this.doorBackLeft = doorBackLeft; + this.doorBackRight = doorBackRight; + this.windowDriver = windowDriver; + this.windowPassenger = windowPassenger; + this.windowBackLeft = windowBackLeft; + this.windowBackRight = windowBackRight; + } + + /// + /// Initializes a new instance of the struct with default values. + /// + public VehicleParams() + { + } + + /// + /// Determines whether any of the vehicle parameters are set. + /// + /// true if any parameter is set; otherwise, false. + public bool IsSet() + { + return engine != -1 || lights != -1 || alarm != -1 || doors != -1 || bonnet != -1 || boot != -1 || objective != -1 || siren != -1 || doorDriver != -1 || + doorPassenger != -1 || doorBackLeft != -1 || doorBackRight != -1 || windowDriver != -1 || windowPassenger != -1 || windowBackLeft != -1 || windowBackRight != -1; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehiclePassengerSyncPacket.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehiclePassengerSyncPacket.cs new file mode 100644 index 00000000..dd31af91 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehiclePassengerSyncPacket.cs @@ -0,0 +1,76 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents synchronization data for a vehicle passenger. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehiclePassengerSyncPacket +{ + /// + /// Gets the ID of the player. + /// + public readonly int PlayerID; + + /// + /// Gets the ID of the vehicle. + /// + public readonly int VehicleID; + + /// + /// Gets the combined data for seat, drive-by, cuffed state, weapon, and additional key. + /// + public readonly ushort DriveBySeatAdditionalKeyWeapon; + + /// + /// Gets the seat ID of the passenger. + /// + public byte SeatId => (byte)(DriveBySeatAdditionalKeyWeapon & 0xb111111); + + /// + /// Gets a value indicating whether the passenger is in drive-by mode. + /// + public bool DriveBy => (DriveBySeatAdditionalKeyWeapon & 0b1000000) != 0; + + /// + /// Gets a value indicating whether the passenger is cuffed. + /// + public bool Cuffed => (DriveBySeatAdditionalKeyWeapon & 0b10000000) != 0; + + /// + /// Gets the weapon ID of the passenger. + /// + public byte WeaponId => (byte)((DriveBySeatAdditionalKeyWeapon >> 8) & 0b111111); + + /// + /// Gets the additional key pressed by the passenger. + /// + public byte AdditionalKey => (byte)(DriveBySeatAdditionalKeyWeapon >> 14); + + /// + /// Gets the keys pressed by the passenger. + /// + public readonly ushort Keys; + + /// + /// Gets the health and armor of the passenger. + /// + public readonly Vector2 HealthArmour; + + /// + /// Gets the left-right movement of the passenger. + /// + public readonly ushort LeftRight; + + /// + /// Gets the up-down movement of the passenger. + /// + public readonly ushort UpDown; + + /// + /// Gets the position of the passenger. + /// + public readonly Vector3 Position; +}; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSCMEvent.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSCMEvent.cs new file mode 100644 index 00000000..c6f6bb24 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSCMEvent.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents events related to vehicle SCM (Scriptable Content Management). +/// +public enum VehicleSCMEvent : uint +{ + /// + /// Event for setting a paint job on a vehicle. + /// + VehicleSCMEvent_SetPaintjob = 1, + + /// + /// Event for adding a component to a vehicle. + /// + VehicleSCMEvent_AddComponent, + + /// + /// Event for setting the color of a vehicle. + /// + VehicleSCMEvent_SetColour, + + /// + /// Event for entering or exiting a mod shop. + /// + VehicleSCMEvent_EnterExitModShop +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSpawnData.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSpawnData.cs new file mode 100644 index 00000000..e67afc4a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleSpawnData.cs @@ -0,0 +1,52 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the data required to spawn a vehicle. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleSpawnData +{ + /// + /// Gets the respawn delay for the vehicle. + /// + public readonly Seconds respawnDelay; + + /// + /// Gets the model ID of the vehicle. + /// + public readonly int modelID; + + /// + /// Gets the position where the vehicle will spawn. + /// + public readonly Vector3 position; + + /// + /// Gets the Z rotation of the vehicle. + /// + public readonly float zRotation; + + /// + /// Gets the primary color of the vehicle. + /// + public readonly int colour1; + + /// + /// Gets the secondary color of the vehicle. + /// + public readonly int colour2; + + /// + /// Gets a value indicating whether the vehicle has a siren. + /// + public readonly BlittableBoolean siren; + + /// + /// Gets the interior ID of the vehicle. + /// + public readonly int interior; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleTrailerSyncPacket.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleTrailerSyncPacket.cs new file mode 100644 index 00000000..a49232d7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleTrailerSyncPacket.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents synchronization data for a vehicle trailer. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleTrailerSyncPacket +{ + /// + /// Gets the ID of the vehicle. + /// + public readonly int VehicleID; + + /// + /// Gets the ID of the player associated with the vehicle. + /// + public readonly int PlayerID; + + /// + /// Gets the position of the trailer. + /// + public readonly Vector3 Position; + + /// + /// Gets the quaternion representing the trailer's rotation. + /// + public readonly Vector4 Quat; + + /// + /// Gets the velocity of the trailer. + /// + public readonly Vector3 Velocity; + + /// + /// Gets the turn velocity of the trailer. + /// + public readonly Vector3 TurnVelocity; +}; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleUnoccupiedSyncPacket.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleUnoccupiedSyncPacket.cs new file mode 100644 index 00000000..86875ac0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleUnoccupiedSyncPacket.cs @@ -0,0 +1,56 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents synchronization data for an unoccupied vehicle. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct VehicleUnoccupiedSyncPacket +{ + /// + /// Gets the ID of the vehicle. + /// + public readonly int VehicleID; + + /// + /// Gets the ID of the player associated with the vehicle. + /// + public readonly int PlayerID; + + /// + /// Gets the seat ID in the vehicle. + /// + public readonly byte SeatID; + + /// + /// Gets the roll vector of the vehicle. + /// + public readonly Vector3 Roll; + + /// + /// Gets the rotation vector of the vehicle. + /// + public readonly Vector3 Rotation; + + /// + /// Gets the position of the vehicle. + /// + public readonly Vector3 Position; + + /// + /// Gets the velocity of the vehicle. + /// + public readonly Vector3 Velocity; + + /// + /// Gets the angular velocity of the vehicle. + /// + public readonly Vector3 AngularVelocity; + + /// + /// Gets the health of the vehicle. + /// + public readonly float Health; +}; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleVelocitySetType.cs b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleVelocitySetType.cs new file mode 100644 index 00000000..c7a135ca --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Components/Vehicles/VehicleVelocitySetType.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Specifies the type of velocity to set for a vehicle. +/// +public enum VehicleVelocitySetType : byte +{ + /// + /// Represents normal velocity. + /// + Normal = 0, + + /// + /// Represents angular velocity. + /// + Angular +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/ConfigOptionType.cs b/src/SampSharp.OpenMp.Core/Api/Core/ConfigOptionType.cs new file mode 100644 index 00000000..67a0bb00 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/ConfigOptionType.cs @@ -0,0 +1,37 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the type of a configuration option. +/// +public enum ConfigOptionType +{ + /// + /// No type. + /// + None = -1, + + /// + /// An integer number. + /// + Int = 0, + + /// + /// A string. + /// + String = 1, + + /// + /// A floating point number. + /// + Float = 2, + + /// + /// A list of strings. + /// + Strings = 3, + + /// + /// A boolean value. + /// + Bool = 4 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/IConfig.cs b/src/SampSharp.OpenMp.Core/Api/Core/IConfig.cs new file mode 100644 index 00000000..75f3cf46 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/IConfig.cs @@ -0,0 +1,158 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible))] +public readonly partial struct IConfig +{ + /// + /// Get a variable as a string. + /// + /// The key of the variable. + /// The value of the variable or if not found. + // TODO: is it nullable? + public partial string? GetString(string key); + + /// + /// Get a variable as an integer. + /// + /// The key of the variable. + /// A pointer to the value. + public partial BlittableRef GetInt(string key); + + /// + /// Get a variable as a float. + /// + /// The key of the variable. + /// A pointer to the value. + public partial BlittableRef GetFloat(string key); + + [OpenMpApiFunction("getStrings")] + private partial Size GetStringsImpl(string key, SpanLite output); + + private partial Size GetStringsCount(string key); + + /// + /// Gets a list of strings. + /// + /// The key of the variable. + /// An array containing the string values. + public unsafe string?[] GetStrings(string key) + { + var count = GetStringsCount(key); + + if (count.Value == 0) + { + return []; + } + + var ptr = Marshal.AllocHGlobal(count.Value.ToInt32() * sizeof(StringView)); + + try + { + var output = new SpanLite((StringView*)ptr, count); + GetStringsImpl(key, output); + + var result = new string?[count.Value.ToInt32()]; + var index = 0; + foreach (var value in output.AsSpan()) + { + result[index++] = value; + } + + return result; + } + finally + { + Marshal.FreeHGlobal(ptr); + } + } + + /// + /// Gets the type of a variable. + /// + /// The key of the variable. + /// tHE type of the variable. + [OpenMpApiFunction("getType")] + public partial ConfigOptionType GetValueType(string key); + + /// + /// Gets the number of bans. + /// + /// The number of bans. + public partial Size GetBansCount(); + + /// + /// Gets the ban at the specified . + /// + /// The index of the ban. + /// The ban entry. + public partial BanEntry? GetBan(Size index); + + /// + /// Adds a ban. + /// + /// The ban entry to add. + public partial void AddBan(BanEntry entry); + + /// + /// Removes a ban. + /// + /// The index of the ban to remove. + [OpenMpApiOverload("_index")] + public partial void RemoveBan(Size index); + + /// + /// Removes a ban. + /// + /// The ban entry to remove. + public partial void RemoveBan(BanEntry entry); + + /// + /// Writes the bans to the file. + /// + public partial void WriteBans(); + + /// + /// Reloads the bans. + /// + public partial void ReloadBans(); + + /// + /// Clears all bans. + /// + public partial void ClearBans(); + + /// + /// Checks if a ban entry is banned. + /// + /// The ban entry to check. + /// if the entry is banned; otherwise, . + public partial bool IsBanned(BanEntry entry); + + private partial void GetNameFromAlias(string alias, out Pair result); + + /// + /// Get an option name from an alias if available. + /// + /// The alias to find. + /// A pair of bool which is true if the alias is deprecated and a string with the real config name. + public (bool, string?) GetNameFromAlias(string alias) + { + GetNameFromAlias(alias, out var pair); + return (pair.First, pair.Second); + } + + // TODO: public partial void enumOptions(OptionEnumeratorCallback& callback); // enumerator callback not available + + /// + /// Get a variable as a boolean. + /// + /// The key of the variable. + /// A pointer to the value. + public partial BlittableRef GetBool(string key); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/ICore.cs b/src/SampSharp.OpenMp.Core/Api/Core/ICore.cs new file mode 100644 index 00000000..0785cd6a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/ICore.cs @@ -0,0 +1,139 @@ +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(ILogger))] +public readonly partial struct ICore +{ + /// + /// Gets the version of the open.mp server. + /// + /// The version of the open.mp server package. + public partial SemanticVersion GetVersion(); + + /// + /// Get the version of the NetworkBitStream class the core was built with. + /// + /// The version of the NetworkBitStream class. + public partial int GetNetworkBitStreamVersion(); + + /// + /// Gets the player pool + /// + /// The player pool. + public partial IPlayerPool GetPlayers(); + + /// + /// Gets the core event dispatcher. + /// + /// The core event dispatcher. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Gets the server configuration. + /// + /// Yhe server configuration. + public partial IConfig GetConfig(); + + /// + /// Gets a list of available networks. + /// + /// A list of available networks. + public partial FlatPtrHashSet GetNetworks(); + + /// + /// Gets the tick count. + /// + /// The tick count. + public partial uint GetTickCount(); + + /// + /// Sets the server gravity. + /// + /// The server gravity. + public partial void SetGravity(float gravity); + + /// + /// Gets the server gravity. + /// + /// The server gravity. + public partial float GetGravity(); + + /// + /// Sets the server weather. + /// + /// The server weather. + public partial void SetWeather(int weather); + + /// + /// Sets the server world time. + /// + /// The world time, truncated to whole hours on the native side. + public partial void SetWorldTime([MarshalUsing(typeof(HoursMarshaller))] TimeSpan time); + + /// + /// Toggles server stunt bonuses. + /// + /// if stunt bonuses should be enabled. + public partial void UseStuntBonuses(bool enable); + + /// + /// Sets string data during runtime. + /// + /// The type of the data. + /// The data value. + public partial void SetData(SettableCoreDataType type, string data); + + /// + /// Sets the sleep value for each main thread update cycle. + /// + /// The sleep duration. + public partial void SetThreadSleep(Microseconds value); + + /// + /// Toggle dynamic ticks instead of static duration sleep. + /// + /// if dynamic ticks should be enabled. + public partial void UseDynTicks(bool enable); + + /// + /// Clear all entities that vanish on GM exit. + /// + public partial void ResetAll(); + + /// + /// Create all entities that appear on GM start. + /// + public partial void ReloadAll(); + + /// + /// Get the name of a weapon. + /// + /// The weapon. + /// The name of the weapon. + public partial string GetWeaponName(PlayerWeapon weapon); + + /// + /// Attempt to connect a new bot to the server. + /// + /// The bot name (player name). + /// The bot script to execute. + public partial void ConnectBot(string name, string script); + + /// + /// Gets the ticks per second. + /// + /// Ticks per second. + public partial uint TickRate(); + + /// + /// Gets the version hash of the open.mp server. + /// + /// The version hash. + public partial string GetVersionHash(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/ICoreEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Core/ICoreEventHandler.cs new file mode 100644 index 00000000..be47fb2f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/ICoreEventHandler.cs @@ -0,0 +1,17 @@ +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface ICoreEventHandler +{ + /// + /// Called when the server ticks. + /// + /// The number of microseconds since the last tick. + /// The current time. + void OnTick(Microseconds elapsed, TimePoint now); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/ILogger.cs b/src/SampSharp.OpenMp.Core/Api/Core/ILogger.cs new file mode 100644 index 00000000..e519afbb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/ILogger.cs @@ -0,0 +1,70 @@ +using System.Text; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly unsafe partial struct ILogger +{ + /// + /// Prints a new line to the console. + /// + /// The message to log. + public partial void PrintLn(byte* msg); + + /// + /// Prints a new line to the console with the specified severity level. + /// + /// The severity level at which to log the line. + /// The message to log. + public partial void LogLn(LogLevel level, byte* msg); + + /// + /// Prints a new line to the console in UTF-8 encoding. + /// + /// The message to log. + public partial void PrintLnU8(byte* msg); + /// + /// Prints a new line to the console of the specified log type in UTF-8 encoding. + /// + /// The severity level at which to log the line. + /// The message to log. + public partial void LogLnU8(LogLevel level, byte* msg); + + /// + /// Logs a new line to the console with the specified severity level. + /// + /// The severity level at which to log the line. + /// The message to log. + public void LogLine(LogLevel level, string msg) + { + ArgumentNullException.ThrowIfNull(msg); + + var arr = new byte[Encoding.UTF8.GetByteCount(msg) + 1]; + Encoding.UTF8.GetBytes(msg, arr); + + fixed (byte* msgPtr = arr) + { + LogLn(level, msgPtr); + } + } + + /// + /// Prints a new line to the console. + /// + /// The message to log. + public void PrintLine(string msg) + { + ArgumentNullException.ThrowIfNull(msg); + + var arr = new byte[Encoding.UTF8.GetByteCount(msg) + 1]; + Encoding.UTF8.GetBytes(msg, arr); + + fixed (byte* msgPtr = arr) + { + PrintLn(msgPtr); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/LogLevel.cs b/src/SampSharp.OpenMp.Core/Api/Core/LogLevel.cs new file mode 100644 index 00000000..dd3931da --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/LogLevel.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the severity level of a log message. +/// +public enum LogLevel +{ + /// + /// A debug message. + /// + Debug, + + /// + /// An informational message. + /// + Message, + + /// + /// A warning message. + /// + Warning, + + /// + /// An error message. + /// + Error +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Core/SettableCoreDataType.cs b/src/SampSharp.OpenMp.Core/Api/Core/SettableCoreDataType.cs new file mode 100644 index 00000000..241dde21 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Core/SettableCoreDataType.cs @@ -0,0 +1,42 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Types of data can be set in core during runtime. +/// +public enum SettableCoreDataType +{ + /// + /// The server name. + /// + ServerName, + + /// + /// The server mode text. + /// + ModeText, + + /// + /// The active map name. + /// + MapName, + + /// + /// The language of the server. + /// + Language, + + /// + /// The website URL of the server. + /// + URL, + + /// + /// The password users must enter to join the server. + /// + Password, + + /// + /// The password users must enter to log in as an admin. + /// + AdminPassword +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Entity/IEntity.cs b/src/SampSharp.OpenMp.Core/Api/Entity/IEntity.cs new file mode 100644 index 00000000..315bc1fb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Entity/IEntity.cs @@ -0,0 +1,46 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IIDProvider))] +public readonly partial struct IEntity +{ + /// + /// Gets the position of this entity. + /// + /// The position of this entity. + public partial Vector3 GetPosition(); + + /// + /// Sets the position of this entity. + /// + /// The position to set. + public partial void SetPosition(Vector3 position); + + /// + /// Gets the rotation of this entity. + /// + /// The rotation of this entity. + public partial GTAQuat GetRotation(); + + /// + /// Sets the rotation of this entity. + /// + /// The rotation to set. + public partial void SetRotation(GTAQuat rotation); + + /// + /// Gets the virtual world of this entity. + /// + /// The virtual world of this entity. + public partial int GetVirtualWorld(); + + /// + /// Sets the virtual world of this entity. + /// + /// The virtual world to set. + public partial void SetVirtualWorld(int vw); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Entity/IIDProvider.cs b/src/SampSharp.OpenMp.Core/Api/Entity/IIDProvider.cs new file mode 100644 index 00000000..72ba3494 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Entity/IIDProvider.cs @@ -0,0 +1,14 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct IIDProvider +{ + /// + /// Gets the identifier of this unit. + /// + /// The identifier of the unit. + public partial int GetID(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/EventDispatcherInterop.cs b/src/SampSharp.OpenMp.Core/Api/Events/EventDispatcherInterop.cs new file mode 100644 index 00000000..853a20c6 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/EventDispatcherInterop.cs @@ -0,0 +1,26 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +internal partial class EventDispatcherInterop +{ + [LibraryImport("SampSharp", EntryPoint = "IEventDispatcher_addEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool AddEventHandler(nint dispatcherHandle, nint handlerHandle, EventPriority priority); + + [LibraryImport("SampSharp", EntryPoint = "IEventDispatcher_removeEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool RemoveEventHandler(nint dispatcherHandle, nint handlerHandle); + + [LibraryImport("SampSharp", EntryPoint = "IEventDispatcher_hasEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool HasEventHandler(nint dispatcherHandle, nint handlerHandle, out EventPriority priority); + + [LibraryImport("SampSharp", EntryPoint = "IEventDispatcher_count")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial Size Count(nint dispatcherHandle); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerMarshaller.cs b/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerMarshaller.cs new file mode 100644 index 00000000..02ea9c0d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerMarshaller.cs @@ -0,0 +1,78 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a base class for event handler marshallers. This type is automatically generated by code generated for +/// event handlers decorated with the . +/// +/// The type of the event handler. +public abstract class EventHandlerMarshaller : IEventHandlerMarshaller where TEventHandler : class +{ + private readonly Dictionary _handlers = []; + + /// + public EventHandlerReference Marshal(TEventHandler handler) + { + return new EventHandlerReference(this, handler); + } + + internal nint IncreaseReferenceCount(TEventHandler handler) + { + if (_handlers.TryGetValue(handler, out var reference)) + { + _handlers[handler] = reference with + { + RefCount = reference.RefCount + 1 + }; + + return reference.Handle; + } + + var (newHandle, data) = Create(handler); + + _handlers[handler] = new HandlerData(newHandle, 1, data); + + return newHandle; + } + + internal void DecreaseReferenceCount(TEventHandler handler) + { + if (!_handlers.TryGetValue(handler, out var reference)) + { + return; + } + + if (reference.RefCount == 1) + { + _handlers.Remove(handler); + Free(reference.Handle); + } + + _handlers[handler] = reference with + { + RefCount = reference.RefCount - 1 + }; + } + + internal nint? GetReference(TEventHandler handler) + { + return _handlers.TryGetValue(handler, out var reference) + ? reference.Handle + : null; + } + + /// + /// Creates the unmanaged counterpart of the specified event . + /// + /// The event handler for which to create the unmanaged counterpart. + /// An unmanaged handle and an object of luggage which will be kept so GC won't clean it up. + protected abstract (nint, object) Create(TEventHandler handler); + + /// + /// Frees the resources associated with the specified unmanaged . + /// + /// The unmanaged handle to free. + protected abstract void Free(nint handle); + + private readonly record struct HandlerData(nint Handle, int RefCount, object Luggage); + +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerReference.cs b/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerReference.cs new file mode 100644 index 00000000..d9c62a38 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/EventHandlerReference.cs @@ -0,0 +1,45 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a reference to a . +/// +/// The interface type of the event handler. +public readonly struct EventHandlerReference where TEventHandler : class +{ + private readonly EventHandlerMarshaller _marshaller; + private readonly TEventHandler _handler; + + internal EventHandlerReference(EventHandlerMarshaller marshaller, TEventHandler handler) + { + _marshaller = marshaller; + _handler = handler; + } + + /// + /// Gets the unmanaged handle of the event handler. Returns if this event handler reference has + /// not been created or has been freed. + /// + public nint? Handle => _marshaller?.GetReference(_handler); + + /// + /// Creates a new reference to the native event handler or increases its reference count. + /// + /// The created reference to the native event handler. + public nint Create() + { + return _marshaller?.IncreaseReferenceCount(_handler) ?? throw new InvalidOperationException("Invalid state"); + } + + /// + /// Decreases the reference count of the native event handler or frees its resources. + /// + public void Free() + { + if (_marshaller == null) + { + throw new InvalidOperationException("Invalid state"); + } + + _marshaller.DecreaseReferenceCount(_handler); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/EventPriority.cs b/src/SampSharp.OpenMp.Core/Api/Events/EventPriority.cs new file mode 100644 index 00000000..1ac59987 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/EventPriority.cs @@ -0,0 +1,32 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the priority of receiving events in an . +/// +public enum EventPriority : sbyte +{ + /// + /// The highest priority. For handlers that must receive events first. + /// + Highest = sbyte.MinValue, + + /// + /// A fairly high priority. For handlers that must receive events before most other handlers. + /// + FairlyHigh = Highest / 2, + + /// + /// The default priority. For handlers that must receive events at the default priority. + /// + Default = 0, + + /// + /// A fairly low priority. For handlers that must receive events after most other handlers. + /// + FairlyLow = Lowest / 2, + + /// + /// The lowest priority. For handlers that must receive events last. + /// + Lowest = sbyte.MaxValue +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/IEventDispatcher.cs b/src/SampSharp.OpenMp.Core/Api/Events/IEventDispatcher.cs new file mode 100644 index 00000000..f26030f3 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/IEventDispatcher.cs @@ -0,0 +1,87 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct IEventDispatcher : IUnmanagedInterface where T : class, IEventHandler +{ + private readonly nint _handle; + + /// + /// Initializes a new instance of the struct. + /// + /// The pointer handle. + public IEventDispatcher(nint handle) + { + _handle = handle; + } + + /// + public nint Handle => _handle; + + /// + public bool HasValue => Handle != 0; + + /// + /// Adds the specified to this event dispatcher. + /// + /// The event handler to add. + /// The priority at which the handler should receive the events. + /// if the handler was added; otherwise, . + public bool AddEventHandler(T handler, EventPriority priority = EventPriority.Default) + { + var handlerHandle = T.Marshaller.Marshal(handler).Create(); + + return EventDispatcherInterop.AddEventHandler(_handle, handlerHandle, priority); + } + + /// + /// Removes the specified from this event dispatcher. + /// + /// The event handler to remove. + /// if the event handler was removed; otherwise, . + public bool RemoveEventHandler(T handler) + { + var reference = T.Marshaller.Marshal(handler); + var handlerHandle = reference.Handle; + + if (!handlerHandle.HasValue) + { + return false; + } + + if (EventDispatcherInterop.RemoveEventHandler(_handle, handlerHandle.Value)) + { + reference.Free(); + return true; + } + + return false; + } + + /// + /// Returns a value indicating whether the specified is registered with this event dispatcher. + /// + /// The event handler to check + /// The priority at which the handler receives the events. + /// if the event handler was added; otherwise, . + public bool HasEventHandler(T handler, out EventPriority priority) + { + var handlerHandle = T.Marshaller.Marshal(handler).Handle; + priority = default; + + return handlerHandle.HasValue && EventDispatcherInterop.HasEventHandler(_handle, handlerHandle.Value, out priority); + } + + /// + /// Returns the number of event handlers registered with this event dispatcher. + /// + /// The number of event handlers registered. + public int Count() + { + return (int)EventDispatcherInterop.Count(_handle); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/IEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Events/IEventHandler.cs new file mode 100644 index 00000000..e2ee2236 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/IEventHandler.cs @@ -0,0 +1,14 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Base interface for event handlers. This interface is automatically implemented by the code generator for event +/// handlers which are marked with the . +/// +/// The type of the event handler interface. +public interface IEventHandler where TEventHandler : class +{ + /// + /// Gets the marshaller which can marshal the event handler to its unmanaged representation. + /// + static abstract IEventHandlerMarshaller Marshaller { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/IEventHandlerMarshaller.cs b/src/SampSharp.OpenMp.Core/Api/Events/IEventHandlerMarshaller.cs new file mode 100644 index 00000000..37884010 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/IEventHandlerMarshaller.cs @@ -0,0 +1,15 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides methods for marshalling a managed event handler to a +/// +/// The type of the event handler interface. +public interface IEventHandlerMarshaller where TEventHandler : class +{ + /// + /// Marshals the specified managed event handler to a . + /// + /// The managed event handler to marshal. + /// The unmanaged event handler. + EventHandlerReference Marshal(TEventHandler handler); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/IIndexedEventDispatcher.cs b/src/SampSharp.OpenMp.Core/Api/Events/IIndexedEventDispatcher.cs new file mode 100644 index 00000000..42435377 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/IIndexedEventDispatcher.cs @@ -0,0 +1,93 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct IIndexedEventDispatcher : IUnmanagedInterface where T : class, IEventHandler +{ + private readonly nint _handle; + + /// + public nint Handle => _handle; + + /// + public bool HasValue => Handle != 0; + + /// + /// Gets the number of event handlers registered. + /// + /// The number of registered event handlers. + public int Count() + { + return (int)IndexedEventDispatcherInterop.Count(_handle); + } + + /// + /// Gets the number of event handlers registered for the specified index. + /// + /// The index to get the number of event handlers for. + /// The number of registered event handlers. + public int Count(int index) + { + return (int)IndexedEventDispatcherInterop.Count(_handle, index); + } + + /// + /// Adds the specified to this indexed event dispatcher. + /// + /// The event handler to add. + /// The index for which to add the handler. + /// The priority at which the handler should receive the events. + /// if the event handler was added; otherwise, . + public bool AddEventHandler(T handler, int index, EventPriority priority = EventPriority.Default) + { + var handlerHandle = T.Marshaller.Marshal(handler).Create(); + + return IndexedEventDispatcherInterop.AddEventHandler(_handle, handlerHandle, index, priority); + } + + /// + /// Removes the specified from this indexed event dispatcher. + /// + /// The event handler to remove. + /// The index from which to remove the handler. + /// if the event handler was removed; otherwise, . + public bool RemoveEventHandler(T handler, int index) + { + var reference = T.Marshaller.Marshal(handler); + var handlerHandle = reference.Handle; + + if (!handlerHandle.HasValue) + { + return false; + } + + if (IndexedEventDispatcherInterop.RemoveEventHandler(_handle, handlerHandle.Value, index)) + { + reference.Free(); + return true; + } + + return false; + } + + /// + /// Returns a value indicating whether the specified is registered at the specified + /// . + /// + /// The handler to check. + /// The index to check + /// The priority at which the handler should receive the events. + /// if handler is registered with this event dispatcher at the given index; + /// otherwise . + public bool HasEventHandler(T handler, int index, out EventPriority priority) + { + var handlerHandle = T.Marshaller.Marshal(handler).Handle; + priority = default; + + return handlerHandle.HasValue && IndexedEventDispatcherInterop.HasEventHandler(_handle, handlerHandle.Value, index, out priority); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Events/IndexedEventDispatcherInterop.cs b/src/SampSharp.OpenMp.Core/Api/Events/IndexedEventDispatcherInterop.cs new file mode 100644 index 00000000..f2ea65d7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Events/IndexedEventDispatcherInterop.cs @@ -0,0 +1,31 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +internal partial class IndexedEventDispatcherInterop +{ + [LibraryImport("SampSharp", EntryPoint = "IIndexedEventDispatcher_count")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial Size Count(nint dispatcherHandle); + + [LibraryImport("SampSharp", EntryPoint = "IIndexedEventDispatcher_count_index")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + public static partial Size Count(nint dispatcherHandle, Size index); + + [LibraryImport("SampSharp", EntryPoint = "IIndexedEventDispatcher_addEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool AddEventHandler(nint dispatcherHandle, nint handlerHandle, Size index, EventPriority priority); + + [LibraryImport("SampSharp", EntryPoint = "IIndexedEventDispatcher_removeEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool RemoveEventHandler(nint dispatcherHandle, nint handlerHandle, Size index); + + [LibraryImport("SampSharp", EntryPoint = "IIndexedEventDispatcher_hasEventHandler")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + [return: MarshalAs(UnmanagedType.Bool)] + public static partial bool HasEventHandler(nint dispatcherHandle, nint handlerHandle, Size index, out EventPriority priority); + +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/GTAQuat.cs b/src/SampSharp.OpenMp.Core/Api/GTAQuat.cs new file mode 100644 index 00000000..6ec7163e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/GTAQuat.cs @@ -0,0 +1,67 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a quaternion in the GTA coordinate space. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct GTAQuat +{ + /// + /// The W component of this quaternion. + /// + public readonly float W; + + /// + /// The X component of this quaternion. + /// + public readonly float X; + + /// + /// The Y component of this quaternion. + /// + public readonly float Y; + + /// + /// The Z component of this quaternion. + /// + public readonly float Z; + + /// + /// Initializes a new instance of the struct. + /// + /// The X component of the quaternion. + /// The Y component of the quaternion. + /// The Z component of the quaternion. + /// The W component of the quaternion. + public GTAQuat(float x, float y, float z, float w) + { + W = w; + X = x; + Y = y; + Z = z; + } + + /// + /// Converts a to a . + /// + /// The value to convert + public static implicit operator Quaternion(GTAQuat gtaQuat) + { + // GTA quaternions are fubar, correct the components in our coordinate space. + return new Quaternion(-gtaQuat.X, -gtaQuat.Y, -gtaQuat.Z, gtaQuat.W); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator GTAQuat(Quaternion quat) + { + // GTA quaternions are fubar, correct the components in our coordinate space. + return new GTAQuat(-quat.X, -quat.Y, -quat.Z, quat.W); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/HybridString.cs b/src/SampSharp.OpenMp.Core/Api/HybridString.cs new file mode 100644 index 00000000..13e2a02a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/HybridString.cs @@ -0,0 +1,116 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a string that can be either stack-allocated or heap-allocated. +/// +[NumberedTypeGenerator(nameof(Size), 24)] +[NumberedTypeGenerator(nameof(Size), 25)] +[NumberedTypeGenerator(nameof(Size), 32)] +[NumberedTypeGenerator(nameof(Size), 46)] +[StructLayout(LayoutKind.Explicit)] +[DebuggerDisplay("{DebuggerDisplay(),nq}")] +public readonly struct HybridString16 +{ + private const int Size = 16; + + // First bit is 1 if dynamic and 0 if static; the rest are the length + [FieldOffset(0)] private readonly Size _lenDynamic; + + [FieldOffset(Std.Size.Length), MarshalAs(UnmanagedType.ByValArray, SizeConst = Size)] + private readonly byte[]? _static; + + /// + /// Initializes a new instance of the struct. + /// + /// The value to set. + /// Throw if heap allocation is required; this has not been implemented. + public HybridString16(string? inp) + { + if (inp == null) + { + _static = new byte[Size]; + } + else + { + var requiredSize = Encoding.GetByteCount(inp); + if (requiredSize < Size) // last byte is for null terminator + { + _static = new byte[Size]; + Encoding.GetBytes(inp, 0, inp.Length, _static, 0); + + _lenDynamic = new Size(new nint((long)inp.Length << 1)); + } + else + { + throw new NotImplementedException("dynamic string size not implemented"); + } + } + } + + /// + /// Copies the value of this to a destination . + /// + /// The destination span. + public void CopyTo(Span dest) + { + MemoryMarshal.Cast(dest)[0] = Length; + AsSpan().CopyTo(dest[Std.Size.Length..]); + } + + /// + /// Gets a value indicating whether the string is dynamic (heap allocated). + /// + public bool IsDynamic => (_lenDynamic.Value.ToInt64() & 1) != 0; + + /// + /// Gets the length of the string. + /// + public int Length => (int)(_lenDynamic.Value.ToInt64() >> 1); + + /// + /// Gets a span representing this string. + /// + /// The span representing this string. + public unsafe Span AsSpan() + { + return IsDynamic + ? new Span(GetDynamicStorage().Data, Length) + : new Span(_static, 0, Length); + } + + private unsafe HybridStringDynamicStorage GetDynamicStorage() + { + fixed (byte* ptr = _static) + { + return *(HybridStringDynamicStorage*)ptr; + } + } + + /// + public override string ToString() + { + return Encoding.GetString(AsSpan()); + } + + internal string DebuggerDisplay() + { + string value; + + try + { + value = ToString(); + } + catch + { + value = ""; + } + return $"HybridString<{Size}> {{Length = {Length}, IsDynamic = {IsDynamic}, Value = {value}}}"; + } + + private static Encoding Encoding => Encoding.UTF8; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/HybridStringDynamicStorage.cs b/src/SampSharp.OpenMp.Core/Api/HybridStringDynamicStorage.cs new file mode 100644 index 00000000..00212a09 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/HybridStringDynamicStorage.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +internal readonly unsafe struct HybridStringDynamicStorage +{ + public readonly byte* Data; + public readonly delegate* unmanaged[Cdecl] FreePointer; + + private HybridStringDynamicStorage(byte* data, delegate* unmanaged[Cdecl] freePointer) + { + Data = data; + FreePointer = freePointer; + } + + public static HybridStringDynamicStorage Allocate(int length) + { + var data = (byte*)Marshal.AllocHGlobal(length); + return new HybridStringDynamicStorage(data, &Free); + } + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + internal static void Free(nint data) + { + Marshal.FreeHGlobal(data); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Network/BanEntry.cs b/src/SampSharp.OpenMp.Core/Api/Network/BanEntry.cs new file mode 100644 index 00000000..33576216 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/BanEntry.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents an entry in the ban list. +/// +/// The IP address of the banned player. +/// The time when the ban was issued. +/// The name of the banned player, if available. +/// The reason for the ban, if provided. +[NativeMarshalling(typeof(BanEntryMarshaller))] +public record BanEntry(string Address, DateTimeOffset Time, string? Name, string? Reason) +{ + /// + /// Initializes a new instance of the class with only an address. + /// The ban time is set to the current UTC time, and the name and reason are left null. + /// + /// The IP address of the banned player. + public BanEntry(string address) : this(address, DateTimeOffset.UtcNow, null, null) + { + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/BanEntryMarshaller.cs b/src/SampSharp.OpenMp.Core/Api/Network/BanEntryMarshaller.cs new file mode 100644 index 00000000..e1ebc4dc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/BanEntryMarshaller.cs @@ -0,0 +1,72 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a marshaller entrypoint for marshalling to its unmanaged counterpart. +/// +[CustomMarshaller(typeof(BanEntry), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(BanEntry), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +public static unsafe class BanEntryMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static int BufferSize { get; } = Marshal.SizeOf(); + + public static BlittableStructRef ConvertToUnmanaged(BanEntry managed, Span callerAllocatedBuffer) + { + var native = ToNative(managed); + + + var ptr = (nint)Unsafe.AsPointer(ref callerAllocatedBuffer.GetPinnableReference()); + Marshal.StructureToPtr(native, ptr, false); + + return new BlittableStructRef(ptr); + } + + private static Native ToNative(BanEntry entry) + { + return new Native(new HybridString46(entry.Address), + TimePoint.FromDateTimeOffset(entry.Time), + new HybridString25(entry.Name), + new HybridString32(entry.Reason)); + } + } + + public static class NativeToManaged + { + public static BanEntry? ConvertToManaged(BlittableStructRef unmanaged) + { + if (!unmanaged.HasValue) + { + return null; + } + + var native = unmanaged.GetValueOrDefault(); + + return FromNative(native); + } + + private static BanEntry FromNative(Native native) + { + return new BanEntry(native.AddressString.ToString(), + native.Time.ToDateTimeOffset(), + native.Name.ToString(), + native.Reason.ToString()); + } + } + + [StructLayout(LayoutKind.Sequential)] + public readonly struct Native(HybridString46 addressString, TimePoint time, HybridString25 name, HybridString32 reason) + { + public readonly HybridString46 AddressString = addressString; + public readonly TimePoint Time = time; + public readonly HybridString25 Name = name; // MAX_PLAYER_NAME + 1 + public readonly HybridString32 Reason = reason; + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Network/ClientVersion.cs b/src/SampSharp.OpenMp.Core/Api/Network/ClientVersion.cs new file mode 100644 index 00000000..5c2d129d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/ClientVersion.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the different versions of the SA-MP client. +/// +public enum ClientVersion : byte +{ + /// + /// SA-MP 0.3.7 version. + /// + SAMP_037, + + /// + /// SA-MP 0.3.DL version. + /// + SAMP_03DL, + + /// + /// open.mp version. + /// + openmp +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/ENetworkType.cs b/src/SampSharp.OpenMp.Core/Api/Network/ENetworkType.cs new file mode 100644 index 00000000..6cf14a1b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/ENetworkType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the network type used by the application. +/// +public enum ENetworkType +{ + /// + /// The legacy RakNet network type. + /// + RakNetLegacy, + + /// + /// The ENet network type. + /// + ENet, + + /// + /// Marks the end of the network type enumeration. + /// + End +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetwork.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetwork.cs new file mode 100644 index 00000000..f9541da4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetwork.cs @@ -0,0 +1,140 @@ +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.Std; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible))] +public readonly partial struct INetwork +{ + /// + /// Gets the network type of the network. + /// + /// The network type of the network. + public partial ENetworkType GetNetworkType(); + + /// + /// Gets the dispatcher which dispatches network events. + /// + /// An event dispatcher for network events. + public partial IEventDispatcher GetEventDispatcher(); + + /// + /// Gets the dispatcher which dispatches incoming network events. + /// + /// An event dispatcher for incoming network events. + public partial IEventDispatcher GetInEventDispatcher(); + + /// + /// Gets the dispatcher which dispatches outgoing network events. + /// + /// An event dispatcher for outgoing network events. + public partial IEventDispatcher GetOutEventDispatcher(); + + /// + /// Sends a packet to a network peer. + /// + /// The network peer to send the packet to. + /// The data span with the length in bits. + /// The channel to use for sending the packet. + /// Whether to dispatch packet-related events. + /// true if the packet was sent successfully; otherwise, false. + public partial bool SendPacket(IPlayer peer, SpanLite data, int channel, bool dispatchEvents = true); + + /// + /// Broadcasts a packet to all peers on this network. + /// + /// The data span with the length in bits. + /// The channel to use for broadcasting the packet. + /// The peer to exclude from the broadcast. + /// Whether to dispatch packet-related events. + /// true if the packet was broadcast successfully; otherwise, false. + public partial bool BroadcastPacket(SpanLite data, int channel, IPlayer exceptPeer = default, bool dispatchEvents = true); + + /// + /// Sends an RPC to a network peer. + /// + /// The network peer to send the RPC to. + /// The RPC ID for the current network. + /// The data span with the length in bits. + /// The channel to use for sending the RPC. + /// Whether to dispatch RPC-related events. + /// true if the RPC was sent successfully; otherwise, false. + public partial bool SendRPC(IPlayer peer, int id, SpanLite data, int channel, bool dispatchEvents = true); + + /// + /// Broadcasts an RPC to all peers on this network. + /// + /// The RPC ID for the current network. + /// The data span with the length in bits. + /// The channel to use for broadcasting the RPC. + /// The peer to exclude from the broadcast. + /// Whether to dispatch RPC-related events. + /// true if the RPC was broadcast successfully; otherwise, false. + public partial bool BroadcastRPC(int id, SpanLite data, int channel, IPlayer exceptPeer = default, bool dispatchEvents = true); + + /// + /// Gets network statistics for a specific player or the entire network. + /// + /// The player to get statistics for, or default for the entire network. + /// The network statistics. + public partial NetworkStats GetStatistics(IPlayer player = default); + + /// + /// Gets the last ping for a peer on this network. + /// + /// The network peer to get the ping for. + /// The last ping value, or 0 if the peer is not on this network. + public partial uint GetPing(IPlayer peer); + + /// + /// Disconnects a peer from the network. + /// + /// The network peer to disconnect. + public partial void Disconnect(IPlayer peer); + + /// + /// Bans a peer from the network. + /// + /// The ban entry containing details about the ban. + /// The duration of the ban before it expires. + public partial void Ban(BanEntry entry, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan expire); + + /// + /// Unbans a peer from the network. + /// + /// The ban entry to remove. + public partial void Unban(BanEntry entry); + + /// + /// Updates server parameters. + /// + public partial void Update(); + + /// + /// Gets the dispatcher which dispatches incoming network events bound to a specific RPC ID. + /// + /// An indexed event dispatcher for incoming RPC events. + public partial IIndexedEventDispatcher GetPerRPCInEventDispatcher(); + + /// + /// Gets the dispatcher which dispatches incoming network events bound to a specific packet ID. + /// + /// An indexed event dispatcher for incoming packet events. + public partial IIndexedEventDispatcher GetPerPacketInEventDispatcher(); + + /// + /// Gets the dispatcher which dispatches outgoing network events bound to a specific RPC ID. + /// + /// An indexed event dispatcher for outgoing RPC events. + public partial IIndexedEventDispatcher GetPerRPCOutEventDispatcher(); + + /// + /// Gets the dispatcher which dispatches outgoing network events bound to a specific packet ID. + /// + /// An indexed event dispatcher for outgoing packet events. + public partial IIndexedEventDispatcher GetPerPacketOutEventDispatcher(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetworkComponent.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetworkComponent.cs new file mode 100644 index 00000000..4b49427d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetworkComponent.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IComponent))] +public readonly partial struct INetworkComponent +{ + /// + public static UID ComponentId => new(0xea9799fd79cf8442); // ID for RakNetLegacyNetworkComponent + + /// + /// Gets the network provided by this component. + /// + /// The network. + public partial INetwork GetNetwork(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetworkEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetworkEventHandler.cs new file mode 100644 index 00000000..c11a750d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetworkEventHandler.cs @@ -0,0 +1,21 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface INetworkEventHandler +{ + /// + /// Called when a peer connects to the network. + /// + /// The player representing the connected peer. + void OnPeerConnect(IPlayer peer); + + /// + /// Called when a peer disconnects from the network. + /// + /// The player representing the disconnected peer. + /// The reason for the disconnection. + void OnPeerDisconnect(IPlayer peer, PeerDisconnectReason reason); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetworkInEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetworkInEventHandler.cs new file mode 100644 index 00000000..c366f637 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetworkInEventHandler.cs @@ -0,0 +1,26 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for and . +/// +[OpenMpEventHandler] +public partial interface INetworkInEventHandler +{ + /// + /// Called when a packet is received from a player. + /// + /// The player who sent the packet. + /// The ID of the packet. + /// The bit stream containing the packet data. + /// if the packet should be handled; otherwise, . + bool OnReceivePacket(IPlayer peer, int id, NetworkBitStream bs); + + /// + /// Called when an RPC is received from a player. + /// + /// The player who sent the RPC. + /// The ID of the RPC. + /// The bit stream containing the RPC data. + /// if the RPC should be handled; otherwise, . + bool OnReceiveRPC(IPlayer peer, int id, NetworkBitStream bs); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetworkOutEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetworkOutEventHandler.cs new file mode 100644 index 00000000..a24c0c17 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetworkOutEventHandler.cs @@ -0,0 +1,26 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for and . +/// +[OpenMpEventHandler] +public partial interface INetworkOutEventHandler +{ + /// + /// Called when a packet is sent to a player. + /// + /// The player receiving the packet. + /// The packet ID. + /// The bit stream containing the packet data. + /// if the packet should be sent; otherwise, . + bool OnSendPacket(IPlayer peer, int id, NetworkBitStream bs); + + /// + /// Called when an RPC is sent to a player. + /// + /// The player receiving the RPC. + /// The RPC ID. + /// The bit stream containing the RPC data. + /// if the RPC should be sent; otherwise, . + bool OnSendRPC(IPlayer peer, int id, NetworkBitStream bs); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/INetworkQueryExtension.cs b/src/SampSharp.OpenMp.Core/Api/Network/INetworkQueryExtension.cs new file mode 100644 index 00000000..fa0214f4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/INetworkQueryExtension.cs @@ -0,0 +1,33 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtension))] +public readonly partial struct INetworkQueryExtension +{ + /// + public static UID ExtensionId => new(0xfd46e147ea474971); + + /// + /// Adds a query rule to the server's query response. + /// + /// The name of the rule. + /// The value of the rule. + /// true if the rule was added successfully; otherwise, false. + public partial bool AddRule(string rule, string value); + + /// + /// Removes a query rule from the server's query response. + /// + /// The name of the rule to remove. + /// true if the rule was removed successfully; otherwise, false. + public partial bool RemoveRule(string rule); + + /// + /// Checks if a query rule is valid. + /// + /// The name of the rule to check. + /// true if the rule is valid; otherwise, false. + public partial bool IsValidRule(string rule); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkInEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkInEventHandler.cs new file mode 100644 index 00000000..b2a179bc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkInEventHandler.cs @@ -0,0 +1,16 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for and . +/// +[OpenMpEventHandler] +public partial interface ISingleNetworkInEventHandler +{ + /// + /// Called when a network packet is received from a player. + /// + /// The player who sent the packet. + /// The bit stream containing the packet data. + /// if the packet should be handled; otherwise, . + bool OnReceive(IPlayer peer, NetworkBitStream bs); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkOutEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkOutEventHandler.cs new file mode 100644 index 00000000..c4bc3c4b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/ISingleNetworkOutEventHandler.cs @@ -0,0 +1,16 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for and . +/// +[OpenMpEventHandler] +public partial interface ISingleNetworkOutEventHandler +{ + /// + /// Called when a packet is sent to a player. + /// + /// The player to whom the packet is being sent. + /// The network bit stream containing the packet data. + /// if the packet should be sent; otherwise, . + bool OnSend(IPlayer peer, NetworkBitStream bs); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/NetworkStats.cs b/src/SampSharp.OpenMp.Core/Api/Network/NetworkStats.cs new file mode 100644 index 00000000..b0384559 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/NetworkStats.cs @@ -0,0 +1,115 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents network statistics for a connection. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct NetworkStats +{ + /// + /// Gets the time when the connection started, in milliseconds since the application started. + /// + public readonly uint ConnectionStartTime; + + /// + /// Gets the size of the message send buffer. + /// + public readonly uint MessageSendBuffer; + + /// + /// Gets the total number of messages sent. + /// + public readonly uint MessagesSent; + + /// + /// Gets the total number of bytes sent. + /// + public readonly uint TotalBytesSent; + + /// + /// Gets the total number of acknowledgements sent. + /// + public readonly uint AcknowlegementsSent; + + /// + /// Gets the number of acknowledgements currently pending. + /// + public readonly uint AcknowlegementsPending; + + /// + /// Gets the number of messages currently on the resend queue. + /// + public readonly uint MessagesOnResendQueue; + + /// + /// Gets the total number of message resends. + /// + public readonly uint MessageResends; + + /// + /// Gets the total number of bytes resent for messages. + /// + public readonly uint MessagesTotalBytesResent; + + /// + /// Gets the packet loss percentage. + /// + public readonly float Packetloss; + + /// + /// Gets the total number of messages received. + /// + public readonly uint MessagesReceived; + + /// + /// Gets the number of messages received per second. + /// + public readonly uint MessagesReceivedPerSecond; + + /// + /// Gets the total number of bytes received. + /// + public readonly uint BytesReceived; + + /// + /// Gets the total number of acknowledgements received. + /// + public readonly uint AcknowlegementsReceived; + + /// + /// Gets the total number of duplicate acknowledgements received. + /// + public readonly uint DuplicateAcknowlegementsReceived; + + /// + /// Gets the current bits per second rate. + /// + public readonly double BitsPerSecond; + + /// + /// Gets the bits per second rate for sent data. + /// + public readonly double BpsSent; + + /// + /// Gets the bits per second rate for received data. + /// + public readonly double BpsReceived; + + /// + /// Gets a value indicating whether the connection is active. + /// + public readonly bool IsActive; + + /// + /// Gets the connection mode. + /// + public readonly int ConnectMode; + + /// + /// Gets the elapsed time of the connection, in milliseconds. + /// + public readonly uint ConnectionElapsedTime; +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/NewConnectionResult.cs b/src/SampSharp.OpenMp.Core/Api/Network/NewConnectionResult.cs new file mode 100644 index 00000000..358dd73b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/NewConnectionResult.cs @@ -0,0 +1,37 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the result of a new connection attempt in the open.mp server. +/// +public enum NewConnectionResult +{ + /// + /// The connection attempt should be ignored. + /// + Ignore, + + /// + /// The connection attempt failed due to a version mismatch. + /// + VersionMismatch, + + /// + /// The connection attempt failed due to an invalid or bad name. + /// + BadName, + + /// + /// The connection attempt failed due to an unsupported or bad modification. + /// + BadMod, + + /// + /// The connection attempt failed because no player slot was available. + /// + NoPlayerSlot, + + /// + /// The connection attempt was successful. + /// + Success +}; diff --git a/src/SampSharp.OpenMp.Core/Api/Network/PeerAddress.cs b/src/SampSharp.OpenMp.Core/Api/Network/PeerAddress.cs new file mode 100644 index 00000000..04107718 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/PeerAddress.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a network peer address, supporting both IPv4 and IPv6. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PeerAddress +{ + /// + /// Indicates whether the address is an IPv6 address. + /// + public readonly bool Ipv6; + + /// + /// The IPv4 address represented as a 32-bit unsigned integer. + /// This field is only used if is false. + /// + public readonly uint V4; + + /// + /// The raw bytes of the address. + /// For IPv6, this contains the full 16-byte address. + /// For IPv4, this may be unused. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public readonly byte[] Bytes; + + /// + /// Converts the peer address to an instance. + /// + /// An representing the peer address. + public IPAddress ToAddress() + { + if (Ipv6) + { + Span buf = stackalloc byte[46]; // INET6_ADDRSTRLEN + Bytes.CopyTo(buf); + return new IPAddress(buf); + } + else + { + return new IPAddress(V4); + } + } +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/PeerDisconnectReason.cs b/src/SampSharp.OpenMp.Core/Api/Network/PeerDisconnectReason.cs new file mode 100644 index 00000000..a7e5ae91 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/PeerDisconnectReason.cs @@ -0,0 +1,32 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the reasons for a peer disconnecting from the network. +/// +public enum PeerDisconnectReason +{ + /// + /// The peer was disconnected due to a timeout. + /// + Timeout, + + /// + /// The peer voluntarily quit the session. + /// + Quit, + + /// + /// The peer was kicked from the session. + /// + Kicked, + + /// + /// The peer was disconnected for a custom reason. + /// + Custom, + + /// + /// The peer was disconnected because the mode ended. + /// + ModeEnd +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/PeerNetworkData.cs b/src/SampSharp.OpenMp.Core/Api/Network/PeerNetworkData.cs new file mode 100644 index 00000000..56fec245 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/PeerNetworkData.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the network data of a peer. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PeerNetworkData +{ + /// + /// Represents the network ID of a peer, including its address and port. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct NetworkID + { + /// + /// Gets the address of the peer. + /// + public readonly PeerAddress address; + + /// + /// Gets the port of the peer. + /// + public readonly ushort port; + + /// + /// Converts the network ID to an instance. + /// + /// An representing the peer's address and port. + public IPEndPoint ToEndpoint() + { + return new IPEndPoint(address.ToAddress(), port); + } + } + + /// + /// Gets the network interface associated with the peer. + /// + public readonly INetwork network; + + /// + /// Gets the network ID of the peer. + /// + public readonly NetworkID networkID; +} diff --git a/src/SampSharp.OpenMp.Core/Api/Network/PeerRequestParams.cs b/src/SampSharp.OpenMp.Core/Api/Network/PeerRequestParams.cs new file mode 100644 index 00000000..81d1723b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Network/PeerRequestParams.cs @@ -0,0 +1,41 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the parameters of a peer request in the network API. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PeerRequestParams +{ + /// + /// Gets the version of the client making the request. + /// + public readonly ClientVersion Version; + + /// + /// Gets the name of the client version as a string view. + /// + public readonly StringView VersionName; + + /// + /// Gets a value indicating whether the client is a bot. + /// + public readonly bool Bot; + + /// + /// Gets the name of the client making the request. + /// + public readonly StringView Name; + + /// + /// Gets the serial number of the client making the request. + /// + public readonly StringView Serial; + + /// + /// Gets a value indicating whether the client is using the official client. + /// + public readonly bool IsUsingOfficialClient; +}; diff --git a/src/SampSharp.OpenMp.Core/Api/NetworkBitStream.cs b/src/SampSharp.OpenMp.Core/Api/NetworkBitStream.cs new file mode 100644 index 00000000..822860d0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/NetworkBitStream.cs @@ -0,0 +1,10 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi] +public readonly partial struct NetworkBitStream +{ + // TODO: not yet implemented / NetworkBitStream +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/OpenMpConstants.cs b/src/SampSharp.OpenMp.Core/Api/OpenMpConstants.cs new file mode 100644 index 00000000..f8ab697e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/OpenMpConstants.cs @@ -0,0 +1,365 @@ +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Contains constants provided by the open.mp SDK. +/// +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Mirrors open.mp SDK constant names")] +public static class OpenMpConstants +{ + /// + /// The maximum number of seats in a vehicle. + /// + public const int MAX_SEATS = 4; + + /// + /// The maximum number of players in the server. + /// + public const int PLAYER_POOL_SIZE = 1000; + + /// + /// The maximum number of NPCs in the server. + /// + public const int MPC_POOL_SIZE = 1000; + + /// + /// The maximum number of vehicles in the server. + /// + public const int VEHICLE_POOL_SIZE = 2000; + + /// + /// The maximum number of classes in the server. + /// + public const int CLASS_POOL_SIZE = 320; + + /// + /// The maximum number of objects in the server. + /// + public const int OBJECT_POOL_SIZE = 2000; + + /// + /// The maximum number of objects clients with a version of 0.3.7 can see. + /// + public const int OBJECT_POOL_SIZE_037 = 1000; + + /// + /// The number of weapon slots a player has. + /// + public const int MAX_WEAPON_SLOTS = 13; + + /// + /// The maximum number of vehicle models. + /// + public const int MAX_VEHICLE_MODELS = 611 - 400 + 1; + + /// + /// The highest weapon ID. + /// + public const int MAX_WEAPON_ID = 47; + + /// + /// The number of skills a player has. + /// + public const int NUM_SKILL_LEVELS = 11; + + /// + /// The slot ID which represents an invalid weapon slot. + /// + public const byte INVALID_WEAPON_SLOT = 0xFF; + + /// + /// The minimum length of a player name. + /// + public const int MIN_PLAYER_NAME = 3; + + /// + /// The maximum length of a player name. + /// + public const int MAX_PLAYER_NAME = 24; + + /// + /// The highest animation ID. + /// + public const int MAX_ANIMATIONS = 1813; + + /// + /// The maximum level a player can have in a skill. + /// + public const int MAX_SKILL_LEVEL = 999; + + /// + /// The ID used to represent an invalid vehicle ID. + /// + public const int INVALID_VEHICLE_ID = 0xFFFF; + + /// + /// The ID used to represent an invalid object ID. + /// + public const int INVALID_OBJECT_ID = 0xFFFF; + + /// + /// The ID used to represent an invalid player ID. + /// + public const int INVALID_PLAYER_ID = 0xFFFF; + + /// + /// The ID used to represent an invalid actor ID. + /// + public const int INVALID_ACTOR_ID = 0xFFFF; + + /// + /// The distance at which objects are streamed in. + /// + public const float STREAM_DISTANCE = 200; + + /// + /// The number of objects which can be attached to a player. + /// + public const int MAX_ATTACHED_OBJECT_SLOTS = 10; + + /// + /// The number of materials an object can have. + /// + public const int MAX_OBJECT_MATERIAL_SLOTS = 16; + + /// + /// The maximum number of text labels in the server + /// + public const int TEXT_LABEL_POOL_SIZE = 1024; + + /// + /// The ID used to represent an invalid text label ID. + /// + public const int INVALID_TEXT_LABEL_ID = 0xFFFF; + + /// + /// The maximum number of pickups in the server. + /// + public const int PICKUP_POOL_SIZE = 4096; + + /// + /// The maximum number of global text draws in the server. + /// + public const int GLOBAL_TEXTDRAW_POOL_SIZE = 2048; + + /// + /// The maximum number of player text draws in the server. + /// + public const int PLAYER_TEXTDRAW_POOL_SIZE = 256; + + /// + /// The highest vehicle component ID. + /// + public const int MAX_VEHICLE_COMPONENTS = 194; + + /// + /// The ID used to represent an invalid vehicle component ID. + /// + public const int INVALID_COMPONENT_ID = 0; + + /// + /// The highest vehicle component slot. + /// + public const int MAX_VEHICLE_COMPONENT_SLOT = 16; + + /// + /// The highest vehicle component slot in an RPC. + /// + public const int MAX_VEHICLE_COMPONENT_SLOT_IN_RPC = 14; + + /// + /// The maximum number of text labels in the server. + /// + public const int MAX_TEXT_LABELS = 1024; + + /// + /// The maximum number of global text draws which can be shown to a player. + /// + public const int MAX_GLOBAL_TEXTDRAWS = 2048; + + /// + /// The maximum number of player text draws which can be shown to a player. + /// + public const int MAX_PLAYER_TEXTDRAWS = 256; + + /// + /// The ID used to represent an invalid text draw ID. + /// + public const int INVALID_TEXTDRAW = 0xFFFF; + + /// + /// The maximum number of actors in the server. + /// + public const int ACTOR_POOL_SIZE = 1000; + + /// + /// The maximum number of menus in the server. + /// + public const int MENU_POOL_SIZE = 128; + + /// + /// The maximum number of items in a menu. + /// + public const int MAX_MENU_ITEMS = 12; + + /// + /// The maximum length of text in a menu. + /// + public const int MAX_MENU_TEXT_LENGTH = 32; + + /// + /// The ID used to represent an invalid menu ID. + /// + public const int INVALID_MENU_ID = 0xFF; + + /// + /// The ID used to represent an invalid dialog ID. + /// + public const int INVALID_DIALOG_ID = -1; + + /// + /// The maximum ID a dialog can have. + /// + public const int MAX_DIALOG = 32768; + + /// + /// The ID used to represent an invalid gang zone ID. + /// + public const int INVALID_GANG_ZONE_ID = -1; + + /// + /// The ID used to represent an invalid pickup ID. + /// + public const int INVALID_PICKUP_ID = -1; + + /// + /// The ID used to represent an invalid object model ID. + /// + public const int INVALID_OBJECT_MODEL_ID = -1; + + /// + /// The ID used to represent an invalid menu item ID. + /// + public const int INVALID_MENU_ITEM_ID = -1; + + /// + /// The maximum number of gang zones in the server. + /// + public const int GANG_ZONE_POOL_SIZE = 1024; + + /// + /// The maximum number of players which can be streamed in for a player. + /// + public const int MAX_STREAMED_PLAYERS = 200; + + /// + /// The maximum number of actors which can be streamed in for a player. + /// + public const int MAX_STREAMED_ACTORS = 50; + + /// + /// The maximum number of vehicles which can be streamed in for a player. + /// + public const int MAX_STREAMED_VEHICLES = 700; + + /// + /// The team ID used to represent no team. + /// + public const int TEAM_NONE = 255; + + /// + /// The seat ID used to represent no seat. + /// + public const int SEAT_NONE = -1; + + /// + /// The default upper X/Y bounds of the world + /// + public const float MAX_WORLD_BOUNDS = 20000.0f; + + /// + /// The default lower X/Y bounds of the world. + /// + public const float MIN_WORLD_BOUNDS = -20000.0f; + + /// + /// The maximum length of a text draw string. + /// + public const int MAX_TEXTDRAW_STR_LENGTH = 800; + + /// + /// The maximum number of connected vehicle carriages. + /// + public const int MAX_VEHICLE_CARRIAGES = 3; + + /// + /// The highest game text style ID. + /// + public const int MAX_GAMETEXT_STYLES = 16; + + /// + /// The minimum ID for custom skins. + /// + public const int MIN_CUSTOM_SKIN_ID = 20001; + + /// + /// The maximum ID for custom skins. + /// + public const int MAX_CUSTOM_SKIN_ID = 30000; + + /// + /// The minimum ID for custom object models. + /// + public const int MIN_CUSTOM_OBJECT_ID = -30000; + + /// + /// The maximum ID for custom object models. + /// + public const int MAX_CUSTOM_OBJECT_ID = -1000; + + /// + /// The ID used to represent an invalid path ID. + /// + public const int INVALID_PATH_ID = -1; + + /// + /// The ID used to represent an invalid node ID. + /// + public const int INVALID_NODE_ID = -1; + + /// + /// The ID used to represent an invalid record ID. + /// + public const int INVALID_RECORD_ID = -1; + + /// + /// The ID used to represent an invalid model ID. + /// + public const ushort INVALID_MODEL_ID = 65535; + + /// + /// The ID of the question mark object model. + /// + public const int QUESTION_MARK_MODEL_ID = 18631; + + /// + /// The speed at which NPCs will move when using the "auto" movement type. + /// + public const float NPC_MOVE_SPEED_AUTO = -1.0f; + + /// + /// The speed at which NPCs will move when using the "walk" movement type. + /// + public const float NPC_MOVE_SPEED_WALK = 0.1552086f; + + /// + /// The speed at which NPCs will move when using the "jog" movement type. + /// + public const float NPC_MOVE_SPEED_JOG = 0.56444f; + + /// + /// The speed at which NPCs will move when using the "sprint" movement type. + /// + public const float NPC_MOVE_SPEED_SPRINT = 0.926784f; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/BodyPart.cs b/src/SampSharp.OpenMp.Core/Api/Player/BodyPart.cs new file mode 100644 index 00000000..171b2bc0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/BodyPart.cs @@ -0,0 +1,42 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the body parts of a player that can be targeted or affected. +/// +public enum BodyPart +{ + /// + /// The torso of the player. + /// + Torso = 3, + + /// + /// The groin of the player. + /// + Groin, + + /// + /// The left arm of the player. + /// + LeftArm, + + /// + /// The right arm of the player. + /// + RightArm, + + /// + /// The left leg of the player. + /// + LeftLeg, + + /// + /// The right leg of the player. + /// + RightLeg, + + /// + /// The head of the player. + /// + Head +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/BodyPartExtensions.cs b/src/SampSharp.OpenMp.Core/Api/Player/BodyPartExtensions.cs new file mode 100644 index 00000000..98bb0bbc --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/BodyPartExtensions.cs @@ -0,0 +1,38 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides extension methods for the enum. +/// +public static class BodyPartExtensions +{ + private static readonly string[] _names = + [ + "invalid", + "invalid", + "invalid", + "torso", + "groin", + "left arm", + "right arm", + "left leg", + "right leg", + "head" + ]; + + /// + /// Gets the name of the specified . + /// + /// The body part to get the name for. + /// The name of the body part, or "invalid" if the body part is not valid. + public static string GetName(this BodyPart bodyPart) + { + var number = (int)bodyPart; + + if (number < 0 || number >= _names.Length) + { + return "invalid"; + } + + return _names[number]; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/EPlayerNameStatus.cs b/src/SampSharp.OpenMp.Core/Api/Player/EPlayerNameStatus.cs new file mode 100644 index 00000000..5d019120 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/EPlayerNameStatus.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the status of a player's name during validation or update operations. +/// +public enum EPlayerNameStatus +{ + /// + /// The player's name was successfully updated. + /// + Updated, + + /// + /// The player's name is already taken by another player. + /// + Taken, + + /// + /// The player's name is invalid and does not meet the required criteria. + /// + Invalid +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayer.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayer.cs new file mode 100644 index 00000000..1b41b56c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayer.cs @@ -0,0 +1,959 @@ +using System.Numerics; +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IEntity))] +public readonly partial struct IPlayer +{ + /// + /// Kicks the player from the server. + /// + public partial void Kick(); + + /// + /// Bans the player from the server. + /// + /// The reason for banning the player. + public partial void Ban(string reason); + + /// + /// Checks if the player is a bot (NPC). + /// + /// if the player is a bot; otherwise, . + public partial bool IsBot(); + + /// + /// Gets the player's network data. + /// + /// A reference to the player's network data. + public partial BlittableStructRef GetNetworkData(); + + /// + /// Gets the player's ping. + /// + /// The player's ping in milliseconds. + public partial uint GetPing(); + + /// + /// Sends a packet to the player. + /// + /// The data to send. + /// The channel to send the packet on. + /// Whether to dispatch events for the packet. + /// if the packet was sent successfully; otherwise, . + public partial bool SendPacket(SpanLite data, int channel, bool dispatchEvents = true); + + /// + /// Sends an RPC to the player. + /// + /// The RPC ID. + /// The data to send. + /// The channel to send the RPC on. + /// Whether to dispatch events for the RPC. + /// if the RPC was sent successfully; otherwise, . + public partial bool SendRPC(int id, SpanLite data, int channel, bool dispatchEvents = true); + + /// + /// Broadcasts an RPC to the player's streamed peers. + /// + /// The RPC ID. + /// The data to broadcast. + /// The channel to broadcast the RPC on. + /// Whether to skip broadcasting to the player. + public partial void BroadcastRPCToStreamed(int id, SpanLite data, int channel, bool skipFrom = false); + + /// + /// Broadcasts a packet to the player's streamed peers. + /// + /// The data to broadcast. + /// The channel to broadcast the packet on. + /// Whether to skip broadcasting to the player. + public partial void BroadcastPacketToStreamed(SpanLite data, int channel, bool skipFrom = true); + + /// + /// Broadcasts a sync packet to the player's streamed peers. + /// + /// The data to broadcast. + /// The channel to broadcast the sync packet on. + public partial void BroadcastSyncPacket(SpanLite data, int channel); + + /// + /// Spawns the player. + /// + public partial void Spawn(); + + /// + /// Gets the player's client version. + /// + /// The player's client version. + public partial ClientVersion GetClientVersion(); + + /// + /// Gets the player's client version name. + /// + /// The player's client version name. + public partial string GetClientVersionName(); + + /// + /// Sets the player's position and finds the proper Z coordinate for the map. + /// + /// The position to set. + public partial void SetPositionFindZ(Vector3 pos); + + /// + /// Sets the player's camera position. + /// + /// The position to set the camera to. + public partial void SetCameraPosition(Vector3 pos); + + /// + /// Gets the player's camera position. + /// + /// The player's camera position. + public partial Vector3 GetCameraPosition(); + + /// + /// Sets the direction the player's camera looks at. + /// + /// The position to look at. + /// The type of camera cut. + public partial void SetCameraLookAt(Vector3 pos, int cutType); + + /// + /// Gets the direction the player's camera looks at. + /// + /// The position the camera is looking at. + public partial Vector3 GetCameraLookAt(); + + /// + /// Sets the camera to a position behind the player. + /// + public partial void SetCameraBehind(); + + /// + /// Interpolates the player's camera position. + /// + /// The starting position. + /// The ending position. + /// The duration of the interpolation in milliseconds. + /// The type of camera cut. + public partial void InterpolateCameraPosition(Vector3 from, Vector3 to, int time, PlayerCameraCutType cutType); + + /// + /// Interpolates the player's camera look-at position. + /// + /// The starting position. + /// The ending position. + /// The duration of the interpolation in milliseconds. + /// The type of camera cut. + public partial void InterpolateCameraLookAt(Vector3 from, Vector3 to, int time, PlayerCameraCutType cutType); + + /// + /// Attaches the player's camera to an object. + /// + /// The object to attach the camera to. + public partial void AttachCameraToObject(IObject obj); + + /// + /// Attaches the player's camera to a player object. + /// + /// The player object to attach the camera to. + [OpenMpApiOverload("_player")] + public partial void AttachCameraToObject(IPlayerObject obj); + + /// + /// Sets the player's name. + /// + /// The new name for the player. + /// The status of the name change. + public partial EPlayerNameStatus SetName(string name); + + /// + /// Gets the player's name. + /// + /// The player's name. + public partial string GetName(); + + /// + /// Gets the player's serial (GPCI). + /// + /// The player's serial. + public partial string GetSerial(); + + /// + /// Gives a weapon to the player. + /// + /// The weapon to give. + public partial void GiveWeapon(WeaponSlotData weapon); + + /// + /// Removes a weapon from the player. + /// + /// The weapon to remove. + public partial void RemoveWeapon(byte weapon); + + /// + /// Sets the ammunition for a specific weapon slot. + /// + /// The weapon slot data containing the weapon and ammunition information. + public partial void SetWeaponAmmo(WeaponSlotData data); + + /// + /// Gets all weapons and their associated data for the player. + /// + /// A reference to the player's weapon slots. + public partial BlittableStructRef GetWeapons(); + + private partial void GetWeaponSlot(int slot, out WeaponSlotData data); + + /// + /// Gets the weapon data for a specific weapon slot. + /// + /// The weapon slot index. + /// The weapon slot data. + public WeaponSlotData GetWeaponSlot(int slot) + { + GetWeaponSlot(slot, out var data); + return data; + } + + /// + /// Resets all weapons for the player. + /// + public partial void ResetWeapons(); + + /// + /// Sets the player's currently armed weapon. + /// + /// The weapon ID to set as armed. + public partial void SetArmedWeapon(int weapon); + + /// + /// Gets the player's currently armed weapon. + /// + /// The weapon ID of the currently armed weapon. + public partial int GetArmedWeapon(); + + /// + /// Gets the ammunition count for the player's currently armed weapon. + /// + /// The ammunition count for the armed weapon. + public partial int GetArmedWeaponAmmo(); + + /// + /// Sets the player's shop name. + /// + /// The name of the shop. + public partial void SetShopName(string name); + + /// + /// Gets the player's shop name. + /// + /// The name of the shop. + public partial string GetShopName(); + + /// + /// Sets the player's drunk level. + /// + /// The drunk level to set. + public partial void SetDrunkLevel(int level); + + /// + /// Gets the player's drunk level. + /// + /// The player's drunk level. + public partial int GetDrunkLevel(); + + /// + /// Sets the player's color. + /// + /// The color to set. + public partial void SetColour(Colour colour); + + /// + /// Gets the player's color. + /// + /// A reference to the player's color. + public partial ref Colour GetColour(); + + /// + /// Sets the color of another player as seen by this player. + /// + /// The other player. + /// The color to set for the other player. + public partial void SetOtherColour(IPlayer other, Colour colour); + + /// + /// Gets the color of another player as seen by this player. + /// + /// The other player. + /// When this method returns, contains the color of the other player. + /// if the color was successfully retrieved; otherwise, . + public partial bool GetOtherColour(IPlayer other, out Colour colour); + + /// + /// Sets whether the player is controllable. + /// + /// to make the player controllable; otherwise, . + public partial void SetControllable(bool controllable); + + /// + /// Gets whether the player is controllable. + /// + /// if the player is controllable; otherwise, . + public partial bool GetControllable(); + + /// + /// Sets whether the player is spectating. + /// + /// to make the player spectate; otherwise, . + public partial void SetSpectating(bool spectating); + + /// + /// Sets the player's wanted level. + /// + /// The wanted level to set. + public partial void SetWantedLevel(uint level); + + /// + /// Gets the player's wanted level. + /// + /// The player's wanted level. + public partial uint GetWantedLevel(); + + /// + /// Plays a sound for the player at a specific position. + /// + /// The sound ID to play. + /// The position where the sound should be played. + public partial void PlaySound(int sound, Vector3 pos); + + /// + /// Gets the ID of the last sound played for the player. + /// + /// The ID of the last played sound. + public partial int LastPlayedSound(); + + /// + /// Plays an audio stream for the player. + /// + /// The URL of the audio stream. + /// to play the audio at a specific position; otherwise, . + /// The position where the audio should be played. + /// The maximum distance at which the audio can be heard. + public partial void PlayAudio(string url, bool usePos = false, Vector3 pos = default, float distance = 0); + + /// + /// Reports a crime committed by a player. + /// + /// The player who committed the crime. + /// The crime ID. + /// TODO: Clarify the meaning of the return value. + public partial bool PlayerCrimeReport(IPlayer suspect, int crime); + + /// + /// Stops the currently playing audio for the player. + /// + public partial void StopAudio(); + + /// + /// Gets the URL of the last played audio stream. + /// + /// The URL of the last played audio stream. + public partial string LastPlayedAudio(); + + /// + /// Creates an explosion at a specific position for the player. + /// + /// The position of the explosion. + /// The type of explosion. + /// The radius of the explosion. + public partial void CreateExplosion(Vector3 vec, int type, float radius); + + /// + /// Sends a death message to the player. + /// + /// The player who died. + /// The player who killed the other player. + /// The weapon ID used to kill the player. + public partial void SendDeathMessage(IPlayer player, IPlayer killer, int weapon); + + /// + /// Sends an empty death message to the player. + /// + public partial void SendEmptyDeathMessage(); + + /// + /// Removes default objects near a specific position for the player. + /// + /// The model ID of the objects to remove. + /// The position near which to remove objects. + /// The radius within which to remove objects. + public partial void RemoveDefaultObjects(uint model, Vector3 pos, float radius); + + /// + /// Forces the player to reselect their class. + /// + public partial void ForceClassSelection(); + + /// + /// Sets the player's money. + /// + /// The amount of money to set. + public partial void SetMoney(int money); + + /// + /// Gives the player additional money. + /// + /// The amount of money to give. + public partial void GiveMoney(int money); + + /// + /// Resets the player's money to zero. + /// + public partial void ResetMoney(); + + /// + /// Gets the player's current money. + /// + /// The amount of money the player has. + public partial int GetMoney(); + + /// + /// Sets a map icon for the player. + /// + /// The ID of the map icon. + /// The position of the map icon. + /// The type of the map icon. + /// The color of the map icon. + /// The style of the map icon. + public partial void SetMapIcon(int id, Vector3 pos, int type, Colour colour, MapIconStyle style); + + /// + /// Removes a map icon for the player. + /// + /// The ID of the map icon to remove. + public partial void UnsetMapIcon(int id); + + /// + /// Enables or disables stunt bonuses for the player. + /// + /// to enable stunt bonuses; otherwise, . + public partial void UseStuntBonuses(bool enable); + + /// + /// Toggles the visibility of another player's name tag for this player. + /// + /// The other player. + /// to show the name tag; to hide it. + public partial void ToggleOtherNameTag(IPlayer other, bool toggle); + + /// + /// Sets the in-game time for the player. + /// + /// The hour to set (0-23), truncated to whole hours on the native side. + /// The minute to set (0-59), truncated to whole minutes on the native side. + public partial void SetTime( + [MarshalUsing(typeof(HoursMarshaller))] TimeSpan hr, + [MarshalUsing(typeof(MinutesMarshaller))] TimeSpan min); + + private partial void GetTime(out Pair result); + + /// + /// Gets the in-game time for the player. + /// + /// A tuple containing the hour and minute. + public (int hour, int minutes) GetTime() + { + GetTime(out var result); + return ((int)((TimeSpan)result.First).TotalHours, (int)((TimeSpan)result.Second).TotalMinutes); + } + + /// + /// Enables or disables the in-game clock for the player. + /// + /// to enable the clock; otherwise, . + public partial void UseClock(bool enable); + + /// + /// Checks if the in-game clock is enabled for the player. + /// + /// if the clock is enabled; otherwise, . + public partial bool HasClock(); + + /// + /// Enables or disables widescreen mode for the player. + /// + /// to enable widescreen mode; otherwise, . + public partial void UseWidescreen(bool enable); + + /// + /// Checks if widescreen mode is enabled for the player. + /// + /// if widescreen mode is enabled; otherwise, . + public partial bool HasWidescreen(); + + /// + /// Sets the player's transformation matrix. + /// + /// The transformation matrix to set. + public partial void SetTransform(GTAQuat tm); + + /// + /// Sets the player's health. + /// + /// The health value to set. + public partial void SetHealth(float health); + + /// + /// Gets the player's health. + /// + /// The player's current health. + public partial float GetHealth(); + + /// + /// Sets the player's score. + /// + /// The score value to set. + public partial void SetScore(int score); + + /// + /// Gets the player's score. + /// + /// The player's current score. + public partial int GetScore(); + + /// + /// Sets the player's armor value. + /// + /// The armor value to set. + public partial void SetArmour(float armour); + + /// + /// Gets the player's armor value. + /// + /// The player's current armor value. + public partial float GetArmour(); + + /// + /// Sets the player's gravity. + /// + /// The gravity value to set. + public partial void SetGravity(float gravity); + + /// + /// Gets the player's gravity. + /// + /// The player's current gravity value. + public partial float GetGravity(); + + /// + /// Sets the in-game world time for the player. + /// + /// The world time to set, truncated to whole hours on the native side. + public partial void SetWorldTime([MarshalUsing(typeof(HoursMarshaller))] TimeSpan time); + + /// + /// Applies an animation to the player. + /// + /// The animation data to apply. + /// The synchronization type for the animation. + public partial void ApplyAnimation(AnimationData animation, PlayerAnimationSyncType syncType); + + /// + /// Clears all animations for the player. + /// + /// The synchronization type for clearing animations. + public partial void ClearAnimations(PlayerAnimationSyncType syncType); + + /// + /// Gets the player's current animation data. + /// + /// The player's current animation data. + public partial PlayerAnimationData GetAnimationData(); + + /// + /// Gets the player's surfing data. + /// + /// The player's current surfing data. + public partial PlayerSurfingData GetSurfingData(); + + /// + /// Streams this player in for another player. + /// + /// The player for whom this player will be streamed in. + public partial void StreamInForPlayer(IPlayer other); + + /// + /// Checks if another player is streamed in for this player. + /// + /// The player to check. + /// if the other player is streamed in for this player; otherwise, . + public partial bool IsStreamedInForPlayer(IPlayer other); + + /// + /// Streams another player out for this player. + /// + /// The player to stream out. + public partial void StreamOutForPlayer(IPlayer other); + + /// + /// Gets the set of players for whom this player is currently streamed in. + /// + /// A set of players for whom this player is streamed in. + public partial FlatPtrHashSet StreamedForPlayers(); + + /// + /// Gets the current state of the player. + /// + /// The player's current state. + public partial PlayerState GetState(); + + /// + /// Sets the player's team. + /// + /// The team ID to set. + public partial void SetTeam(int team); + + /// + /// Gets the player's team. + /// + /// The team ID of the player. + public partial int GetTeam(); + + /// + /// Sets the player's skin. + /// + /// The skin ID to set. + /// to send the update to other players; otherwise, . + public partial void SetSkin(int skin, bool send = true); + + /// + /// Gets the player's skin. + /// + /// The skin ID of the player. + public partial int GetSkin(); + + /// + /// Sets a chat bubble for the player. + /// + /// The text to display in the chat bubble. + /// The color of the chat bubble. + /// The draw distance for the chat bubble. + /// The duration for which the chat bubble will be displayed. + public partial void SetChatBubble(string text, ref Colour colour, float drawDist, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan expire); + + /// + /// Sends a client message to the player. + /// + /// The color of the message. + /// The message to send. + public partial void SendClientMessage(ref Colour colour, string message); + + /// + /// Sends a chat message to the player. + /// + /// The player sending the message. + /// The message to send. + public partial void SendChatMessage(IPlayer sender, string message); + + /// + /// Sends a command to the player. + /// + /// The command message to send. + public partial void SendCommand(string message); + + /// + /// Sends game text to the player. + /// + /// The game text to display. + /// The duration for which the text will be displayed. + /// The style of the game text. + public partial void SendGameText(string message, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan time, int style); + + /// + /// Hides game text for the player. + /// + /// The style of the game text to hide. + public partial void HideGameText(int style); + + /// + /// Checks if game text is currently displayed for the player. + /// + /// The style of the game text to check. + /// if game text is displayed; otherwise, . + public partial bool HasGameText(int style); + + /// + /// Gets the currently displayed game text for the player, if any. + /// + /// The style of the game text to retrieve. + /// The message of the game text. + /// The duration for which the text is displayed. + /// The remaining time for the text to be displayed. + /// if game text is currently displayed; otherwise, . + public partial bool GetGameText(int style, out string? message, [MarshalUsing(typeof(MillisecondsMarshaller))] out TimeSpan time, [MarshalUsing(typeof(MillisecondsMarshaller))] out TimeSpan remaining); + + /// + /// Sets the player's weather. + /// + /// The weather ID to set. + public partial void SetWeather(int weatherId); + + /// + /// Gets the player's current weather. + /// + /// The weather ID of the player. + public partial int GetWeather(); + + /// + /// Sets the player's world bounds. + /// + /// The coordinates defining the world bounds. + public partial void SetWorldBounds(Vector4 coords); + + /// + /// Gets the player's world bounds. + /// + /// The coordinates defining the player's world bounds. + public partial Vector4 GetWorldBounds(); + + /// + /// Sets the player's fighting style. + /// + /// The fighting style to set. + /// See https://open.mp/docs/scripting/resources/fightingstyles for more details. + public partial void SetFightingStyle(PlayerFightingStyle style); + + /// + /// Gets the player's fighting style. + /// + /// The player's current fighting style. + /// See https://open.mp/docs/scripting/resources/fightingstyles for more details. + public partial PlayerFightingStyle GetFightingStyle(); + + /// + /// Sets the player's skill level for a specific weapon. + /// + /// The weapon skill to set. + /// The skill level to set. + /// See https://open.mp/docs/scripting/resources/weaponskills for more details. + public partial void SetSkillLevel(PlayerWeaponSkill skill, int level); + + /// + /// Sets the player's special action. + /// + /// The special action to set. + public partial void SetAction(PlayerSpecialAction action); + + /// + /// Gets the player's special action. + /// + /// The player's current special action. + public partial PlayerSpecialAction GetAction(); + + /// + /// Sets the player's velocity. + /// + /// The velocity to set. + public partial void SetVelocity(Vector3 velocity); + + /// + /// Gets the player's velocity. + /// + /// The player's current velocity. + public partial Vector3 GetVelocity(); + + /// + /// Sets the player's interior. + /// + /// The interior ID to set. + public partial void SetInterior(uint interior); + + /// + /// Gets the player's interior. + /// + /// The player's current interior ID. + public partial uint GetInterior(); + + /// + /// Gets the player's key data. + /// + /// A reference to the player's key data. + public partial ref PlayerKeyData GetKeyData(); + + /// + /// Gets the player's skill levels for all weapon types. + /// + /// A reference to the player's skill levels. + /// See https://open.mp/docs/scripting/resources/weaponskills for more details. + public partial ref SkillsArray GetSkillLevels(); + + /// + /// Gets the player's aim data. + /// + /// A reference to the player's aim data. + public partial ref PlayerAimData GetAimData(); + + /// + /// Gets the player's bullet data. + /// + /// A reference to the player's bullet data. + public partial ref PlayerBulletData GetBulletData(); + + /// + /// Enables or disables camera targeting for the player. + /// + /// to enable camera targeting; otherwise, . + public partial void UseCameraTargeting(bool enable); + + /// + /// Checks if camera targeting is enabled for the player. + /// + /// if camera targeting is enabled; otherwise, . + public partial bool HasCameraTargeting(); + + /// + /// Removes the player from their vehicle. + /// + /// to forcefully remove the player; otherwise, . + public partial void RemoveFromVehicle(bool force); + + /// + /// Gets the player the camera is targeting. + /// + /// The player the camera is targeting, or null if no player is targeted. + public partial IPlayer GetCameraTargetPlayer(); + + /// + /// Gets the vehicle the camera is targeting. + /// + /// The vehicle the camera is targeting, or null if no vehicle is targeted. + public partial IVehicle GetCameraTargetVehicle(); + + /// + /// Gets the object the camera is targeting. + /// + /// The object the camera is targeting, or null if no object is targeted. + public partial IObject GetCameraTargetObject(); + + /// + /// Gets the actor the camera is targeting. + /// + /// The actor the camera is targeting, or null if no actor is targeted. + public partial IActor GetCameraTargetActor(); + + /// + /// Gets the player the player is targeting. + /// + /// The player being targeted, or null if no player is targeted. + public partial IPlayer GetTargetPlayer(); + + /// + /// Gets the actor the player is targeting. + /// + /// The actor being targeted, or null if no actor is targeted. + public partial IActor GetTargetActor(); + + /// + /// Sets whether the player should collide with remote vehicles. + /// + /// to enable collisions; otherwise, . + public partial void SetRemoteVehicleCollisions(bool collide); + + /// + /// Makes the player spectate another player. + /// + /// The player to spectate. + /// The spectate mode. + public partial void SpectatePlayer(IPlayer target, PlayerSpectateMode mode); + + /// + /// Makes the player spectate a vehicle. + /// + /// The vehicle to spectate. + /// The spectate mode. + public partial void SpectateVehicle(IVehicle target, PlayerSpectateMode mode); + + /// + /// Gets the player's spectate data. + /// + /// A reference to the player's spectate data. + public partial ref PlayerSpectateData GetSpectateData(); + + /// + /// Sends a client check request to the player. + /// + /// The type of action to check. + /// The memory address to check. + /// The offset to check. + /// The number of bytes to check. + public partial void SendClientCheck(int actionType, int address, int offset, int count); + + /// + /// Toggles ghost mode for the player. + /// + /// to enable ghost mode; otherwise, . + public partial void ToggleGhostMode(bool toggle); + + /// + /// Checks if ghost mode is enabled for the player. + /// + /// if ghost mode is enabled; otherwise, . + public partial bool IsGhostModeEnabled(); + + /// + /// Gets the number of default objects removed for the player. + /// + /// The number of default objects removed. + public partial int GetDefaultObjectsRemoved(); + + /// + /// Checks if the player is about to be kicked. + /// + /// if the player is about to be kicked; otherwise, . + public partial bool GetKickStatus(); + + /// + /// Clears all tasks for the player. + /// + /// The synchronization type for clearing tasks. + public partial void ClearTasks(PlayerAnimationSyncType syncType); + + /// + /// Allows or disallows the player to use weapons. + /// + /// to allow weapons; otherwise, . + public partial void AllowWeapons(bool allow); + + /// + /// Checks if the player is allowed to use weapons. + /// + /// if the player is allowed to use weapons; otherwise, . + public partial bool AreWeaponsAllowed(); + + /// + /// Allows or disallows the player to teleport by clicking the map. + /// + /// to allow teleporting; otherwise, . + public partial void AllowTeleport(bool allow); + + /// + /// Checks if the player is allowed to teleport by clicking the map. + /// + /// if teleporting is allowed; otherwise, . + public partial bool IsTeleportAllowed(); + + /// + /// Checks if the player is using an official client. + /// + /// if the player is using an official client; otherwise, . + public partial bool IsUsingOfficialClient(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerChangeEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerChangeEventHandler.cs new file mode 100644 index 00000000..34857384 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerChangeEventHandler.cs @@ -0,0 +1,46 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerChangeEventHandler +{ + /// + /// Called when a player's score changes. + /// + /// The player whose score changed. + /// The new score of the player. + void OnPlayerScoreChange(IPlayer player, int score); + + /// + /// Called when a player's name changes. + /// + /// The player whose name changed. + /// The previous name of the player. + void OnPlayerNameChange(IPlayer player, string oldName); + + /// + /// Called when a player's interior changes. + /// + /// The player whose interior changed. + /// The new interior ID. + /// The previous interior ID. + void OnPlayerInteriorChange(IPlayer player, uint newInterior, uint oldInterior); + + /// + /// Called when a player's state changes. + /// + /// The player whose state changed. + /// The new state of the player. + /// The previous state of the player. + void OnPlayerStateChange(IPlayer player, PlayerState newState, PlayerState oldState); + + /// + /// Called when a player's key state changes. + /// + /// The player whose key state changed. + /// The new key state. + /// The previous key state. + void OnPlayerKeyStateChange(IPlayer player, uint newKeys, uint oldKeys); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerCheckEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerCheckEventHandler.cs new file mode 100644 index 00000000..b36edbd5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerCheckEventHandler.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerCheckEventHandler +{ + /// + /// Called when a client check response is received. + /// + /// The player who sent the response. + /// The type of action being checked. + /// The memory address being checked. + /// The results of the check. + void OnClientCheckResponse(IPlayer player, int actionType, int address, int results); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerClickEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerClickEventHandler.cs new file mode 100644 index 00000000..450b91f1 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerClickEventHandler.cs @@ -0,0 +1,25 @@ +using System.Numerics; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerClickEventHandler +{ + /// + /// Called when a player clicks on the map. + /// + /// The player who clicked on the map. + /// The position on the map that was clicked. + void OnPlayerClickMap(IPlayer player, Vector3 pos); + + /// + /// Called when a player clicks on another player. + /// + /// The player who performed the click. + /// The player who was clicked. + /// The source of the click event. + void OnPlayerClickPlayer(IPlayer player, IPlayer clicked, PlayerClickSource source); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerConnectEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerConnectEventHandler.cs new file mode 100644 index 00000000..e5ed6ebb --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerConnectEventHandler.cs @@ -0,0 +1,35 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerConnectEventHandler +{ + /// + /// Called when a player is attempting to connect to the server. + /// + /// The player attempting to connect. + /// The IP address of the player. + /// The port used by the player. + void OnIncomingConnection(IPlayer player, string ipAddress, ushort port); + + /// + /// Called when a player successfully connects to the server. + /// + /// The player who connected. + void OnPlayerConnect(IPlayer player); + + /// + /// Called when a player disconnects from the server. + /// + /// The player who disconnected. + /// The reason for the disconnection. + void OnPlayerDisconnect(IPlayer player, PeerDisconnectReason reason); + + /// + /// Called when a player's client starts initializing. + /// + /// The player whose client has started initializing. + void OnPlayerClientInit(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerDamageEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerDamageEventHandler.cs new file mode 100644 index 00000000..7e5d2e0d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerDamageEventHandler.cs @@ -0,0 +1,36 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerDamageEventHandler +{ + /// + /// Called when a player dies. + /// + /// The player who died. + /// The player who killed the other player, or null if no killer. + /// The reason for the death (e.g., weapon ID). + void OnPlayerDeath(IPlayer player, IPlayer killer, int reason); + + /// + /// Called when a player takes damage. + /// + /// The player who took damage. + /// The player who caused the damage, or null if no specific source. + /// The amount of damage taken. + /// The weapon ID used to cause the damage. + /// The body part that was hit. + void OnPlayerTakeDamage(IPlayer player, IPlayer from, float amount, uint weapon, BodyPart part); + + /// + /// Called when a player gives damage to another player. + /// + /// The player who caused the damage. + /// The player who received the damage. + /// The amount of damage dealt. + /// The weapon ID used to deal the damage. + /// The body part that was hit. + void OnPlayerGiveDamage(IPlayer player, IPlayer to, float amount, uint weapon, BodyPart part); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs new file mode 100644 index 00000000..5d60a0c5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerPool.cs @@ -0,0 +1,211 @@ +using System.Numerics; +using System.Runtime.InteropServices.Marshalling; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// This type represents a pointer to an unmanaged open.mp interface. +/// +[OpenMpApi(typeof(IExtensible), typeof(IReadOnlyPool))] +public readonly partial struct IPlayerPool +{ + /// + /// Gets a set of all available players and bots (anything in the pool). + /// + public partial FlatPtrHashSet Entries(); + + /// + /// Gets a set of all available players only. + /// + public partial FlatPtrHashSet Players(); + + /// + /// Gets a set of all available bots only. + /// + public partial FlatPtrHashSet Bots(); + + /// + /// Gets a dispatcher for player spawn events. + /// + public partial IEventDispatcher GetPlayerSpawnDispatcher(); + + /// + /// Gets a dispatcher for player connection events. + /// + public partial IEventDispatcher GetPlayerConnectDispatcher(); + + /// + /// Gets a dispatcher for player streaming events. + /// + public partial IEventDispatcher GetPlayerStreamDispatcher(); + + /// + /// Gets a dispatcher for player text and command events. + /// + public partial IEventDispatcher GetPlayerTextDispatcher(); + + /// + /// Gets a dispatcher for player shooting events. + /// + public partial IEventDispatcher GetPlayerShotDispatcher(); + + /// + /// Gets a dispatcher for player data change events. + /// + public partial IEventDispatcher GetPlayerChangeDispatcher(); + + /// + /// Gets a dispatcher for player damage and death events. + /// + public partial IEventDispatcher GetPlayerDamageDispatcher(); + + /// + /// Gets a dispatcher for player clicking events. + /// + public partial IEventDispatcher GetPlayerClickDispatcher(); + + /// + /// Gets a dispatcher for player client check response events. + /// + public partial IEventDispatcher GetPlayerCheckDispatcher(); + + /// + /// Gets a dispatcher for player update events. + /// + public partial IEventDispatcher GetPlayerUpdateDispatcher(); + + /// + /// Gets a dispatcher for player pool events. + /// + public partial IEventDispatcher> GetPoolEventDispatcher(); + + /// + /// Checks if a name is taken by any player, excluding a specific player. + /// + /// The name to check. + /// The player to exclude from the check. + /// true if the name is taken; otherwise, false. + public partial bool IsNameTaken(string name, IPlayer skip); + + /// + /// Sends a client message to all players. + /// + /// The colour of the message. + /// The message to send. + public partial void SendClientMessageToAll(ref Colour colour, string message); + + /// + /// Sends a chat message to all players. + /// + /// The player sending the message. + /// The message to send. + public partial void SendChatMessageToAll(IPlayer from, string message); + + /// + /// Sends a game text message to all players. + /// + /// The message to display. + /// The duration to display the message. + /// The style of the message. + public partial void SendGameTextToAll(string message, [MarshalUsing(typeof(MillisecondsMarshaller))] TimeSpan time, int style); + + /// + /// Hides a game text message for all players. + /// + /// The style of the message to hide. + public partial void HideGameTextForAll(int style); + + /// + /// Sends a death message to all players. + /// + /// The player who killed. + /// The player who was killed. + /// The weapon used. + public partial void SendDeathMessageToAll(IPlayer killer, IPlayer killee, int weapon); + + /// + /// Sends an empty death message to all players. + /// + public partial void SendEmptyDeathMessageToAll(); + + /// + /// Creates an explosion for all players. + /// + /// The position of the explosion. + /// The type of explosion. + /// The radius of the explosion. + public partial void CreateExplosionForAll(Vector3 vec, int type, float radius); + + private partial void RequestPlayer(ref PeerNetworkData netData, ref PeerRequestParams parms, out Pair result); + + /// + /// Requests a new player with the given network parameters. + /// + /// The network data for the player. + /// The request parameters. + /// A tuple containing the result of the connection and the player instance. + public (NewConnectionResult, IPlayer) RequestPlayer(ref PeerNetworkData netData, ref PeerRequestParams parms) + { + RequestPlayer(ref netData, ref parms, out var result); + return result; + } + + /// + /// Broadcasts a packet to all players. + /// + /// The data span with the length in bits. + /// The channel to use. + /// The player to exclude from the broadcast. + /// Whether to dispatch packet-related events. + public partial void BroadcastPacket(SpanLite data, int channel, IPlayer skipFrom = default, bool dispatchEvents = true); + + /// + /// Broadcasts an RPC to all players. + /// + /// The RPC ID. + /// The data span with the length in bits. + /// The channel to use. + /// The player to exclude from the broadcast. + /// Whether to dispatch RPC-related events. + public partial void BroadcastRPC(int id, SpanLite data, int channel, IPlayer skipFrom = default, bool dispatchEvents = true); + + /// + /// Checks if a player name is valid. + /// + /// The name to validate. + /// true if the name is valid; otherwise, false. + public partial bool IsNameValid(string name); + + /// + /// Allows or disallows the use of a specific character in player names. + /// + /// The character to allow or disallow. + /// Whether to allow the character. + public partial void AllowNickNameCharacter(char character, bool allow); + + /// + /// Checks if a specific character is allowed in player names. + /// + /// The character to check. + /// true if the character is allowed; otherwise, false. + public partial bool IsNickNameCharacterAllowed(char character); + + /// + /// Gets the default colour assigned to a player ID when they first connect. + /// + /// The player ID. + /// The default colour. + public partial Colour GetDefaultColour(int pid); + + /// + /// Converts this instance to a read-only player pool. + /// + /// A read-only player pool. + public IReadOnlyPool AsPool() + { + return (IReadOnlyPool)this; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerShotEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerShotEventHandler.cs new file mode 100644 index 00000000..aaa9ff87 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerShotEventHandler.cs @@ -0,0 +1,53 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerShotEventHandler +{ + + /// + /// Called when a player fires a shot that does not hit any target. + /// + /// The player who fired the shot. + /// The data of the bullet fired. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerShotMissed(IPlayer player, ref PlayerBulletData bulletData); + + /// + /// Called when a player fires a shot that hits another player. + /// + /// The player who fired the shot. + /// The player who was hit by the shot. + /// The data of the bullet fired. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerShotPlayer(IPlayer player, IPlayer target, ref PlayerBulletData bulletData); + + /// + /// Called when a player fires a shot that hits a vehicle. + /// + /// The player who fired the shot. + /// The vehicle that was hit by the shot. + /// The data of the bullet fired. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerShotVehicle(IPlayer player, IVehicle target, ref PlayerBulletData bulletData); + + /// + /// Called when a player fires a shot that hits an object. + /// + /// The player who fired the shot. + /// The object that was hit by the shot. + /// The data of the bullet fired. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerShotObject(IPlayer player, IObject target, ref PlayerBulletData bulletData); + + /// + /// Called when a player fires a shot that hits a player-owned object. + /// + /// The player who fired the shot. + /// The player-owned object that was hit by the shot. + /// The data of the bullet fired. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerShotPlayerObject(IPlayer player, IPlayerObject target, ref PlayerBulletData bulletData); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerSpawnEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerSpawnEventHandler.cs new file mode 100644 index 00000000..ffcebdf1 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerSpawnEventHandler.cs @@ -0,0 +1,21 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerSpawnEventHandler +{ + /// + /// Called when a player requests to spawn. + /// + /// The player requesting to spawn. + /// if the player is allowed to spawn; otherwise, . + bool OnPlayerRequestSpawn(IPlayer player); + + /// + /// Called when a player successfully spawns. + /// + /// The player who has spawned. + void OnPlayerSpawn(IPlayer player); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerStreamEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerStreamEventHandler.cs new file mode 100644 index 00000000..4e44d202 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerStreamEventHandler.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerStreamEventHandler +{ + /// + /// Called when a player is streamed in for another player. + /// + /// The player being streamed in. + /// The player for whom the other player is being streamed in. + void OnPlayerStreamIn(IPlayer player, IPlayer forPlayer); + + /// + /// Called when a player is streamed out for another player. + /// + /// The player being streamed out. + /// The player for whom the other player is being streamed out. + void OnPlayerStreamOut(IPlayer player, IPlayer forPlayer); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerTextEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerTextEventHandler.cs new file mode 100644 index 00000000..02920c43 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerTextEventHandler.cs @@ -0,0 +1,24 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerTextEventHandler +{ + /// + /// Called when a player sends a text message. + /// + /// The player who sent the message. + /// The text message sent by the player. + /// if the message should be displayed; otherwise, . + bool OnPlayerText(IPlayer player, string message); + + /// + /// Called when a player sends a command text. + /// + /// The player who sent the command. + /// The command text sent by the player. + /// if the command has been processed; otherwise, . + bool OnPlayerCommandText(IPlayer player, string message); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/IPlayerUpdateEventHandler.cs b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerUpdateEventHandler.cs new file mode 100644 index 00000000..99cc97ef --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/IPlayerUpdateEventHandler.cs @@ -0,0 +1,18 @@ +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides the events for . +/// +[OpenMpEventHandler] +public partial interface IPlayerUpdateEventHandler +{ + /// + /// Called when a player has sent an update. + /// + /// The player being updated. + /// The current time point of the update. + /// if the event should be processed; otherwise, to ignore it. + bool OnPlayerUpdate(IPlayer player, TimePoint now); +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/MapIconStyle.cs b/src/SampSharp.OpenMp.Core/Api/Player/MapIconStyle.cs new file mode 100644 index 00000000..d0b2718f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/MapIconStyle.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the style of a map icon in the game. +/// +public enum MapIconStyle +{ + /// + /// The map icon is visible only if it is within the range of the minimap. + /// + Local, + + /// + /// The map icon is visible globally and will stick to the edge of the minimap if it is outside the minimap range. + /// + Global, + + /// + /// The map icon represents a local checkpoint and is visible only if it is within the range of the minimap. + /// + LocalCheckpoint, + + /// + /// The map icon represents a global checkpoint and will stick to the edge of the minimap if it is outside the minimap range. + /// + GlobalCheckpoint +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerAimData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAimData.cs new file mode 100644 index 00000000..5664b816 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAimData.cs @@ -0,0 +1,46 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the aiming data of a player, including camera position, direction, and weapon state. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerAimData +{ + /// + /// The forward vector of the player's camera, indicating the direction the camera is facing. + /// + public readonly Vector3 camFrontVector; + + /// + /// The position of the player's camera in the game world. + /// + public readonly Vector3 camPos; + + /// + /// The Z-axis aiming position of the player. + /// + public readonly float aimZ; + + /// + /// The zoom level of the player's camera. + /// + public readonly float camZoom; + + /// + /// The aspect ratio of the player's camera. + /// + public readonly float aspectRatio; + + /// + /// The current weapon state of the player. + /// + public readonly PlayerWeaponState weaponState; + + /// + /// The mode of the player's camera. + /// + public readonly byte camMode; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationData.cs new file mode 100644 index 00000000..d1b32c3b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationData.cs @@ -0,0 +1,20 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the animation data of a player, including animation ID and flags. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerAnimationData +{ + /// + /// The ID of the animation being played by the player. + /// + public readonly ushort ID; + + /// + /// The flags associated with the animation, indicating its properties or behavior. + /// + public readonly ushort flags; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationSyncType.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationSyncType.cs new file mode 100644 index 00000000..138f964a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerAnimationSyncType.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the synchronization type for a player's animation. +/// +public enum PlayerAnimationSyncType +{ + /// + /// The animation is not synchronized with other players. + /// + NoSync, + + /// + /// The animation is synchronized with other players. + /// + Sync, + + /// + /// The animation is synchronized with other players, but only affects others. + /// + SyncOthers +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletData.cs new file mode 100644 index 00000000..f08ddbe9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletData.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents data related to a player's bullet, including its origin, hit position, and other details. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerBulletData +{ + /// + /// The origin position of the bullet when it was fired. + /// + public readonly Vector3 origin; + + /// + /// The position where the bullet hit. + /// + public readonly Vector3 hitPos; + + /// + /// The offset of the bullet relative to the player's position. + /// + public readonly Vector3 offset; + + /// + /// The weapon ID used to fire the bullet. + /// + public readonly byte weapon; + + /// + /// The type of object or entity that was hit by the bullet. + /// + public readonly PlayerBulletHitType hitType; + + /// + /// The ID of the object or entity that was hit by the bullet. + /// + public readonly ushort hitID; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletHitType.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletHitType.cs new file mode 100644 index 00000000..c889a87f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerBulletHitType.cs @@ -0,0 +1,32 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the types of entities that can be hit by a player's bullet. +/// +public enum PlayerBulletHitType : byte +{ + /// + /// No entity was hit. + /// + None, + + /// + /// A player was hit. + /// + Player = 1, + + /// + /// A vehicle was hit. + /// + Vehicle = 2, + + /// + /// An object was hit. + /// + Object = 3, + + /// + /// A player object was hit. + /// + PlayerObject = 4 +} diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerCameraCutType.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerCameraCutType.cs new file mode 100644 index 00000000..8d7ec180 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerCameraCutType.cs @@ -0,0 +1,17 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the type of camera transition for a player. +/// +public enum PlayerCameraCutType +{ + /// + /// Instantly cuts the camera to the new position. + /// + Cut, + + /// + /// Smoothly moves the camera to the new position. + /// + Move +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerClickSource.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerClickSource.cs new file mode 100644 index 00000000..9dbed4be --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerClickSource.cs @@ -0,0 +1,12 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the source of a player's click action. +/// +public enum PlayerClickSource +{ + /// + /// The player clicked on the scoreboard. + /// + Scoreboard +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerFightingStyle.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerFightingStyle.cs new file mode 100644 index 00000000..7680b24f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerFightingStyle.cs @@ -0,0 +1,37 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the fighting styles a player can use. +/// +public enum PlayerFightingStyle +{ + /// + /// The normal fighting style. + /// + Normal = 4, + + /// + /// The boxing fighting style. + /// + Boxing = 5, + + /// + /// The kung fu fighting style. + /// + KungFu = 6, + + /// + /// The knee head fighting style. + /// + KneeHead = 7, + + /// + /// The grab kick fighting style. + /// + GrabKick = 15, + + /// + /// The elbow fighting style. + /// + Elbow = 16 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerKeyData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerKeyData.cs new file mode 100644 index 00000000..a4f732dd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerKeyData.cs @@ -0,0 +1,25 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the key data of a player, including pressed keys and directional input. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerKeyData +{ + /// + /// The bitmask representing the keys currently pressed by the player. + /// + public readonly uint keys; + + /// + /// The state of the up and down directional keys. + /// + public readonly ushort upDown; + + /// + /// The state of the left and right directional keys. + /// + public readonly ushort leftRight; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpecialAction.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpecialAction.cs new file mode 100644 index 00000000..1f7526d8 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpecialAction.cs @@ -0,0 +1,107 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the special actions a player can perform. +/// +public enum PlayerSpecialAction +{ + /// + /// No special action is being performed. + /// + None, + + /// + /// The player is ducking. + /// + Duck, + + /// + /// The player is using a jetpack. + /// + Jetpack, + + /// + /// The player is entering a vehicle. + /// + EnterVehicle, + + /// + /// The player is exiting a vehicle. + /// + ExitVehicle, + + /// + /// The player is performing the first dance animation. + /// + Dance1, + + /// + /// The player is performing the second dance animation. + /// + Dance2, + + /// + /// The player is performing the third dance animation. + /// + Dance3, + + /// + /// The player is performing the fourth dance animation. + /// + Dance4, + + /// + /// The player has their hands up. + /// + HandsUp = 10, + + /// + /// The player is using a cellphone. + /// + Cellphone, + + /// + /// The player is sitting. + /// + Sitting, + + /// + /// The player has stopped using a cellphone. + /// + StopCellphone, + + /// + /// The player is drinking beer. + /// + Beer = 20, + + /// + /// The player is smoking. + /// + Smoke, + + /// + /// The player is drinking wine. + /// + Wine, + + /// + /// The player is drinking Sprunk. + /// + Sprunk, + + /// + /// The player is cuffed. + /// + Cuffed, + + /// + /// The player is carrying something. + /// + Carry, + + /// + /// The player is performing the pissing action. + /// + Pissing = 68 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateData.cs new file mode 100644 index 00000000..3fdfc5b4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateData.cs @@ -0,0 +1,46 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents data related to a player's spectating state. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerSpectateData +{ + /// + /// Represents the type of spectating a player is performing. + /// + public enum ESpectateType + { + /// + /// The player is not spectating. + /// + None, + + /// + /// The player is spectating a vehicle. + /// + Vehicle, + + /// + /// The player is spectating another player. + /// + Player + } + + /// + /// Indicates whether the player is currently spectating. + /// + public readonly bool spectating; + + /// + /// The ID of the entity (player or vehicle) being spectated. + /// + public readonly int spectateID; + + /// + /// The type of entity being spectated. + /// + public readonly ESpectateType type; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateMode.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateMode.cs new file mode 100644 index 00000000..fa5612f9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSpectateMode.cs @@ -0,0 +1,22 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the spectate mode a player can use while spectating. +/// +public enum PlayerSpectateMode +{ + /// + /// The normal spectate mode, where the camera follows the target freely. + /// + Normal = 1, + + /// + /// The fixed spectate mode, where the camera remains in a fixed position. + /// + Fixed, + + /// + /// The side spectate mode, where the camera follows the target from the side. + /// + Side +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerState.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerState.cs new file mode 100644 index 00000000..0d7476b0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerState.cs @@ -0,0 +1,57 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the various states a player can be in during gameplay. +/// +public enum PlayerState +{ + /// + /// The player is not in any specific state. + /// + None = 0, + + /// + /// The player is on foot. + /// + OnFoot = 1, + + /// + /// The player is driving a vehicle. + /// + Driver = 2, + + /// + /// The player is a passenger in a vehicle. + /// + Passenger = 3, + + /// + /// The player is exiting a vehicle. + /// + ExitVehicle = 4, + + /// + /// The player is entering a vehicle as the driver. + /// + EnterVehicleDriver = 5, + + /// + /// The player is entering a vehicle as a passenger. + /// + EnterVehiclePassenger = 6, + + /// + /// The player is wasted (dead). + /// + Wasted = 7, + + /// + /// The player has spawned in the game world. + /// + Spawned = 8, + + /// + /// The player is spectating another player or entity. + /// + Spectating = 9 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerSurfingData.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSurfingData.cs new file mode 100644 index 00000000..b31dc0a5 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerSurfingData.cs @@ -0,0 +1,52 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents data related to a player's surfing state, such as the type of surface and its offset. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct PlayerSurfingData +{ + /// + /// Represents the type of surface the player is surfing on. + /// + public enum Type + { + /// + /// The player is not surfing on any surface. + /// + None, + + /// + /// The player is surfing on a vehicle. + /// + Vehicle, + + /// + /// The player is surfing on an object. + /// + Object, + + /// + /// The player is surfing on a player-owned object. + /// + PlayerObject + } + + /// + /// The type of surface the player is surfing on. + /// + public readonly Type type; + + /// + /// The ID of the surface the player is surfing on. + /// + public readonly int ID; + + /// + /// The offset of the player relative to the surface they are surfing on. + /// + public readonly Vector3 offset; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeapon.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeapon.cs new file mode 100644 index 00000000..35b63a87 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeapon.cs @@ -0,0 +1,257 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the weapons a player can use in the game. +/// +public enum PlayerWeapon +{ + /// + /// The player is unarmed and using fists. + /// + Fist, + + /// + /// The brass knuckle weapon. + /// + BrassKnuckle, + + /// + /// The golf club weapon. + /// + GolfClub, + + /// + /// The nightstick weapon. + /// + NiteStick, + + /// + /// The knife weapon. + /// + Knife, + + /// + /// The baseball bat weapon. + /// + Bat, + + /// + /// The shovel weapon. + /// + Shovel, + + /// + /// The pool stick weapon. + /// + PoolStick, + + /// + /// The katana weapon. + /// + Katana, + + /// + /// The chainsaw weapon. + /// + Chainsaw, + + /// + /// The first dildo weapon. + /// + Dildo, + + /// + /// The second dildo weapon. + /// + Dildo2, + + /// + /// The first vibrator weapon. + /// + Vibrator, + + /// + /// The second vibrator weapon. + /// + Vibrator2, + + /// + /// The flower weapon. + /// + Flower, + + /// + /// The cane weapon. + /// + Cane, + + /// + /// The grenade weapon. + /// + Grenade, + + /// + /// The tear gas weapon. + /// + Teargas, + + /// + /// The Molotov cocktail weapon. + /// + Moltov, + + /// + /// The Colt .45 weapon. + /// + Colt45 = 22, + + /// + /// The silenced pistol weapon. + /// + Silenced, + + /// + /// The Desert Eagle weapon. + /// + Deagle, + + /// + /// The shotgun weapon. + /// + Shotgun, + + /// + /// The sawed-off shotgun weapon. + /// + Sawedoff, + + /// + /// The SPAS-12 shotgun weapon. + /// + Shotgspa, + + /// + /// The UZI weapon. + /// + UZI, + + /// + /// The MP5 weapon. + /// + MP5, + + /// + /// The AK-47 weapon. + /// + AK47, + + /// + /// The M4 weapon. + /// + M4, + + /// + /// The TEC-9 weapon. + /// + TEC9, + + /// + /// The rifle weapon. + /// + Rifle, + + /// + /// The sniper rifle weapon. + /// + Sniper, + + /// + /// The rocket launcher weapon. + /// + RocketLauncher, + + /// + /// The heat-seeking rocket launcher weapon. + /// + HeatSeeker, + + /// + /// The flamethrower weapon. + /// + FlameThrower, + + /// + /// The minigun weapon. + /// + Minigun, + + /// + /// The satchel charge weapon. + /// + Satchel, + + /// + /// The bomb weapon. + /// + Bomb, + + /// + /// The spray can weapon. + /// + SprayCan, + + /// + /// The fire extinguisher weapon. + /// + FireExtinguisher, + + /// + /// The camera weapon. + /// + Camera, + + /// + /// The night vision goggles. + /// + Night_Vis_Goggles, + + /// + /// The thermal goggles. + /// + Thermal_Goggles, + + /// + /// The parachute. + /// + Parachute, + + /// + /// The vehicle weapon, used for vehicle-related damage. + /// + Vehicle = 49, + + /// + /// The helicopter blades weapon. + /// + Heliblades, + + /// + /// The explosion weapon. + /// + Explosion, + + /// + /// The drowning weapon, used for drowning-related damage. + /// + Drown = 53, + + /// + /// The collision weapon, used for collision-related damage. + /// + Collision, + + /// + /// The end of the weapon list. + /// + End +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponExtensions.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponExtensions.cs new file mode 100644 index 00000000..9f1c9e74 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponExtensions.cs @@ -0,0 +1,83 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Provides extension methods for the enum. +/// +public static class PlayerWeaponExtensions +{ + private static readonly string[] _names = + [ + "Fist", + "Brass Knuckles", + "Golf Club", + "Nite Stick", + "Knife", + "Baseball Bat", + "Shovel", + "Pool Cue", + "Katana", + "Chainsaw", + "Dildo", + "Dildo", + "Vibrator", + "Vibrator", + "Flowers", + "Cane", + "Grenade", + "Teargas", + "Molotov Cocktail", // 18 + "Invalid", + "Invalid", + "Invalid", + "Colt 45", // 22 + "Silenced Pistol", + "Desert Eagle", + "Shotgun", + "Sawn-off Shotgun", + "Combat Shotgun", + "UZI", + "MP5", + "AK47", + "M4", + "TEC9", + "Rifle", + "Sniper Rifle", + "Rocket Launcher", + "Heat Seaker", + "Flamethrower", + "Minigun", + "Satchel Explosives", + "Bomb", + "Spray Can", + "Fire Extinguisher", + "Camera", + "Night Vision Goggles", + "Thermal Goggles", + "Parachute", // 46 + "Invalid", + "Invalid", + "Vehicle", // 49 + "Helicopter Blades", // 50 + "Explosion", // 51 + "Invalid", + "Drowned", // 53 + "Splat" + ]; + + /// + /// Gets the name of the specified . + /// + /// The weapon to get the name for. + /// The name of the weapon, or "Invalid" if the weapon is not valid. + public static string GetName(this PlayerWeapon weapon) + { + var number = (int)weapon; + + if (number < 0 || number >= _names.Length) + { + return "Invalid"; + } + + return _names[number]; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponSkill.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponSkill.cs new file mode 100644 index 00000000..19d7141b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponSkill.cs @@ -0,0 +1,67 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the weapon skills a player can have. +/// +public enum PlayerWeaponSkill +{ + /// + /// Invalid weapon skill. + /// + Invalid = -1, + + /// + /// The skill level for pistols. + /// + Pistol, + + /// + /// The skill level for silenced pistols. + /// + SilencedPistol, + + /// + /// The skill level for Desert Eagle pistols. + /// + DesertEagle, + + /// + /// The skill level for shotguns. + /// + Shotgun, + + /// + /// The skill level for sawn-off shotguns. + /// + SawnOff, + + /// + /// The skill level for SPAS-12 shotguns. + /// + SPAS12, + + /// + /// The skill level for Uzi submachine guns. + /// + Uzi, + + /// + /// The skill level for MP5 submachine guns. + /// + MP5, + + /// + /// The skill level for AK-47 assault rifles. + /// + AK47, + + /// + /// The skill level for M4 assault rifles. + /// + M4, + + /// + /// The skill level for sniper rifles. + /// + Sniper +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponState.cs b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponState.cs new file mode 100644 index 00000000..4b5455b3 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/PlayerWeaponState.cs @@ -0,0 +1,32 @@ +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the state of a player's weapon. +/// +public enum PlayerWeaponState : sbyte +{ + /// + /// The weapon state is unknown. + /// + Unknown = -1, + + /// + /// The weapon has no bullets remaining. + /// + NoBullets, + + /// + /// The weapon has only one bullet remaining. + /// + LastBullet, + + /// + /// The weapon has more than one bullet remaining. + /// + MoreBullets, + + /// + /// The weapon is currently reloading. + /// + Reloading +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/SkillsArray.cs b/src/SampSharp.OpenMp.Core/Api/Player/SkillsArray.cs new file mode 100644 index 00000000..dd5f5328 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/SkillsArray.cs @@ -0,0 +1,16 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents an array of skill levels for a player. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct SkillsArray +{ + /// + /// The array of skill levels, where each index corresponds to a specific skill. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = OpenMpConstants.NUM_SKILL_LEVELS)] + public readonly ushort[] Values; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlotData.cs b/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlotData.cs new file mode 100644 index 00000000..11eb9684 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlotData.cs @@ -0,0 +1,33 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents data for a weapon slot, including the weapon ID and the amount of ammunition. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct WeaponSlotData +{ + /// + /// Initializes a new instance of the struct. + /// + /// The ID of the weapon in the slot. + /// The amount of ammunition for the weapon. + public WeaponSlotData(byte id, int ammo) + { + Id = id; + Ammo = ammo; + } + + /// + /// Gets the ID of the weapon in the slot. + /// + public readonly byte Id; + + /// + /// Gets the amount of ammunition for the weapon in the slot. + /// + public readonly int Ammo; + + // TODO: WeaponInfo related extensions from open.mp-sdk. +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlots.cs b/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlots.cs new file mode 100644 index 00000000..b1aeab88 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/Player/WeaponSlots.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents the weapon slots of a player, containing data for each weapon slot. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct WeaponSlots +{ + /// + /// The maximum number of weapon slots available. + /// + public const int MAX_WEAPON_SLOTS = 13; + + /// + /// The array of weapon slot data, where each index corresponds to a specific weapon slot. + /// + [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAX_WEAPON_SLOTS)] + public readonly WeaponSlotData[] Data; + + /// + /// Initializes a new instance of the struct. + /// + /// The array of weapon slot data. + /// Thrown if the length of is not equal to . + public WeaponSlots(WeaponSlotData[] data) + { + if (data.Length != MAX_WEAPON_SLOTS) + { + throw new ArgumentException("Slot count should be MAX_WEAPON_SLOTS", nameof(data)); + } + + Data = data; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/SemanticVersion.cs b/src/SampSharp.OpenMp.Core/Api/SemanticVersion.cs new file mode 100644 index 00000000..b49b96b9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/SemanticVersion.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a semantic version. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct SemanticVersion +{ + /// + /// The major version number. + /// + public readonly byte Major; + + /// + /// The minor version number. + /// + public readonly byte Minor; + + /// + /// The patch version number. + /// + public readonly byte Patch; + + /// + /// The pre-release version number. + /// + public readonly ushort Prerel; + + /// + /// Converts this to a . + /// + /// The version representation of this semantic version. + public Version AsVersion() + { + return new Version(Major, Minor, Patch, Prerel); + } + + /// + public override string ToString() + { + return $"{Major}.{Minor}.{Patch}.{Prerel}"; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Api/UID.cs b/src/SampSharp.OpenMp.Core/Api/UID.cs new file mode 100644 index 00000000..90d51a7d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Api/UID.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Api; + +/// +/// Represents a unique identifier. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct UID +{ + private readonly ulong _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The underlying value. + public UID(ulong value) + { + _value = value; + } + + /// + public override string ToString() + { + return _value.ToString("x16"); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/NumberedTypeGeneratorAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/NumberedTypeGeneratorAttribute.cs new file mode 100644 index 00000000..828d412a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/NumberedTypeGeneratorAttribute.cs @@ -0,0 +1,22 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// This attribute marks a type which should be cloned by the source generator with a changed constant field value. This type is mainly used by and related types. +/// +/// The name of the field which should be updated. +/// The updated value to set to the field. +[AttributeUsage(AttributeTargets.Struct, AllowMultiple = true)] +public class NumberedTypeGeneratorAttribute(string fieldName, int value) : Attribute +{ + /// + /// Gets the name of the field which should be updated. + /// + public string FieldName { get; } = fieldName; + + /// + /// Gets the updated value to set to the field. + /// + public int Value { get; } = value; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiAttribute.cs new file mode 100644 index 00000000..080c4d5e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiAttribute.cs @@ -0,0 +1,31 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// This attributes marks a struct as an open.mp API interface. The struct must be marked as partial. All partial +/// methods in the struct will be considered as open.mp API functions and will have P/Invoke implementations generated +/// by the source generator. The invoked function name will be +/// {interface}_{method_in_pascalCase}{overloadAppendix}. For example for the function IConfig.RemoveBan, +/// the C function will be IConfig_removeBan. The overload appendix can be controlled using the . +/// +/// Specifies which open.mp API interface struct types this struct implements. Equality +/// members, cast operators and forwarding methods will be generated for all specified interfaces. +[AttributeUsage(AttributeTargets.Struct)] +public class OpenMpApiAttribute(params Type[] baseTypeList) : Attribute +{ + /// + /// Gets which open.mp API interface struct types this struct implements. Equality members, cast operators and + /// forwarding methods will be generated for all specified interfaces. + /// + public Type[] BaseTypeList { get; } = baseTypeList; + + /// + /// Gets or sets the name of the component library that contains the open.mp API functions. Defaults to "SampSharp". + /// + public string Library { get; set; } = "SampSharp"; + + /// + /// Gets or sets the name of the open.mp API interface. Defaults to the struct name. + /// + public string? NativeTypeName { get; set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiFunctionAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiFunctionAttribute.cs new file mode 100644 index 00000000..90294559 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiFunctionAttribute.cs @@ -0,0 +1,15 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Specifies the name of the open.mp API function that this method represents. By default, the name of the method +/// converted to camel case is used as the function name. +/// +/// The name of the open.mp API function. +[AttributeUsage(AttributeTargets.Method)] +public class OpenMpApiFunctionAttribute(string functionName) : Attribute +{ + /// + /// Gets the name of the open.mp API function. + /// + public string FunctionName { get; } = functionName; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiOverloadAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiOverloadAttribute.cs new file mode 100644 index 00000000..38f7f27a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiOverloadAttribute.cs @@ -0,0 +1,15 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Specifies the overload name to append to the open.mp API function name. For example, if overload is "_index" for +/// IConfig.RemoveBan, the C function will be "IConfig_removeBan_index". +/// +/// The overload name to append to the function name. +[AttributeUsage(AttributeTargets.Method)] +public class OpenMpApiOverloadAttribute(string overload) : Attribute +{ + /// + /// Gets the overload name to append to the function name. + /// + public string Overload { get; } = overload; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiPartialAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiPartialAttribute.cs new file mode 100644 index 00000000..7f569abd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpApiPartialAttribute.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// This attribute marks an open.mp API interface struct as partial. Partial API structs will not implement their own managed interface. +/// +[AttributeUsage(AttributeTargets.Struct)] +public class OpenMpApiPartialAttribute : Attribute; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpEventHandlerAttribute.cs b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpEventHandlerAttribute.cs new file mode 100644 index 00000000..ffa5e7ff --- /dev/null +++ b/src/SampSharp.OpenMp.Core/CodeGeneration/OpenMpEventHandlerAttribute.cs @@ -0,0 +1,19 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// This attributes marks an interface as an open.mp event handler. The interface must be marked as partial. +/// +[AttributeUsage(AttributeTargets.Interface)] +public class OpenMpEventHandlerAttribute : Attribute +{ + /// + /// Gets or sets the name of the library that contains the native event handler. Defaults to "SampSharp". + /// + public string Library { get; set; } = "SampSharp"; + + /// + /// The name of the open.mp event handler. Defaults to the interface name without the first character (if the + /// interface name starts with an 'I'). + /// + public string? NativeTypeName { get; set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/ConsoleLogger/LoggerTextWriter.cs b/src/SampSharp.OpenMp.Core/ConsoleLogger/LoggerTextWriter.cs new file mode 100644 index 00000000..ce6c0b61 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/ConsoleLogger/LoggerTextWriter.cs @@ -0,0 +1,106 @@ +using System.Text; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// A text writer that writes to the open.mp logger. +/// +/// The logger to write to. +/// The log level at which to write to the logger. +public class LoggerTextWriter(ILogger logger, LogLevel logLevel) : TextWriter +{ + private readonly StringBuilder _buffer = new(); + + /// + public override Encoding Encoding => Encoding.UTF8; + + /// + public override void Write(char value) + { + if (value == '\n') + { + WriteBuffer(); + } + else + { + _buffer.Append(value); + } + } + + /// + public override void Write(string? value) + { + if (value == null) + { + return; + } + + if (value.Contains('\n')) + { + foreach (var ch in value) + { + Write(ch); + } + } + else + { + _buffer.Append(value); + } + } + + /// + public override void WriteLine(string? value) + { + if (value == null) + { + WriteBuffer(); + return; + } + + if (value.Contains('\n')) + { + foreach (var line in value.Split('\n')) + { + WriteLine(line.Trim('\r')); + } + return; + } + + if (_buffer.Length > 0) + { + _buffer.Append(value); + WriteBuffer(); + } + else + { + WriteLineToLogger(value); + } + } + + /// + public override void WriteLine() + { + WriteBuffer(); + } + + /// + public override void Flush() + { + if (_buffer.Length > 0) + { + WriteBuffer(); + } + } + + private void WriteBuffer() + { + WriteLineToLogger(_buffer.ToString()); + _buffer.Clear(); + } + + private void WriteLineToLogger(string? line) + { + logger.LogLine(logLevel, line ?? string.Empty); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/ConsoleLogger/StartupContextLoggingExtensions.cs b/src/SampSharp.OpenMp.Core/ConsoleLogger/StartupContextLoggingExtensions.cs new file mode 100644 index 00000000..451f4385 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/ConsoleLogger/StartupContextLoggingExtensions.cs @@ -0,0 +1,23 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides extension methods related to open.mp logging for the . +/// +public static class StartupContextLoggingExtensions +{ + /// + /// When called, sets the console output and error streams to log to the open.mp logger. + /// + /// The startup context. + /// The startup context. + public static IStartupContext UseOpenMpLogger(this IStartupContext context) + { + Console.SetOut(new LoggerTextWriter((ILogger)context.Core, LogLevel.Message)); + Console.SetError(new LoggerTextWriter((ILogger)context.Core, LogLevel.Error)); + + return context; + } + +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/ExceptionHandler.cs b/src/SampSharp.OpenMp.Core/ExceptionHandler.cs new file mode 100644 index 00000000..e774147d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/ExceptionHandler.cs @@ -0,0 +1,8 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Represents a method that handles exceptions. +/// +/// The context in which the exception has occurred. +/// The exception which has occurred. +public delegate void ExceptionHandler(string context, Exception exception); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Extensions/Extension.cs b/src/SampSharp.OpenMp.Core/Extensions/Extension.cs new file mode 100644 index 00000000..a263c637 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Extensions/Extension.cs @@ -0,0 +1,194 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// Represents a base class for managed extensions which can be attached to extensible open.mp entities. The implementation must have an [Extension(...)] attribute. +/// +public abstract partial class Extension : IDisposable +{ + // Must keep a reference to this delegate to prevent it from being garbage collected + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable + private readonly Action _free; + + private nint? _unmanagedCounterpart; + private GCHandle? _gcHandle; + private IExtensible? _appliedTo; + + /// + /// Initializes a new instance of the class. + /// + protected Extension() + { + _free = FreeExtension; + var id = ExtensionIdProvider.GetId(GetType()); + var free = Marshal.GetFunctionPointerForDelegate(_free); + var handle = GCHandle.Alloc(this, GCHandleType.Normal); + + _gcHandle = handle; + _unmanagedCounterpart = CreateUnmanaged(id, free, GCHandle.ToIntPtr(handle)); + } + + /// + /// Finalizes an instance of the class. + /// + ~Extension() + { + // Theoretically, this should never be called, as one of the unmanaged resources is a GC handle pointing to this + // object. But just to be sure, we'll free the resources here as well. + FreeUnmanagedResources(); + } + + /// + /// Gets a value indicating whether this extension has been disposed. + /// + [MemberNotNullWhen(false, nameof(_gcHandle), nameof(_unmanagedCounterpart))] + protected bool IsDisposed => !_unmanagedCounterpart.HasValue; + + /// + /// Detaches this extension from the extensible it is currently applied to and destroys all resources held by this extension. + /// + public void Dispose() + { + Detach(); + + FreeUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// This method is called when the extension is being cleaned up. The extension may be cleaned up because it is + /// being removed from an extensible or because the extensible is being disposed. + /// + protected virtual void Cleanup() + { + } + + /// + /// Gets the managed extension counterpart of the specified unmanaged extension. + /// + /// The unmanaged extension. + /// The managed extension or if the unmanaged extension is in an invalid state. + internal static Extension? Get(IExtension ext) + { + var handle = GetHandle(ext.Handle); + var gcHandle = GCHandle.FromIntPtr(handle); + + if (!gcHandle.IsAllocated) + { + return null; + } + + return gcHandle.Target as Extension; + } + + /// + /// Gets a pointer to the unmanaged counterpart of this extension. + /// + /// A pointer. + internal IExtension GetUnmanaged() + { + ObjectDisposedException.ThrowIf(IsDisposed, GetType()); + + return new IExtension(_unmanagedCounterpart.Value); + } + + /// + /// Frees all unmanaged resources held by this extension. + /// + private void FreeUnmanagedResources() + { + FreeUnmanagedCounterpart(); + FreeHandle(); + } + + /// + /// Free the unmanaged counterpart of this extension. + /// + private void FreeUnmanagedCounterpart() + { + if (_unmanagedCounterpart.HasValue) + { + Delete(_unmanagedCounterpart.Value); + _unmanagedCounterpart = null; + _appliedTo = null; + } + } + + /// + /// Frees the GC handle held by this extension. + /// + private void FreeHandle() + { + if (_gcHandle.HasValue) + { + _gcHandle.Value.Free(); + _gcHandle = null; + } + } + + /// + /// Removes the extension from the extensible it is currently applied to. + /// + private void Detach() + { + if (_appliedTo.HasValue && _unmanagedCounterpart.HasValue) + { + _appliedTo.Value.RemoveExtension(new IExtension(_unmanagedCounterpart.Value)); + _appliedTo = null; + } + } + + /// + /// Attaches the extension to the specified extensible. + /// + /// The extensible entity to attach this extension to. + /// Throw if this extension was already attached to another + /// entity. + internal void Attach(IExtensible extensible) + { + if (_appliedTo.HasValue) + { + throw new InvalidOperationException("Can only apply to one extensible"); + } + + _appliedTo = extensible; + } + + /// + /// Entry-point from the unmanaged side to free the extension. + /// + private void FreeExtension() + { + try + { + Cleanup(); + _appliedTo = null; + } + catch(Exception e) + { + SampSharpExceptionHandler.HandleException($"{GetType().FullName}.FreeExtension",e); + } + finally + { + FreeHandle(); + + // no need to clean up the unmanaged counterpart - it's already been cleaned up by the caller + _unmanagedCounterpart = null; + } + } + + [LibraryImport("SampSharp", EntryPoint = "ManagedExtensionImpl_create")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static partial nint CreateUnmanaged(UID id, nint freePointer, nint handle); + + [LibraryImport("SampSharp", EntryPoint = "ManagedExtensionImpl_delete")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static partial void Delete(nint handle); + + [LibraryImport("SampSharp", EntryPoint = "ManagedExtensionImpl_getHandle")] + [UnmanagedCallConv(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] + private static partial nint GetHandle(nint handle); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Extensions/ExtensionAttribute.cs b/src/SampSharp.OpenMp.Core/Extensions/ExtensionAttribute.cs new file mode 100644 index 00000000..9610269a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Extensions/ExtensionAttribute.cs @@ -0,0 +1,16 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides a unique identifier for an extension. +/// +/// The unique identifier of the extension. +[AttributeUsage(AttributeTargets.Class, Inherited = false)] +public class ExtensionAttribute(ulong uid) : Attribute +{ + /// + /// Gets the unique identifier of the extension. + /// + public UID Uid => new(uid); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Extensions/ExtensionIdProvider.cs b/src/SampSharp.OpenMp.Core/Extensions/ExtensionIdProvider.cs new file mode 100644 index 00000000..b9e45f14 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Extensions/ExtensionIdProvider.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +internal static class ExtensionIdProvider where T : Extension +{ + public static UID Id { get; } = ExtensionIdProvider.GetId(typeof(T)); +} + +internal static class ExtensionIdProvider +{ + private static readonly ConcurrentDictionary _idCache = new(); + + public static UID GetId(Type type) + { + return _idCache.GetOrAdd(type, static t => + { + if (Attribute.GetCustomAttribute(t, typeof(ExtensionAttribute), false) is not ExtensionAttribute attribute) + { + throw new ArgumentException("Type does not have an ExtensionAttribute.", nameof(type)); + } + return attribute.Uid; + }); + } +} diff --git a/src/SampSharp.OpenMp.Core/IStartup.cs b/src/SampSharp.OpenMp.Core/IStartup.cs new file mode 100644 index 00000000..8713c5c9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/IStartup.cs @@ -0,0 +1,13 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Provides methods for initializing a SampSharp application. +/// +public interface IStartup +{ + /// + /// Initializes the application with the specified . + /// + /// The context to use for initialization. + void Initialize(IStartupContext context); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/IStartupContext.cs b/src/SampSharp.OpenMp.Core/IStartupContext.cs new file mode 100644 index 00000000..cedcc93b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/IStartupContext.cs @@ -0,0 +1,44 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides the properties and events which provide context to the startup of the application. +/// +public interface IStartupContext +{ + /// + /// Gets the open.mp component list. + /// + IComponentList ComponentList { get; } + + /// + /// Gets the configured startup configurator. + /// + IStartup Configurator { get; } + + /// + /// Gets the open.mp core. + /// + ICore Core { get; } + + /// + /// Gets information about the SampSharp open.mp component. + /// + SampSharpInfo Info { get; } + + /// + /// Gets or sets the handler for unhandled exceptions which may occur during the application's lifetime. + /// + ExceptionHandler UnhandledExceptionHandler { get; set; } + + /// + /// Occurs when the application is being cleaned up. + /// + event EventHandler? Cleanup; + + /// + /// Occurs when the application has been initialized. + /// + event EventHandler? Initialized; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/IUnmanagedInterface.cs b/src/SampSharp.OpenMp.Core/IUnmanagedInterface.cs new file mode 100644 index 00000000..abdc9612 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/IUnmanagedInterface.cs @@ -0,0 +1,18 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Provides the methods to work with structs which represent pointers to open.mp interfaces. This interface is the +/// managed counterpart the unmanaged interface. +/// +public interface IUnmanagedInterface +{ + /// + /// Gets the handle to the unmanaged interface instance. + /// + nint Handle { get; } + + /// + /// Gets a value indicating whether the pointer has a value. + /// + bool HasValue { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/LaunchInstructions.cs b/src/SampSharp.OpenMp.Core/LaunchInstructions.cs new file mode 100644 index 00000000..1134ec40 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/LaunchInstructions.cs @@ -0,0 +1,217 @@ +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace SampSharp.OpenMp.Core; + +internal static partial class LaunchInstructions +{ + private static readonly Regex _slnProjectRegex = ProjectInSolutionRegex(); + + public static void Write() + { + Console.WriteLine("-------------------------------------"); + Console.WriteLine("SampSharp"); + Console.WriteLine("-------------------------------------"); + Console.ForegroundColor = ConsoleColor.DarkRed; + Console.WriteLine("ERROR: This SampSharp gamemode must be run using an open.mp server."); + Console.ResetColor(); + Console.WriteLine("See <> for more information."); + Console.WriteLine(); + + if (!IsRunningInVisualStudio()) + { + return; + } + + var dir = GetProjectDir(); + if (dir != null) + { + Console.WriteLine("It appears you are running this application in Visual Studio. Would you like SampSharp to update your launchSettings.json with the following configuration?"); + + Console.WriteLine(); + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine(""" + ------------------------------------- + { + "profiles": { + "open.mp": { + "commandName": "Executable", + "executablePath": "C:\path\to\server\omp-server.exe", + "workingDirectory": "C:\path\to\server\", + "commandLineArgs": "-c sampsharp.directory=$(TargetDir) -c sampsharp.assembly=\"$(TargetName)\"" + } + } + } + ------------------------------------- + """); + Console.WriteLine(); + Console.WriteLine($"Detected project path: {dir}"); + Console.ResetColor(); + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("WARNING: This will replace any existing launch profiles for your project."); + Console.ResetColor(); + + if (!PromptYesNo()) + { + return; + } + + var props = dir.CreateSubdirectory("Properties"); + + var serverDir = PromptServerDirectory(); + + var launchSettingsPath = Path.Combine(props.FullName, "launchSettings.json"); + + serverDir = serverDir.Replace(@"\", @"\\"); + + File.WriteAllText(launchSettingsPath, + $$""" + { + "profiles": { + "open.mp": { + "commandName": "Executable", + "executablePath": "{{serverDir}}omp-server.exe", + "workingDirectory": "{{serverDir}}", + "commandLineArgs": "-c sampsharp.directory=$(TargetDir) -c sampsharp.assembly=\"$(TargetName)\"" + } + } + } + """); + + Console.WriteLine($"File written to {launchSettingsPath}"); + Console.WriteLine(); + + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine("You will find the 'open.mp' launch option in the dropdown next to the 'Start' button in Visual Studio."); + Console.ResetColor(); + } + } + + private static bool PromptYesNo() + { + Console.Write("Press (y)es / (n)o: "); + while (true) + { + var key = Console.ReadKey(); + + switch (key.Key) + { + case ConsoleKey.N: + Console.WriteLine(); + return false; + case ConsoleKey.Y: + Console.WriteLine(); + return true; + } + } + + } + + private static string PromptServerDirectory() + { + while (true) + { + Console.Write("Enter the path to your open.mp server directory: "); + + var dir = Console.ReadLine(); + if (!Directory.Exists(dir)) + { + Console.WriteLine("Directory not found."); + continue; + } + + var exe = Path.Combine(dir, "omp-server.exe"); + + if (!File.Exists(exe)) + { + Console.WriteLine("Invalid directory."); + continue; + } + + if (!dir.EndsWith('/') && !dir.EndsWith('\\')) + { + dir += Path.DirectorySeparatorChar; + } + return dir; + } + } + + private static bool IsRunningInVisualStudio() + { + return Debugger.IsAttached && Environment.GetEnvironmentVariable("VisualStudioVersion") != null; + } + + private static DirectoryInfo? GetProjectDir() + { + var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); + for (var i = 0; i < 10; i++) + { + // does this dir contain a .csproj file? assume this dir is the project root + var csproj = dir.GetFiles("*.csproj"); + if (csproj.Length != 0) + { + return dir; + } + + // does this dir contain a .sln file? read it and find the project root + var sln = dir.GetFiles("*.sln"); + var result = sln.Select(GetProjectDirFromSolution).FirstOrDefault(x => x != null); + if (result != null) + { + return result; + } + + dir = dir.Parent; + if (dir == null) + { + break; + } + } + + return null; + } + + private static DirectoryInfo? GetProjectDirFromSolution(FileInfo sln) + { + var asmName = Assembly.GetEntryAssembly()?.GetName().Name; + + if (asmName == null) + { + return null; + } + + var matches = new List(); + var lines = File.ReadAllLines(sln.FullName); + + foreach (var line in lines) + { + var match = _slnProjectRegex.Match(line); + if (match.Success) + { + var path = match.Groups["path"].Value; + var name = match.Groups["name"].Value; + + if (name.Contains(asmName)) + { + matches.Add(path); + } + } + } + + if (matches.Count > 0) + { + var best = matches.OrderBy(x => x.Length).First(); + var file = new DirectoryInfo(Path.GetDirectoryName(Path.Combine(sln.Directory!.FullName, best))!); + return file; + } + + return null; + } + + [GeneratedRegex(""" + Project\("{[0-9A-Fa-f\-]+}"\)\s*=\s*"(?[^"]+)"\s*,\s*"(?[^"]+)" + """)] + private static partial Regex ProjectInSolutionRegex(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Marshalling/BlittableBoolean.cs b/src/SampSharp.OpenMp.Core/Marshalling/BlittableBoolean.cs new file mode 100644 index 00000000..a8e7ce23 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Marshalling/BlittableBoolean.cs @@ -0,0 +1,77 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Represents a blittable boolean value. That is, a boolean value that is represented in memory as a single byte. +/// +/// The boolean value +public readonly struct BlittableBoolean(bool value) : IEquatable +{ + private const byte True = 1; + private const byte False = 0; + + private readonly byte _data = value ? True : False; + + /// + public bool Equals(BlittableBoolean other) + { + return (bool)this == (bool)other; + } + + /// + public override bool Equals(object? obj) + { + return (obj is BlittableBoolean other && Equals(other)) || (obj is bool b && (bool)this == b); + } + + /// + public override int GetHashCode() + { + return ((bool)this).GetHashCode(); + } + + /// + /// Compares two values for equality. + /// + /// The left hand side value. + /// The right hand side value. + /// The result of the comparison. + public static bool operator ==(BlittableBoolean lhs, BlittableBoolean rhs) + { + return lhs.Equals(rhs); + } + + /// + /// Compares two values for inequality. + /// + /// The left hand side value. + /// The right hand side value. + /// The result of the comparison. + public static bool operator !=(BlittableBoolean lhs, BlittableBoolean rhs) + { + return !lhs.Equals(rhs); + } + + /// + /// Implicitly converts a to a . + /// + /// The value to convert. + public static implicit operator bool(BlittableBoolean b) + { + return b._data != 0; + } + + /// + /// Implicitly converts a to a . + /// + /// The value to convert. + public static implicit operator BlittableBoolean(bool b) + { + return new BlittableBoolean(b); + } + + /// + public override string ToString() + { + return ((bool)this).ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Marshalling/BlittableRef.cs b/src/SampSharp.OpenMp.Core/Marshalling/BlittableRef.cs new file mode 100644 index 00000000..edabf70e --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Marshalling/BlittableRef.cs @@ -0,0 +1,63 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core +{ + /// + /// Represents a pointer to an unmanaged value. + /// + /// the type of the unmanaged value. + [StructLayout(LayoutKind.Sequential)] + public readonly struct BlittableRef where T : unmanaged + { + private readonly nint _ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// The pointer value. + public BlittableRef(nint ptr) + { + _ptr = ptr; + } + + /// + /// Gets a value indicating whether the pointer has a value (is not a null pointer). + /// + public bool HasValue => _ptr != 0; + + /// + /// Gets a reference to the value this pointer points to. + /// + public unsafe ref T Value + { + get + { + if (!HasValue) + { + throw new InvalidOperationException("Value is not set"); + } + return ref Unsafe.AsRef((void*)_ptr); + } + } + + /// + /// Gets the value this pointer points to or the default value of if the pointer is null. + /// + /// The value this pointer points to or the default value of if the pointer is null. + public T GetValueOrDefault() + { + return HasValue ? Value : default; + } + + /// + /// Gets the value this pointer points to or the specified if the pointer is null. + /// + /// The default value to return if the pointer is null. + /// The value this pointer points to or the default value if the pointer is null. + public T GetValueorDefault(T defaultValue) + { + return HasValue ? Value : defaultValue; + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Marshalling/BlittableStructRef.cs b/src/SampSharp.OpenMp.Core/Marshalling/BlittableStructRef.cs new file mode 100644 index 00000000..76de1c80 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Marshalling/BlittableStructRef.cs @@ -0,0 +1,62 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core; + +/// +/// Represents a pointer to a structure. +/// +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct BlittableStructRef where T : struct +{ + private readonly nint _ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// The pointer value. + public BlittableStructRef(nint ptr) + { + _ptr = ptr; + } + + /// + /// Gets a value indicating whether the pointer has a value (is not a null pointer). + /// + public bool HasValue => _ptr != 0; + + /// + /// Gets the value this pointer points to. Uses marshalling for marshalling the structure. + /// + public T Value + { + get + { + if (!HasValue) + { + throw new InvalidOperationException("Value is not set"); + } + + return Marshal.PtrToStructure(_ptr); + } + } + + /// + /// Gets the value this pointer points to or the default value of if the pointer is null. + /// + /// The value this pointer points to or the default value of if the pointer is null. + public T GetValueOrDefault() + { + return HasValue ? Value : default; + } + + /// + /// Gets the value this pointer points to or the specified if the pointer is null. + /// + /// The default value to return if the pointer is null. + /// The value this pointer points to or the default value if the pointer is null. + public T GetValueorDefault(T defaultValue) + { + return HasValue ? Value : defaultValue; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringView.cs b/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringView.cs new file mode 100644 index 00000000..faadc4e6 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringView.cs @@ -0,0 +1,60 @@ +using System.Collections; +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.RobinHood; + +/// +/// Represent a pointer to an robin_hood::unordered_flat_set of values. robin_hood::unordered_flat_set is part of the robin_hood C++ library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct FlatHashSetStringView : IReadOnlyCollection +{ + private readonly nint _data; + + /// + /// Gets the number of elements in this set. + /// + public int Count => RobinHoodInterop.FlatHashSetStringView_size(_data).Value.ToInt32(); + + /// + public IEnumerator GetEnumerator() + { + var iter = RobinHoodInterop.FlatHashSetStringView_begin(_data); + + while (iter != RobinHoodInterop.FlatHashSetStringView_end(_data)) + { + yield return iter.Get().ToString(); + iter = RobinHoodInterop.FlatHashSetStringView_inc(iter); + } + } + + /// + /// Adds a value to this set. + /// + /// The value to add. + public void Emplace(string value) + { + ArgumentNullException.ThrowIfNull(value); + + scoped StringViewMarshaller.ManagedToNative valueMarshaller = new(); + + try + { + Span buffer = stackalloc byte[StringViewMarshaller.ManagedToNative.BufferSize]; + valueMarshaller.FromManaged(value, buffer); + var valueMarshalled = valueMarshaller.ToUnmanaged(); + + RobinHoodInterop.FlatHashSetStringView_emplace(_data, valueMarshalled); + } + finally + { + valueMarshaller.Free(); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringViewMarshaller.cs b/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringViewMarshaller.cs new file mode 100644 index 00000000..59f393cf --- /dev/null +++ b/src/SampSharp.OpenMp.Core/RobinHood/FlatHashSetStringViewMarshaller.cs @@ -0,0 +1,17 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.RobinHood; + +/// +/// Represents a marshaller entrypoint for marshalling a native structure to to a managed enumerable of values. +/// +[CustomMarshaller(typeof(IEnumerable), MarshalMode.UnmanagedToManagedIn, typeof(FlatHashSetStringViewMarshaller))] +public static class FlatHashSetStringViewMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static IEnumerable ConvertToManaged(FlatHashSetStringView set) + { + return set; + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSet.cs b/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSet.cs new file mode 100644 index 00000000..35c3dc47 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSet.cs @@ -0,0 +1,105 @@ +using System.Collections; +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.RobinHood; + +/// +/// Represent a pointer to an robin_hood::unordered_flat_set of open.mp interface pointers of type . robin_hood::unordered_flat_set is part of the robin_hood C++ library. +/// +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct FlatPtrHashSet : IReadOnlyCollection where T : unmanaged, IUnmanagedInterface +{ + private readonly nint _data; + + internal FlatPtrHashSet(IntPtr data) + { + _data = data; + } + + /// + /// Gets the number of elements in this set. + /// + public int Count => RobinHoodInterop.FlatPtrHashSet_size(_data).ToInt32(); + + /// + /// Returns an enumerator that iterates through the set. + /// + /// The enumerator. + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + internal FlatPtrHashSetIterator Begin() + { + return RobinHoodInterop.FlatPtrHashSet_begin(_data); + } + + internal FlatPtrHashSetIterator End() + { + return RobinHoodInterop.FlatPtrHashSet_end(_data); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Represents an enumerator for a . + /// + public struct Enumerator : IEnumerator + { + private readonly FlatPtrHashSet _set; + private FlatPtrHashSetIterator? _iterator; + + internal Enumerator(FlatPtrHashSet set) + { + _set = set; + } + + /// + public bool MoveNext() + { + var end = _set.End(); + if (!_iterator.HasValue) + { + _iterator = _set.Begin(); + return _iterator != end; + } + + if (_iterator == end) + { + return false; + } + + var iter = _iterator.Value; + iter.Advance(); + _iterator = iter; + return _iterator != end; + } + + /// + public void Reset() + { + throw new NotSupportedException(); + } + + /// + public readonly T Current => _iterator?.Get() ?? throw new InvalidOperationException(); + + readonly object IEnumerator.Current => Current; + + /// + public void Dispose() + { + _iterator = null; + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSetIterator.cs b/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSetIterator.cs new file mode 100644 index 00000000..b7d8f782 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/RobinHood/FlatPtrHashSetIterator.cs @@ -0,0 +1,47 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.RobinHood; + +[StructLayout(LayoutKind.Sequential)] +internal struct FlatPtrHashSetIterator : IEquatable +{ + private nint _value; // NodePtr mKeyVals{nullptr}; + private nint _info; // uint8_t const* mInfo{nullptr}; + + public readonly T Get() where T : unmanaged + { + return StructPointer.Dereference(_value); + } + + public readonly bool Equals(FlatPtrHashSetIterator other) + { + return _value == other._value; + } + + public override readonly bool Equals(object? obj) + { + return obj is FlatPtrHashSetIterator other && Equals(other); + } + + public override readonly int GetHashCode() + { + return _value.GetHashCode(); + } + + public void Advance() + { + var u = RobinHoodInterop.FlatPtrHashSet_inc(this); + _value = u._value; + _info = u._info; + } + + public static bool operator ==(FlatPtrHashSetIterator a, FlatPtrHashSetIterator b) + { + return a.Equals(b); + } + + public static bool operator !=(FlatPtrHashSetIterator a, FlatPtrHashSetIterator b) + { + return !a.Equals(b); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/RobinHood/RobinHoodInterop.cs b/src/SampSharp.OpenMp.Core/RobinHood/RobinHoodInterop.cs new file mode 100644 index 00000000..4762fe67 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/RobinHood/RobinHoodInterop.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core.RobinHood; + +internal static class RobinHoodInterop +{ + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatPtrHashSet_begin(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatPtrHashSet_end(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatPtrHashSet_inc(FlatPtrHashSetIterator value); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern Size FlatPtrHashSet_size(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatHashSetStringView_begin(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatHashSetStringView_end(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern FlatPtrHashSetIterator FlatHashSetStringView_inc(FlatPtrHashSetIterator value); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern Size FlatHashSetStringView_size(nint data); + + [DllImport("SampSharp", CallingConvention = CallingConvention.Cdecl)] + public static extern void FlatHashSetStringView_emplace(nint data, StringView value); + +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj b/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj new file mode 100644 index 00000000..4c139fe4 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + true + True + true + $(BaseIntermediateOutputPath)\SourceGeneratorFiles + + + + + + + + + + + + + + + + + diff --git a/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj.DotSettings b/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj.DotSettings new file mode 100644 index 00000000..fb0e3371 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/SampSharp.OpenMp.Core.csproj.DotSettings @@ -0,0 +1,61 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + False + False + True \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/SampSharpExceptionHandler.cs b/src/SampSharp.OpenMp.Core/SampSharpExceptionHandler.cs new file mode 100644 index 00000000..3c7b1e57 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/SampSharpExceptionHandler.cs @@ -0,0 +1,67 @@ +using System.Diagnostics; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides a way to handle exceptions within SampSharp that would otherwise be unhandled. +/// +public static class SampSharpExceptionHandler +{ + private static ExceptionHandler _exceptionHandler = DefaultExceptionHandler; + + private static void DefaultExceptionHandler(string context, Exception exception) + { + Console.WriteLine($"Uncaught exception during {context}:"); + Console.WriteLine(exception.ToString()); + } + + internal static void SetExceptionHandler(ExceptionHandler handler) + { + _exceptionHandler = handler; + } + + /// + /// Handles an exception that occurred in the given context. + /// + /// A context which explains where the exception occurred. + /// The exception which occured. + public static void HandleException(string context, Exception exception) + { + try + { + _exceptionHandler(context, exception); + } + catch(Exception ex) + { + try + { + if (Console.IsOutputRedirected) + { + using var sw = new StreamWriter(Console.OpenStandardOutput()); + sw.WriteLine($"An exception occurred while handling an exception ({context}):"); + sw.WriteLine(ex.ToString()); + sw.WriteLine(); + sw.WriteLine("Original exception:"); + sw.WriteLine(exception.ToString()); + } + else + { + Console.WriteLine($"An exception occurred while handling an exception ({context}):"); + Console.WriteLine(ex.ToString()); + Console.WriteLine(); + Console.WriteLine("Original exception"); + Console.WriteLine(exception.ToString()); + } + } + catch + { + // void + } + } + + if (Debugger.IsAttached) + { + Debugger.BreakForUserUnhandledException(exception); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/SampSharpInfo.cs b/src/SampSharp.OpenMp.Core/SampSharpInfo.cs new file mode 100644 index 00000000..755a9c39 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/SampSharpInfo.cs @@ -0,0 +1,27 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides information about the SampSharp open.mp component. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct SampSharpInfo +{ + /// + /// The size of this structure. Can be used to check if the structure is the correct version. + /// + public readonly Size Size; + + /// + /// The unmanaged <> managed API version of the SampSharp open.mp component. + /// + public readonly int ApiVersion; + + /// + /// The version of the SampSharp open.mp component. + /// + public readonly SemanticVersion Version; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/SampSharpInitParams.cs b/src/SampSharp.OpenMp.Core/SampSharpInitParams.cs new file mode 100644 index 00000000..fc147749 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/SampSharpInitParams.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides the parameters for initializing the SampSharp application as provided by the SampSharp open.mp component. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly ref struct SampSharpInitParams +{ + /// + /// The size of this structure. Can be used to check if the structure is the correct version. + /// + public readonly Size Size; + + private readonly BlittableStructRef _info; + + /// + /// The open.mp core. + /// + public readonly ICore Core; + + /// + /// The open.mp component list. + /// + public readonly IComponentList ComponentList; + + /// + /// Gets information about the SampSharp open.mp component. + /// + public SampSharpInfo Info => _info.Value; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/StartupContext.cs b/src/SampSharp.OpenMp.Core/StartupContext.cs new file mode 100644 index 00000000..151e4bc3 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/StartupContext.cs @@ -0,0 +1,118 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.OpenMp.Core; + +/// +/// Represents the context in which the application is started. +/// +public sealed class StartupContext : IStartupContext +{ + /// + /// The API version of the native open.mp component which supports this version of the managed API. This is used to check for version mismatches between the managed and native components. + /// + private const int SupportedApiVersion = 1; + + private IStartup? _configurator; + private ExceptionHandler _unhandledExceptionHandler; + + /// + /// Initializes a new instance of the class. + /// + /// The initialization parameters. + public StartupContext(SampSharpInitParams init) + { + VersionCheck(init); + Core = init.Core; + ComponentList = init.ComponentList; + Info = init.Info; + _unhandledExceptionHandler = (context, ex) => + { + Core.LogLine(LogLevel.Error, $"Unhandled exception during {context}:"); + Core.LogLine(LogLevel.Error, ex.ToString()); + }; + SampSharpExceptionHandler.SetExceptionHandler(_unhandledExceptionHandler); + } + + /// + public ICore Core { get; } + + /// + public IComponentList ComponentList { get; } + + /// + public SampSharpInfo Info { get; } + + /// + public IStartup Configurator => _configurator ?? throw new InvalidOperationException("The configurator has not been set."); + + /// + public ExceptionHandler UnhandledExceptionHandler + { + get => _unhandledExceptionHandler; + set + { + _unhandledExceptionHandler = value; + SampSharpExceptionHandler.SetExceptionHandler(value); + } + } + + /// + public event EventHandler? Cleanup; + + /// + public event EventHandler? Initialized; + + /// + /// Internal method. Do not invoke manually. + /// + public void InitializeUsing(IStartup configurator) + { + _configurator = configurator; + + configurator.Initialize(this); + Initialized?.Invoke(this, EventArgs.Empty); + } + + /// + /// Internal method. Do not invoke manually. + /// + public void InvokeCleanup() + { + Cleanup?.Invoke(this, EventArgs.Empty); + } + + /// + /// Internal method. Do not invoke manually. + /// + public static void MainInfoProvider() + { + LaunchInstructions.Write(); + } + + private static void VersionCheck(SampSharpInitParams init) + { + unsafe + { + var initParamsSizeMatches = init.Size.Value == sizeof(SampSharpInitParams); + var infoSizeMatches = init.Info.Size.Value == sizeof(SampSharpInfo); + var apiVersionMatches = init.Info.ApiVersion == SupportedApiVersion; + + if (initParamsSizeMatches && infoSizeMatches && apiVersionMatches) + { + return; + } + + var version = typeof(StartupContext).Assembly.GetName().Version!.ToString(3); + + // TODO: Write docs + var message = $"SampSharp version mismatch. The SampSharp open.mp component does not support SampSharp.OpenMp.Core v{version}. See https://sampsharp.net/version-mismatch.html for more details."; + + if (initParamsSizeMatches && infoSizeMatches) + { + init.Core.LogLine(LogLevel.Error, message); + } + + Environment.FailFast(message); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/BooleanMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/BooleanMarshaller.cs new file mode 100644 index 00000000..77f05719 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/BooleanMarshaller.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(bool), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(bool), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(bool), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(bool), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +[CustomMarshaller(typeof(bool), MarshalMode.ManagedToUnmanagedRef, typeof(Bidirectional))] +[CustomMarshaller(typeof(bool), MarshalMode.UnmanagedToManagedRef, typeof(Bidirectional))] +public static class BooleanMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static BlittableBoolean ConvertToUnmanaged(bool managed) + { + return managed; + } + } + + public static class NativeToManaged + { + public static bool ConvertToManaged(BlittableBoolean unmanaged) + { + return unmanaged; + } + } + + public static class Bidirectional + { + public static BlittableBoolean ConvertToUnmanaged(bool managed) + { + return managed; + } + public static bool ConvertToManaged(BlittableBoolean unmanaged) + { + return unmanaged; + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/Hours.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/Hours.cs new file mode 100644 index 00000000..b5d05116 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/Hours.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a duration in hours which is represented in memory like an std::chrono::hours from the C++ +/// standard library. MSVC uses duration<int, ratio<3600>>, so the layout is a single 32-bit int. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Hours +{ + private readonly int _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration value in hours. + public Hours(int value) + { + _value = value; + } + + /// + /// Converts this duration to a . + /// + /// The duration as a . + public TimeSpan AsTimeSpan() + { + return TimeSpan.FromHours(_value); + } + + /// + /// Converts an value to a . + /// + /// The value to convert. + public static implicit operator TimeSpan(Hours hours) + { + return hours.AsTimeSpan(); + } + + /// + /// Converts a to an value. + /// + /// The time span to convert. + public static implicit operator Hours(TimeSpan timeSpan) + { + return new Hours((int)(timeSpan.Ticks / TimeSpan.TicksPerHour)); + } + + /// + public override string ToString() + { + return _value.ToString(); + } +} diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/HoursMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/HoursMarshaller.cs new file mode 100644 index 00000000..26639570 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/HoursMarshaller.cs @@ -0,0 +1,32 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native +/// structure. +/// +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class HoursMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static Hours ConvertToUnmanaged(TimeSpan managed) + { + return new Hours((int)managed.TotalHours); + } + } + + public static class NativeToManaged + { + public static TimeSpan ConvertToManaged(Hours unmanaged) + { + return unmanaged.AsTimeSpan(); + } + } +#pragma warning restore CS1591 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/Microseconds.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/Microseconds.cs new file mode 100644 index 00000000..01a16566 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/Microseconds.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a duration in microseconds which is represented in memory like an std::chrono::Microseconds from +/// the C++ standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Microseconds +{ + private readonly long _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration value in microseconds. + public Microseconds(long value) + { + _value = value; + } + + /// + /// Converts this duration to a . + /// + /// The duration as a . + public TimeSpan AsTimeSpan() + { + return TimeSpan.FromMicroseconds(_value); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator TimeSpan(Microseconds microseconds) + { + return microseconds.AsTimeSpan(); + } + + /// + /// Converts a to a . + /// + /// The time span to convert. + public static implicit operator Microseconds(TimeSpan timeSpan) + { + return new Microseconds(timeSpan.Ticks / TimeSpan.TicksPerMicrosecond); + } + + /// + public override string ToString() + { + return _value.ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/MicrosecondsMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/MicrosecondsMarshaller.cs new file mode 100644 index 00000000..a6f6f97b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/MicrosecondsMarshaller.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class MicrosecondsMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static Microseconds ConvertToUnmanaged(TimeSpan managed) + { + return new Microseconds((long)managed.TotalMicroseconds); + } + } + public static class NativeToManaged + { + public static TimeSpan ConvertToManaged(Microseconds unmanaged) + { + return unmanaged.AsTimeSpan(); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/Milliseconds.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/Milliseconds.cs new file mode 100644 index 00000000..f8c60af7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/Milliseconds.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a duration in milliseconds which is represented in memory like an std::chrono::Milliseconds from +/// the C++ standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Milliseconds +{ + private readonly long _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration value in milliseconds. + public Milliseconds(long value) + { + _value = value; + } + + /// + /// Converts this duration to a . + /// + /// The duration as a . + public TimeSpan AsTimeSpan() + { + return TimeSpan.FromMilliseconds(_value); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator TimeSpan(Milliseconds milliseconds) + { + return milliseconds.AsTimeSpan(); + } + + /// + /// Converts a to a . + /// + /// The time span to convert. + public static implicit operator Milliseconds(TimeSpan timeSpan) + { + return new Milliseconds(timeSpan.Ticks / TimeSpan.TicksPerMillisecond); + } + + /// + public override string ToString() + { + return _value.ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/MillisecondsMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/MillisecondsMarshaller.cs new file mode 100644 index 00000000..bbbfbac6 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/MillisecondsMarshaller.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class MillisecondsMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static Milliseconds ConvertToUnmanaged(TimeSpan managed) + { + return new Milliseconds((long)managed.TotalMilliseconds); + } + } + public static class NativeToManaged + { + public static TimeSpan ConvertToManaged(Milliseconds unmanaged) + { + return unmanaged.AsTimeSpan(); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/Minutes.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/Minutes.cs new file mode 100644 index 00000000..4172c586 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/Minutes.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a duration in minutes which is represented in memory like an std::chrono::minutes from the C++ +/// standard library. MSVC uses duration<int, ratio<60>>, so the layout is a single 32-bit int. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Minutes +{ + private readonly int _value; + + /// + /// Initializes a new instance of the struct. + /// + /// The duration value in minutes. + public Minutes(int value) + { + _value = value; + } + + /// + /// Converts this duration to a . + /// + /// The duration as a . + public TimeSpan AsTimeSpan() + { + return TimeSpan.FromMinutes(_value); + } + + /// + /// Converts a value to a . + /// + /// The value to convert. + public static implicit operator TimeSpan(Minutes minutes) + { + return minutes.AsTimeSpan(); + } + + /// + /// Converts a to a value. + /// + /// The time span to convert. + public static implicit operator Minutes(TimeSpan timeSpan) + { + return new Minutes((int)(timeSpan.Ticks / TimeSpan.TicksPerMinute)); + } + + /// + public override string ToString() + { + return _value.ToString(); + } +} diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/MinutesMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/MinutesMarshaller.cs new file mode 100644 index 00000000..1c9d27ac --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/MinutesMarshaller.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class MinutesMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static Minutes ConvertToUnmanaged(TimeSpan managed) + { + return new Minutes((int)managed.TotalMinutes); + } + } + public static class NativeToManaged + { + public static TimeSpan ConvertToManaged(Minutes unmanaged) + { + return unmanaged.AsTimeSpan(); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/Seconds.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/Seconds.cs new file mode 100644 index 00000000..16cbcf7a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/Seconds.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a duration in seconds which is represented in memory like an std::chrono::Seconds from the C++ +/// standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Seconds +{ + private readonly long _value; + + /// + /// Initializes a new instance of the struct. + /// + /// + public Seconds(long value) + { + _value = value; + } + + /// + /// Converts this duration to a . + /// + /// The duration as a . + public TimeSpan AsTimeSpan() + { + return TimeSpan.FromSeconds(_value); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator TimeSpan(Seconds seconds) + { + return seconds.AsTimeSpan(); + } + + /// + /// Converts a to a . + /// + /// The time span to convert. + public static implicit operator Seconds(TimeSpan timeSpan) + { + return new Seconds(timeSpan.Ticks / TimeSpan.TicksPerSecond); + } + + /// + public override string ToString() + { + return _value.ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/SecondsMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/SecondsMarshaller.cs new file mode 100644 index 00000000..1dacd631 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/SecondsMarshaller.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(TimeSpan), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class SecondsMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static Seconds ConvertToUnmanaged(TimeSpan managed) + { + return new Seconds((long)managed.TotalSeconds); + } + } + public static class NativeToManaged + { + public static TimeSpan ConvertToManaged(Seconds unmanaged) + { + return unmanaged.AsTimeSpan(); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/TimePoint.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/TimePoint.cs new file mode 100644 index 00000000..04cb1a94 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/TimePoint.cs @@ -0,0 +1,61 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a point in time which is represented in memory like an std::chrono::TimePoint from the C++ +/// standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct TimePoint +{ + private readonly long _value; + + private TimePoint(long value) + { + _value = value; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The point in time the value should represent. + /// The new time point value. + public static TimePoint FromDateTimeOffset(DateTimeOffset time) + { + var ticksSinceEpoch = time.UtcTicks - DateTimeOffset.UnixEpoch.Ticks; + return new TimePoint(ticksSinceEpoch); + } + + /// + /// Converts this time point to a . + /// + /// The converted value. + public DateTimeOffset ToDateTimeOffset() + { + return new DateTimeOffset(_value + DateTimeOffset.UnixEpoch.Ticks, TimeSpan.Zero); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator DateTimeOffset(TimePoint timePoint) + { + return timePoint.ToDateTimeOffset(); + } + + /// + /// Converts a to a . + /// + /// The value to convert. + public static implicit operator TimePoint(DateTimeOffset time) + { + return FromDateTimeOffset(time); + } + /// + public override string ToString() + { + return _value.ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Chrono/TimePointMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/Chrono/TimePointMarshaller.cs new file mode 100644 index 00000000..4754f43c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Chrono/TimePointMarshaller.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices.Marshalling; + +namespace SampSharp.OpenMp.Core.Std.Chrono; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(DateTimeOffset), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(DateTimeOffset), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(DateTimeOffset), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(DateTimeOffset), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +public static class TimePointMarshaller +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static class ManagedToNative + { + public static TimePoint ConvertToUnmanaged(DateTimeOffset managed) + { + return TimePoint.FromDateTimeOffset(managed); + } + } + public static class NativeToManaged + { + public static DateTimeOffset ConvertToManaged(TimePoint unmanaged) + { + return unmanaged.ToDateTimeOffset(); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/Pair.cs b/src/SampSharp.OpenMp.Core/Std/Pair.cs new file mode 100644 index 00000000..3137766c --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Pair.cs @@ -0,0 +1,59 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a pair of values which is represented in memory like an std::Pair from the C++ standard library. +/// +/// The type of the first value in the pair. +/// The type of the second value in the pair. +[StructLayout(LayoutKind.Sequential)] +public readonly struct Pair + where T1 : unmanaged + where T2 : unmanaged +{ + /// + /// The first value in the pair. + /// + public readonly T1 First; + + /// + /// The second value in the pair. + /// + public readonly T2 Second; + + /// + /// Deconstructs the pair into its two values. + /// + /// The first value from the pair. + /// The second value from the pair. + public void Deconstruct(out T1 first, out T2 second) + { + first = First; + second = Second; + } + + /// + public override string ToString() + { + return $"({First}, {Second})"; + } + + /// + /// Converts a pair to a tuple. + /// + /// The pair to convert. + public static implicit operator (T1, T2)(Pair pair) + { + return (pair.First, pair.Second); + } + + /// + /// Converts a tuple to a pair. + /// + /// The tuple to convert. + public static implicit operator Pair((T1 first,T2 second) tuple) + { + return (tuple.first, tuple.second); + } +} diff --git a/src/SampSharp.OpenMp.Core/Std/Size.cs b/src/SampSharp.OpenMp.Core/Std/Size.cs new file mode 100644 index 00000000..545bc050 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/Size.cs @@ -0,0 +1,64 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a size which is represented in memory like an std::size_t from the C++ standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly struct Size +{ + /// + /// Gets the length in bytes of the Size structure. + /// + public const int Length = 8; // 64-bits + + static Size() + { + if (Length != Marshal.SizeOf()) + { + throw new InvalidOperationException("Size structure has an unexpected size. Are you running a 32-bit build of SampSharp?"); + } + } + + /// + /// Initializes a new instance of the struct. + /// + /// The size value. + public Size(nint value) + { + Value = value; + } + + /// + /// Gets the size value. + /// + public nint Value { get; } + + /// + /// Converts the size to an . + /// + /// The converted value. + public int ToInt32() + { + return Value.ToInt32(); + } + + /// + /// Converts the size to an . + /// + /// The value to convert. + public static explicit operator int(Size value) + { + return value.ToInt32(); + } + + /// + /// Converts an to a size. + /// + /// The value to convert. + public static implicit operator Size(int value) + { + return new Size(value); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/SpanLite.cs b/src/SampSharp.OpenMp.Core/Std/SpanLite.cs new file mode 100644 index 00000000..fb98f0d9 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/SpanLite.cs @@ -0,0 +1,34 @@ +using System.Runtime.InteropServices; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a span which is represented in memory like an std::span_t from the C++ standard library. +/// +/// +[StructLayout(LayoutKind.Sequential)] +public readonly unsafe struct SpanLite where T : unmanaged +{ + private readonly T* _data; + private readonly Size _size; + + /// + /// Initializes a new instance of the struct. + /// + /// A pointer to the underlying sequence. + /// The number of elements. + public SpanLite(T* data, Size size) + { + _data = data; + _size = size; + } + + /// + /// Converts the span to a . + /// + /// The converted span. + public Span AsSpan() + { + return new Span(_data, _size.Value.ToInt32()); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/StringView.cs b/src/SampSharp.OpenMp.Core/Std/StringView.cs new file mode 100644 index 00000000..73c67d6b --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/StringView.cs @@ -0,0 +1,83 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a view of a string which is represented in memory like an std::basic_string_view from the C++ +/// standard library. +/// +[StructLayout(LayoutKind.Sequential)] +public readonly unsafe struct StringView : ISpanFormattable +{ + internal readonly byte* _reference; + private readonly Size _size; + + /// + /// Initializes a new instance of the struct. + /// + /// A pointer to the underlying sequence. + /// The number of characters. + public StringView(byte* data, Size size) + { + _reference = data; + _size = size; + } + + /// + /// Converts this view to a read-only span of bytes. + /// + /// The readonly span of bytes. + public ReadOnlySpan AsSpan() + { + return new ReadOnlySpan(_reference, _size.Value.ToInt32()); + } + + /// + /// Converts this view to a using + /// (UTF-8 by default, can be overridden at startup). + /// + /// The converted string. + public override string? ToString() + { + return _reference == null ? null : StringViewMarshaller.Encoding.GetString(AsSpan()); + } + + /// + /// Converts this view to a using UTF-8 encoding. + /// + /// The format to use. + /// The provider to use to format the value. + /// + public string ToString(string? format, IFormatProvider? formatProvider) + { + return ToString() ?? string.Empty; + } + + + /// Tries to format the value of this string view into the provided span of characters. The string view is + /// converted using UTF-8 encoding. + /// The span in which to write this instance's value formatted as a span of + /// characters. + /// When this method returns, contains the number of characters that were written in + /// . + /// A span containing the characters that represent a standard or custom format string that + /// defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// + /// if the formatting was successful; otherwise, . + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + return StringViewMarshaller.Encoding.TryGetChars(AsSpan(), destination, out charsWritten); + } + + /// + /// Converts a to a using UTF-8 encoding. + /// + /// + public static implicit operator string?(StringView view) + { + return view.ToString(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Std/StringViewMarshaller.cs b/src/SampSharp.OpenMp.Core/Std/StringViewMarshaller.cs new file mode 100644 index 00000000..a4ff8211 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Std/StringViewMarshaller.cs @@ -0,0 +1,143 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using System.Text; + +namespace SampSharp.OpenMp.Core.Std; + +/// +/// Represents a marshaller entrypoint for marshalling to a native structure. +/// +[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToNative))] +[CustomMarshaller(typeof(string), MarshalMode.UnmanagedToManagedOut, typeof(ManagedToNative))] +[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedOut, typeof(NativeToManaged))] +[CustomMarshaller(typeof(string), MarshalMode.UnmanagedToManagedIn, typeof(NativeToManaged))] +[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedRef, typeof(Bidirectional))] +[CustomMarshaller(typeof(string), MarshalMode.UnmanagedToManagedRef, typeof(Bidirectional))] +public static unsafe class StringViewMarshaller +{ + /// + /// Encoding used for every managed and native + /// crossing this marshaller. Defaults to UTF-8 (what open.mp clients speak). + /// Gamemodes targeting clients with Cyrillic chat usually set + /// this to Windows-1251 at startup (requires System.Text.Encoding.CodePages + /// + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)). + /// + public static Encoding Encoding { get; set; } = Encoding.UTF8; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public ref struct ManagedToNative + { + public static int BufferSize => 128; + + private byte* _heapBuffer; + private int _byteCount; + + private Span _buffer; + + public void FromManaged(string? managed, Span buffer) + { + _buffer = buffer; + + if (managed == null) + { + return; + } + + _byteCount = Encoding.GetByteCount(managed); + + if (_byteCount < buffer.Length) + { + Encoding.GetBytes(managed, buffer[.._byteCount]); + buffer[_byteCount] = 0; + _heapBuffer = null; + } + else + { + // buffer on stack too small, allocate on heap + _heapBuffer = (byte*)Marshal.AllocHGlobal(_byteCount + 1); + + var heapBuffer = new Span(_heapBuffer, _byteCount + 1) + { + [_byteCount] = 0 + }; + Encoding.GetBytes(managed, heapBuffer); + } + + } + + public readonly StringView ToUnmanaged() + { + return _heapBuffer == null + ? new StringView((byte*)Unsafe.AsPointer(ref _buffer.GetPinnableReference()), new Size(_byteCount)) + : new StringView(_heapBuffer, new Size(_byteCount)); + } + + public void Free() + { + if (_heapBuffer != null) + { + Marshal.FreeHGlobal((nint)_heapBuffer); + _heapBuffer = null; + } + } + } + + public static class NativeToManaged + { + public static string? ConvertToManaged(StringView unmanaged) + { + return unmanaged.ToString(); + } + } + + public ref struct Bidirectional + { + private byte* _heapBuffer; + private int _byteCount; + private string? _result; + + public void FromManaged(string? managed) + { + if (managed == null) + { + return; + } + + _byteCount = Encoding.GetByteCount(managed); + _heapBuffer = (byte*)Marshal.AllocHGlobal(_byteCount + 1); + + var heapBuffer = new Span(_heapBuffer, _byteCount + 1) + { + [_byteCount] = 0 + }; + Encoding.GetBytes(managed, heapBuffer); + } + + public readonly StringView ToUnmanaged() + { + return new StringView(_heapBuffer, new Size(_byteCount)); + } + + public void FromUnmanaged(StringView value) + { + _result = value.ToString(); + } + + public readonly string? ToManaged() + { + return _result; + } + + public void Free() + { + if(_heapBuffer != null) + { + Marshal.FreeHGlobal((nint)_heapBuffer); + _heapBuffer = null; + _byteCount = 0; + } + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/StructPointer.cs b/src/SampSharp.OpenMp.Core/StructPointer.cs new file mode 100644 index 00000000..2d83604d --- /dev/null +++ b/src/SampSharp.OpenMp.Core/StructPointer.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace SampSharp.OpenMp.Core; + +/// +/// Provides methods to work with structs which represent pointers to open.mp interfaces. +/// +internal static unsafe class StructPointer +{ + /// + /// Reinterprets the given pointer as an open.mp pointer struct of type . + /// + /// The open.mp pointer struct type. + /// The pointer to reinterpret. + /// The open.mp pointer type. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T AsStruct(nint pointer) where T : unmanaged, IUnmanagedInterface + { + Debug.Assert(sizeof(T) == sizeof(nint)); + return *(T*)&pointer; + } + + /// + ///Deference the given pointer and return the value as a struct of type . + /// + /// The unmanaged type the pointer points to. + /// The pointer to dereference. + /// The dereferenced value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Dereference(nint pointer) where T : unmanaged + { + return *(T*)pointer; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/ExecutionType.cs b/src/SampSharp.OpenMp.Core/Threading/ExecutionType.cs new file mode 100644 index 00000000..2e95b7b0 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/ExecutionType.cs @@ -0,0 +1,7 @@ +namespace SampSharp.OpenMp.Core; + +internal enum ExecutionType +{ + Post, + Send +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/MainThreadTaskAwaiter.cs b/src/SampSharp.OpenMp.Core/Threading/MainThreadTaskAwaiter.cs new file mode 100644 index 00000000..8fbeef46 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/MainThreadTaskAwaiter.cs @@ -0,0 +1,30 @@ +using System.Runtime.CompilerServices; + +namespace SampSharp.OpenMp.Core; + +/// Represents an awaiter for the . +/// +public readonly struct MainThreadTaskAwaiter : INotifyCompletion +{ + /// Gets a value indicating whether the task is completed. + public bool IsCompleted => SynchronizationContextExtension.Active.IsMainThread(); + + /// Gets the result of the task. + public void GetResult() + { + // task never returns a result and never throws. + } + + /// + public void OnCompleted(Action continuation) + { + ArgumentNullException.ThrowIfNull(continuation); + + if (SynchronizationContextExtension.Active.IsMainThread()) + { + continuation(); + } + + SynchronizationContextExtension.Active.Invoke(continuation); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/SampSharpSynchronizationContext.cs b/src/SampSharp.OpenMp.Core/Threading/SampSharpSynchronizationContext.cs new file mode 100644 index 00000000..23cc4e2f --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/SampSharpSynchronizationContext.cs @@ -0,0 +1,50 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.OpenMp.Core; + +internal class SampSharpSynchronizationContext : SynchronizationContext +{ + private readonly ObjectPool _pool = new DefaultObjectPool(new SendOrPostCallbackItemPooledObjectPolicy()); + private readonly ConcurrentQueue _queue = new(); + + public override void Send(SendOrPostCallback d, object? state) + { + var item = _pool.Get(); + item.Set(ExecutionType.Send, d, state, null); + + _queue.Enqueue(item); + + item.ExecutionCompleteWaitHandle.WaitOne(); + + var ex = item.Exception; + + _pool.Return(item); + + if (ex != null) + { + throw ex; + } + } + + public override void Post(SendOrPostCallback d, object? state) + { + // Queue the item and don't wait for its execution to complete. Let the item return itself to the pool when it's + // done. + var item = _pool.Get(); + item.Set(ExecutionType.Post, d, state, _pool); + _queue.Enqueue(item); + } + + public override SynchronizationContext CreateCopy() + { + // Do not copy + return this; + } + + public SendOrPostCallbackItem? GetMessage() + { + _queue.TryDequeue(out var result); + return result; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItem.cs b/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItem.cs new file mode 100644 index 00000000..520fa7fd --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItem.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.OpenMp.Core; + +internal class SendOrPostCallbackItem : IDisposable +{ + private readonly ManualResetEvent _asyncWaitHandle = new(false); + private ExecutionType _executionType; + private SendOrPostCallback? _method; + private object? _state; + private ObjectPool? _pool; + + public void Set(ExecutionType executionType, SendOrPostCallback method, object? state, ObjectPool? returnToPool) + { + _executionType = executionType; + _method = method; + _state = state; + _pool = returnToPool; + } + + public void Reset() + { + _asyncWaitHandle.Reset(); + Exception = null; + _method = null; + _state = null; + _pool = null; + } + + public Exception? Exception { get; private set; } + + public WaitHandle ExecutionCompleteWaitHandle => _asyncWaitHandle; + + public void Execute() + { + try + { + if (_executionType == ExecutionType.Send) + { + try + { + _method!(_state); + } + catch (Exception e) + { + Exception = e; + } + finally + { + _asyncWaitHandle.Set(); + } + } + else + { + _method!(_state); + } + } + finally + { + _pool?.Return(this); + } + } + + public void Dispose() + { + _asyncWaitHandle?.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItemPooledObjectPolicy.cs b/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItemPooledObjectPolicy.cs new file mode 100644 index 00000000..842d8fff --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/SendOrPostCallbackItemPooledObjectPolicy.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.OpenMp.Core; + +internal class SendOrPostCallbackItemPooledObjectPolicy : PooledObjectPolicy +{ + public override SendOrPostCallbackItem Create() + { + return new SendOrPostCallbackItem(); + } + + public override bool Return(SendOrPostCallbackItem obj) + { + obj.Reset(); + return true; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/StartupContextThreadingExtensions.cs b/src/SampSharp.OpenMp.Core/Threading/StartupContextThreadingExtensions.cs new file mode 100644 index 00000000..e956cb2a --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/StartupContextThreadingExtensions.cs @@ -0,0 +1,27 @@ +namespace SampSharp.OpenMp.Core; + +/// +/// Provides extension methods for related to threading. +/// +public static class StartupContextThreadingExtensions +{ + /// + /// Configures a which allows synchronization with the main thread using . + /// + /// The startup context. + /// The startup context. + public static IStartupContext UseSynchronizationContext(this IStartupContext context) + { + var ext = context.Core.TryGetExtension(); + + if (ext == null) + { + ext = new SynchronizationContextExtension(context.Core); + + context.Core.AddExtension(ext); + context.Cleanup += (_, _) => ext.Dispose(); + } + + return context; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/SyncToMainThreadTask.cs b/src/SampSharp.OpenMp.Core/Threading/SyncToMainThreadTask.cs new file mode 100644 index 00000000..61232fc7 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/SyncToMainThreadTask.cs @@ -0,0 +1,12 @@ +namespace SampSharp.OpenMp.Core; + +/// Represents a task which, when awaited, will switch the continuation to the main thread. +public readonly struct SyncToMainThreadTask +{ + /// Gets the awaiter for this task. + /// The await for this task. + public MainThreadTaskAwaiter GetAwaiter() + { + return new MainThreadTaskAwaiter(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/SynchronizationContextExtension.cs b/src/SampSharp.OpenMp.Core/Threading/SynchronizationContextExtension.cs new file mode 100644 index 00000000..2c2299c1 --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/SynchronizationContextExtension.cs @@ -0,0 +1,67 @@ +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.OpenMp.Core; + +[Extension(0x05e87f7adbdc0b7d)] +internal class SynchronizationContextExtension : Extension, ICoreEventHandler +{ + private readonly ICore _core; + private static SynchronizationContextExtension? _active; + + private readonly int _mainThreadId = Environment.CurrentManagedThreadId; + private readonly SampSharpSynchronizationContext _context = new(); + + public SynchronizationContextExtension(ICore core) + { + _core = core; + _active = this; + + core.GetEventDispatcher().AddEventHandler(this); + + SynchronizationContext.SetSynchronizationContext(_context); + } + + public static SynchronizationContextExtension Active => _active ?? throw new InvalidOperationException("No active synchronization context."); + + public bool IsMainThread() + { + return _mainThreadId == Environment.CurrentManagedThreadId; + } + + public void Invoke(Action continuation) + { + _context.Send(_ => continuation(), null); + } + + protected override void Cleanup() + { + _active = null; + + SynchronizationContext.SetSynchronizationContext(null); + + _core.GetEventDispatcher().RemoveEventHandler(this); + } + + public void OnTick(Microseconds elapsed, TimePoint now) + { + while (true) + { + var message = _context.GetMessage(); + + if (message == null) + { + break; + } + + try + { + message.Execute(); + } + catch (Exception ex) + { + SampSharpExceptionHandler.HandleException("async", ex); + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Core/Threading/TaskHelper.cs b/src/SampSharp.OpenMp.Core/Threading/TaskHelper.cs new file mode 100644 index 00000000..7a8336af --- /dev/null +++ b/src/SampSharp.OpenMp.Core/Threading/TaskHelper.cs @@ -0,0 +1,21 @@ +namespace SampSharp.OpenMp.Core; + +/// Provides helper methods for dealing with tasks. +public static class TaskHelper +{ + /// Returns a task which, when awaited, will switch the continuation to the main thread. + /// A task which moves the continuation to the main thread. + public static SyncToMainThreadTask SwitchToMainThread() + { + return new SyncToMainThreadTask(); + } + + /// + /// Gets a value indicating whether the current thread is the main thread. + /// + /// if the current thread is the main thread; otherwise, . + public static bool IsMainThread() + { + return SynchronizationContextExtension.Active.IsMainThread(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities.Commands/CommandInfo.cs b/src/SampSharp.OpenMp.Entities.Commands/CommandInfo.cs new file mode 100644 index 00000000..3949fd0e --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/CommandInfo.cs @@ -0,0 +1,18 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// Describes a registered command. +public class CommandInfo +{ + /// Initializes a new instance. + public CommandInfo(string name, CommandParameterInfo[] parameters) + { + Name = name; + Parameters = parameters; + } + + /// Command name (without leading slash). + public string Name { get; } + + /// Parameter descriptors for the command (excluding the prefix and DI parameters). + public CommandParameterInfo[] Parameters { get; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/CommandParameterAttribute.cs b/src/SampSharp.OpenMp.Entities.Commands/CommandParameterAttribute.cs new file mode 100644 index 00000000..30d6da19 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/CommandParameterAttribute.cs @@ -0,0 +1,12 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// Provides additional info for a command parameter. +[AttributeUsage(AttributeTargets.Parameter)] +public class CommandParameterAttribute : Attribute +{ + /// Override for the displayed parameter name (used in usage messages). + public string? Name { get; set; } + + /// Override parser type. Must implement and have a parameterless ctor. + public Type? Parser { get; set; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/CommandParameterInfo.cs b/src/SampSharp.OpenMp.Entities.Commands/CommandParameterInfo.cs new file mode 100644 index 00000000..11a7ccaf --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/CommandParameterInfo.cs @@ -0,0 +1,32 @@ +using SampSharp.Entities.SAMP.Commands.Parsers; + +namespace SampSharp.Entities.SAMP.Commands; + +/// Describes a single parsed command parameter. +public class CommandParameterInfo +{ + /// Initializes a new instance. + public CommandParameterInfo(string name, ICommandParameterParser parser, bool isRequired, object? defaultValue, int parameterIndex) + { + Name = name; + Parser = parser; + IsRequired = isRequired; + DefaultValue = defaultValue; + Index = parameterIndex; + } + + /// Display name (used in usage messages). + public string Name { get; } + + /// Parser that converts input text into the parameter value. + public ICommandParameterParser Parser { get; } + + /// Required parameters must be supplied; optional ones use . + public bool IsRequired { get; } + + /// Default value used when an optional parameter is omitted. + public object? DefaultValue { get; } + + /// Index of this parameter inside the invoker's args array. + public int Index { get; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/CommandServiceBase.cs b/src/SampSharp.OpenMp.Entities.Commands/CommandServiceBase.cs new file mode 100644 index 00000000..5674dcb4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/CommandServiceBase.cs @@ -0,0 +1,346 @@ +using System.Reflection; +using SampSharp.Entities.SAMP.Commands.Parsers; + +namespace SampSharp.Entities.SAMP.Commands; + +/// +/// Base class for command dispatchers. Subclasses provide the source of methods +/// to scan () and the format of the usage message +/// (); everything else — argument parsing, +/// invocation, dependency injection, entity-to-component coercion — is shared. +/// +/// +/// The dispatcher reflects every in the registry once at +/// construction and compiles an expression-tree invoker per command method, so +/// dispatch at runtime costs only a dictionary lookup + parser run. +/// +public abstract class CommandServiceBase +{ + private readonly Dictionary> _commands = new(); + private readonly IEntityManager _entityManager; + private readonly int _prefixParameters; + + /// Initializes a new instance. + /// ECS entity manager. + /// Source of types to scan for commands. + /// + /// Number of "prefix" parameters that are supplied by the caller (not parsed from chat input). + /// For player commands this is 1 (the invoking player). + /// + protected CommandServiceBase(IEntityManager entityManager, ISystemRegistry systemRegistry, int prefixParameters) + { + ArgumentOutOfRangeException.ThrowIfNegative(prefixParameters); + _entityManager = entityManager; + SystemRegistry = systemRegistry; + _prefixParameters = prefixParameters; + + SystemRegistry.Register(LoadCommands); + } + + /// Source of registered types for the scanner. + protected ISystemRegistry SystemRegistry { get; } + + /// Validates and (optionally) rewrites the input text. Default: rejects empty strings. + protected virtual bool ValidateInputText(ref string inputText) + { + return !string.IsNullOrEmpty(inputText); + } + + /// Invokes a command from with the given arguments. + protected InvokeResult Invoke(IServiceProvider services, object[] prefix, string inputText) + { + if (_prefixParameters > 0) + { + ArgumentNullException.ThrowIfNull(prefix); + if (prefix.Length != _prefixParameters) + { + throw new ArgumentException($"prefix must contain {_prefixParameters} values", nameof(prefix)); + } + } + + if (!ValidateInputText(ref inputText)) + { + return InvokeResult.CommandNotFound; + } + + // First word == command name. (TODO: command groups would carry spaces; not supported yet.) + var spaceIndex = inputText.IndexOf(' '); + var name = spaceIndex < 0 ? inputText : inputText[..spaceIndex]; + inputText = inputText[name.Length..]; + + if (!_commands.TryGetValue(name, out var commands)) + { + return InvokeResult.CommandNotFound; + } + + var invalidParameters = false; + var success = false; + + foreach (var command in commands) + { + var cmdInput = inputText; + var accept = true; + var useDefault = false; + + foreach (var p in command.Info.Parameters) + { + if (useDefault) + { + command.Arguments[p.Index] = p.DefaultValue; + continue; + } + + if (p.Parser.TryParse(services, ref cmdInput, out var parsed)) + { + command.Arguments[p.Index] = parsed; + continue; + } + + if (p.IsRequired) + { + accept = false; + break; + } + + useDefault = true; + command.Arguments[p.Index] = p.DefaultValue; + } + + if (accept) + { + if (_prefixParameters > 0) + { + Array.Copy(prefix!, command.Arguments, _prefixParameters); + } + + if (services.GetService(command.SystemType) is ISystem system) + { + var result = command.Invoke(system, command.Arguments, services, _entityManager); + // void / null and any non-(false|0) return value count as "handled". + // Matches legacy SampSharp semantics: explicit `false` / `0` opts out; + // everything else (including void → null) wins. This is critical for + // the common `[PlayerCommand] public void Foo(Player p)` style. + success = result switch + { + bool b => b, + int i => i != 0, + MethodResult mr => mr.Value, + _ => true + }; + } + } + else + { + invalidParameters = true; + } + + Array.Clear(command.Arguments, 0, command.Arguments.Length); + + if (success) + { + return InvokeResult.Success; + } + } + + if (!invalidParameters) + { + return InvokeResult.CommandNotFound; + } + + var usage = GetUsageMessage([..commands.Select(c => c.Info)]); + return new InvokeResult(InvokeResponse.InvalidArguments, usage); + } + + /// + /// Provides the methods to register as commands. Override to filter or augment. + /// Each yielded tuple is (method, info-from-attribute). + /// + protected abstract IEnumerable<(MethodInfo method, ICommandMethodInfo commandInfo)> ScanMethods(); + + /// Returns a human-readable usage message for one or more command overloads. + protected virtual string GetUsageMessage(CommandInfo[] commands) + { + return commands.Length == 1 + ? CommandText(commands[0]) + : $"Usage: {string.Join(" -or- ", commands.Select(CommandText))}"; + } + + /// Builds a parser for the given parameter. Override to support custom types. + protected virtual ICommandParameterParser? CreateParameterParser(ParameterInfo[] parameters, int index) + { + var p = parameters[index]; + var t = p.ParameterType; + + if (t == typeof(int)) + { + return new IntParser(); + } + + if (t == typeof(string)) + { + return index == parameters.Length - 1 ? new StringParser() : new WordParser(); + } + + if (t == typeof(float)) + { + return new FloatParser(); + } + + if (t == typeof(double)) + { + return new DoubleParser(); + } + + if (t == typeof(Player) || t == typeof(EntityId)) + { + return new PlayerParser(); // @TODO: support some other components + } + + return t.IsEnum ? new EnumParser(t) : null; + } + + /// + /// Tries to collect parameter info. Default impl walks parameters skipping the prefix, + /// asks for each; parameters without a parser are + /// treated as DI services. + /// + protected virtual bool TryCollectParameters(ParameterInfo[] parameters, int prefixParameters, + out CommandParameterInfo[]? result) + { + if (parameters.Length < prefixParameters) + { + result = null; + return false; + } + + var list = new List(); + var parameterIndex = prefixParameters; + var optionalSeen = false; + + for (var i = prefixParameters; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var attribute = parameter.GetCustomAttribute(); + + var name = !string.IsNullOrWhiteSpace(attribute?.Name) ? attribute!.Name! : parameter.Name!; + ICommandParameterParser? parser = null; + + if (attribute?.Parser != null && typeof(ICommandParameterParser).IsAssignableFrom(attribute.Parser)) + { + parser = Activator.CreateInstance(attribute.Parser) as ICommandParameterParser; + } + + parser ??= CreateParameterParser(parameters, i); + + if (parser == null) + { + parameterIndex++; + continue; + } + + var optional = parameter.HasDefaultValue; + if (!optional && optionalSeen) + { + // Required after optional is illegal + result = null; + return false; + } + + optionalSeen |= optional; + + list.Add(new CommandParameterInfo(name, parser, !optional, parameter.DefaultValue, parameterIndex++)); + } + + result = list.ToArray(); + return true; + } + + /// Extracts the command name from a method when the attribute didn't override it. + protected virtual string GetCommandName(MethodInfo method) + { + var name = method.Name.ToLowerInvariant(); + if (name.EndsWith("command", StringComparison.Ordinal)) + { + name = name[..^7]; + } + + return name; + } + + private void LoadCommands() + { + foreach (var (method, info) in ScanMethods()) + { + var name = info.Name ?? GetCommandName(method); + if (string.IsNullOrEmpty(name)) + { + continue; + } + + // Only bool / int / void are accepted (matching the legacy convention). + if (method.ReturnType != typeof(bool) && method.ReturnType != typeof(int) && + method.ReturnType != typeof(void)) + { + continue; + } + + var methodParameters = method.GetParameters(); + if (!TryCollectParameters(methodParameters, _prefixParameters, out var parameters)) + { + continue; + } + + var commandInfo = new CommandInfo(name, parameters!); + + // Build per-parameter sources for MethodInvokerFactory. + var argsPtr = 0; + var sources = methodParameters.Select(p => new MethodParameterSource(p)).ToArray(); + for (var i = 0; i < sources.Length; i++) + { + var src = sources[i]; + var t = src.Info.ParameterType; + + if (typeof(Component).IsAssignableFrom(t)) + { + src.ParameterIndex = argsPtr++; + src.IsComponent = true; + } + else if (parameters!.FirstOrDefault(p => p.Index == i) != null) + { + src.ParameterIndex = argsPtr++; + } + else + { + src.IsService = true; + } + } + + var data = new CommandData( + commandInfo, + MethodInvokerFactory.Compile(method, sources), + method.DeclaringType!, + new object?[parameters!.Length + _prefixParameters]); + + if (!_commands.TryGetValue(commandInfo.Name, out var list)) + { + _commands[commandInfo.Name] = list = []; + } + + list.Add(data); + } + } + + private static string CommandText(CommandInfo command) + { + if (command.Parameters.Length == 0) + { + return $"Usage: /{command.Name}"; + } + + var args = string.Join(" ", + command.Parameters.Select(a => a.IsRequired ? $"[{a.Name}]" : $"<{a.Name}>")); + return $"Usage: /{command.Name} {args}"; + } + + private sealed record CommandData(CommandInfo Info, MethodInvoker Invoke, Type SystemType, object?[] Arguments); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities.Commands/EcsBuilderCommandsExtensions.cs b/src/SampSharp.OpenMp.Entities.Commands/EcsBuilderCommandsExtensions.cs new file mode 100644 index 00000000..9570bfbb --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/EcsBuilderCommandsExtensions.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace SampSharp.Entities.SAMP.Commands; + +/// Extensions to register the player-commands subsystem. +public static class EcsBuilderCommandsExtensions +{ + /// + /// Registers with the default + /// implementation. Uses + /// + /// so a custom registered earlier wins. + /// + public static IServiceCollection AddPlayerCommands(this IServiceCollection services) + { + services.TryAddSingleton(); + return services; + } + + /// + /// Wires on + /// OnPlayerCommandText: any chat input not claimed by an + /// [Event] listener gets forwarded to . + /// + public static IEcsBuilder UsePlayerCommands(this IEcsBuilder builder) + { + return builder.UseMiddleware("OnPlayerCommandText"); + } + + /// + /// Registers the player-commands subsystem: adds the default + /// implementation and wires up the + /// on OnPlayerCommandText. + /// + public static IEcsHostBuilder UsePlayerCommands(this IEcsHostBuilder hostBuilder) + { + hostBuilder.ConfigureServices(services => services.AddPlayerCommands()); + hostBuilder.Configure(builder => builder.UsePlayerCommands()); + + return hostBuilder; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/ICommandMethodInfo.cs b/src/SampSharp.OpenMp.Entities.Commands/ICommandMethodInfo.cs new file mode 100644 index 00000000..a012816e --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/ICommandMethodInfo.cs @@ -0,0 +1,8 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// Describes metadata about a command method (extracted from the attribute). +public interface ICommandMethodInfo +{ + /// The overridden command name; = derive from method name. + string? Name { get; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/IPlayerCommandService.cs b/src/SampSharp.OpenMp.Entities.Commands/IPlayerCommandService.cs new file mode 100644 index 00000000..0845fe42 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/IPlayerCommandService.cs @@ -0,0 +1,15 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// +/// Dispatches chat input from a player to a method marked with +/// . +/// +public interface IPlayerCommandService +{ + /// Invokes a player command from the given chat input. + /// Service provider used to resolve handler systems and DI parameters. + /// The invoking player. + /// Raw chat input including the leading slash. + /// if a handler claimed the input; otherwise . + bool Invoke(IServiceProvider services, EntityId player, string inputText); +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/InvokeResponse.cs b/src/SampSharp.OpenMp.Entities.Commands/InvokeResponse.cs new file mode 100644 index 00000000..3adb2120 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/InvokeResponse.cs @@ -0,0 +1,12 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// Possible responses from a command invocation. +public enum InvokeResponse +{ + /// Command executed successfully. + Success, + /// No command matches the input text. + CommandNotFound, + /// Command was found but its arguments could not be parsed. + InvalidArguments, +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/InvokeResult.cs b/src/SampSharp.OpenMp.Entities.Commands/InvokeResult.cs new file mode 100644 index 00000000..d315bdeb --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/InvokeResult.cs @@ -0,0 +1,26 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// Result of a command invocation. +public readonly struct InvokeResult +{ + /// Singleton "command not found" result. + public static readonly InvokeResult CommandNotFound = new(InvokeResponse.CommandNotFound); + + /// Singleton "success" result. + public static readonly InvokeResult Success = new(InvokeResponse.Success); + + /// Initializes a new instance. + /// The response. + /// Usage message — only meaningful for . + public InvokeResult(InvokeResponse response, string? usageMessage = null) + { + Response = response; + UsageMessage = usageMessage; + } + + /// The response code. + public InvokeResponse Response { get; } + + /// Usage hint text shown to the player when arguments are invalid. + public string? UsageMessage { get; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/DoubleParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/DoubleParser.cs new file mode 100644 index 00000000..c5a9bf9c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/DoubleParser.cs @@ -0,0 +1,22 @@ +using System.Globalization; + +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Parses a single word (invariant culture). +public class DoubleParser : ICommandParameterParser +{ + private readonly WordParser _wordParser = new(); + + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + if (!_wordParser.TryParse(services, ref inputText, out var sub) || sub is not string word + || !double.TryParse(word, NumberStyles.Float, CultureInfo.InvariantCulture, out var num)) + { + result = null; + return false; + } + result = num; + return true; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/EnumParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/EnumParser.cs new file mode 100644 index 00000000..d2462637 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/EnumParser.cs @@ -0,0 +1,56 @@ +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Parses an enum by integer value or by (case-insensitive) substring of name. +public class EnumParser : ICommandParameterParser +{ + private readonly Type _enumType; + private readonly WordParser _wordParser = new(); + + /// Initializes a new instance. + /// If is not an enum type. + public EnumParser(Type enumType) + { + ArgumentNullException.ThrowIfNull(enumType); + if (!enumType.IsEnum) + { + throw new ArgumentException("Type must be an enum", nameof(enumType)); + } + + _enumType = enumType; + } + + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + if (!_wordParser.TryParse(services, ref inputText, out var sub) || sub is not string word) + { + result = null; + return false; + } + + if (int.TryParse(word, out var intWord) && Enum.IsDefined(_enumType, intWord)) + { + result = Enum.ToObject(_enumType, intWord); + return true; + } + + var lowerWord = word.ToLowerInvariant(); + var names = Enum.GetNames(_enumType) + .Where(n => n.Contains(lowerWord, StringComparison.InvariantCultureIgnoreCase)) + .ToArray(); + + if (names.Length > 1) + { + names = Enum.GetNames(_enumType).Where(n => n.Contains(word, StringComparison.Ordinal)).ToArray(); + } + + if (names.Length == 1) + { + result = Enum.Parse(_enumType, names[0]); + return true; + } + + result = null; + return false; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/FloatParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/FloatParser.cs new file mode 100644 index 00000000..0c15767d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/FloatParser.cs @@ -0,0 +1,22 @@ +using System.Globalization; + +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Parses a single word (invariant culture). +public class FloatParser : ICommandParameterParser +{ + private readonly WordParser _wordParser = new(); + + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + if (!_wordParser.TryParse(services, ref inputText, out var sub) || sub is not string word + || !float.TryParse(word, NumberStyles.Float, CultureInfo.InvariantCulture, out var num)) + { + result = null; + return false; + } + result = num; + return true; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/ICommandParameterParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/ICommandParameterParser.cs new file mode 100644 index 00000000..1459ee7c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/ICommandParameterParser.cs @@ -0,0 +1,12 @@ +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Parses one command parameter from chat input. +public interface ICommandParameterParser +{ + /// Tries to parse the next token from . + /// Service provider (used by parsers that need DI, e.g. ). + /// Remaining input text. Consumed text is removed. + /// Parsed value on success. + /// on success. + bool TryParse(IServiceProvider services, ref string inputText, out object? result); +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/IntParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/IntParser.cs new file mode 100644 index 00000000..953bad22 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/IntParser.cs @@ -0,0 +1,19 @@ +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Parses a single word. +public class IntParser : ICommandParameterParser +{ + private readonly WordParser _wordParser = new(); + + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + if (!_wordParser.TryParse(services, ref inputText, out var sub) || sub is not string word || !int.TryParse(word, out var num)) + { + result = null; + return false; + } + result = num; + return true; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/PlayerParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/PlayerParser.cs new file mode 100644 index 00000000..194386bc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/PlayerParser.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// +/// Parses a reference. Accepts either: +/// +/// integer playerid (e.g. /kick 5) +/// full player name (case-insensitive) +/// name prefix — picks the player with the lowest playerid among matches +/// +/// Returns the matched so the command pipeline can convert it to a component. +/// +public class PlayerParser : ICommandParameterParser +{ + private readonly WordParser _wordParser = new(); + + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + if (!_wordParser.TryParse(services, ref inputText, out var sub) || sub is not string word) + { + result = null; + return false; + } + + var entityProvider = services.GetRequiredService(); + + if (int.TryParse(word, out var intWord)) + { + var byId = entityProvider.GetPlayer(intWord); + if (byId is { IsComponentAlive: true }) + { + result = byId.Entity; + return true; + } + } + + var entityManager = services.GetRequiredService(); + var players = entityManager.GetComponents(); + + Player? bestCandidate = null; + foreach (var player in players) + { + if (!player.IsComponentAlive) continue; + var name = player.Name; + if (name.Equals(word, StringComparison.OrdinalIgnoreCase)) + { + result = player.Entity; + return true; + } + if (!name.StartsWith(word, StringComparison.OrdinalIgnoreCase)) continue; + if (bestCandidate == null || player.Id < bestCandidate.Id) + bestCandidate = player; + } + + result = bestCandidate?.Entity; + return bestCandidate != null; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/StringParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/StringParser.cs new file mode 100644 index 00000000..856d5a65 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/StringParser.cs @@ -0,0 +1,20 @@ +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Consumes ALL remaining input text (used for the last parameter, e.g. chat messages). +public class StringParser : ICommandParameterParser +{ + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + inputText = inputText.TrimStart(); + if (inputText.Length == 0) + { + result = null; + return false; + } + + result = inputText; + inputText = string.Empty; + return true; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/Parsers/WordParser.cs b/src/SampSharp.OpenMp.Entities.Commands/Parsers/WordParser.cs new file mode 100644 index 00000000..1df2f182 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/Parsers/WordParser.cs @@ -0,0 +1,21 @@ +namespace SampSharp.Entities.SAMP.Commands.Parsers; + +/// Consumes the next whitespace-delimited word. +public class WordParser : ICommandParameterParser +{ + /// + public bool TryParse(IServiceProvider services, ref string inputText, out object? result) + { + result = null; + inputText = inputText.TrimStart(); + if (inputText.Length == 0) return false; + + var index = inputText.IndexOf(' '); + if (index == 0) return false; + + var str = index < 0 ? inputText : inputText[..index]; + inputText = inputText[str.Length..]; + result = str; + return true; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandAttribute.cs b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandAttribute.cs new file mode 100644 index 00000000..1dd8472d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandAttribute.cs @@ -0,0 +1,34 @@ +using JetBrains.Annotations; + +namespace SampSharp.Entities.SAMP.Commands; + +/// +/// Marks an instance method on an as a player command. +/// The method is invoked when a connected player sends matching chat input +/// (a slash followed by the command name and optional arguments). +/// +/// +/// The signature is (Player player, [args...]). The first parameter +/// must be the invoking (or ); +/// subsequent parameters are parsed from the chat input via +/// . Return type may be +/// , or . +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] +[MeansImplicitUse] +public class PlayerCommandAttribute : Attribute, ICommandMethodInfo +{ + /// Initializes a new instance with the command name inferred from the method name (lowercased, trailing "Command" stripped). + public PlayerCommandAttribute() + { + } + + /// Initializes a new instance with the explicit command . + public PlayerCommandAttribute(string name) + { + Name = name; + } + + /// + public string? Name { get; set; } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandProcessingMiddleware.cs b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandProcessingMiddleware.cs new file mode 100644 index 00000000..abdb1f7f --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandProcessingMiddleware.cs @@ -0,0 +1,41 @@ +namespace SampSharp.Entities.SAMP.Commands; + +/// +/// Middleware on OnPlayerCommandText that, if the [Event]-handlers leave +/// the input unhandled (return /), +/// dispatches it through . +/// +public class PlayerCommandProcessingMiddleware +{ + private readonly EventDelegate _next; + + /// Initializes a new instance. + public PlayerCommandProcessingMiddleware(EventDelegate next) + { + _next = next; + } + + /// Invokes the middleware. + public object? Invoke(EventContext context, IPlayerCommandService commandService) + { + var result = _next(context); + + // Successful response → done. We treat anything truthy as "handled" (matches EventDispatcher semantics). + if (IsHandled(result)) + return result; + + if (context.Arguments is [EntityId player, string text, ..]) + return commandService.Invoke(context.EventServices, player, text); + + return result; + } + + private static bool IsHandled(object? result) => result switch + { + null => false, + bool b => b, + int i => i != 0, + MethodResult mr => mr.Value, + _ => true, + }; +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandService.cs b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandService.cs new file mode 100644 index 00000000..74511bd7 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/PlayerCommandService.cs @@ -0,0 +1,72 @@ +using System.Reflection; +using SampSharp.Entities.Utilities; + +namespace SampSharp.Entities.SAMP.Commands; + +/// +/// Default implementation. Scans every +/// loaded for methods marked +/// and dispatches matching chat input +/// (slash-prefixed) to them. The first parameter of the handler is the +/// invoking . +/// +public class PlayerCommandService : CommandServiceBase, IPlayerCommandService +{ + private readonly IEntityManager _entityManager; + + /// Initializes a new instance. + public PlayerCommandService(IEntityManager entityManager, ISystemRegistry systemRegistry) + : base(entityManager, systemRegistry, prefixParameters: 1) + { + _entityManager = entityManager; + } + + /// + public bool Invoke(IServiceProvider services, EntityId player, string inputText) + { + var result = Invoke(services, [(object)player], inputText); + + if (result.Response != InvokeResponse.InvalidArguments) + return result.Response == InvokeResponse.Success; + + // Auto-print usage hint to the invoking player on InvalidArguments. + _entityManager.GetComponent(player)?.SendClientMessage(result.UsageMessage ?? "Invalid arguments."); + return true; + } + + /// + protected override IEnumerable<(MethodInfo method, ICommandMethodInfo commandInfo)> ScanMethods() + { + // Same enumeration mode as EventDispatcher: instance + non-public + ISystem-types from registry. + var scanner = ClassScanner.Create() + .IncludeTypes(SystemRegistry.GetSystemTypes().Span) + .IncludeNonPublicMembers(); + + return scanner.ScanMethods() + .Select(t => (t.method, (ICommandMethodInfo)t.attribute)); + } + + /// + protected override bool ValidateInputText(ref string inputText) + { + if (!base.ValidateInputText(ref inputText)) return false; + if (!inputText.StartsWith('/') || inputText.Length <= 1) return false; + inputText = inputText[1..]; + return true; + } + + /// + protected override string GetUsageMessage(CommandInfo[] commands) + => commands.Length == 1 + ? CommandText(commands[0]) + : $"Usage: {string.Join(" -or- ", commands.Select(CommandText))}"; + + private static string CommandText(CommandInfo command) + { + if (command.Parameters.Length == 0) + return $"Usage: /{command.Name}"; + var args = string.Join(" ", + command.Parameters.Select(a => a.IsRequired ? $"[{a.Name}]" : $"<{a.Name}>")); + return $"Usage: /{command.Name} {args}"; + } +} diff --git a/src/SampSharp.OpenMp.Entities.Commands/SampSharp.OpenMp.Entities.Commands.csproj b/src/SampSharp.OpenMp.Entities.Commands/SampSharp.OpenMp.Entities.Commands.csproj new file mode 100644 index 00000000..fd5ed664 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities.Commands/SampSharp.OpenMp.Entities.Commands.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + enable + enable + SampSharp.Entities.SAMP.Commands + True + + + + + + + + + + + + + + + diff --git a/src/SampSharp.OpenMp.Entities/Components/Component.cs b/src/SampSharp.OpenMp.Entities/Components/Component.cs new file mode 100644 index 00000000..b12cdaf4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Components/Component.cs @@ -0,0 +1,187 @@ +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; + +namespace SampSharp.Entities; + +/// Represents a component which can be attached to an entity. +public abstract class Component +{ + private IEntityManager? _manager; + + /// Gets the manager of the entity of this component. + protected internal virtual IEntityManager Manager + { + get => _manager ?? throw new InvalidOperationException("Component not yet added to entity manager."); + internal set => _manager = value; + } + + /// Gets the parent entity of the entity to which this component has been attached. + public EntityId Parent => Manager.GetParent(Entity); + + /// Gets the entity to which this component has been attached. + public virtual EntityId Entity { get; internal set; } + + /// Gets a value indicating whether this component is alive (has not been destroyed). + public bool IsComponentAlive { get; private set; } = true; + + /// + /// Gets a value indicating whether this component is being destroyed. This property is set to when the + /// destruction of this component has been initiated. + /// + public bool IsDestroying { get; private set; } + + /// Gets a component of the specified type attached to the entity. + /// The type of the component to find. + /// The found component or if no component of the specified type could be found. + [Pure] + public T? GetComponent() where T : Component + { + return Manager.GetComponent(Entity); + } + + /// Adds a component of the specified type to the entity with the specified + /// constructor . + /// The type of the component to add. + /// The arguments of the constructor of the component. + /// The created component. + public T AddComponent(params object[] args) where T : Component + { + return Manager.AddComponent(Entity, args); + } + + /// Adds a component of the specified type to the entity. + /// The type of the component to add. + /// The created component. + public T AddComponent() where T : Component + { + return Manager.AddComponent(Entity); + } + + /// Adds a component of the specified type to the entity. + /// The type of the component to add. + /// The instance of component to be added. + public void AddComponent(T component) where T : Component + { + Manager.AddComponent(Entity, component); + } + + /// Destroys the components of the specified type attached to the + /// entity. + /// The type of the components to destroy. + public void DestroyComponents() where T : Component + { + Manager.Destroy(Entity); + } + + /// Destroys the entity. + public void DestroyEntity() + { + Manager.Destroy(Entity); + } + + /// Destroys this component. + public void Destroy() + { + Manager.Destroy(this); + } + + /// Gets all components of the specified type attached to the entity. + /// The type of the components to find. + /// A collection of the found components. + [Pure] + public T[] GetComponents() where T : Component + { + return Manager.GetComponents(Entity); + } + + /// Gets a component of the specified type attached to a child entity of the + /// entity using a depth first search. + /// The type of the component to find. + /// The found component or if no component of the specified type could be found. + [Pure] + public T? GetComponentInChildren() where T : Component + { + return Manager.GetComponentInChildren(Entity); + } + + /// Gets all components of the specified type attached to a child entity of the + /// entity. + /// The type of the components to find. + /// A collection of the found components. + [Pure] + public T[] GetComponentsInChildren() where T : Component + { + return Manager.GetComponentsInChildren(Entity); + } + + /// Gets a component of the specified type attached to a parent entity of the + /// entity. + /// The type of the component to find. + /// The found component or if no component of the specified type could be found. + [Pure] + public T? GetComponentInParent() where T : Component + { + return Manager.GetComponentInParent(Entity); + } + + /// Gets all components of the specified type attached to a parent entity of the + /// entity. + /// The type of the components to find. + /// A collection of the found components. + [Pure] + public T[] GetComponentsInParent() where T : Component + { + return Manager.GetComponentsInParent(Entity); + } + + /// This method is invoked after this component has been attached the an entity. + protected virtual void OnInitializeComponent() + { + } + + /// This method is invoked before this component is destroyed and removed from its entity. + protected virtual void OnDestroyComponent() + { + } + + internal void InvokeInitialize() + { + OnInitializeComponent(); + } + + internal void InvokedDestroy() + { + IsDestroying = true; + OnDestroyComponent(); + IsComponentAlive = false; + } + + /// Implements the operator true. Returns if the specified is + /// alive. + /// The component. + /// if the specified is alive; otherwise. + public static bool operator true([NotNullWhen(true)]Component? component) + { + return component is { IsComponentAlive: true }; + } + + /// Implements the operator false. Returns if the specified is + /// not alive. + /// The component. + /// if the specified is not alive; + /// otherwise. + public static bool operator false([NotNullWhen(false)]Component? component) + { + return component is not { IsComponentAlive: true }; + } + + /// Implements the operator !. Returns if the specified is not + /// alive. + /// The component. + /// if the specified is not alive; otherwise + /// . + public static bool operator !([NotNullWhen(false)]Component? component) + { + return component is not { IsComponentAlive: true }; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Components/ComponentExtension.cs b/src/SampSharp.OpenMp.Entities/Components/ComponentExtension.cs new file mode 100644 index 00000000..e42953e1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Components/ComponentExtension.cs @@ -0,0 +1,33 @@ +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +/// +/// Represents an extension to an open.mp component which binds its lifetime to the lifetime of an . +/// +/// The component to which this extension is bound. +[Extension(0x53d3b0b0bbb28a7f)] +public sealed class ComponentExtension(Component component) : Extension +{ + + /// + /// Gets the component to which this extension is bound. + /// + public Component Component { get; } = component; + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + public bool IsOmpEntityDestroyed { get; private set; } + + /// + protected override void Cleanup() + { + IsOmpEntityDestroyed = true; + + if (!Component.IsDestroying) + { + Component.DestroyEntity(); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/ComponentNode.cs b/src/SampSharp.OpenMp.Entities/Entities/ComponentNode.cs new file mode 100644 index 00000000..6bbde4ad --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/ComponentNode.cs @@ -0,0 +1,15 @@ +namespace SampSharp.Entities; + +internal sealed class ComponentNode +{ + public Component? Component; + public ComponentNode? Next; + public ComponentNode? Previous; + + public void Reset() + { + Component = null; + Previous = null; + Next = null; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/ComponentNodeObjectPolicy.cs b/src/SampSharp.OpenMp.Entities/Entities/ComponentNodeObjectPolicy.cs new file mode 100644 index 00000000..7b27a521 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/ComponentNodeObjectPolicy.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.Entities; + +internal sealed class ComponentNodeObjectPolicy : PooledObjectPolicy +{ + public override ComponentNode Create() + { + return new ComponentNode(); + } + + public override bool Return(ComponentNode obj) + { + obj.Reset(); + return true; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/ComponentStore.cs b/src/SampSharp.OpenMp.Entities/Entities/ComponentStore.cs new file mode 100644 index 00000000..6d12b3dc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/ComponentStore.cs @@ -0,0 +1,193 @@ +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.Entities; + +internal sealed class ComponentStore(ObjectPool componentPool) +{ + /// A list of components attached to the entity by component type. Components are stored in a linked list + /// and are stored in the list associated with their type and every base type of their type. + private readonly Dictionary _components = new(); + + public bool IsEmpty => _components.All(x => x.Value.Count == 0); + + public void ReturnComponents() + { + foreach (var kv in _components) + { + var entry = kv.Value.First; + while (entry != null) + { + var current = entry; + entry = entry.Next; + + componentPool.Return(current); + } + } + + _components.Clear(); + } + + public int GetCount() where T : Component + { + return !_components.TryGetValue(typeof(T), out var data) + ? 0 + : data.Count; + } + + public T[] GetAll() where T : Component + { + if (!_components.TryGetValue(typeof(T), out var data)) + { + return []; + } + + var result = new T[data.Count]; + + var index = 0; + var current = data.First; + + while (current != null) + { + result[index++] = (T)current.Component!; + current = current.Next; + } + + return result; + } + + public int GetAll(T[] array, int startIndex) where T : Component + { + if (!_components.TryGetValue(typeof(T), out var data)) + { + return 0; + } + + var index = startIndex; + var current = data.First; + + if (array.Length < index + data.Count) + { + throw new ArgumentException("The length of the array is less than the number of elements to be copied.", nameof(array)); + } + + while (current != null) + { + array[index++] = (T)current.Component!; + current = current.Next; + } + + return index - startIndex; + } + + public T? Get() where T : Component + { + return !_components.TryGetValue(typeof(T), out var data) + ? null + : data.First?.Component as T; + } + + public void Add(T component) where T : Component + { + var current = component.GetType(); + + Add(current, component); + do + { + current = current.BaseType!; + + Add(current, component); + } while (current != typeof(Component)); + } + + public void Remove(Component component) + { + var current = component.GetType(); + + // Remove the component in every type list + Remove(current, component); + do + { + current = current.BaseType!; + + Remove(current, component); + } while (current != typeof(Component)); + } + + private void Add(Type type, Component component) + { + var newEntry = componentPool.Get(); + newEntry.Component = component; + + if (_components.TryGetValue(type, out var data)) + { + newEntry.Next = data.First; + if (data.First != null) + { + data.First.Previous = newEntry; + } + + data.First = newEntry; + data.Count++; + _components[type] = data; + } + else + { + _components[type] = new Data + { + First = newEntry, + Count = 1 + }; + } + } + + private void Remove(Type type, Component component) + { + if (!_components.TryGetValue(type, out var data)) + { + return; + } + + var entry = data.First; + while (entry != null) + { + if (entry.Component != component) + { + entry = entry.Next; + continue; + } + + if (entry.Previous != null) + { + entry.Previous.Next = entry.Next; + } + + if (entry.Next != null) + { + entry.Next.Previous = entry.Previous; + } + + if (data.First == entry) + { + data.First = entry.Next; + } + + componentPool.Return(entry); + data.Count--; + _components[type] = data; + return; + } + } + + private struct Data + { + /// + /// First linked list node. + /// + public ComponentNode? First; + + /// + /// Number of components in the list. + /// + public int Count; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/EntityId.cs b/src/SampSharp.OpenMp.Entities/Entities/EntityId.cs new file mode 100644 index 00000000..4ad47f5c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/EntityId.cs @@ -0,0 +1,66 @@ +namespace SampSharp.Entities; + +/// Represents an identifier of an entity. +public readonly record struct EntityId +{ + /// An empty entity identifier. + public static readonly EntityId Empty = new(); + + private readonly Guid _id; + + private EntityId(Guid id) + { + _id = id; + } + + /// Gets a value indicating whether this handle is empty. + public bool IsEmpty => _id == Guid.Empty; + + /// Creates a fresh entity handle backed by a new . + public static EntityId NewEntityId() + { + return new EntityId(Guid.NewGuid()); + } + + /// + public override string ToString() + { + return IsEmpty ? "(Empty)" : $"(Id = {_id})"; + } + + /// + /// Performs an implicit conversion from to . + /// Returns the entity of the component. + /// + public static implicit operator EntityId(Component component) + { + return component?.Entity ?? default; + } + + /// + /// Performs an implicit conversion from to . + /// Returns if the specified is not empty. + /// + public static implicit operator bool(EntityId value) + { + return !value.IsEmpty; + } + + /// Implements the operator . + public static bool operator true(EntityId value) + { + return !value.IsEmpty; + } + + /// Implements the operator . + public static bool operator false(EntityId value) + { + return value.IsEmpty; + } + + /// Implements the operator !. + public static bool operator !(EntityId value) + { + return value.IsEmpty; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/EntityManager.cs b/src/SampSharp.OpenMp.Entities/Entities/EntityManager.cs new file mode 100644 index 00000000..5e9c15d0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/EntityManager.cs @@ -0,0 +1,473 @@ +using System.Reflection; +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.Entities; + +internal class EntityManager : IEntityManager +{ + private readonly ComponentStore _components; + private readonly Dictionary _entities = new(100); + private readonly ObjectPool _entityPool; + private readonly HashSet _rootEntities = new(100); + + public EntityManager() + { + var componentPool = new DefaultObjectPool(new ComponentNodeObjectPolicy(), 100); + _entityPool = new DefaultObjectPool(new EntityNodeObjectPolicy(componentPool), 100); + _components = new ComponentStore(componentPool); + } + + public T AddComponent(EntityId entity, params object[] args) where T : Component + { + return AddComponent(entity, default, args); + } + + public T AddComponent(EntityId entity) where T : Component + { + return AddComponent(entity, default, []); + } + + public T AddComponent(EntityId entity, EntityId parent) where T : Component + { + return AddComponent(entity, parent, []); + } + + public void AddComponent(EntityId entity, T component) where T : Component + { + AddComponent(entity, default, component); + } + + public T AddComponent(EntityId entity, EntityId parent, params object[] args) where T : Component + { + var component = (T)Activator.CreateInstance(typeof(T), + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, args, null)!; + + AddComponent(entity, parent, component); + + return component; + } + + public void AddComponent(EntityId entity, EntityId parent, T component) where T : Component + { + var entityNode = AddEntityNode(entity, parent); + + // Connect component to entity + component.Entity = entity; + component.Manager = this; + + // Add component to components store of entity and global components store + entityNode.Components.Add(component); + _components.Add(component); + + component.InvokeInitialize(); + } + + public void Destroy(Component component) + { + ArgumentNullException.ThrowIfNull(component); + + if (!component.IsComponentAlive) + { + return; + } + + if (!_entities.TryGetValue(component.Entity, out var node)) + { + throw new InvalidOperationException("Invalid entity manager state."); + } + + RemoveOne(node, component); + } + + public void Destroy(EntityId entity) + { + if (!_entities.TryGetValue(entity, out var node)) + { + return; + } + + while (node.FirstChild != null) + { + Destroy(node.FirstChild.Id); + } + + foreach (var component in node.Components.GetAll()) + { + if (!component.IsComponentAlive) + { + continue; + } + + node.Components.Remove(component); + _components.Remove(component); + component.InvokedDestroy(); + } + + RemoveOne(node); + } + + public void Destroy(EntityId entity) where T : Component + { + if (!_entities.TryGetValue(entity, out var node)) + { + return; + } + + var count = node.Components.GetCount(); + + switch (count) + { + case 0: + return; + case 1: + { + var component = node.Components.Get()!; + RemoveOne(node, component); + break; + } + default: + { + var components = node.Components.GetAll(); + + foreach (var component in components) + { + RemoveOne(node, component); + } + + break; + } + } + } + + public EntityId[] GetChildren(EntityId entity) + { + if (!_entities.TryGetValue(entity, out var node)) + { + return []; + } + + var result = new EntityId[node.ChildCount]; + + var current = node.FirstChild; + var index = 0; + while (current != null) + { + result[index++] = current.Id; + current = current.Next; + } + + return result; + } + + public EntityId[] GetRootEntities() + { + if (_rootEntities.Count == 0) + { + return []; + } + + var result = new EntityId[_rootEntities.Count]; + _rootEntities.CopyTo(result); + return result; + } + + public T? GetComponent() where T : Component + { + return _components.Get(); + } + + public T? GetComponent(EntityId entity) where T : Component + { + return _entities.TryGetValue(entity, out var node) + ? node.Components.Get() + : null; + } + + public T? GetComponentInChildren(EntityId entity) where T : Component + { + if (!_entities.TryGetValue(entity, out var entityNode)) + { + return null; + } + + return Search(entityNode); + + static T? Search(EntityNode node) + { + // shallow search + var current = node.FirstChild; + while (current != null) + { + var result = current.Components.Get(); + if (result != null) + { + return result; + } + + current = current.Next; + } + + // deep search + current = node.FirstChild; + + while (current != null) + { + var result = Search(current); + if (result != null) + { + return result; + } + + current = current.Next; + } + + return null; + } + } + + public T? GetComponentInParent(EntityId entity) where T : Component + { + _entities.TryGetValue(entity, out var entry); + + var current = entry?.Parent; + + while (current != null) + { + var result = current.Components.Get(); + + if (result != null) + { + return result; + } + + current = current.Parent; + } + + return null; + } + + public T[] GetComponents() where T : Component + { + return _components.GetAll(); + } + + public T[] GetComponents(EntityId entity) where T : Component + { + return _entities.TryGetValue(entity, out var node) + ? node.Components.GetAll() + : []; + } + + public T[] GetComponentsInChildren(EntityId entity) where T : Component + { + if (!_entities.TryGetValue(entity, out var entityNode)) + { + return []; + } + + var count = Count(entityNode); + + if (count == 0) + { + return []; + } + + var result = new T[count]; + + Search(result, 0, entityNode); + return result; + + static int Count(EntityNode node) + { + var result = 0; + var current = node.FirstChild; + + while (current != null) + { + var components = current.Components.GetCount(); + result += components; + + Count(current); + + current = current.Next; + } + + return result; + } + + static int Search(T[] array, int startIndex, EntityNode node) + { + var index = startIndex; + var current = node.FirstChild; + while (current != null) + { + index += current.Components.GetAll(array, index); + current = current.Next; + } + + current = node.FirstChild; + + while (current != null) + { + index += Search(array, index, current); + current = current.Next; + } + + return index - startIndex; + } + } + + public T[] GetComponentsInParent(EntityId entity) where T : Component + { + if (!_entities.TryGetValue(entity, out var entityNode)) + { + return []; + } + + var count = Count(entityNode); + + if (count == 0) + { + return []; + } + + var result = new T[count]; + Search(result, 0, entityNode); + + return result; + + static int Count(EntityNode node) + { + var result = 0; + var current = node.Parent; + + while (current != null) + { + result += current.Components.GetCount(); + current = current.Parent; + } + + + return result; + } + + static void Search(T[] array, int startIndex, EntityNode node) + { + var index = startIndex; + var current = node.Parent; + while (current != null) + { + index += current.Components.GetAll(array, index); + current = current.Parent; + } + } + } + + public EntityId GetParent(EntityId entity) + { + if (_entities.TryGetValue(entity, out var node)) + { + return node.Parent?.Id ?? default; + } + + return default; + } + + public bool Exists(EntityId entity) + { + return _entities.ContainsKey(entity); + } + + private EntityNode AddEntityNode(EntityId entity, EntityId parent) + { + if (!_entities.TryGetValue(entity, out var entityNode)) + { + // New entity + EntityNode? parentNode = null; + + if (parent != default && !_entities.TryGetValue(parent, out parentNode)) + { + // Parent is new too + parentNode = AddEntityNode(parent, default); + } + + // Create the entity + _entities[entity] = entityNode = _entityPool.Get(); + entityNode.Id = entity; + + if (parentNode != null) + { + // Assign the parent in the node + entityNode.Parent = parentNode; + + // Append the entity to the parent + parentNode.AppendChild(entityNode); + } + + // Keep track of roots + if (parentNode == null) + { + _rootEntities.Add(entity); + } + } + else + { + // Existing entity + if (parent != default) + { + // Verify parent matches + if (entityNode.Parent == null || entityNode.Parent.Id != parent) + { + throw new ArgumentException("The specified parent does not match the current parent entity.", + nameof(parent)); + } + } + } + + return entityNode; + } + + private void RemoveOne(EntityNode node, T component) where T : Component + { + node.Components.Remove(component); + _components.Remove(component); + + component.InvokedDestroy(); + + if (node.IsEmpty) + { + RemoveOne(node); + } + } + + private void RemoveOne(EntityNode node) + { + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + + if (node.Parent == null) + { + _rootEntities.Remove(node.Id); + } + else + { + node.Parent.ChildCount--; + if (node.Parent.FirstChild == node) + { + node.Parent.FirstChild = node.Next; + } + + if (node.Parent.IsEmpty) + { + RemoveOne(node.Parent); + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/EntityNode.cs b/src/SampSharp.OpenMp.Entities/Entities/EntityNode.cs new file mode 100644 index 00000000..3b5c4623 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/EntityNode.cs @@ -0,0 +1,46 @@ +namespace SampSharp.Entities; + +internal sealed class EntityNode +{ + // An entity has a 0-1 parent, 0-n children and 0-n components + public EntityNode(ComponentStore components) + { + Components = components; + } + + public readonly ComponentStore Components; + + public int ChildCount; + public EntityNode? FirstChild; + public EntityId Id; + public EntityNode? Next; + public EntityNode? Parent; + public EntityNode? Previous; + public bool IsEmpty => ChildCount == 0 && Components.IsEmpty; + + public void AppendChild(EntityNode child) + { + ChildCount++; + if (FirstChild == null) + { + FirstChild = child; + return; + } + + // insert as first + child.Next = FirstChild; + FirstChild.Previous = child; + FirstChild = child; + } + + public void Reset() + { + Id = default; + Next = null; + Previous = null; + Parent = null; + FirstChild = null; + ChildCount = 0; + Components.ReturnComponents(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/EntityNodeObjectPolicy.cs b/src/SampSharp.OpenMp.Entities/Entities/EntityNodeObjectPolicy.cs new file mode 100644 index 00000000..fee202c1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/EntityNodeObjectPolicy.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.Entities; + +internal sealed class EntityNodeObjectPolicy(ObjectPool componentPool) : PooledObjectPolicy +{ + public override EntityNode Create() + { + return new EntityNode(new ComponentStore(componentPool)); + } + + public override bool Return(EntityNode obj) + { + obj.Reset(); + return true; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Entities/IEntityManager.cs b/src/SampSharp.OpenMp.Entities/Entities/IEntityManager.cs new file mode 100644 index 00000000..3ac40c03 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Entities/IEntityManager.cs @@ -0,0 +1,166 @@ +using System.Diagnostics.Contracts; + +namespace SampSharp.Entities; + +/// Provides functionality for the creation, modification and destruction of entities. +public interface IEntityManager +{ + /// + /// Adds a component of the specified type to the specified + /// with the specified constructor . + /// + /// The type of the component to add. + /// The entity to add the component to. + /// The arguments of the constructor of the component. + /// The created component. + T AddComponent(EntityId entity, params object[] args) where T : Component; + + /// + /// Adds a component of the specified type to the specified + /// with the specified constructor . The entity is attached as a child to the specified + /// . + /// + /// The type of the component to add. + /// The entity to add the component to. + /// The parent of the entity to which the entity is to be attached. + /// The arguments of the constructor of the component. + /// The created component. + T AddComponent(EntityId entity, EntityId parent, params object[] args) where T : Component; + + /// Adds a component of the specified type to the specified . + /// The type of the component to add. + /// The entity to add the component to. + /// The created component. + T AddComponent(EntityId entity) where T : Component; + + /// + /// Adds a component of the specified type to the specified . + /// The entity is attached as a child to the specified . + /// + /// The type of the component to add. + /// The entity to add the component to. + /// The parent of the entity to which the entity is to be attached. + /// The created component. + T AddComponent(EntityId entity, EntityId parent) where T : Component; + + /// Adds a component of the specified type to the specified . + /// The type of the component to add. + /// The entity to add the component to. + /// The instance of the component to be added. + void AddComponent(EntityId entity, T component) where T : Component; + + /// + /// Adds a component of the specified type to the specified . + /// The entity is attached as a child to the specified . + /// + /// The type of the component to add. + /// The entity to add the component to. + /// The parent of the entity to which the entity is to be attached. + /// The instance of the component to be added. + void AddComponent(EntityId entity, EntityId parent, T component) where T : Component; + + /// Destroys the specified . + /// The component to destroy. + void Destroy(Component component); + + /// Destroys the specified . + /// The entity. + void Destroy(EntityId entity); + + /// Destroys the components of the specified type attached to the specified + /// . + /// The type of the components to destroy. + /// The entity of which to destroy its components of the specified type . + void Destroy(EntityId entity) where T : Component; + + /// Gets the children of the specified . + /// The entity to get the children of. + /// An array with the entities of which the parent is the specified . + [Pure] + EntityId[] GetChildren(EntityId entity); + + /// Gets all root entities with no parent. + /// An array with all entities without a parent. + [Pure] + EntityId[] GetRootEntities(); + + /// Gets a component of the specified type attached to any entity. + /// The type of the component to find. + /// The found component or if no component of the specified type could be found. + [Pure] + T? GetComponent() where T : Component; + + /// Gets a component of the specified type attached to the specified . + /// The type of the component to find. + /// The entity to get the component from. + /// The found component or if no component of the specified type could be found. + [Pure] + T? GetComponent(EntityId entity) where T : Component; + + /// + /// Gets a component of the specified type attached to a child entity of the specified + /// using a depth-first search. + /// + /// The type of the component to find. + /// The entity to get the component from. + /// The found component or if no component of the specified type could be found. + [Pure] + T? GetComponentInChildren(EntityId entity) where T : Component; + + /// Gets a component of the specified type attached to a parent entity of the + /// specified . + /// The type of the component to find. + /// The entity to get the component from. + /// The found component or if no component of the specified type could be found. + [Pure] + T? GetComponentInParent(EntityId entity) where T : Component; + + /// Gets all components of the specified type attached to any entity. + /// The type of the components to find. + /// A collection of the found components. + [Pure] + T[] GetComponents() where T : Component; + + /// Gets all components of the specified type attached to the specified . + /// The type of the components to find. + /// The entity to get the components from. + /// A collection of the found components. + [Pure] + T[] GetComponents(EntityId entity) where T : Component; + + /// Gets all components of the specified type attached to a child entity of the + /// specified . + /// The type of the components to find. + /// The entity to get the components from. + /// A collection of the found components. + [Pure] + T[] GetComponentsInChildren(EntityId entity) where T : Component; + + /// Gets all components of the specified type attached to a parent entity of the + /// specified . + /// The type of the components to find. + /// The entity to get the components from. + /// A collection of the found components. + [Pure] + T[] GetComponentsInParent(EntityId entity) where T : Component; + + /// Gets the parent entity of the specified . + /// The entity to get its parent. + /// + /// The parent entity of the specified . is returned if the + /// specified does not have a parent. + /// + [Pure] + EntityId GetParent(EntityId entity); + + /// Returns a value indicating whether the specified exists. + /// The entity to check its existence. + /// if the specified exists; otherwise, . + [Pure] + bool Exists(EntityId entity); +} diff --git a/src/SampSharp.OpenMp.Entities/Events/EcsBuilderEventScopeExtensions.cs b/src/SampSharp.OpenMp.Entities/Events/EcsBuilderEventScopeExtensions.cs new file mode 100644 index 00000000..b04db986 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EcsBuilderEventScopeExtensions.cs @@ -0,0 +1,14 @@ +namespace SampSharp.Entities; + +/// Provides extended functionality for configuring a instance. +public static class EcsBuilderEventScopeExtensions +{ + /// Enabled a Dependency Injection scope for the event with the specified . + /// The ECS builder in which to enable the scope. + /// The name of the event to add the scope to. + /// A reference to this instance after the operation has completed. + public static IEcsBuilder EnableEventScope(this IEcsBuilder builder, string name) + { + return builder.UseMiddleware(name); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EcsBuilderUseMiddlewareExtensions.cs b/src/SampSharp.OpenMp.Entities/Events/EcsBuilderUseMiddlewareExtensions.cs new file mode 100644 index 00000000..9d1c555a --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EcsBuilderUseMiddlewareExtensions.cs @@ -0,0 +1,154 @@ +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities; + +/// Provides extended functionality for configuring a instance. +public static class EcsBuilderUseMiddlewareExtensions +{ + private static readonly MethodInfo _getServiceInfo = + typeof(EcsBuilderUseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!; + + /// Adds a middleware to the handler of the event with the specified . + /// The ECS builder to add the middleware to. + /// The name of the event. + /// The middleware to add to the event. + /// A reference to this instance after the operation has completed. + public static IEcsBuilder UseMiddleware(this IEcsBuilder builder, string name, Func middleware) + { + builder.Services.GetRequiredService().UseMiddleware(name, middleware); + return builder; + } + + /// Adds a middleware to the handler of the event with the specified . + /// The ECS builder to add the middleware to. + /// The name of the event. + /// The middleware to add to the event. + /// A reference to this instance after the operation has completed. + public static IEcsBuilder UseMiddleware(this IEcsBuilder builder, string name, Func, object?> middleware) + { + return builder.UseMiddleware(name, next => + { + return context => + { + return middleware(context, SimpleNext); + + object? SimpleNext() + { + return next(context); + } + }; + }); + } + + /// Adds a middleware of the specified type to the handler of the event + /// with the specified . + /// The type of the middleware. + /// The ECS builder to add the middleware to. + /// The name of the event. + /// The arguments for the constructor of the event. + /// A reference to this instance after the operation has completed. + public static IEcsBuilder UseMiddleware(this IEcsBuilder builder, string name, params object[] args) + { + return builder.UseMiddleware(name, typeof(TMiddleware), args); + } + + /// Adds a middleware of the specified type to the handler of the event with + /// the specified . + /// The ECS builder to add the middleware to. + /// The name of the event. + /// The type of the middleware. + /// The arguments for the constructor of the event. + /// A reference to this instance after the operation has completed. + public static IEcsBuilder UseMiddleware(this IEcsBuilder builder, string name, Type middleware, params object[] args) + { + var applicationServices = builder.Services; + return builder.UseMiddleware(name, next => + { + var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var invokeMethods = methods.Where(m => string.Equals(m.Name, "Invoke", StringComparison.Ordinal)) + .ToArray(); + + if (invokeMethods.Length != 1) + { + throw new InvalidOperationException(); + } + + var methodInfo = invokeMethods[0]; + + var parameters = methodInfo.GetParameters(); + if (parameters.Length < 1 || parameters[0] + .ParameterType != typeof(EventContext)) + { + throw new InvalidOperationException(); + } + + var ctorArgs = new object[args.Length + 1]; + ctorArgs[0] = next; + Array.Copy(args, 0, ctorArgs, 1, args.Length); + var instance = ActivatorUtilities.CreateInstance(applicationServices, middleware, ctorArgs); + if (parameters.Length == 1) + { + return methodInfo.CreateDelegate(instance); + } + + var factory = Compile(methodInfo, parameters); + + return context => + { + var serviceProvider = context.EventServices ?? applicationServices; + if (serviceProvider == null) + { + throw new InvalidOperationException(); + } + + return factory(instance, context, serviceProvider); + }; + }); + } + + private static Func Compile(MethodInfo methodInfo, ParameterInfo[] parameters) + { + var middleware = typeof(T); + + var eventContextArg = Expression.Parameter(typeof(EventContext), "eventContext"); + var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); + var instanceArg = Expression.Parameter(middleware, "middleware"); + + var methodArguments = new Expression[parameters.Length]; + methodArguments[0] = eventContextArg; + for (var i = 1; i < parameters.Length; i++) + { + var parameterType = parameters[i] + .ParameterType; + if (parameterType.IsByRef) + { + throw new NotSupportedException(); + } + + var parameterTypeExpression = new Expression[] { providerArg, Expression.Constant(parameterType, typeof(Type)) }; + + var getServiceCall = Expression.Call(_getServiceInfo, parameterTypeExpression); + methodArguments[i] = Expression.Convert(getServiceCall, parameterType); + } + + Expression middlewareInstanceArg = instanceArg; + if (methodInfo.DeclaringType != typeof(T)) + { + middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType!); + } + + var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments); + + var lambda = Expression.Lambda>(body, instanceArg, eventContextArg, providerArg); + + return lambda.Compile(); + } + + private static object GetService(IServiceProvider sp, Type type) + { + var service = sp.GetService(type); + return service ?? throw new InvalidOperationException(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventAttribute.cs b/src/SampSharp.OpenMp.Entities/Events/EventAttribute.cs new file mode 100644 index 00000000..1c9ef418 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventAttribute.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace SampSharp.Entities; + +/// Indicates a method is to be invoked when an event occurs. +[AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] +public class EventAttribute : Attribute +{ + /// Gets or sets the name of the event which should invoke the method. If this value is null, the method name is used as the event name. + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventContext.cs b/src/SampSharp.OpenMp.Entities/Events/EventContext.cs new file mode 100644 index 00000000..3f9d1bf9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventContext.cs @@ -0,0 +1,14 @@ +namespace SampSharp.Entities; + +/// Contains context information about a fired event. +public abstract class EventContext +{ + /// Gets the name of the event. + public abstract string Name { get; } + + /// Gets the arguments of the event. + public abstract object[] Arguments { get; } + + /// Gets the service provider which can be used for providing services for events. + public abstract IServiceProvider EventServices { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventContextImpl.cs b/src/SampSharp.OpenMp.Entities/Events/EventContextImpl.cs new file mode 100644 index 00000000..9053a764 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventContextImpl.cs @@ -0,0 +1,26 @@ +namespace SampSharp.Entities; + +internal class EventContextImpl : EventContext +{ + private object[]? _arguments; + + public EventContextImpl(string name, IServiceProvider eventServices) + { + Name = name; + EventServices = eventServices; + } + + public override string Name { get; } + public override IServiceProvider EventServices { get; } + public override object[] Arguments => _arguments!; + + public void SetArguments(ReadOnlySpan arguments) + { + if (_arguments == null || _arguments.Length != arguments.Length) + { + _arguments = new object[arguments.Length]; + } + + arguments.CopyTo(_arguments); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventDelegate.cs b/src/SampSharp.OpenMp.Entities/Events/EventDelegate.cs new file mode 100644 index 00000000..b869a944 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventDelegate.cs @@ -0,0 +1,6 @@ +namespace SampSharp.Entities; + +/// Invokes the next event handler with the specified event context. +/// The event context. +/// The return value of the event. +public delegate object? EventDelegate(EventContext context); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventDispatcher.cs b/src/SampSharp.OpenMp.Entities/Events/EventDispatcher.cs new file mode 100644 index 00000000..da165db8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventDispatcher.cs @@ -0,0 +1,288 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Logging; +using SampSharp.Entities.Utilities; +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +#pragma warning disable CS0618 // Type or member is obsolete +internal class EventDispatcher : IEventDispatcher, IEventService +#pragma warning restore CS0618 // Type or member is obsolete +{ + private static readonly Type[] _defaultParameterTypes = + [ + typeof(string) + ]; + + private readonly Dictionary _events = new(); + private readonly IServiceProvider _serviceProvider; + private readonly IEntityManager _entityManager; + private readonly ILogger _logger; + + /// Initializes a new instance of the class. + public EventDispatcher(IServiceProvider serviceProvider, IEntityManager entityManager, ILogger logger, ISystemRegistry systemRegistry) + { + _serviceProvider = serviceProvider; + _entityManager = entityManager; + _logger = logger; + + systemRegistry.Register(() => LoadTargetSites(systemRegistry)); + } + + public void UseMiddleware(string name, Func middleware) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(middleware); + + var @event = GetOrCreateEvent(name); + @event.ClearCache(); + @event.Middleware.Add(middleware); + } + + public object? Invoke(string name, params ReadOnlySpan arguments) + { + ArgumentNullException.ThrowIfNull(name); + + if (!_events.TryGetValue(name, out var @event)) + { + return null; + } + + if(!@event.Cache.TryGetValue(NullValue.Instance, out var invoke)) + { + invoke = CreateEventInvoke(@event, null); + @event.Cache.TryAdd(NullValue.Instance, invoke); + } + + return invoke(arguments); + } + + [return: NotNullIfNotNull(nameof(defaultValue))] + public T? InvokeAs(string name, T defaultValue, params ReadOnlySpan arguments) + { + ArgumentNullException.ThrowIfNull(name); + + if (!_events.TryGetValue(name, out var @event)) + { + return defaultValue; + } + + var defaultKey = defaultValue ?? (object)NullValue.Instance; + if(!@event.Cache.TryGetValue(defaultKey, out var invoke)) + { + invoke = CreateEventInvoke(@event, defaultValue); + @event.Cache.TryAdd(defaultKey, invoke); + } + + var result = invoke(arguments); + + if(result is Task task) + { + return task.IsCompleted ? task.Result : defaultValue; + } + + if (typeof(T) == typeof(bool) && result is MethodResult m) + { + var value = m.Value; + return Unsafe.As(ref value); + } + + return result is T resultAsT + ? resultAsT + : defaultValue; + } + + private Event GetOrCreateEvent(string name) + { + if (!_events.TryGetValue(name, out var @event)) + { + _events[name] = @event = new Event(name); + } + + return @event; + } + + private object? InnerInvoke(EventContext context, Event @event, object? defaultResult) + { + object? result = null; + + foreach (var targetSite in @event.TargetSites) + { + targetSite.Target ??= _serviceProvider.GetService(targetSite.TargetType); + + // System is not loaded. Skip target site + if (targetSite.Target == null) + { + continue; + } + + var targetResult = targetSite.Invoke(targetSite.Target, context); + + // Do not consider default result as invocation result + if (targetResult is null || targetResult == defaultResult) + { + continue; + } + + result = targetResult; + } + + return result ?? defaultResult; + } + + private void LoadTargetSites(ISystemRegistry systemRegistry) + { + // Find methods with EventAttribute in any loaded system. + var events = ClassScanner.Create() + .IncludeTypes(systemRegistry.GetSystemTypes().Span) + .IncludeNonPublicMembers() + .ScanMethods(); + + // Gather event data, compile invoker and add the data to the events collection. + foreach (var (target, method, attribute) in events) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Adding event listener on {type}.{method}.", method.DeclaringType, method.Name); + } + + var name = attribute.Name ?? method.Name; + var @event = GetOrCreateEvent(name); + + var (paramCount, paramSources) = GetParameterSources(method); + + var targetSite = CreateTargetSite(target, method, paramSources, paramCount); + @event.TargetSites.Add(targetSite); + } + } + + private static (int paramCount, MethodParameterSource[] paramSources) GetParameterSources(MethodInfo method) + { + var paramIndex = 0; // The current pointer in the event arguments array. + var parameterSources = method.GetParameters() + .Select(info => new MethodParameterSource(info)) + .ToArray(); + + // Determine the source of each parameter. + foreach (var source in parameterSources) + { + var type = source.Info.ParameterType; + + if (typeof(Component).IsAssignableFrom(type)) + { + // Components are provided by the entity in the arguments array of the event. + source.ParameterIndex = paramIndex++; + source.IsComponent = true; + } + else if (type.IsValueType || type.IsArray || _defaultParameterTypes.Contains(type) || type.GetCustomAttribute() != null) + { + // Default types are passed straight through. + source.ParameterIndex = paramIndex++; + } + else + { + // Other types are provided trough Dependency Injection. + source.IsService = true; + } + } + + return (paramIndex, parameterSources); + } + + private TargetSiteData CreateTargetSite(Type target, MethodInfo method, MethodParameterSource[] parameterInfos, int sourceParamCount) + { + var compiled = MethodInvokerFactory.Compile(method, parameterInfos); + var targetSiteName = $"{method.DeclaringType?.FullName}.{method.Name}"; + + return new TargetSiteData(target, (instance, eventContext) => + { + try + { + var args = eventContext.Arguments; + if (sourceParamCount == args.Length) + { + return compiled.Invoke(instance, args, eventContext.EventServices, _entityManager); + } + + _logger.LogError( + "Event \"{eventName}\" argument count mismatch: dispatcher passed {argsLength} arg(s), handler {targetSite}({handlerParams}) expects {sourceParamCount}", + eventContext.Name, + args.Length, + targetSiteName, + string.Join(", ", method.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}")), + sourceParamCount); + } + catch(Exception ex) + { + SampSharpExceptionHandler.HandleException(targetSiteName, ex); + } + + return null; + }); + } + + /// + /// Creates an invoker for an event which creates and manages the context for the event. + /// + private EventInvokeDelegate CreateEventInvoke(Event @event, object? defaultResult) + { + var context = new EventContextImpl(@event.Name, _serviceProvider); + + // In order to chain the middleware from first to last, the middleware must be nested from last to first + EventDelegate invoke = ctx => InnerInvoke(ctx, @event, defaultResult); + for (var i = @event.Middleware.Count - 1; i >= 0; i--) + { + invoke = @event.Middleware[i](invoke); + } + + return args => + { + try + { + context.SetArguments(args); + + var result = invoke(context); + return result switch + { + Task task => !task.IsCompleted ? null : (task.Result ? MethodResult.True : MethodResult.False), + Task task => !task.IsCompleted ? null : task.Result, + Task => null, + _ => result + }; + } + catch(Exception ex) + { + SampSharpExceptionHandler.HandleException(@event.Name, ex); + return null; + } + }; + } + + + private delegate object? EventInvokeDelegate(ReadOnlySpan args); + + private sealed record Event(string Name) + { + public List TargetSites { get; } = []; + public List> Middleware { get; } = []; + public ConcurrentDictionary Cache { get; } = new(); + + public void ClearCache() + { + Cache.Clear(); + } + } + + private sealed record NullValue + { + public static NullValue Instance { get; } = new(); + } + + private sealed record TargetSiteData(Type TargetType, Func Invoke) + { + public object? Target { get; set; } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventParameterAttribute.cs b/src/SampSharp.OpenMp.Entities/Events/EventParameterAttribute.cs new file mode 100644 index 00000000..91af94e4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventParameterAttribute.cs @@ -0,0 +1,7 @@ +namespace SampSharp.Entities; + +/// +/// Indicates that the class should be handled as a parameter for an event instead of as an injectable service. +/// +[AttributeUsage(AttributeTargets.Class)] +public class EventParameterAttribute : Attribute; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/EventScopeMiddleware.cs b/src/SampSharp.OpenMp.Entities/Events/EventScopeMiddleware.cs new file mode 100644 index 00000000..8868a7ec --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/EventScopeMiddleware.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities; + +/// Represents a middleware which adds a Dependency Injection scope to the of an event. +public class EventScopeMiddleware +{ + private readonly EventContextScoped _context = new(); + private readonly EventDelegate _next; + + /// Initializes a new instance of the class. + /// The next middleware handler. + public EventScopeMiddleware(EventDelegate next) + { + _next = next; + } + + /// Invokes the middleware. + public object? Invoke(EventContext context) + { + using var scope = context.EventServices.CreateScope(); + + _context.BaseContext = context; + _context.Scope = scope; + + var result = _next(_context); + + _context.BaseContext = null; + _context.Scope = null; + + return result; + } + + private sealed class EventContextScoped : EventContext + { + public EventContext? BaseContext { get; set; } + public IServiceScope? Scope { get; set; } + + public override string Name => BaseContext!.Name; + public override object[] Arguments => BaseContext!.Arguments; + + public override IServiceProvider EventServices => Scope!.ServiceProvider; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/IEventDispatcher.cs b/src/SampSharp.OpenMp.Entities/Events/IEventDispatcher.cs new file mode 100644 index 00000000..db27f8f4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/IEventDispatcher.cs @@ -0,0 +1,27 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities; + +/// Provides functionality for handling events. +public interface IEventDispatcher +{ + /// Adds a middleware to the handler of the event with the specified . + /// The name of the event. + /// The middleware to add to the event. + void UseMiddleware(string name, Func middleware); + + /// Invokes the event with the specified and . + /// The name of the event. + /// The arguments of the event. + /// The result of the event. + object? Invoke(string name, params ReadOnlySpan arguments); + + /// Invokes the event with the specified and . + /// The name of the event. + /// The default value to be returned in case no event handler returned a result. + /// The arguments of the event. + /// The result as returned by an event handler or if no non-null value was returned. + [return: NotNullIfNotNull(nameof(defaultValue))] + public T? InvokeAs(string name, T defaultValue, params ReadOnlySpan arguments); +} diff --git a/src/SampSharp.OpenMp.Entities/Events/IEventService.cs b/src/SampSharp.OpenMp.Entities/Events/IEventService.cs new file mode 100644 index 00000000..86f866b4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/IEventService.cs @@ -0,0 +1,5 @@ +namespace SampSharp.Entities; + +/// Provides functionality for handling events. +[Obsolete("Deprecated. Use 'IEventDispatcher instead.")] +public interface IEventService : IEventDispatcher; \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/MethodInvoker.cs b/src/SampSharp.OpenMp.Entities/Events/MethodInvoker.cs new file mode 100644 index 00000000..3314592a --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/MethodInvoker.cs @@ -0,0 +1,9 @@ +namespace SampSharp.Entities; + +/// Invoker for an instance method with dependency injection. +/// The target instance to invoke the method on. +/// The arguments of the method excluding the injected dependencies. +/// The service provider from which dependencies are loaded. +/// The entity manager from which components are loaded. +/// The result of the method. +public delegate object MethodInvoker(object target, object?[]? args, IServiceProvider services, IEntityManager? entityManager); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/MethodInvokerFactory.cs b/src/SampSharp.OpenMp.Entities/Events/MethodInvokerFactory.cs new file mode 100644 index 00000000..16e23bf1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/MethodInvokerFactory.cs @@ -0,0 +1,204 @@ +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities; + +/// Provides a compiler for an invoke method for an instance method with injected dependencies and entity-to-component conversion. +public static class MethodInvokerFactory +{ + private static readonly MethodInfo _getComponentInfo = typeof(IEntityManager).GetMethod(nameof(IEntityManager.GetComponent), + BindingFlags.Public | BindingFlags.Instance, null, [typeof(EntityId)], null)!; + + private static readonly MethodInfo _getServiceInfo = + typeof(MethodInvokerFactory).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!; + + /// Compiles the invoker for the specified method. + /// The method information. + /// The sources of the parameters. + /// The value returned if the method is not invoked when a parameter could not be converted to the correct component. + /// Indicates whether the return value should be converted to a value. + /// The method invoker. + public static MethodInvoker Compile(MethodInfo methodInfo, MethodParameterSource[] parameterSources, object? uninvokedReturnValue = null, bool retBoolToResult = true) + { + if (methodInfo.DeclaringType == null) + { + throw new ArgumentException("Method must have declaring type", nameof(methodInfo)); + } + + // Input arguments + var instanceArg = Expression.Parameter(typeof(object), "instance"); + var argsArg = Expression.Parameter(typeof(object[]), "args"); + var serviceProviderArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); + var entityManagerArg = Expression.Parameter(typeof(IEntityManager), "entityManager"); + var entityEmpty = Expression.Constant(EntityId.Empty, typeof(EntityId)); + Expression? argsCheckExpression = null; + + var locals = new List(); + var expressions = new List(); + var methodArguments = new Expression[parameterSources.Length]; + + for (var i = 0; i < parameterSources.Length; i++) + { + var source = parameterSources[i]; + var parameterType = source + .Info.ParameterType; + if (parameterType.IsByRef) + { + throw new NotSupportedException("Reference parameters are not supported"); + } + + if (source.IsComponent) + { + // Get component from entity + + // Declare local variables + var entityArg = Expression.Parameter(typeof(EntityId), $"entity{i}"); + var componentArg = Expression.Parameter(source + .Info.ParameterType, $"component{i}"); + var componentNull = Expression.Constant(null, source.Info.ParameterType); + + locals.Add(entityArg); + locals.Add(componentArg); + + // Constant index in args array + Expression index = Expression.Constant(source.ParameterIndex); + + // Assign entity from args array to entity variable. + var getEntityExpression = Expression.Assign(entityArg, Expression.Convert(Expression.ArrayIndex(argsArg, index), typeof(EntityId))); + expressions.Add(getEntityExpression); + + // If entity is not null, convert entity to component. Assign component to component variable. + var getComponentInfo = _getComponentInfo.MakeGenericMethod(source.Info.ParameterType); + var getComponentExpression = Expression.Assign(componentArg, + Expression.Condition(Expression.Equal(entityArg, entityEmpty), componentNull, + Expression.Call(entityManagerArg, getComponentInfo, entityArg))); + expressions.Add(getComponentExpression); + + // If an entity was provided in the args list, the entity must be convertible to the component. Add + // check for entity to either be null or the component to not be null. + var checkExpression = Expression.OrElse(Expression.Equal(entityArg, entityEmpty), Expression.NotEqual(componentArg, componentNull)); + + argsCheckExpression = argsCheckExpression == null + ? checkExpression + : Expression.AndAlso(argsCheckExpression, checkExpression); + + // Add component variable as the method argument. + methodArguments[i] = componentArg; + } + else if (source.IsService) + { + // Get service + var getServiceCall = Expression.Call(_getServiceInfo, serviceProviderArg, Expression.Constant(parameterType, typeof(Type))); + methodArguments[i] = Expression.Convert(getServiceCall, parameterType); + } + else if (source.ParameterIndex >= 0) + { + // Pass through. Args come in as boxed object[]; an exact unbox-to-T cast + // (Expression.Convert(object, T)) only succeeds when the boxed value's + // runtime type is exactly T. open.mp dispatchers commonly hand us a + // uint where the [Event] handler signature wants int (and similar + // numeric mismatches across enums) — for those we route through + // Convert.ChangeType so a boxed uint → int conversion succeeds. + Expression index = Expression.Constant(source.ParameterIndex); + var getValue = Expression.ArrayIndex(argsArg, index); + + if (RequiresPrimitiveConversion(parameterType)) + { + var convertMethod = typeof(MethodInvokerFactory).GetMethod( + nameof(ConvertNumeric), BindingFlags.NonPublic | BindingFlags.Static)!; + var converted = Expression.Call(convertMethod, getValue, Expression.Constant(parameterType, typeof(Type))); + methodArguments[i] = Expression.Convert(converted, parameterType); + } + else + { + methodArguments[i] = Expression.Convert(getValue, parameterType); + } + } + } + + var service = Expression.Convert(instanceArg, methodInfo.DeclaringType); + Expression body = Expression.Call(service, methodInfo, methodArguments); + + if (body.Type == typeof(void)) + { + body = Expression.Block(body, Expression.Constant(null)); + } + else if (retBoolToResult && body.Type == typeof(bool)) + { + var boxMethod = typeof(MethodResult).GetMethod(nameof(MethodResult.From))!; + body = Expression.Call(boxMethod, body); + } + else if (body.Type != typeof(object)) + { + body = Expression.Convert(body, typeof(object)); + } + + if (argsCheckExpression != null) + { + body = Expression.Condition(argsCheckExpression, body, Expression.Constant(uninvokedReturnValue, typeof(object))); + } + + if (locals.Count > 0 || expressions.Count > 0) + { + expressions.Add(body); + body = Expression.Block(locals, expressions); + } + + var lambda = Expression.Lambda(body, instanceArg, argsArg, serviceProviderArg, entityManagerArg); + + return lambda.Compile(); + } + + private static object GetService(IServiceProvider serviceProvider, Type type) + { + return serviceProvider.GetRequiredService(type); + } + + /// + /// Returns for primitive-numeric / enum types where the + /// dispatcher might box a value of a different (but assignable) numeric type + /// (uint↔int, ushort↔int, the enum's underlying type, etc.). + /// + private static bool RequiresPrimitiveConversion(Type t) + { + if (t.IsEnum) return true; + return Type.GetTypeCode(t) is + TypeCode.SByte or TypeCode.Byte or + TypeCode.Int16 or TypeCode.UInt16 or + TypeCode.Int32 or TypeCode.UInt32 or + TypeCode.Int64 or TypeCode.UInt64 or + TypeCode.Single or TypeCode.Double or + TypeCode.Char; + } + + /// + /// Runtime numeric/enum coercion. Same-type pass-through; otherwise routes + /// through + /// (or for enum targets). When + /// the source isn't (e.g. a Vector3 mistakenly + /// landed at an int slot due to dispatcher arg-order mismatch), returns + /// the value as-is so the outer + /// throws an with the actual managed + /// types — much easier to debug than "Object must implement IConvertible". + /// + private static object? ConvertNumeric(object? value, Type targetType) + { + if (value is null) return null; + var sourceType = value.GetType(); + if (sourceType == targetType) return value; + + if (value is not IConvertible) return value; + + if (targetType.IsEnum) + { + var underlying = Enum.GetUnderlyingType(targetType); + var asUnderlying = sourceType == underlying + ? value + : Convert.ChangeType(value, underlying, CultureInfo.InvariantCulture); + return Enum.ToObject(targetType, asUnderlying); + } + return Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/MethodParameterSource.cs b/src/SampSharp.OpenMp.Entities/Events/MethodParameterSource.cs new file mode 100644 index 00000000..f2d5213a --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/MethodParameterSource.cs @@ -0,0 +1,33 @@ +using System.Reflection; + +namespace SampSharp.Entities; + +/// Provides information about the origin of a parameter of a method. +public class MethodParameterSource +{ + /// Initializes a new instance of the class. + /// The parameter information. + public MethodParameterSource(ParameterInfo info) + { + Info = info; + } + + /// Gets the parameter information. + public ParameterInfo Info { get; } + + /// + /// The index in the arguments array which contains the value for this parameter. A value of -1 indicates this + /// parameter is not supplied by the arguments array. + /// + public int ParameterIndex { get; set; } = -1; + + /// Gets or sets a value indicating whether the value of this parameter is a service which should be + /// retrieved from the service provider. + public bool IsService { get; set; } + + /// + /// Gets or sets a value indicating whether this value of this parameter is a component which should be retrieved of + /// the entity provided in the arguments array. + /// + public bool IsComponent { get; set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Events/MethodResult.cs b/src/SampSharp.OpenMp.Entities/Events/MethodResult.cs new file mode 100644 index 00000000..41957b24 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Events/MethodResult.cs @@ -0,0 +1,19 @@ +namespace SampSharp.Entities; + +/// +/// Boxed boolean event-handler return value. 's +/// invokers convert -returning handlers to +/// so the dispatcher can distinguish "explicit +/// false" from "no handler ran" (which is ). Public so +/// middlewares can interpret the truthiness of an upstream result. +/// +public sealed record MethodResult(bool Value) +{ + /// Singleton "true" instance. + public static MethodResult True { get; } = new(true); + /// Singleton "false" instance. + public static MethodResult False { get; } = new(false); + + /// Returns the singleton for the given . + public static object From(bool value) => value ? True : False; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/EcsBuilder.cs b/src/SampSharp.OpenMp.Entities/Hosting/EcsBuilder.cs new file mode 100644 index 00000000..61061161 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/EcsBuilder.cs @@ -0,0 +1,6 @@ +namespace SampSharp.Entities; + +internal class EcsBuilder(IServiceProvider services) : IEcsBuilder +{ + public IServiceProvider Services { get; } = services; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs b/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs new file mode 100644 index 00000000..816d50af --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/EcsHost.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +[Extension(0x57e43771d28c5e7e)] +internal class EcsHost(IServiceProvider serviceProvider, UnhandledExceptionHandler? exceptionHandler) : Extension +{ + private IServiceProvider? _serviceProvider = serviceProvider; + + public IServiceProvider ServiceProvider => _serviceProvider ?? throw new InvalidOperationException(); + + public void Start(IStartupContext context) + { + context.UseSynchronizationContext(); + + context.UnhandledExceptionHandler = UnhandledExceptionHandler; + + LoadSystems(); + + // Fire initial event + OnGameModeInit(); + } + + protected override void Cleanup() + { + OnGameModeExit(); + + if (_serviceProvider is IDisposable disposable) + { + // TODO: This cleanup is called so late - we can't unsubscribe event handlers anymore, but the disposables in registered systems will try to unsubscribe them. This may cause a System.ExecutionEngineException on shutdown. + disposable.Dispose(); + _serviceProvider = null; + } + } + + private void UnhandledExceptionHandler(string context, Exception exception) + { + if (exceptionHandler != null) + { + exceptionHandler(ServiceProvider, context, exception); + } + else + { + DefaultExceptionHandler(context, exception); + } + } + + private void DefaultExceptionHandler(string context, Exception exception) + { + ServiceProvider.GetRequiredService().CreateLogger(context) + .LogError(exception, "Unhandled exception during: {context}", context); + } + + private void OnGameModeInit() + { + ServiceProvider.GetRequiredService().Invoke("OnGameModeInit"); + } + + private void OnGameModeExit() + { + ServiceProvider.GetRequiredService().Invoke("OnGameModeExit"); + } + + private void LoadSystems() + { + ServiceProvider.GetRequiredService().LoadSystems(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/EcsHostBuilder.cs b/src/SampSharp.OpenMp.Entities/Hosting/EcsHostBuilder.cs new file mode 100644 index 00000000..c72443c6 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/EcsHostBuilder.cs @@ -0,0 +1,154 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SampSharp.Entities.Logging; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +[Extension(0xb0eac2ea9239714c)] +internal sealed class EcsHostBuilder : Extension, IEcsHostBuilder +{ + private readonly List> _serviceConfigurations = []; + private readonly List> _ecsConfigurations = []; + private readonly List> _loggerConfigurations = []; + private bool _systemsLoadingDisabled; + private UnhandledExceptionHandler? _unhandledExceptionHandler; + private Func? _serviceProviderFactory; + + public IEcsHostBuilder Configure(Action build) + { + ArgumentNullException.ThrowIfNull(build); + _ecsConfigurations.Add(build); + return this; + } + + public IEcsHostBuilder ConfigureServices(Action build) + { + ArgumentNullException.ThrowIfNull(build); + _serviceConfigurations.Add(build); + return this; + } + + public IEcsHostBuilder ConfigureServices(Action build) + { + ArgumentNullException.ThrowIfNull(build); + return ConfigureServices((_, services) => build(services)); + } + + public IEcsHostBuilder ConfigureLogging(Action builder) + { + ArgumentNullException.ThrowIfNull(builder); + _loggerConfigurations.Add(builder); + return this; + } + + public IEcsHostBuilder ConfigureUnhandledExceptionhandler(UnhandledExceptionHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + _unhandledExceptionHandler = handler; + return this; + } + + public IEcsHostBuilder UseServiceProviderFactory(IServiceProviderFactory serviceProviderFactory) + where TContainerBuilder : notnull + { + ArgumentNullException.ThrowIfNull(serviceProviderFactory); + _serviceProviderFactory = services => serviceProviderFactory.CreateServiceProvider(serviceProviderFactory.CreateBuilder(services)); + return this; + } + + public IEcsHostBuilder DisableDefaultSystemsLoading() + { + _systemsLoadingDisabled = true; + return this; + } + + internal EcsHost Build(IStartupContext context) + { + var serviceProvider = BuildServiceProvider(context); + Configure(new EcsBuilder(serviceProvider)); + + return new EcsHost(serviceProvider, _unhandledExceptionHandler); + } + + private IServiceProvider BuildServiceProvider(IStartupContext context) + { + var environment = new SampSharpEnvironment(context.Configurator.GetType().Assembly, context.Core, context.ComponentList); + + var services = new ServiceCollection(); + + ConfigureDefaultServices(services); + + services.AddSingleton(environment); + ConfigureServices(environment, services); + + var factory = _serviceProviderFactory ?? DefaultServiceProviderFactory; + return factory(services); + } + + private void ConfigureDefaultServices(IServiceCollection services) + { + services + .AddLogging(builder => + { + builder.AddOpenMp(); + ConfigureLogger(builder); + }) + .AddSingleton() + .AddSingleton(sp => sp.GetRequiredService()) +#pragma warning disable CS0618 // Type or member is obsolete + .AddSingleton(sp => sp.GetRequiredService()) +#pragma warning restore CS0618 // Type or member is obsolete + .AddSingleton() + .AddSingleton(x => x.GetRequiredService()) + .AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSystem() + .AddSystem() + .AddSamp() + ; + } + + private static IServiceProvider DefaultServiceProviderFactory(IServiceCollection services) + { + return services.BuildServiceProvider(); + } + + + private void Configure(IEcsBuilder builder) + { + foreach (var configuration in _ecsConfigurations) + { + configuration(builder); + } + + _ecsConfigurations.Clear(); + } + + private void ConfigureLogger(ILoggingBuilder builder) + { + foreach (var configuration in _loggerConfigurations) + { + configuration(builder); + } + + _loggerConfigurations.Clear(); + } + + private void ConfigureServices(SampSharpEnvironment environment, IServiceCollection services) + { + foreach (var configuration in _serviceConfigurations) + { + configuration(environment, services); + } + + if (!_systemsLoadingDisabled) + { + services.AddSystemsInAssembly(environment.EntryAssembly); + _systemsLoadingDisabled = true; + } + + _serviceConfigurations.Clear(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/HostContextBinder.cs b/src/SampSharp.OpenMp.Entities/Hosting/HostContextBinder.cs new file mode 100644 index 00000000..85cff7c8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/HostContextBinder.cs @@ -0,0 +1,37 @@ +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +/// +/// Binds the lifecycle of the ECS host to the lifecycle of the startup context, ensuring that the ECS host is properly initialized and cleaned up along with the application. +/// +internal class HostContextBinder(IStartupContext context, EcsHostBuilder hostBuilder) : IDisposable +{ + private EcsHost? _host; + + public void Bind() + { + context.Initialized += OnContextInitialized; + context.Cleanup += OnContextCleanup; + } + + private void OnContextInitialized(object? sender, EventArgs e) + { + _host = hostBuilder.Build(context); + context.Core.AddExtension(_host); + _host.Start(context); + } + + private void OnContextCleanup(object? sender, EventArgs e) + { + Dispose(); + } + + public void Dispose() + { + context.Initialized -= OnContextInitialized; + context.Cleanup -= OnContextCleanup; + + _host?.Dispose(); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/IEcsBuilder.cs b/src/SampSharp.OpenMp.Entities/Hosting/IEcsBuilder.cs new file mode 100644 index 00000000..23a66825 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/IEcsBuilder.cs @@ -0,0 +1,8 @@ +namespace SampSharp.Entities; + +/// Provides functionality for configuring the SampSharp Entity Component System. +public interface IEcsBuilder +{ + /// Gets the service provider. + IServiceProvider Services { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/IEcsHostBuilder.cs b/src/SampSharp.OpenMp.Entities/Hosting/IEcsHostBuilder.cs new file mode 100644 index 00000000..13436675 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/IEcsHostBuilder.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace SampSharp.Entities; + +/// +/// Defines methods for configuring the Entity Component System (ECS) framework, including systems, services, logging, +/// exception handling, and service provider factories. +/// +public interface IEcsHostBuilder +{ + /// + /// Configures the ECS framework itself, allowing for configuration of systems, components, and other ECS-related features. + /// + /// A delegate that configures the ECS builder. + /// The updated host builder. + IEcsHostBuilder Configure(Action build); + + /// + /// Configures the services used by the application. + /// + /// A delegate that configures the service collection. + /// The updated host builder. + IEcsHostBuilder ConfigureServices(Action build); + + /// + /// Configures the services used by the application. + /// + /// A delegate that configures the service collection. + /// The updated host builder. + IEcsHostBuilder ConfigureServices(Action build); + + /// + /// Configures the logging used by the application. + /// + /// A delegate that configures the logging builder. + /// The updated host builder. + IEcsHostBuilder ConfigureLogging(Action builder); + + /// + /// Configures the unhandled exception handler used by the application. + /// + /// The handler for unhandled exceptions during the execution of the application. + /// The updated host builder. + IEcsHostBuilder ConfigureUnhandledExceptionhandler(UnhandledExceptionHandler handler); + + /// + /// Configures the service provider factory used by the application. + /// + /// The type of the container builder. + /// The factory to use to create the service provider. + /// The updated host builder. + IEcsHostBuilder UseServiceProviderFactory(IServiceProviderFactory serviceProviderFactory) + where TContainerBuilder : notnull; + + /// + /// Prevents the framework from automatically loading systems from the entry assembly. This is useful if you want to manually control which systems are loaded. + /// + /// The updated host builder. + IEcsHostBuilder DisableDefaultSystemsLoading(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/IEcsStartup.cs b/src/SampSharp.OpenMp.Entities/Hosting/IEcsStartup.cs new file mode 100644 index 00000000..8edcb065 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/IEcsStartup.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +/// +/// Provides methods for configuring an Entity Component System in SampSharp. +/// +public interface IEcsStartup : IStartup +{ + /// + /// Register services into the . + /// + /// The to add the services to. + void ConfigureServices(IServiceCollection services); + + /// + /// Configures the application. + /// + /// An for the app to configure. + void Configure(IEcsBuilder builder); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/SafeEventHandlerSampSharpEnvironmentExtensions.cs b/src/SampSharp.OpenMp.Entities/Hosting/SafeEventHandlerSampSharpEnvironmentExtensions.cs new file mode 100644 index 00000000..1aab2785 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/SafeEventHandlerSampSharpEnvironmentExtensions.cs @@ -0,0 +1,122 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities; + +/// +/// Provides extension methods for safely adding event handlers to components within a . +/// +public static class SafeEventHandlerSampSharpEnvironmentExtensions +{ + extension(SampSharpEnvironment environment) + { + /// + /// Attempts to add an event handler to the specified component. + /// + /// The type of the component to which the event handler will be added. + /// The type of the event handler. + /// A function that provides the event dispatcher for the specified component. + /// The event handler instance to add to the component. + /// The priority with which the event handler should be registered. The default is . + /// An representing the event handler registration, or if the registration failed. + public IDisposable? TryAddEventHandler(Func> dispatcherProvider, TEventHandler handler, EventPriority priority = EventPriority.Default) + where TComponent : unmanaged, IComponent.IManagedInterface + where TEventHandler : class, IEventHandler + { + var component = environment.Components.QueryComponent(); + + if (!component.HasValue) + { + return null; + } + + var dispatcher = dispatcherProvider(component); + + if (!dispatcher.HasValue) + { + return null; + } + + if (!dispatcher.AddEventHandler(handler, priority)) + { + return null; + } + + return new SafeEventHandlerRegistration(environment, handler, dispatcherProvider); + } + + /// + /// Adds an event handler to the specified component. + /// + /// The type of the component to which the event handler will be added. + /// The type of the event handler. + /// A function that provides the event dispatcher for the specified component. + /// The event handler instance to add to the component. + /// The priority with which the event handler should be registered. The default is . + /// An representing the event handler registration. + /// Thrown if the event handler could not be added. + public IDisposable AddEventHandler(Func> dispatcherProvider, TEventHandler handler, EventPriority priority = EventPriority.Default) + where TComponent : unmanaged, IComponent.IManagedInterface + where TEventHandler : class, IEventHandler + { + var registration = TryAddEventHandler(environment, dispatcherProvider, handler, priority); + if (registration is null) + { + throw new InvalidOperationException("Failed to add event handler."); + } + + return registration; + } + + /// + /// Attempts to add an event handler to the core. + /// + /// The type of the event handler. + /// A function that provides the event dispatcher for the specified core. + /// The event handler instance to add to the core component. + /// The priority with which the event handler should be registered. The default is . + /// An representing the event handler registration, or if the registration failed. + public IDisposable? TryAddEventHandler(Func> dispatcherProvider, TEventHandler handler, EventPriority priority = EventPriority.Default) + where TEventHandler : class, IEventHandler + { + if (!environment.Core.HasValue) + { + return null; + } + + var dispatcher = dispatcherProvider(environment.Core); + + if (!dispatcher.HasValue) + { + return null; + } + + if (!dispatcher.AddEventHandler(handler, priority)) + { + return null; + } + + return new SafeEventHandlerRegistration(environment, handler, dispatcherProvider); + } + + /// + /// Adds an event handler to the core. + /// + /// The type of the event handler. + /// A function that provides the event dispatcher for the specified core. + /// The event handler instance to add to the core component. + /// The priority with which the event handler should be registered. The default is . + /// An representing the event handler registration. + /// Thrown if the event handler could not be added. + public IDisposable AddEventHandler(Func> dispatcherProvider, TEventHandler handler, EventPriority priority = EventPriority.Default) + where TEventHandler : class, IEventHandler + { + var registration = TryAddEventHandler(environment, dispatcherProvider, handler, priority); + if (registration is null) + { + throw new InvalidOperationException("Failed to add event handler."); + } + + return registration; + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs b/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs new file mode 100644 index 00000000..881146dd --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/SampSharpEnvironment.cs @@ -0,0 +1,12 @@ +using System.Reflection; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities; + +/// +/// Represents the environment of a SampSharp application. +/// +/// The assembly which was configured to launch in open.mp. +/// The of open.mp. +/// The of open.mp. +public record SampSharpEnvironment(Assembly EntryAssembly, ICore Core, IComponentList Components); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/StartupContextEcsExtensions.cs b/src/SampSharp.OpenMp.Entities/Hosting/StartupContextEcsExtensions.cs new file mode 100644 index 00000000..770001a7 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/StartupContextEcsExtensions.cs @@ -0,0 +1,36 @@ +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +/// +/// Contains extension methods for to configure the ECS system. +/// +public static class StartupContextEcsExtensions +{ + /// + /// Configures the ECS system for the SampSharp application. + /// + /// The startup context. + /// A instance which can be used to further configure the ECS system. + public static IEcsHostBuilder UseEntities(this IStartupContext context) + { + var builder = context.Core.TryGetExtension(); + if (builder != null) + { + return builder; + } + + builder = new EcsHostBuilder(); + context.Core.AddExtension(builder); + + if (context.Configurator is IEcsStartup startup) + { + builder.Configure(startup.Configure); + builder.ConfigureServices(startup.ConfigureServices); + } + + new HostContextBinder(context, builder).Bind(); + + return builder; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Hosting/UnhandledExceptionHandler.cs b/src/SampSharp.OpenMp.Entities/Hosting/UnhandledExceptionHandler.cs new file mode 100644 index 00000000..9e084e47 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Hosting/UnhandledExceptionHandler.cs @@ -0,0 +1,9 @@ +namespace SampSharp.Entities; + +/// +/// Represents a method that handles an unhandled exception occurred within SampSharp.OpenMp.Entities. +/// +/// The service provider. +/// Context in which the exception occurred. +/// The exception. +public delegate void UnhandledExceptionHandler(IServiceProvider serviceProvider, string context, Exception exception); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Logging/OmpLogger.cs b/src/SampSharp.OpenMp.Entities/Logging/OmpLogger.cs new file mode 100644 index 00000000..5acdcfc0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Logging/OmpLogger.cs @@ -0,0 +1,80 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using SampSharp.OpenMp.Core; +using ILogger = SampSharp.OpenMp.Core.Api.ILogger; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using OmpLogLevel = SampSharp.OpenMp.Core.Api.LogLevel; + +namespace SampSharp.Entities.Logging; + +internal class OmpLogger(ILogger inner, LogLevel minLogLevel, string name, ObjectPool objectPool) + : Microsoft.Extensions.Logging.ILogger +{ + private readonly Dictionary _writers = new() + { + [OmpLogLevel.Debug] = new LoggerTextWriter(inner, OmpLogLevel.Debug), + [OmpLogLevel.Message] = new LoggerTextWriter(inner, OmpLogLevel.Message), + [OmpLogLevel.Warning] = new LoggerTextWriter(inner, OmpLogLevel.Warning), + [OmpLogLevel.Error] = new LoggerTextWriter(inner, OmpLogLevel.Error) + }; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var sb = objectPool.Get(); + + try + { + if (eventId.Id != 0) + { + sb.Append($"[{eventId.Id,2}]`"); + } + + if (logLevel is LogLevel.Trace or LogLevel.Critical) + { + sb.Append($" [{logLevel}]"); + } + + sb.Append($"{name} - {formatter(state, exception)}"); + + if (exception != null) + { + sb.AppendLine(); + sb.Append(exception); + } + + _writers[Convert(logLevel)].WriteLine(sb.ToString()); + } + finally + { + objectPool.Return(sb); + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= minLogLevel; + } + + public IDisposable? BeginScope(TState state) where TState : notnull + { + return null; + } + + private static OmpLogLevel Convert(LogLevel level) + { + return level switch + { + LogLevel.Trace or LogLevel.Debug => OmpLogLevel.Debug, + LogLevel.Warning => OmpLogLevel.Warning, + LogLevel.Error or LogLevel.Critical => OmpLogLevel.Error, + _ => OmpLogLevel.Message + }; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProvider.cs b/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProvider.cs new file mode 100644 index 00000000..bf7315dc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProvider.cs @@ -0,0 +1,27 @@ +using System.Collections.Concurrent; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace SampSharp.Entities.Logging; + +internal class OmpLoggerProvider(OpenMp.Core.Api.ILogger innerLogger, LogLevel minLogLevel) : ILoggerProvider +{ + private readonly ObjectPool _stringBuilders = new DefaultObjectPool(new StringBuilderPooledObjectPolicy()); + private readonly ConcurrentDictionary _loggers = []; + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, CreateNewLogger); + } + + private ILogger CreateNewLogger(string name) + { + return new OmpLogger(innerLogger, minLogLevel, name, _stringBuilders); + } + + public void Dispose() + { + // + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProviderExtensions.cs b/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProviderExtensions.cs new file mode 100644 index 00000000..12dd6a07 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Logging/OmpLoggerProviderExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; + +namespace SampSharp.Entities.Logging; + +/// +/// Provides extension methods for adding an open.mp logger to an . +/// +public static class OmpLoggerProviderExtensions +{ + /// + /// Adds an open.mp logger to the logging builder. + /// + /// The logger builder + /// The minimum log level to write to the open.mp logger. + public static void AddOpenMp(this ILoggingBuilder builder, LogLevel minLogLevel = LogLevel.Trace) + { + builder.Services.TryAddSingleton(sp => + new OmpLoggerProvider((OpenMp.Core.Api.ILogger)sp.GetRequiredService().Core, minLogLevel)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Math/GtaVector.cs b/src/SampSharp.OpenMp.Entities/Math/GtaVector.cs new file mode 100644 index 00000000..30ab7e1e --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Math/GtaVector.cs @@ -0,0 +1,27 @@ +using System.Numerics; + +namespace SampSharp.Entities; + +/// +/// Provides various default vector constants for GTA: San Andreas. +/// +public static class GtaVector +{ + /// Returns a with components 0, 0, 1. + public static Vector3 Up { get; } = new(0, 0, 1); + + /// Returns a with components 0, 0, -1. + public static Vector3 Down { get; } = new(0, 0, -1); + + /// Returns a with components -1, 0, 0. + public static Vector3 Left { get; } = new(-1, 0, 0); + + /// Returns a with components 1, 0, 0. + public static Vector3 Right { get; } = new(1, 0, 0); + + /// Returns a with components 0, 1, 0. + public static Vector3 Forward { get; } = new(0, 1, 0); + + /// Returns a with components 0, -1, 0. + public static Vector3 Backward { get; } = new(0, -1, 0); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Math/MathHelper.cs b/src/SampSharp.OpenMp.Entities/Math/MathHelper.cs new file mode 100644 index 00000000..1e0d4e12 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Math/MathHelper.cs @@ -0,0 +1,202 @@ +using System.Diagnostics.Contracts; +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities; + +/// Contains commonly used pre-calculated values and mathematical operations. +[Pure] +public static class MathHelper +{ + /// Represents the value of pi divided by two(1.57079637). + public const float PiOver2 = (float)(Math.PI / 2.0); + + /// Represents the value of pi divided by four(0.7853982). + public const float PiOver4 = (float)(Math.PI / 4.0); + + /// Represents the value of pi times two(6.28318548). + public const float TwoPi = (float)(Math.PI * 2.0); + + /// Returns the Cartesian coordinate for one axis of a point that is defined by a given triangle and two normalized barycentric (areal) coordinates. + /// The coordinate on one axis of vertex 1 of the defining triangle. + /// The coordinate on the same axis of vertex 2 of the defining triangle. + /// The coordinate on the same axis of vertex 3 of the defining triangle. + /// + /// The normalized barycentric (areal) coordinate b2, equal to the weighting factor for vertex 2, the coordinate of which is specified in + /// value2. + /// + /// + /// The normalized barycentric (areal) coordinate b3, equal to the weighting factor for vertex 3, the coordinate of which is specified in + /// value3. + /// + /// Cartesian coordinate of the specified point with respect to the axis being used. + public static float Barycentric(float value1, float value2, float value3, float amount1, float amount2) + { + return value1 + (value2 - value1) * amount1 + (value3 - value1) * amount2; + } + + /// Performs a Catmull-Rom interpolation using the specified positions. + /// The first position in the interpolation. + /// The second position in the interpolation. + /// The third position in the interpolation. + /// The fourth position in the interpolation. + /// Weighting factor. + /// A position that is the result of the Catmull-Rom interpolation. + public static float CatmullRom(float value1, float value2, float value3, float value4, float amount) + { + // Using formula from http://www.mvps.org/directx/articles/catmull/ + // Internally using doubles not to lose precision + double amountSquared = amount * amount; + var amountCubed = amountSquared * amount; + return (float)(0.5 * (2.0 * value2 + (value3 - value1) * amount + (2.0 * value1 - 5.0 * value2 + 4.0 * value3 - value4) * amountSquared + + (3.0 * value2 - value1 - 3.0 * value3 + value4) * amountCubed)); + } + + /// Calculates the absolute value of the difference of two values. + /// Source value. + /// Source value. + /// Distance between the two values. + public static float Distance(float value1, float value2) + { + return Math.Abs(value1 - value2); + } + + /// Performs a Hermite spline interpolation. + /// Source position. + /// Source tangent. + /// Source position. + /// Source tangent. + /// Weighting factor. + /// The result of the Hermite spline interpolation. + public static float Hermite(float value1, float tangent1, float value2, float tangent2, float amount) + { + // All transformed to double not to lose precision + // Otherwise, for high numbers of param:amount the result is NaN instead of Infinity + double v1 = value1, v2 = value2, t1 = tangent1, t2 = tangent2, s = amount, result; + var sCubed = s * s * s; + var sSquared = s * s; + + switch (amount) + { + case 0f: + result = value1; + break; + case 1f: + result = value2; + break; + default: + result = (2 * v1 - 2 * v2 + t2 + t1) * sCubed + (3 * v2 - 3 * v1 - 2 * t1 - t2) * sSquared + t1 * s + v1; + break; + } + + return (float)result; + } + + /// Interpolates between two values using a cubic equation. + /// Source value. + /// Source value. + /// Weighting value. + /// Interpolated value. + public static float SmoothStep(float value1, float value2, float amount) + { + // It is expected that 0 < amount < 1 + // If amount < 0, return value1 + // If amount > 1, return value2 + var result = float.Clamp(amount, 0f, 1f); + result = Hermite(value1, 0f, value2, 0f, result); + + return result; + } + + /// + /// Gets the Z angle of the specified . + /// + /// The rotation matrix to the Z angle of. + /// The Z angle. + public static float GetZAngleFromRotationMatrix(Matrix4x4 rotationMatrix) + { + return WrapAngle(-MathF.Atan2(rotationMatrix.M21, rotationMatrix.M11)); + } + + /// + /// Gets the yaw, pitch and roll in radians from the specified . + /// + /// The quaternion + /// A vector containing the roll, pitch and yaw components in radians. + public static Vector3 CreateYawPitchRollFromQuaternion(Quaternion quat) + { + // using a smaller epsilon than float.Epsilon + const float epsilon = 0.000002f; + var q = quat; + + var r = 2 * -q.Y * -q.Z - 2 * -q.X * q.W; + float roll, pitch, yaw; + + switch (r) + { + case >= 1.0f - epsilon: + roll = PiOver2; + pitch = -float.Atan2(float.Clamp(-q.Y, -1.0f, 1.0f), float.Clamp(q.W, -1.0f, 1.0f)); + yaw = -float.Atan2(float.Clamp(-q.Z, -1.0f, 1.0f), float.Clamp(q.W, -1.0f, 1.0f)); + break; + case <= -(1.0f - epsilon): + roll = -PiOver2; + pitch = -float.Atan2(float.Clamp(-q.Y, -1.0f, 1.0f), float.Clamp(q.W, -1.0f, 1.0f)); + yaw = -float.Atan2(float.Clamp(-q.Z, -1.0f, 1.0f), float.Clamp(q.W, -1.0f, 1.0f)); + break; + default: + roll = float.Asin(float.Clamp(r, -1.0f, 1.0f)); + pitch = -float.Atan2(float.Clamp(q.X * q.Z + -q.Y * q.W, -1.0f, 1.0f), float.Clamp(0.5f - q.X * q.X - q.Y * q.Y, -1.0f, 1.0f)); + yaw = -float.Atan2(float.Clamp(q.X * q.Y + -q.Z * q.W, -1.0f, 1.0f), float.Clamp(0.5f - q.X *-q.X - q.Z * q.Z, -1.0f, 1.0f)); + break; + } + + roll %= TwoPi; + pitch %= TwoPi; + yaw %= TwoPi; + + return new Vector3(roll, pitch, yaw); + } + + /// + /// Converts the specified vector containing roll, pitch and yaw in radians to a quaternion. + /// + /// The vector containing roll, pitch and yaw in radians. + /// The quaternion. + public static Quaternion CreateQuaternionFromYawPitchRoll(Vector3 rollPitchYaw) + { + // see https://math.stackexchange.com/questions/1477926/quaternion-to-euler-with-some-properties + // It looks like the rotation order is yxz, but angles negated for some reason. + + rollPitchYaw = -rollPitchYaw; + var quat = Quaternion.CreateFromYawPitchRoll(rollPitchYaw.Z, rollPitchYaw.X, rollPitchYaw.Y); + return new GTAQuat(quat.X, quat.Z, quat.Y, quat.W); + } + + /// Reduces a given angle to a value between π and -π. + /// The angle to reduce, in radians. + /// The new angle, in radians. + public static float WrapAngle(float angle) + { + angle = (float)Math.IEEERemainder(angle, TwoPi); + switch (angle) + { + case <= -float.Pi: + angle += TwoPi; + break; + case > float.Pi: + angle -= TwoPi; + break; + } + + return angle; + } + + /// Determines if value is powered by two. + /// A value. + /// if value is powered by two; otherwise, . + public static bool IsPowerOfTwo(int value) + { + return value > 0 && (value & value - 1) == 0; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Actor.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Actor.cs new file mode 100644 index 00000000..f604766d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Actor.cs @@ -0,0 +1,110 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of an actor. +public class Actor : WorldEntity +{ + private readonly IActorsComponent _actors; + private readonly IActor _actor; + + /// Constructs an instance of Actor, should be used internally. + protected Actor(IActorsComponent actors, IActor actor) : base((IEntity)actor) + { + _actors = actors; + _actor= actor; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _actor.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the facing angle of this actor. + public virtual float Angle + { + get => float.RadiansToDegrees(MathHelper.GetZAngleFromRotationMatrix(Matrix4x4.CreateFromQuaternion(_actor.GetRotation()))); + set => Rotation = Quaternion.CreateFromAxisAngle(GtaVector.Up, float.DegreesToRadians(value)); + } + + /// + /// Gets or sets the skin of the actor. + /// + public virtual int Skin + { + get => _actor.GetSkin(); + set => _actor.SetSkin(value); + } + + /// Gets the health of this actor. + public virtual float Health + { + get => _actor.GetHealth(); + set => _actor.SetHealth(value); + } + + /// Gets or sets a value indicating whether this actor is invulnerable. + public virtual bool IsInvulnerable + { + get => _actor.IsInvulnerable(); + set => _actor.SetInvulnerable(value); + } + + /// Determines whether this actor is streamed in for the specified . + /// The player. + /// True if streamed in; False otherwise. + public virtual bool IsStreamedIn(Player player) + { + return _actor.IsStreamedInForPlayer(player); + } + + /// Applies the specified animation to this actor. + /// The animation library from which to apply an animation. + /// The name of the animation to apply, within the specified library. + /// The speed to play the animation. + /// if set to the animation will loop. + /// if set to allow this Actor to move it's x-coordinate. + /// if set to allow this Actor to move it's y-coordinate. + /// if set to freeze this Actor at the end of the animation. + /// The amount of time to play the animation. + /// Thrown if or is null. + public virtual void ApplyAnimation(string library, string name, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, TimeSpan time) + { + ArgumentNullException.ThrowIfNull(library); + ArgumentNullException.ThrowIfNull(name); + _actor.ApplyAnimation(new AnimationData(fDelta, loop, lockX, lockY, freeze, (uint)time.TotalMilliseconds, library, name)); + } + + /// + [Obsolete("Use the TimeSpan overload. This int-milliseconds variant is kept for source compatibility and will be removed.")] + public virtual void ApplyAnimation(string library, string name, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, int time) + => ApplyAnimation(library, name, fDelta, loop, lockX, lockY, freeze, TimeSpan.FromMilliseconds(time)); + + /// Clear any animations applied to this actor. + public virtual void ClearAnimations() + { + _actor.ClearAnimations(); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _actors.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IActor(Actor actor) + { + return actor._actor; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/GangZone.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/GangZone.cs new file mode 100644 index 00000000..6ed8189b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/GangZone.cs @@ -0,0 +1,134 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a gang zone. +public class GangZone : IdProvider +{ + private readonly IGangZonesComponent _gangZones; + private readonly IGangZone _gangZone; + + /// Constructs an instance of GangZone, should be used internally. + protected GangZone(IGangZonesComponent gangZones, IGangZone gangZone) : base((IIDProvider)gangZone) + { + _gangZone = gangZone; + _gangZones = gangZones; + + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _gangZone.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the minimum position of this . + public virtual Vector2 Min => _gangZone.GetPosition().Min; + + /// Gets the maximum position of this . + public virtual Vector2 Max => _gangZone.GetPosition().Max; + + /// Gets the minimum x value for this . + public virtual float MinX => Min.X; + + /// Gets the minimum y value for this . + public virtual float MinY => Min.Y; + + /// Gets the maximum x value for this . + public virtual float MaxX => Max.X; + + /// Gets the maximum y value for this . + public virtual float MaxY => Max.Y; + + /// Gets or sets the color of this . + public virtual Color Color { get; set; } + + /// Shows this . + public virtual void Show() + { + foreach (var player in Manager.GetComponents()) + { + Show(player); + } + } + + /// Shows this to the specified . + /// The player. + public virtual void Show(Player player) + { + Colour clr = Color; + _gangZone.ShowForPlayer(player, ref clr); + } + + /// Hides this . + public virtual void Hide() + { + foreach (var player in Manager.GetComponents()) + { + Hide(player); + } + } + + /// Hides this for the specified . + /// The player. + public virtual void Hide(Player player) + { + _gangZone.HideForPlayer(player); + } + + /// Flashes this . + /// The color. + public virtual void Flash(Color color) + { + foreach (var player in Manager.GetComponents()) + { + Flash(player, color); + } + } + + /// Flashes this for the specified . + /// The player. + /// The color. + public virtual void Flash(Player player, Color color) + { + Colour clr = color; + _gangZone.FlashForPlayer(player, ref clr); + } + + /// Stops this from flash. + public virtual void StopFlash() + { + foreach (var player in Manager.GetComponents()) + { + StopFlash(player); + } + } + + /// Stops this from flash for the specified player. + /// The player. + public virtual void StopFlash(Player player) + { + _gangZone.StopFlashForPlayer(player); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _gangZones.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Color: {Color})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IGangZone(GangZone gangZone) + { + return gangZone._gangZone; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/GlobalObject.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/GlobalObject.cs new file mode 100644 index 00000000..960634b1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/GlobalObject.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + + +/// Represents a component which provides the data and functionality of an object. +public class GlobalObject : WorldEntity +{ + private readonly IObjectsComponent _objects; + private readonly IObject _object; + + /// Constructs an instance of GlobalObject, should be used internally. + protected GlobalObject(IObjectsComponent objects, IObject @object) : base((IEntity)@object) + { + _objects = objects; + _object = @object; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _object.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets whether this object is moving. + public virtual bool IsMoving => _object.IsMoving(); + + /// Gets the model of this object. + public virtual int ModelId => _object.GetModel(); + + /// Gets the draw distance of this object. + public virtual float DrawDistance => _object.GetDrawDistance(); + + /// Moves this object to the given position and rotation with the given speed. + /// The position to which to move this object. + /// The speed at which to move this object. + /// The rotation to which to move this object. + /// The time it will take for the object to move in milliseconds. + public virtual int Move(Vector3 position, float speed, Vector3 rotation) + { + var time = (position - Position).Length() / speed * 1000f; + + var moveDat = new ObjectMoveData(position, rotation, speed); + _object.Move(ref moveDat); + + return (int)time; + } + + /// Moves this object to the given position with the given speed. + /// The position to which to move this object. + /// The speed at which to move this object. + /// The time it will take for the object to move in milliseconds. + public virtual int Move(Vector3 position, float speed) + { + return Move(position, speed, new Vector3(-1000)); + } + + /// Stop this object from moving any further. + public virtual void Stop() + { + _object.Stop(); + } + + /// Sets the material of this object. + /// The material index on the object to change. + /// + /// The model ID on which the replacement texture is located. Use 0 for alpha. Use -1 to change the material color without altering the + /// texture. + /// + /// The name of the txd file which contains the replacement texture (use "none" if not required). + /// The name of the texture to use as the replacement (use "none" if not required). + /// The object color to set (use default(Color) to keep the existing material color). + public virtual void SetMaterial(int materialIndex, int modelId, string txdName, string textureName, Color materialColor) + { + _object.SetMaterial((uint)materialIndex, modelId, txdName, textureName, materialColor); + } + + /// Sets the material text of this object. + /// The material index on the object to change. + /// The text to show on the object. (MAX 2048 characters) + /// The object's material index to replace with text. + /// The font to use. + /// The size of the text (max 255). + /// Whether to write in bold. + /// The color of the text. + /// The background color of the text. + /// The alignment of the text. + public virtual void SetMaterialText(int materialIndex, string text, ObjectMaterialSize materialSize, string fontface, int fontSize, bool bold, Color foreColor, + Color backColor, ObjectMaterialTextAlign textAlignment) + { + _object.SetMaterialText((uint)materialIndex, text, (SampSharp.OpenMp.Core.Api.ObjectMaterialSize)materialSize, fontface, fontSize, bold, foreColor, backColor, (SampSharp.OpenMp.Core.Api.ObjectMaterialTextAlign)textAlignment); + } + + /// Disable collisions between players' cameras and this . + public virtual void DisableCameraCollisions() + { + _object.SetCameraCollision(false); + } + + /// Attaches this object to the specified player. + /// The player. + /// The offset. + /// The rotation. + public virtual void AttachTo(Player target, Vector3 offset, Vector3 rotation) + { + _object.AttachToPlayer(target, offset, rotation); + } + + /// Attaches this object to the specified vehicle. + /// The vehicle. + /// The offset. + /// The rotation. + public virtual void AttachTo(Vehicle target, Vector3 offset, Vector3 rotation) + { + _object.AttachToVehicle(target, offset, rotation); + } + + /// Attaches this object to the specified object. + /// The object. + /// The offset. + /// The rotation. + /// if set to synchronize rotation with objects attached to. + public virtual void AttachTo(GlobalObject target, Vector3 offset, Vector3 rotation, bool syncRotation = false) + { + _object.AttachToObject(target, offset, rotation, syncRotation); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _objects.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Model: {ModelId})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IObject(GlobalObject @object) + { + return @object._object; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/IdProvider.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/IdProvider.cs new file mode 100644 index 00000000..cbeb6cd2 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/IdProvider.cs @@ -0,0 +1,15 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// +/// Represents a component which provides an identifier. +/// +/// The open.mp id provider this component represents. +public abstract class IdProvider(IIDProvider idProvider) : Component +{ + /// + /// Gets the identifier of this component. + /// + public virtual int Id => idProvider.GetID(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/InvalidPlayerNameException.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/InvalidPlayerNameException.cs new file mode 100644 index 00000000..9e996958 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/InvalidPlayerNameException.cs @@ -0,0 +1,25 @@ +namespace SampSharp.Entities.SAMP; + +/// +/// An exception thrown when a player name is invalid. +/// +public class InvalidPlayerNameException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + public InvalidPlayerNameException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The message that describes the error. + public InvalidPlayerNameException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidPlayerNameException(string message, Exception? inner) : base(message, inner) { } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Menu.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Menu.cs new file mode 100644 index 00000000..846508ee --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Menu.cs @@ -0,0 +1,128 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a menu. +public class Menu : IdProvider +{ + private readonly IMenusComponent _menus; + private readonly IMenu _menu; + + /// Constructs an instance of Menu, should be used internally. + protected Menu(IMenusComponent menus, IMenu menu, string title) : base((IIDProvider)menu) + { + _menus = menus; + _menu = menu; + Title = title; // no getter available in IMenu + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _menu.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the title of this menu. + public virtual string Title { get; } + + /// Gets the number of columns in this menu. + public virtual int Columns => _menu.GetColumnCount(); + + /// Gets the position of this menu. + public virtual Vector2 Position => _menu.GetPosition(); + + /// Gets the width of the left column in this menu. + public virtual float Col0Width => _menu.GetColumnWidths().X; + + /// Gets the width of the right column in this menu. + public virtual float Col1Width => _menu.GetColumnWidths().Y; + + /// Gets or sets the caption of the left column in this menu. + public virtual string Col0Header + { + get => _menu.GetColumnHeader(0) ?? string.Empty; + set => _menu.SetColumnHeader(value, 0); + } + + /// Gets or sets the caption of the right column in this menu. + public virtual string Col1Header + { + get => _menu.GetColumnHeader(1) ?? string.Empty; + set => _menu.SetColumnHeader(value, 1); + } + + /// Adds an item to this menu. + /// The text for the left column. + /// The text for the right column. If this menu only has one column, this value is ignored. + /// The index of the row this item was added to. + /// + /// You can only have 12 items per menu (13th goes to the right side of the header of column name (colored), 14th and higher not display at all). Maximum + /// length of menu item is 31 symbols. + /// + public virtual int AddItem(string col0Text, string? col1Text = null) + { + ArgumentNullException.ThrowIfNull(col0Text); + + if (col1Text == null && Columns == 2) + { + throw new ArgumentNullException(nameof(col1Text), "The text for the right column may not be null because this menu has 2 columns."); + } + + var result = _menu.AddCell(col0Text, 0); + + if (Columns == 2) + { + _menu.AddCell(col1Text!, 1); + } + + return result; + } + + /// Shows this menu for the specified . + /// The player. + public virtual void Show(Player player) + { + _menu.ShowForPlayer(player); + } + + /// Hides this menu for the specified . + /// The player. + public virtual void Hide(Player player) + { + _menu.HideForPlayer(player); + } + + /// Disable input for this menu. Any item will lose the ability to be selected. + public virtual void Disable() + { + _menu.Disable(); + } + + /// Disable a specific row in this menu for all players. It will be grayed-out and can't be selected by players. + /// The index of the row to disable. + public virtual void DisableRow(int row) + { + _menu.DisableRow((byte)row); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _menus.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Title: {Title})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IMenu(Menu menu) + { + return menu._menu; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Npc.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Npc.cs new file mode 100644 index 00000000..2ae4b558 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Npc.cs @@ -0,0 +1,204 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; +using INPC = SampSharp.OpenMp.Core.Api.INPC; +using INPCComponent = SampSharp.OpenMp.Core.Api.INPCComponent; + +namespace SampSharp.Entities.SAMP; + +/// +/// ECS-side wrapper around an open.mp (a server-controlled bot +/// built on top of an ). +/// +/// +/// Unlike -based components, does NOT +/// derive from — open.mp's only implements +/// , and its position/rotation setters take an extra +/// "immediate update" boolean. Position / Rotation / VirtualWorld are exposed +/// directly here. +/// +public class Npc : IdProvider +{ + private readonly INPC _npc; + private readonly INPCComponent _npcs; + + /// Constructs an instance of Npc, should be used internally. + protected Npc(INPCComponent npcs, INPC npc) : base((IIDProvider)npc) + { + _npcs = npcs; + _npc = npc; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _npc.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// The underlying handle this NPC drives. + public virtual IPlayer Player => _npc.GetPlayer(); + + /// Gets or sets the NPC's world position. Use for the immediate-update overload. + public virtual Vector3 Position + { + get => _npc.GetPosition(); + set => _npc.SetPosition(value, false); + } + + /// Gets or sets the NPC's rotation as a quaternion. + public virtual GTAQuat Rotation + { + get => _npc.GetRotation(); + set => _npc.SetRotation(value, false); + } + + /// Gets or sets the virtual world this NPC is in. + public virtual int VirtualWorld + { + get => _npc.GetVirtualWorld(); + set => _npc.SetVirtualWorld(value); + } + + /// Sets the NPC's skin model id. + public virtual int Skin + { + set => _npc.SetSkin(value); + } + + /// Gets or sets the current weapon id. + public virtual byte Weapon + { + get => _npc.GetWeapon(); + set => _npc.SetWeapon(value); + } + + /// Gets or sets ammo for the current weapon. + public virtual int Ammo + { + get => _npc.GetAmmo(); + set => _npc.SetAmmo(value); + } + + /// Gets or sets the NPC's health. + public virtual float Health + { + get => _npc.GetHealth(); + set => _npc.SetHealth(value); + } + + /// Gets or sets the NPC's armour. + public virtual float Armour + { + get => _npc.GetArmour(); + set => _npc.SetArmour(value); + } + + /// Gets or sets the NPC's invulnerability flag. + public virtual bool IsInvulnerable + { + get => _npc.IsInvulnerable(); + set => _npc.SetInvulnerable(value); + } + + /// Gets or sets the interior id this NPC is bookkept under (server-side only). + public virtual int Interior + { + get => (int)_npc.GetInterior(); + set => _npc.SetInterior((uint)value); + } + + /// True if the NPC has been killed and not yet respawned. + public virtual bool IsDead => _npc.IsDead(); + + /// True if the NPC is currently following any movement command. + public virtual bool IsMoving => _npc.IsMoving(); + + /// Gets the NPC's current velocity. + public virtual Vector3 Velocity => _npc.GetVelocity(); + + /// + /// Sets the NPC's position. =true broadcasts a sync + /// to streamed-in players right away instead of waiting for the next tick. + /// + public virtual void SetPosition(Vector3 position, bool immediateUpdate) + { + _npc.SetPosition(position, immediateUpdate); + } + + /// Sets the NPC's rotation. See for the immediate-update flag. + public virtual void SetRotation(GTAQuat rotation, bool immediateUpdate) + { + _npc.SetRotation(rotation, immediateUpdate); + } + + /// Spawns the NPC at its currently configured position/rotation. + public virtual void Spawn() + { + _npc.Spawn(); + } + + /// Respawns the NPC keeping its current state. + public virtual void Respawn() + { + _npc.Respawn(); + } + + /// Tells the NPC to walk/jog/sprint/drive to the target position. + public virtual bool MoveTo(Vector3 position, NPCMoveType moveType, float moveSpeed = -1f, float stopRange = 1.0f) + { + return _npc.Move(position, moveType, moveSpeed, stopRange); + } + + /// Stops any active movement. + public virtual void StopMoving() + { + _npc.StopMove(); + } + + /// Applies an animation to this NPC. + public virtual void ApplyAnimation(string library, string name, float fDelta, bool loop, bool lockX, bool lockY, + bool freeze, TimeSpan time) + { + ArgumentNullException.ThrowIfNull(library); + ArgumentNullException.ThrowIfNull(name); + _npc.ApplyAnimation(new AnimationData(fDelta, loop, lockX, lockY, freeze, + (uint)time.TotalMilliseconds, library, name)); + } + + /// Clears any applied animation. + public virtual void ClearAnimations() + { + _npc.ClearAnimations(); + } + + /// Sets the NPC's velocity. + public virtual void SetVelocity(Vector3 velocity, bool update = false) + { + _npc.SetVelocity(velocity, update); + } + + /// True iff this NPC is streamed in for the given player. + public virtual bool IsStreamedIn(Player player) + { + return player != null && _npc.IsStreamedInForPlayer(player); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed && _npcs.HasValue) + { + _npcs.Destroy(_npc); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator INPC(Npc npc) + { + return npc._npc; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Pickup.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Pickup.cs new file mode 100644 index 00000000..9c3a165c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Pickup.cs @@ -0,0 +1,49 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a pickup. +public class Pickup : WorldEntity +{ + private readonly IPickupsComponent _pickups; + private readonly IPickup _pickup; + + /// Constructs an instance of Pickup, should be used internally. + protected Pickup(IPickupsComponent pickups, IPickup pickup) : base((IEntity)pickup) + { + _pickups = pickups; + _pickup = pickup; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _pickup.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the model of this . + public virtual int Model => _pickup.GetModel(); + + /// Gets the type of this . + public virtual PickupType SpawnType => (PickupType)_pickup.GetPickupType(); + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _pickups.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Model: {Model})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IPickup(Pickup pickup) + { + return pickup._pickup; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs new file mode 100644 index 00000000..223fbbe0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Player.cs @@ -0,0 +1,1479 @@ +using System.Net; +using System.Numerics; +using JetBrains.Annotations; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a player. +public class Player : WorldEntity +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IPlayer _rawPlayer; + + /// + /// Safe accessor for the underlying handle. Throws + /// if the component has been destroyed, + /// which means open.mp already fired + /// and the native pointer is (or is about to be) freed. Without this guard, + /// P/Invokes against a stale handle AV the process (0xC0000005) when gamemode + /// code holds onto a reference across disconnect (e.g. via + /// an async continuation). + /// + private IPlayer _player + { + get + { + if (!IsComponentAlive) + throw new ObjectDisposedException(nameof(Player), + "Player has disconnected; native IPlayer handle is no longer valid."); + return _rawPlayer; + } + } + + /// Constructs an instance of Player, should be used internally. + protected Player(IOmpEntityProvider entityProvider, IPlayer player) : base((IEntity)player) + { + _entityProvider = entityProvider; + _rawPlayer = player; + } + + private IPlayerCheckpointData CheckpointData + { + get + { + var data = _player.QueryExtension(); + if (data == null) + { + throw new InvalidOperationException("Missing checkpoint data"); + } + return data; + } + } + + + private IPlayerVehicleData VehicleData + { + get + { + var data = _player. QueryExtension(); + if (data == null) + { + throw new InvalidOperationException("Missing vehicle data"); + } + return data; + } + } + + private IPlayerObjectData ObjectData + { + get + { + var data = _player.QueryExtension(); + + if (data == null) + { + throw new InvalidOperationException("Missing object data"); + } + return data; + } + } + private IPlayerMenuData MenuData + { + get + { + var data = _player.QueryExtension(); + + if (data == null) + { + throw new InvalidOperationException("Missing menu data"); + } + return data; + } + } + + private IPlayerConsoleData ConsoleData + { + get + { + var data = _player.QueryExtension(); + + if (data == null) + { + throw new InvalidOperationException("Missing console data"); + } + return data; + } + } + + private IPlayerTextDrawData TextDrawData + { + get + { + var data = _player.QueryExtension(); + if (data == null) + { + throw new InvalidOperationException("Missing text draw data"); + } + return data; + } + } + + private IPlayerClassData ClassData + { + get + { + var data = _player.QueryExtension(); + if (data == null) + { + throw new InvalidOperationException("Missing class data"); + } + return data; + } + } + + private IPlayerRecordingData RecordingData + { + get + { + var data = _player.QueryExtension(); + if (data == null) + { + throw new InvalidOperationException("Missing recording data"); + } + return data; + } + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _player.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the name of this player. + public virtual string Name + { + get => _player.GetName(); + [Obsolete("Use SetName(string) instead")]set => SetName(value); + } + + /// + /// Sets the name of this player. + /// + /// The name to be set. + /// Thrown if the name is invalid of already in use. + public virtual void SetName(string name) + { + ArgumentNullException.ThrowIfNull(name); + + var result = _player.SetName(name); + switch (result) + { + case EPlayerNameStatus.Invalid: + throw new InvalidPlayerNameException("Player name is invalid"); + case EPlayerNameStatus.Taken: + throw new InvalidPlayerNameException("Player name is already in use"); + } + } + + /// Gets or sets the facing angle of this player. + public virtual float Angle + { + get => float.RadiansToDegrees(MathHelper.GetZAngleFromRotationMatrix(Matrix4x4.CreateFromQuaternion(Rotation))); + set => Rotation = Quaternion.CreateFromAxisAngle(GtaVector.Up, float.DegreesToRadians(value)); + } + + /// Gets or sets the interior of this player. + public virtual int Interior + { + get => (int)_player.GetInterior(); + set => _player.SetInterior((uint)value); + } + + /// Gets or sets the health of this player. + public virtual float Health + { + get => _player.GetHealth(); + set => _player.SetHealth(value); + } + + /// Gets or sets the armor of this player. + public virtual float Armour + { + get => _player.GetArmour(); + set => _player.SetArmour(value); + } + + /// Gets the ammo of the Weapon this player is currently holding. + public virtual int WeaponAmmo => _player.GetArmedWeaponAmmo(); + + /// Gets the WeaponState of the Weapon this player is currently holding. + public virtual WeaponState WeaponState => (WeaponState)_player.GetAimData().weaponState; + + /// Gets the Weapon this player is currently holding. + public virtual Weapon Weapon => (Weapon)_player.GetArmedWeapon(); + + /// Gets the Player this player is aiming at. + public virtual Player? TargetPlayer + { + get + { + var player = _player.GetTargetPlayer(); + if (!player.HasValue) + { + return null; + } + return _entityProvider.GetComponent(player); + } + } + + /// Gets or sets the team this player is in. + public virtual int Team + { + get => _player.GetTeam(); + set => _player.SetTeam(value); + } + + /// Gets or sets the score of this player. + public virtual int Score + { + get => _player.GetScore(); + set => _player.SetScore(value); + } + + /// Gets or sets the drunkenness level of this player. + public virtual int DrunkLevel + { + get => _player.GetDrunkLevel(); + set => _player.SetDrunkLevel(value); + } + + /// Gets or sets the Color of this player. + public virtual Color Color + { + get => _player.GetColour(); + set => _player.SetColour(value); + } + + /// Gets or sets the skin of this player. + public virtual int Skin + { + get => _player.GetSkin(); + set => _player.SetSkin(value); + } + + /// Gets or sets the money of this player. + public virtual int Money + { + get => _player.GetMoney(); + set => _player.SetMoney(value); + } + + /// Gets the state of this player. + public virtual PlayerState State => (PlayerState)_player.GetState(); + + /// Gets the IP of this player as a string. + public virtual string Ip => IpAddress.ToString(); + + /// Gets the IP of this player. + public virtual IPAddress IpAddress => _player.GetNetworkData().Value.networkID.address.ToAddress(); + + /// Gets the end point (IP and port) of this player. + public virtual IPEndPoint EndPoint => _player.GetNetworkData().Value.networkID.ToEndpoint(); + + /// Gets the ping of this player. + public virtual int Ping => (int)_player.GetPing(); + + /// Gets or sets the wanted level of this player. + public virtual int WantedLevel + { + get => (int)_player.GetWantedLevel(); + set => _player.SetWantedLevel((uint)value); + } + + /// Gets or sets the FightStyle of this player. + public virtual FightStyle FightStyle + { + get => (FightStyle)_player.GetFightingStyle(); + set => _player.SetFightingStyle((PlayerFightingStyle)value); + } + + /// Gets or sets the velocity of this player. + public virtual Vector3 Velocity + { + get => _player.GetVelocity(); + set => _player.SetVelocity(value); + } + + /// Gets the vehicle seat this player sits on. + public virtual int VehicleSeat + { + get + { + var data = VehicleData; + + if (data == null) + { + return -1; + } + + return data.GetSeat(); + } + } + + /// Gets the index of the animation this player is playing. + public virtual int AnimationIndex => _player.GetAnimationData().ID; + + /// Gets or sets the SpecialAction of this player. + public virtual SpecialAction SpecialAction + { + get => (SpecialAction)_player.GetAction(); + set => _player.SetAction((PlayerSpecialAction)value); + } + + /// Gets or sets the position of the camera of this player. + /// + /// The getter prefers the real-time client-reported camera position + /// (aimData.camPos) and only falls back to getCameraPosition() + /// (the last value explicitly set via setCameraPosition) when aim data + /// hasn't arrived yet. open.mp's getCameraPosition returns + /// (0,0,0) when the camera is in default 3rd-person mode (i.e. never + /// explicitly set), which is what legacy SampSharp users would not expect — + /// they need the actual visual camera location, not the last server intent. + /// + public virtual Vector3 CameraPosition + { + get + { + var camPos = _player.GetAimData().camPos; + return camPos != Vector3.Zero ? camPos : _player.GetCameraPosition(); + } + set => _player.SetCameraPosition(value); + } + + /// Gets the front Vector3 of this player's camera. + public virtual Vector3 CameraFrontVector => _player.GetAimData().camFrontVector; + + /// Gets the mode of this player's camera. + public virtual CameraMode CameraMode => (CameraMode)_player.GetAimData().camMode; + + /// Gets the Actor this player is aiming at. + public virtual Actor? TargetActor => _entityProvider.GetComponent(_player.GetTargetActor()); + + /// Gets the GlobalObject the camera of this player is pointing at. + public virtual GlobalObject? CameraTargetGlobalObject => _entityProvider.GetComponent(_player.GetCameraTargetObject()); + + /// Gets the PlayerObject the camera of this player is pointing at. + public virtual PlayerObject? CameraTargetPlayerObject => null; // TODO: broken, see https://github.com/openmultiplayer/open.mp/issues/1070 + + /// Gets the GtaVehicle the camera of this player is pointing at. + public virtual Vehicle? CameraTargetVehicle => _entityProvider.GetComponent(_player.GetCameraTargetVehicle()); + + /// Gets the GtaPlayer the camera of this player is pointing at. + public virtual Player? CameraTargetPlayer => _entityProvider.GetComponent(_player.GetCameraTargetPlayer()); + + /// Gets the GtaPlayer the camera of this player is pointing at. + public virtual Actor? CameraTargetActor => _entityProvider.GetComponent(_player.GetCameraTargetActor()); + + /// Gets the entity (player, player object, object, vehicle or actor) the camera of this player is pointing at. + public virtual Component? CameraTargetEntity => + CameraTargetPlayer ?? + CameraTargetActor ?? + (Component?)CameraTargetVehicle ?? + CameraTargetGlobalObject; + + /// Gets whether this player is currently in any vehicle. + public virtual bool InAnyVehicle => Vehicle != null; + + /// Gets whether this player is in his checkpoint. + public virtual bool InCheckpoint => CheckpointData.GetCheckpoint().IsPlayerInside(); + + /// Gets whether this player is in his race-checkpoint. + public virtual bool InRaceCheckpoint => CheckpointData.GetRaceCheckpoint().IsPlayerInside(); + + /// Gets the component (object or vehicle) that this player is surfing. + public virtual Component? SurfingEntity + { + get + { + var surf = _player.GetSurfingData(); + return surf.type switch + { + PlayerSurfingData.Type.Vehicle => _entityProvider.GetVehicle(surf.ID), + PlayerSurfingData.Type.Object => _entityProvider.GetObject(surf.ID), + PlayerSurfingData.Type.PlayerObject => _entityProvider.GetPlayerObject(this, surf.ID), + _ => null + }; + } + } + + + /// Gets the Vehicle this player is currently in. + public virtual Vehicle? Vehicle => _entityProvider.GetComponent(VehicleData.GetVehicle()); + + /// Gets the Menu this player is currently in. + public virtual Menu? Menu + { + get + { + var menuId = MenuData.GetMenuID(); + return menuId == OpenMpConstants.INVALID_MENU_ID ? null : _entityProvider.GetMenu(menuId); + } + } + + /// Gets whether this player is an actual player or an NPC. + public virtual bool IsNpc => _player.IsBot(); + + /// Gets whether this player is logged into RCON. + public virtual bool IsAdmin => ConsoleData.HasConsoleAccess(); + + private static readonly PlayerState[] _deadStates = [PlayerState.None, PlayerState.Spectating, PlayerState.Wasted]; + + /// Gets a value indicating whether this player is alive. + public virtual bool IsAlive => !_deadStates.Contains(State); + + /// Gets this player's game version. + public virtual string Version => + // TODO: more client ver info + _player.GetClientVersionName(); + + /// Gets this player's global computer identifier string. + public virtual string Gpci => _player.GetSerial(); + + /// Gets a value indicating whether this player is selecting a textdraw. + public virtual bool IsSelectingTextDraw => TextDrawData.IsSelecting(); + + /// Gets the amount of time (in milliseconds) that a player has been connected to the server for. + public virtual TimeSpan ConnectedTime + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return TimeSpan.FromMilliseconds(stats.ConnectionElapsedTime); + } + } + + /// Gets the number of messages the server has received from the player. + public virtual int MessagesReceived + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (int)stats.MessagesReceived; + } + } + + /// Gets the number of messages the player has received in the last second. + public virtual int MessagesReceivedPerSecond + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (int)stats.MessagesReceivedPerSecond; + } + } + + /// Gets the number of messages the server has sent to the player. + public virtual int MessagesSent + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (int)stats.MessagesSent; + } + } + + /// Get the amount of information (in bytes) that the server has sent to the player. + public virtual int BytesReceived + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (int)stats.BytesReceived; + } + } + + /// Get the amount of information (in bytes) that the server has received from the player. + public virtual int BytesSent + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (int)stats.TotalBytesSent; + } + } + + /// Get a player's connection status. + public virtual ConnectionStatus ConnectionStatus + { + get + { + var stats = _player.GetNetworkData().Value.network.GetStatistics(); + return (ConnectionStatus)stats.ConnectMode; + } + } + + /// Gets the aspect ratio of this player's camera. + public virtual float AspectCameraRatio => _player.GetAimData().aspectRatio; + + /// Gets the game camera zoom level for this player. + public virtual float CameraZoom => _player.GetAimData().camZoom; + + /// + /// This function can be used to change the spawn information of a specific player. It allows you to automatically set someone's spawn weapons, their + /// team, skin and spawn position, normally used in case of mini games or automatic-spawn systems. + /// + /// The Team-ID of the chosen player. + /// The skin which the player will spawn with. + /// The player's spawn position. + /// The direction in which the player needs to be facing after spawning. + /// The first spawn-weapon for the player. + /// The amount of ammunition for the primary spawn-weapon. + /// The second spawn-weapon for the player. + /// The amount of ammunition for the second spawn-weapon. + /// The third spawn-weapon for the player. + /// The amount of ammunition for the third spawn-weapon. + public virtual void SetSpawnInfo(int team, int skin, Vector3 position, float rotation, Weapon weapon1 = Weapon.None, int weapon1Ammo = 0, + Weapon weapon2 = Weapon.None, int weapon2Ammo = 0, Weapon weapon3 = Weapon.None, int weapon3Ammo = 0) + { + var weapons = new WeaponSlotData[OpenMpConstants.MAX_WEAPON_SLOTS]; + weapons[0] = new WeaponSlotData((byte)weapon1, weapon1Ammo); + weapons[1] = new WeaponSlotData((byte)weapon2, weapon2Ammo); + weapons[2] = new WeaponSlotData((byte)weapon3, weapon3Ammo); + + var info = new PlayerClass(team, skin, position, rotation, new WeaponSlots(weapons)); + + ClassData.SetSpawnInfo(ref info); + } + + /// + /// Get the network statistics of this player. + /// + /// + public NetworkStats GetNetworkStats() + { + return new NetworkStats(_player.GetNetworkData().Value.network.GetStatistics()); + } + + /// (Re)Spawns a player. + public virtual void Spawn() + { + _player.Spawn(); + } + + /// Restore the camera to a place behind the player, after using a function like . + public virtual void PutCameraBehindPlayer() + { + _player.SetCameraBehind(); + } + + /// This sets this player's position then adjusts the Player's z-coordinate to the nearest solid ground under the position. + /// The position to move this player to. + public virtual void SetPositionFindZ(Vector3 position) + { + _player.SetPositionFindZ(position); + } + + /// Check if this player is in range of a point. + /// The furthest distance the player can be from the point to be in range. + /// The point to check the range to. + /// True if this player is in range of the point, otherwise False. + public virtual bool IsInRangeOfPoint(float range, Vector3 point) + { + return GetDistanceFromPoint(point) <= range; + } + + /// Calculate the distance between this player and a map coordinate. + /// The point to calculate the distance from. + /// The distance between the player and the point as a float. + public virtual float GetDistanceFromPoint(Vector3 point) + { + var offset = point - Position; + return offset.Length(); + } + + /// Checks if the specified is streamed in this player's client. + /// Players aren't streamed in on their own client, so if this player is the same as the other Player, it will return false! + /// Players stream out if they are more than 150 meters away (see server.cfg - stream_distance) + /// The player to check is streamed in. + /// True if the other Player is streamed in for this player, False if not. + public virtual bool IsPlayerStreamedIn(Player player) + { + return _player.IsStreamedInForPlayer(player); + } + + /// Set the ammo of this player's weapon. + /// The weapon to set the ammo of. + /// The amount of ammo to set. + public virtual void SetAmmo(Weapon weapon, int ammo) + { + _player.SetWeaponAmmo(new WeaponSlotData((byte)weapon, ammo)); + } + + /// Give this player a with a specified amount of ammo. + /// The Weapon to give to this player. + /// The amount of ammo to give to this player. + public virtual void GiveWeapon(Weapon weapon, int ammo) + { + _player.GiveWeapon(new WeaponSlotData((byte)weapon, ammo)); + } + + + /// Removes all weapons from this player. + public virtual void ResetWeapons() + { + _player.ResetWeapons(); + } + + /// Removes a single from this player. + /// The weapon to remove. + public virtual void RemoveWeapon(Weapon weapon) + { + _player.RemoveWeapon((byte)weapon); + } + + /// Gets or sets this player's gravity. + public virtual float Gravity + { + get => _player.GetGravity(); + set => _player.SetGravity(value); + } + + /// Whether this player is using the official Rockstar/SA-MP client (as opposed to open.mp, mobile/PSP, or an unofficial fork). + public virtual bool IsUsingOfficialClient => _player.IsUsingOfficialClient(); + + /// Whether this player connected using the open.mp client. + public virtual bool IsUsingOmp => _player.GetClientVersion() == ClientVersion.openmp; + + /// Gets this player's . + public virtual ClientVersion ClientVersion => _player.GetClientVersion(); + + /// Sets the armed weapon of this player. + /// The weapon that the player should be armed with. + public virtual void SetArmedWeapon(Weapon weapon) + { + _player.SetArmedWeapon((int)weapon); + } + + /// Get the and ammo in this player's weapon slot. + /// The weapon slot to get data for (0-12). + /// The variable in which to store the weapon, passed by reference. + /// The variable in which to store the ammo, passed by reference. + public virtual void GetWeaponData(int slot, out Weapon weapon, out int ammo) + { + var data = _player.GetWeaponSlot(slot); + weapon = (Weapon)data.Id; + ammo = data.Ammo; + } + + /// Give money to this player. + /// The amount of money to give this player. Use a minus value to take money. + public virtual void GiveMoney(int money) + { + _player.GiveMoney(money); + } + + /// Reset this player's money to $0. + public virtual void ResetMoney() + { + _player.ResetMoney(); + } + + /// Check which keys this player is pressing. + /// + /// Only the FUNCTION of keys can be detected; not actual keys. You can not detect if the player presses space, but you can detect if they press sprint + /// (which can be mapped (assigned) to ANY key, but is space by default)). + /// + /// A set of bits containing this player's key states + /// Up or Down value, passed by reference. + /// Left or Right value, passed by reference. + public virtual void GetKeys(out Keys keys, out int upDown, out int leftRight) + { + var data = _player.GetKeyData(); + keys = (Keys)data.keys; + upDown = data.upDown; + leftRight = data.leftRight; + } + + /// Sets the clock of this player to a specific value. This also changes the daytime. (night/day etc.) + /// Hour to set (0-23). + /// Minutes to set (0-59). + public virtual void SetTime(int hour, int minutes) + { + _player.SetTime(TimeSpan.FromHours(hour), TimeSpan.FromMinutes(minutes)); + } + + /// Get this player's current game time. Set by , or by . + /// The variable to store the hour in, passed by reference. + /// The variable to store the minutes in, passed by reference. + public virtual void GetTime(out int hour, out int minutes) + { + (hour, minutes) = _player.GetTime(); + } + + /// Show/Hide the in-game clock (top right corner) for this player. + /// Time is not synced with other players! + /// True to show, False to hide. + public virtual void ToggleClock(bool toggle) + { + _player.UseClock(toggle); + } + + /// + /// Set this player's weather. If has been used to enable the clock, weather changes will interpolate (gradually change), + /// otherwise will change instantly. + /// + /// The weather to set. + public virtual void SetWeather(int weather) + { + _player.SetWeather(weather); + } + + /// Forces this player to go back to class selection. + /// The player will not return to class selection until they re-spawn. This can be achieved with + public virtual void ForceClassSelection() + { + _player.ForceClassSelection(); + } + + /// Display the cursor and allow this player to select a text draw. + /// The color of the text draw when hovering over with mouse. + public virtual void SelectTextDraw(Color hoverColor) + { + TextDrawData.BeginSelection(hoverColor); + } + + /// Cancel text draw selection with the mouse for this player. + public virtual void CancelSelectTextDraw() + { + TextDrawData.EndSelection(); + } + + /// This function plays a crime report for this player - just like in single-player when CJ commits a crime. + /// The suspect player which will be described in the crime report. + /// The crime ID, which will be reported as a 10-code (i.e. 10-16 if 16 was passed as the crime ID). + public virtual void PlayCrimeReport(Player suspect, int crime) + { + _player.PlayerCrimeReport(suspect, crime); + } + + /// Play an 'audio stream' for this player. Normal audio files also work (e.g. MP3). + /// The url to play. Valid formats are mp3 and ogg/vorbis. A link to a .pls (playlist) file will play that playlist. + /// The position at which to play the audio. + /// The distance over which the audio will be heard. + public virtual void PlayAudioStream(string url, Vector3 position, float distance) + { + ArgumentNullException.ThrowIfNull(url); + _player.PlayAudio(url, true, position, distance); + } + + /// Play an 'audio stream' for this player. Normal audio files also work (e.g. MP3). + /// The url to play. Valid formats are mp3 and ogg/vorbis. A link to a .pls (playlist) file will play that playlist. + public virtual void PlayAudioStream(string url) + { + ArgumentNullException.ThrowIfNull(url); + _player.PlayAudio(url); + } + + /// Allows you to disable collisions between vehicles for a player. + /// if set to disables the collision between vehicles. + public virtual void DisableRemoteVehicleCollisions(bool disable) + { + _player.SetRemoteVehicleCollisions(!disable); + } + + /// Toggles camera targeting functions for a player. + /// if set to the functionality is enabled. + public virtual void EnablePlayerCameraTarget(bool enable) + { + _player.UseCameraTargeting(enable); + } + + /// Stops the current audio stream for this player. + public virtual void StopAudioStream() + { + _player.StopAudio(); + } + + /// Loads or unloads an interior script for this player. (for example the Ammunation menu) + /// The name of the shop, see for shop names. + public virtual void SetShopName(string shopName) + { + ArgumentNullException.ThrowIfNull(shopName); + _player.SetShopName(shopName); + } + + /// Set the skill level of a certain weapon type for this player. + /// The skill parameter is NOT the weapon ID, it is the skill type. + /// The you want to set the skill of. + /// The skill level to set for that weapon, ranging from 0 to 999. (A level out of range will max it out) + public virtual void SetSkillLevel(WeaponSkill skill, int level) + { + _player.SetSkillLevel((PlayerWeaponSkill)skill, level); + } + + /// Attach an object to a specific bone on this player. + /// The index (slot) to assign the object to (0-9). + /// The model to attach. + /// The bone to attach the object to. + /// offset for the object position. + /// rotation of the object. + /// scale of the object. + /// The first object color to set. + /// The second object color to set. + /// True on success, False otherwise. + public virtual bool SetAttachedObject(int index, int modelId, Bone bone, Vector3 offset, Vector3 rotation, Vector3 scale, Color materialColor1, + Color materialColor2) + { + if (index is < 0 or >= OpenMpConstants.MAX_ATTACHED_OBJECT_SLOTS) + { + return false; + } + + var obj = new ObjectAttachmentSlotData(modelId, (int)bone, offset, rotation, scale, materialColor1, materialColor2); + + ObjectData.SetAttachedObject(index, ref obj); + + return true; + } + + /// Remove an attached object from this player. + /// The index of the object to remove (set with ). + /// True on success, False otherwise. + public virtual bool RemoveAttachedObject(int index) + { + if (index is < 0 or >= OpenMpConstants.MAX_ATTACHED_OBJECT_SLOTS) + { + return false; + } + + ObjectData.RemoveAttachedObject(index); + return true; + } + + /// Check if this player has an object attached in the specified index (slot). + /// The index (slot) to check. + /// True if the slot is used, False otherwise. + public virtual bool IsAttachedObjectSlotUsed(int index) + { + if (index is < 0 or >= OpenMpConstants.MAX_ATTACHED_OBJECT_SLOTS) + { + return false; + } + + return ObjectData.HasAttachedObject(index); + } + + /// Enter edition mode for an attached object. + /// The index (slot) of the attached object to edit. + /// True on success, False otherwise. + public virtual bool DoEditAttachedObject(int index) + { + if (index is < 0 or >= OpenMpConstants.MAX_ATTACHED_OBJECT_SLOTS) + { + return false; + } + + ObjectData.EditAttachedObject(index); + + return true; + } + + /// Creates a chat bubble above this player's name tag. + /// The text to display. + /// The text color. + /// The distance from where players are able to see the chat bubble. + /// The time in milliseconds the bubble should be displayed for. + [Obsolete("Use SetChatBubble(string,Color,float,TimeSpan) instead")] + public virtual void SetChatBubble(string text, Color color, float drawDistance, int expireTime) + { + SetChatBubble(text, color, drawDistance, TimeSpan.FromMilliseconds(expireTime)); + } + + /// Creates a chat bubble above this player's name tag. + /// The text to display. + /// The text color. + /// The distance from where players are able to see the chat bubble. + /// The time the bubble should be displayed for. + public virtual void SetChatBubble(string text, Color color, float drawDistance, TimeSpan expireTime) + { + ArgumentNullException.ThrowIfNull(text); + + Colour clr = color; + _player.SetChatBubble(text, ref clr, drawDistance, expireTime); + } + + /// Puts this player in a vehicle. + /// The vehicle for the player to be put in. + /// The ID of the seat to put the player in. + public virtual void PutInVehicle(Vehicle vehicle, int seatId) + { + ((IVehicle)vehicle).PutPlayer(_player, seatId); + } + + /// Puts this player in a vehicle as driver. + /// The vehicle for the player to be put in. + public virtual void PutInVehicle(Vehicle vehicle) + { + PutInVehicle(vehicle, 0); + } + + /// Removes/ejects this player from his vehicle. + /// Force the removal of the player. + /// + /// The exiting animation is not synced for other players. This function will not work when used in the OnPlayerEnterVehicle event, because the player + /// isn't in the vehicle when the callback is called. Use the OnPlayerStateChanged event instead. + /// + public virtual void RemoveFromVehicle(bool force = false) + { + _player.RemoveFromVehicle(force); + } + + /// Toggles whether this player can control themselves, basically freezes them. + /// False to freeze the player or True to unfreeze them. + public virtual void ToggleControllable(bool toggle) + { + _player.SetControllable(toggle); + } + + /// Plays the specified sound for this player at a specific point. + /// The sound to play. + /// Point for the sound to play at. + public virtual void PlaySound(int soundId, Vector3 point) + { + _player.PlaySound(soundId, point); + } + + /// Plays the specified sound for this player. + /// The sound to play. + public virtual void PlaySound(int soundId) + { + _player.PlaySound(soundId, new Vector3()); + } + + /// Apply an animation to this player. + /// + /// The parameter, in most cases is not needed since players sync animations themselves. The + /// parameter can force all players who can see this player to play the animation regardless of whether the player is performing that animation. This is useful in + /// circumstances where the player can't sync the animation themselves. For example, they may be paused. + /// + /// The name of the animation library in which the animation to apply is in. + /// The name of the animation, within the library specified. + /// The speed to play the animation (use 4.1). + /// Set to True for looping otherwise set to False for playing animation sequence only once. + /// + /// Set to False to return player to original x position after animation is complete for moving animations. The opposite effect occurs if set + /// to True. + /// + /// + /// Set to False to return player to original y position after animation is complete for moving animations. The opposite effect occurs if set + /// to True. + /// + /// Will freeze the player in position after the animation finishes. + /// Animation duration. for a never-ending loop. + /// Set to to force the player to sync animation with other players in all instances + public virtual void ApplyAnimation(string animationLibrary, string animationName, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, TimeSpan time, + bool forceSync) + { + ArgumentNullException.ThrowIfNull(animationLibrary); + ArgumentNullException.ThrowIfNull(animationName); + + var anim = new AnimationData(fDelta, loop, lockX, lockY, freeze, (uint)time.TotalMilliseconds, animationLibrary, animationName); + + // TODO: other sync? + _player.ApplyAnimation(anim, forceSync ? PlayerAnimationSyncType.Sync : PlayerAnimationSyncType.NoSync); + } + + /// Apply an animation to this player. + /// The name of the animation library in which the animation to apply is in. + /// The name of the animation, within the library specified. + /// The speed to play the animation (use 4.1). + /// Set to True for looping otherwise set to False for playing animation sequence only once. + /// + /// Set to False to return player to original x position after animation is complete for moving animations. The opposite effect occurs if set + /// to True. + /// + /// + /// Set to False to return player to original y position after animation is complete for moving animations. The opposite effect occurs if set + /// to True. + /// + /// Will freeze the player in position after the animation finishes. + /// Animation duration. for a never-ending loop. + public virtual void ApplyAnimation(string animationLibrary, string animationName, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, TimeSpan time) + { + ApplyAnimation(animationLibrary, animationName, fDelta, loop, lockX, lockY, freeze, time, false); + } + + /// + [Obsolete("Use the TimeSpan overload. This int-milliseconds variant is kept for source compatibility and will be removed.")] + public virtual void ApplyAnimation(string animationLibrary, string animationName, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, int time, + bool forceSync) + => ApplyAnimation(animationLibrary, animationName, fDelta, loop, lockX, lockY, freeze, TimeSpan.FromMilliseconds(time), forceSync); + + /// + [Obsolete("Use the TimeSpan overload. This int-milliseconds variant is kept for source compatibility and will be removed.")] + public virtual void ApplyAnimation(string animationLibrary, string animationName, float fDelta, bool loop, bool lockX, bool lockY, bool freeze, int time) + => ApplyAnimation(animationLibrary, animationName, fDelta, loop, lockX, lockY, freeze, TimeSpan.FromMilliseconds(time), false); + + /// Clears all animations for this player. + /// Specifies whether the animation should be shown to streamed in players. + public virtual void ClearAnimations(bool forceSync) + { + // TODO: other sync? + _player.ClearAnimations(forceSync ? PlayerAnimationSyncType.Sync : PlayerAnimationSyncType.NoSync); + } + + /// Clears all animations for this player. + public virtual void ClearAnimations() + { + ClearAnimations(false); + } + + /// Get the animation library/name this player is playing. + /// String variable that stores the animation library. + /// String variable that stores the animation name. + /// True on success, False otherwise. + public virtual bool GetAnimationName(out string? animationLibrary, out string? animationName) + { + var anim = _player.GetAnimationData(); + var id = anim.ID; + (animationLibrary, animationName) = Animation.GetAnmiation(id); + return true; + } + + /// Sets a checkpoint (red circle) for this player. Also shows a red blip on the radar. + /// + /// Checkpoints created on server-created objects will appear down on the 'real' ground, but will still function correctly. There is no fix available for + /// this issue. A pickup can be used instead. + /// + /// The point to set the checkpoint at. + /// The size of the checkpoint. + public virtual void SetCheckpoint(Vector3 point, float size) + { + var cp = CheckpointData.GetCheckpoint(); + cp.SetPosition(ref point); + cp.SetRadius(size); + cp.Enable(); + } + + /// Disable any initialized checkpoints for this player. + public virtual void DisableCheckpoint() + { + CheckpointData.GetCheckpoint().Disable(); + } + + /// Creates a race checkpoint. When this player enters it, EnterRaceCheckpoint event is called. + /// Type of checkpoint. + /// The point to set the checkpoint at. + /// Coordinates of the next point, for the arrow facing direction. + /// Length (diameter) of the checkpoint + public virtual void SetRaceCheckpoint(CheckpointType type, Vector3 point, Vector3 nextPosition, float size) + { + var cp = CheckpointData.GetRaceCheckpoint(); + cp.SetPosition(ref point); + cp.SetType((RaceCheckpointType)type); + cp.SetRadius(size); + cp.SetNextPosition(ref nextPosition); + cp.Enable(); + } + + /// Disable any initialized race checkpoints for this player. + public virtual void DisableRaceCheckpoint() + { + CheckpointData.GetRaceCheckpoint().Disable(); + } + + /// Set the world boundaries for this player - players can not go out of the boundaries. + /// You can reset the player world bounds by setting the parameters to 20000.0000, -20000.0000, 20000.0000, -20000.0000. + /// The maximum X coordinate the player can go to. + /// The minimum X coordinate the player can go to. + /// The maximum Y coordinate the player can go to. + /// The minimum Y coordinate the player can go to. + public virtual void SetWorldBounds(float xMax, float xMin, float yMax, float yMin) + { + _player.SetWorldBounds(new Vector4(xMax, xMin, yMax, yMin)); + } + + /// Change the color of this player's name tag and radar blip for another Player. + /// The player whose color will be changed. + /// New color. + public virtual void SetPlayerMarker(Player player, Color color) + { + _player.SetOtherColour(player, color); + } + + /// + /// This functions allows you to toggle the drawing of player name tags, health bars and armor bars which display above their head. For use of a similar + /// function like this on a global level, function. + /// + /// must be set to to be able to show name tags with . + /// The player whose name tag will be shown or hidden. + /// True to show name tag, False to hide name tag. + public virtual void ShowNameTagForPlayer(Player player, bool show) + { + _player.ToggleOtherNameTag(player, show); + } + + /// Set the direction this player's camera looks at. To be used in combination with . + /// The coordinates for this player's camera to look at. + /// The style the camera-position changes. + public virtual void SetCameraLookAt(Vector3 point, CameraCut cut) + { + _player.SetCameraLookAt(point, (int)cut); + } + + /// Set the direction this player's camera looks at. To be used in combination with . + /// The coordinates for this player's camera to look at. + public virtual void SetCameraLookAt(Vector3 point) + { + SetCameraLookAt(point, CameraCut.Cut); + } + + /// Move this player's camera from one position to another, within the set time. + /// The position the camera should start to move from. + /// The position the camera should move to. + /// Interpolation duration. + /// The jump cut to use. Defaults to CameraCut.Cut. Set to CameraCut. Move for a smooth movement. + public virtual void InterpolateCameraPosition(Vector3 from, Vector3 to, TimeSpan time, CameraCut cut) + { + _player.InterpolateCameraPosition(from, to, (int)time.TotalMilliseconds, (PlayerCameraCutType)cut); + } + + /// + [Obsolete("Use the TimeSpan overload. This int-milliseconds variant is kept for source compatibility and will be removed.")] + public virtual void InterpolateCameraPosition(Vector3 from, Vector3 to, int time, CameraCut cut) + => InterpolateCameraPosition(from, to, TimeSpan.FromMilliseconds(time), cut); + + /// Interpolate this player's camera's 'look at' point between two coordinates with a set speed. + /// The position the camera should start to move from. + /// The position the camera should move to. + /// Interpolation duration. + /// The jump cut to use. Defaults to CameraCut.Cut (pointless). Set to CameraCut.Move for interpolation. + public virtual void InterpolateCameraLookAt(Vector3 from, Vector3 to, TimeSpan time, CameraCut cut) + { + _player.InterpolateCameraLookAt(from, to, (int)time.TotalMilliseconds, (PlayerCameraCutType)cut); + } + + /// + [Obsolete("Use the TimeSpan overload. This int-milliseconds variant is kept for source compatibility and will be removed.")] + public virtual void InterpolateCameraLookAt(Vector3 from, Vector3 to, int time, CameraCut cut) + => InterpolateCameraLookAt(from, to, TimeSpan.FromMilliseconds(time), cut); + + /// Checks if this player is in a specific vehicle. + /// The vehicle. + /// True if player is in the vehicle; False otherwise. + public virtual bool IsInVehicle(Vehicle vehicle) + { + return Vehicle == vehicle; + } + + /// Toggle stunt bonuses for this player. + /// True to enable stunt bonuses, False to disable them. + public virtual void EnableStuntBonus(bool enable) + { + _player.UseStuntBonuses(enable); + } + + /// Toggle this player's spectate mode. + /// When the spectating is turned off, OnPlayerSpawn will automatically be called. + /// True to enable spectating and False to disable. + public virtual void ToggleSpectating(bool toggle) + { + _player.SetSpectating(toggle); + } + + /// Makes this player spectate (watch) another player. + /// Order is CRITICAL! Ensure that you use before . + /// The Player that should be spectated. + /// The mode to spectate with. + public virtual void SpectatePlayer(Player targetPlayer, SpectateMode mode) + { + _player.SpectatePlayer(targetPlayer, (PlayerSpectateMode)mode); + } + + /// Makes this player spectate (watch) another player. + /// Order is CRITICAL! Ensure that you use before . + /// The Player that should be spectated. + public virtual void SpectatePlayer(Player targetPlayer) + { + SpectatePlayer(targetPlayer, SpectateMode.Normal); + } + + /// Sets this player to spectate another vehicle, i.e. see what its driver sees. + /// Order is CRITICAL! Ensure that you use before . + /// The vehicle to spectate. + /// Spectate mode. + public virtual void SpectateVehicle(Vehicle targetVehicle, SpectateMode mode) + { + _player.SpectateVehicle(targetVehicle, (PlayerSpectateMode)mode); + } + + /// Sets this player to spectate another vehicle, i.e. see what its driver sees. + /// Order is CRITICAL! Ensure that you use before . + /// The vehicle to spectate. + public virtual void SpectateVehicle(Vehicle targetVehicle) + { + SpectateVehicle(targetVehicle, SpectateMode.Normal); + } + + /// Starts recording this player's movements to a file, which can then be reproduced by an NPC. + /// The type of recording. + /// + /// Name of the file which will hold the recorded data. It will be saved in the scriptfiles folder, with an automatically added .rec + /// extension. + /// + public virtual void StartRecordingPlayerData(PlayerRecordingType recordingType, string recordingName) + { + ArgumentNullException.ThrowIfNull(recordingName); + RecordingData.Start((SampSharp.OpenMp.Core.Api.PlayerRecordingType)recordingType, recordingName); + } + + /// Stops all the recordings that had been started with for this player. + public virtual void StopRecordingPlayerData() + { + RecordingData.Stop(); + } + + /// Retrieves the start and end (hit) position of the last bullet a player fired. + /// The origin. + /// The hit position. + public virtual void GetLastShot(out Vector3 origin, out Vector3 hitPosition) + { + var data = _player.GetBulletData(); + + origin = data.origin; + hitPosition = data.hitPos; + } + + /// + /// This function sends a message to this player with a chosen color in the chat. The whole line in the chat box will be in the set color unless color + /// embedding is used. + /// + /// The color of the message. + /// The text that will be displayed. + public virtual void SendClientMessage(Color color, string message) + { + ArgumentNullException.ThrowIfNull(message); + + Colour clr = color; + if (message.Length > 144) + { + _player.SendClientMessage(ref clr, message[..144]); + SendClientMessage(color, message[144..]); + } + else + { + _player.SendClientMessage(ref clr, message); + } + } + + /// + /// This function sends a message to this player with a chosen color in the chat. The whole line in the chat box will be in the set color unless color + /// embedding is used. + /// + /// The color of the message. + /// The composite format string of the text that will be displayed (max 144 characters). + /// An object array that contains zero or more objects to format. + [StringFormatMethod("messageFormat")] + public virtual void SendClientMessage(Color color, string messageFormat, params object[] args) + { + SendClientMessage(color, string.Format(messageFormat, args)); + } + + /// + /// This function sends a message to this player in white in the chat. The whole line in the chat box will be in the set color unless color embedding is + /// used. + /// + /// The text that will be displayed. + public virtual void SendClientMessage(string message) + { + SendClientMessage(Color.White, message); + } + + /// + /// This function sends a message to this player in white in the chat. The whole line in the chat box will be in the set color unless color embedding is + /// used. + /// + /// The composite format string of the text that will be displayed (max 144 characters). + /// An object array that contains zero or more objects to format. + [StringFormatMethod("messageFormat")] + public virtual void SendClientMessage(string messageFormat, params object[] args) + { + SendClientMessage(Color.White, string.Format(messageFormat, args)); + } + + /// Kicks this player from the server. They will have to quit the game and re-connect if they wish to continue playing. + public virtual void Kick() + { + _player.Kick(); + } + + /// + /// Ban this player. The ban will be IP-based, and be saved in the samp.ban file in the server's root directory. allows you to + /// ban with a reason, while you can ban and unban IPs using the RCON banip and unbanip commands. + /// + public virtual void Ban() + { + Ban(string.Empty); + } + + /// Ban this player with a reason. + /// The reason for the ban. + public virtual void Ban(string reason) + { + ArgumentNullException.ThrowIfNull(reason); + _player.Ban(reason); + } + + /// + /// Sends a message in the name the specified to this player. The message will appear in the chat box but can only be seen by + /// this player. The line will start with the the sender's name in their color, followed by the in white. + /// + /// The player which has sent the message. + /// The message that will be sent. + public virtual void SendPlayerMessageToPlayer(Player sender, string message) + { + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(message); + _player.SendChatMessage(sender, message); + } + + /// Shows 'game text' (on-screen text) for a certain length of time for this player. + /// The text to be displayed. + /// The duration of the text being shown in milliseconds. + /// The style of text to be displayed. + [Obsolete("Obsolete. Use GameText(string,TimeSpan,int) instead.")] + public virtual void GameText(string text, int time, int style) + { + GameText(text, TimeSpan.FromMilliseconds(time), style); + } + + /// Shows 'game text' (on-screen text) for a certain length of time for this player. + /// The text to be displayed. + /// The duration of the text being shown. + /// The style of text to be displayed. + public virtual void GameText(string text, TimeSpan time, int style) + { + ArgumentNullException.ThrowIfNull(text); + _player.SendGameText(text, time, style); + } + + /// + /// Creates an explosion for this player. Only this player will see explosion and feel its effects. This is useful when you want to isolate explosions + /// from other players or to make them only appear in specific virtual worlds. + /// + /// The position of the explosion. + /// The explosion type. + /// The radius of the explosion. + public virtual void CreateExplosion(Vector3 position, ExplosionType type, float radius) + { + _player.CreateExplosion(position, (int)type, radius); + } + + /// Adds a death to the kill feed on the right-hand side of the screen of this player. + /// The player that killer the . + /// The player that has been killed. + /// The reason for this player's death. + public virtual void SendDeathMessage(Player killer, Player player, Weapon weapon) + { + _player.SendDeathMessage(player, killer, (int)weapon); + } + + /// Attaches a player's camera to an object. + /// The object to attach the camera to. + public virtual void AttachCameraToObject(GlobalObject @object) + { + _player.AttachCameraToObject(@object); + } + + /// Attaches a player's camera to an object. + /// The object to attach the camera to. + public virtual void AttachCameraToObject(PlayerObject @object) + { + _player.AttachCameraToObject(@object); + } + + /// Lets this player edit the specified . + /// The object to edit. + public virtual void Edit(GlobalObject @object) + { + ObjectData.BeginEditing(@object); + } + + /// Lets this player edit the specified . + /// The object to edit. + public virtual void Edit(PlayerObject @object) + { + ObjectData.BeginEditing(@object); + } + + /// Cancels object editing mode for this player. + public virtual void CancelEdit() + { + ObjectData.EndEditing(); + } + + /// Lets this player select an object. + public virtual void Select() + { + ObjectData.BeginSelecting(); + } + + /// Removes a standard San Andreas model for this player within a specified range. + /// The model identifier. + /// The position at which to remove the model. + /// The radius in which to remove the model. + [Obsolete("Deprecated. Use 'RemoveDefaultObjects' instead.")] + public virtual void RemoveBuilding(int modelId, Vector3 position, float radius) + { + _player.RemoveDefaultObjects((uint)modelId, position, radius); + } + + /// Removes a standard San Andreas model for this player within a specified range. + /// The model identifier. + /// The position at which to remove the model. + /// The radius in which to remove the model. + public virtual void RemoveDefaultObjects(int modelId, Vector3 position, float radius) + { + _player.RemoveDefaultObjects((uint)modelId, position, radius); + } + + /// Place an icon/marker on this player's map. Can be used to mark locations such as banks and hospitals to players. + /// The player's icon identifier, ranging from 0 to 99. This means there is a maximum of 100 map icons. + /// The position to place the map icon at. + /// The type of the marker. + /// The color of the marker. + /// The style of the marker. + public virtual void SetMapIcon(int iconId, Vector3 position, MapIcon type, Color color, MapIconType style) + { + _player.SetMapIcon(iconId, position, (int)type, color, (MapIconStyle)style); + } + + /// Removes a map icon that was set earlier for this player. + /// The player's icon identifier. + public virtual void RemoveMapIcon(int iconId) + { + _player.UnsetMapIcon(iconId); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + Kick(); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Name: {Name})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IPlayer(Player player) + { + return player._player; + } +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerObject.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerObject.cs new file mode 100644 index 00000000..a69b0565 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerObject.cs @@ -0,0 +1,147 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a player object. +public class PlayerObject : WorldEntity +{ + private readonly IPlayerObjectData _playerObjects; + private readonly IPlayerObject _playerObject; + + /// Constructs an instance of PlayerObject, should be used internally. + protected PlayerObject(IPlayerObjectData playerObjects, IPlayerObject playerObject) : base((IEntity)playerObject) + { + _playerObjects = playerObjects; + _playerObject = playerObject; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _playerObject.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets whether this player object is moving. + public virtual bool IsMoving => _playerObject.IsMoving(); + + /// Gets the model of this player object. + public virtual int ModelId => _playerObject.GetModel(); + + /// Gets the draw distance of this player object. + public virtual float DrawDistance => _playerObject.GetDrawDistance(); + + /// Moves this player object to the given position and rotation with the given speed. + /// The position to which to move this player object. + /// The speed at which to move this player object. + /// The rotation to which to move this player object. + /// The time it will take for the object to move in milliseconds. + public virtual int Move(Vector3 position, float speed, Vector3 rotation) + { + var time = (position - Position).Length() / speed * 1000f; + + var moveDat = new ObjectMoveData(position, rotation, speed); + _playerObject.Move(ref moveDat); + + return (int)time; + } + + /// Moves this player object to the given position with the given speed. + /// The position to which to move this player object. + /// The speed at which to move this player object. + /// The time it will take for the object to move in milliseconds. + public virtual int Move(Vector3 position, float speed) + { + return Move(position, speed, new Vector3(-1000)); + } + + /// Stop this player object from moving any further. + public virtual void Stop() + { + _playerObject.Stop(); + } + + /// Sets the material of this player object. + /// The material index on the object to change. + /// + /// The model ID on which the replacement texture is located. Use 0 for alpha. Use -1 to change the material color without altering the + /// texture. + /// + /// The name of the txd file which contains the replacement texture (use "none" if not required). + /// The name of the texture to use as the replacement (use "none" if not required). + /// The object color to set (use default(Color) to keep the existing material color). + public virtual void SetMaterial(int materialIndex, int modelId, string txdName, string textureName, Color materialColor) + { + _playerObject.SetMaterial((uint)materialIndex, modelId, txdName, textureName, materialColor); + } + + /// Sets the material text of this player object. + /// The material index on the object to change. + /// The text to show on the object. (MAX 2048 characters) + /// The object's material index to replace with text. + /// The font to use. + /// The size of the text (max 255). + /// Whether to write in bold. + /// The color of the text. + /// The background color of the text. + /// The alignment of the text. + public virtual void SetMaterialText(int materialIndex, string text, ObjectMaterialSize materialSize, string fontface, int fontSize, bool bold, Color foreColor, + Color backColor, ObjectMaterialTextAlign textAlignment) + { + _playerObject.SetMaterialText((uint)materialIndex, text, (SampSharp.OpenMp.Core.Api.ObjectMaterialSize)materialSize, fontface, fontSize, bold, foreColor, backColor, + (SampSharp.OpenMp.Core.Api.ObjectMaterialTextAlign)textAlignment); + } + + /// Disable collisions between players' cameras and this player object. + public virtual void DisableCameraCollisions() + { + _playerObject.SetCameraCollision(false); + } + + /// Attaches this object to the specified player. + /// The player. + /// The offset. + /// The rotation. + public virtual void AttachTo(Player target, Vector3 offset, Vector3 rotation) + { + _playerObject.AttachToPlayer(target, offset, rotation); + } + + /// Attaches this object to the specified vehicle. + /// The vehicle. + /// The offset. + /// The rotation. + public virtual void AttachTo(Vehicle target, Vector3 offset, Vector3 rotation) + { + _playerObject.AttachToVehicle(target, offset, rotation); + } + + /// Attaches this object to the specified player object. + /// The player object. + /// The offset. + /// The rotation. + public virtual void AttachTo(PlayerObject target, Vector3 offset, Vector3 rotation) + { + _playerObject.AttachToObject(target, offset, rotation); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _playerObjects.Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Model: {ModelId})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IPlayerObject(PlayerObject playerObject) + { + return playerObject._playerObject; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextDraw.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextDraw.cs new file mode 100644 index 00000000..ac5b9290 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextDraw.cs @@ -0,0 +1,179 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a per-player text draw. +public class PlayerTextDraw : IdProvider +{ + private readonly IPlayerTextDrawData _playerTextDraws; + private readonly IPlayerTextDraw _playerTextDraw; + + /// Constructs an instance of , should be used internally. + protected PlayerTextDraw(IPlayerTextDrawData playerTextDraws, IPlayerTextDraw playerTextDraw) : base((IIDProvider)playerTextDraw) + { + _playerTextDraws = playerTextDraws; + _playerTextDraw = playerTextDraw; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _playerTextDraw.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets or sets the size of the letters of this text draw. + public virtual Vector2 LetterSize + { + get => _playerTextDraw.GetLetterSize(); + set => _playerTextDraw.SetLetterSize(value); + } + + /// Gets or sets the size of this text draw box and click-able area. + public virtual Vector2 TextSize + { + get => _playerTextDraw.GetTextSize(); + set => _playerTextDraw.SetTextSize(value); + } + + /// Gets or sets the alignment of this text draw. + public virtual TextDrawAlignment Alignment + { + get => (TextDrawAlignment)_playerTextDraw.GetAlignment(); + set => _playerTextDraw.SetAlignment((TextDrawAlignmentTypes)value); + } + + /// Gets or sets the color of the text of this text draw. + public virtual Color ForeColor + { + get => _playerTextDraw.GetLetterColour(); + set => _playerTextDraw.SetColour(value); + } + + /// Gets or sets a value indicating whether a box is used for this text draw. + public virtual bool UseBox + { + get => _playerTextDraw.HasBox(); + set => _playerTextDraw.UseBox(value); + } + + /// Gets or sets the color of the box of this text draw. + public virtual Color BoxColor + { + get + { + _playerTextDraw.GetBoxColour(out var colour); + return colour; + } + set => _playerTextDraw.SetBoxColour(value); + } + + /// Gets or sets the shadow size of this text draw. + public virtual int Shadow + { + get => _playerTextDraw.GetShadow(); + set => _playerTextDraw.SetShadow(value); + } + + /// Gets or sets the outline size of this text draw. + public virtual int Outline + { + get => _playerTextDraw.GetOutline(); + set => _playerTextDraw.SetOutline(value); + } + + /// Gets or sets the background color of this text draw. + public virtual Color BackColor + { + get => _playerTextDraw.GetBackgroundColour(); + set => _playerTextDraw.SetBackgroundColour(value); + } + + /// Gets or sets the font of this text draw. + public virtual TextDrawFont Font + { + get => (TextDrawFont)_playerTextDraw.GetStyle(); + set => _playerTextDraw.SetStyle((TextDrawStyle)value); + } + + /// Gets or sets a value indicating whether the font of this text draw is rendered as a monospaced font. + public virtual bool Proportional + { + get => _playerTextDraw.IsProportional(); + set => _playerTextDraw.SetProportional(value); + } + + /// Gets or sets a value indicating whether this text draw is selectable by the player. + public virtual bool Selectable + { + get => _playerTextDraw.IsSelectable(); + set => _playerTextDraw.SetSelectable(value); + } + + /// Gets or sets the text of this text draw. + public virtual string Text + { + get => _playerTextDraw.GetText(); + set => _playerTextDraw.SetText(string.IsNullOrEmpty(value) ? "_" : value); + } + + /// Gets or sets the preview model of this text draw. + public virtual int PreviewModel + { + get => _playerTextDraw.GetPreviewModel(); + set => _playerTextDraw.SetPreviewModel(value); + } + + /// Gets the position of this text draw. + public virtual Vector2 Position => _playerTextDraw.GetPosition(); + + + /// Sets the preview object rotation and zoom of this text draw. + /// The rotation of the preview object. + /// The zoom of the preview object. + public virtual void SetPreviewRotation(Vector3 rotation, float zoom = 1.0f) + { + _playerTextDraw.SetPreviewRotation(rotation); + _playerTextDraw.SetPreviewZoom(zoom); + } + + /// Sets the color of the preview vehicle of this text draw. + /// The primary color of the vehicle. + /// The secondary color of the vehicle. + public virtual void SetPreviewVehicleColor(int color1, int color2) + { + _playerTextDraw.SetPreviewVehicleColour(color1, color2); + } + + /// Shows this text draw for the player. + public virtual void Show() + { + _playerTextDraw.Show(); + } + + /// Hides this text draw for the player. + public virtual void Hide() + { + _playerTextDraw.Hide(); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _playerTextDraws.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Text: {Text})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IPlayerTextDraw(PlayerTextDraw playerTextDraw) + { + return playerTextDraw._playerTextDraw; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextLabel.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextLabel.cs new file mode 100644 index 00000000..497a400d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/PlayerTextLabel.cs @@ -0,0 +1,106 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a player 3D text label. +public class PlayerTextLabel : WorldEntity +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IPlayerTextLabelData _playerTextLabels; + private readonly IPlayerTextLabel _playerTextLabel; + + /// Constructs an instance of PlayerTextLabel, should be used internally. + protected PlayerTextLabel(IOmpEntityProvider entityProvider, IPlayerTextLabelData playerTextLabels, IPlayerTextLabel playerTextLabel) : base((IEntity)playerTextLabel) + { + _entityProvider = entityProvider; + _playerTextLabels = playerTextLabels; + _playerTextLabel = playerTextLabel; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _playerTextLabel.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets the color of this player text label. + public virtual Color Color + { + get + { + _playerTextLabel.GetColour(out var colour); + return colour; + } + } + + /// Gets the text of this player text label. + public virtual string Text => _playerTextLabel.GetText(); + + /// Gets the draw distance. + public virtual float DrawDistance => _playerTextLabel.GetDrawDistance(); + + /// Gets a value indicating whether to test the line of sight. + public virtual bool TestLos => _playerTextLabel.GetTestLOS(); + + /// Gets the attached entity. + public virtual Component? AttachedEntity + { + get + { + var attachmentData = _playerTextLabel.GetAttachmentData(); + + if (attachmentData.PlayerId != OpenMpConstants.INVALID_PLAYER_ID) + { + return _entityProvider.GetPlayer(attachmentData.PlayerId); + } + + if (attachmentData.VehicleId != OpenMpConstants.INVALID_VEHICLE_ID) + { + return _entityProvider.GetVehicle(attachmentData.VehicleId); + } + + return null; + } + } + + /// + /// Attaches this player text label to the specified player. + /// + /// The player to attach this player text label to. + /// The offset from the player's position to attach this player text label to. + public virtual void Attach(Player player, Vector3 offset = default) + { + _playerTextLabel.AttachToPlayer(player, offset); + } + + /// + /// Attaches this player text label to the specified vehicle. + /// + /// The vehicle to attach this text label to. + /// The offset from the vehicle's position to attach this text label to. + public virtual void Attach(Vehicle vehicle, Vector3 offset = default) + { + _playerTextLabel.AttachToVehicle(vehicle, offset); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _playerTextLabels.Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Text: {Text})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IPlayerTextLabel(PlayerTextLabel playerTextLabel) + { + return playerTextLabel._playerTextLabel; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/TextDraw.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/TextDraw.cs new file mode 100644 index 00000000..09459c12 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/TextDraw.cs @@ -0,0 +1,199 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a text draw. +public class TextDraw : IdProvider +{ + private readonly ITextDrawsComponent _textDraws; + private readonly ITextDraw _textDraw; + + /// Constructs an instance of , should be used internally. + protected TextDraw(ITextDrawsComponent textDraws, ITextDraw textDraw) : base((IIDProvider)textDraw) + { + _textDraws = textDraws; + _textDraw = textDraw; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _textDraw.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets or sets the size of the letters of this text draw. + public virtual Vector2 LetterSize + { + get => _textDraw.GetLetterSize(); + set => _textDraw.SetLetterSize(value); + } + + /// Gets or sets the size of this text draw box and click-able area. + public virtual Vector2 TextSize + { + get => _textDraw.GetTextSize(); + set => _textDraw.SetTextSize(value); + } + + /// Gets or sets the alignment of this text draw. + public virtual TextDrawAlignment Alignment + { + get => (TextDrawAlignment)_textDraw.GetAlignment(); + set => _textDraw.SetAlignment((TextDrawAlignmentTypes)value); + } + + /// Gets or sets the color of the text of this text draw. + public virtual Color ForeColor + { + get => _textDraw.GetLetterColour(); + set => _textDraw.SetColour(value); + } + + /// Gets or sets a value indicating whether a box is used for this text draw. + public virtual bool UseBox + { + get => _textDraw.HasBox(); + set => _textDraw.UseBox(value); + } + + /// Gets or sets the color of the box of this text draw. + public virtual Color BoxColor + { + get + { + _textDraw.GetBoxColour(out var colour); + return colour; + } + set => _textDraw.SetBoxColour(value); + } + + /// Gets or sets the shadow size of this text draw. + public virtual int Shadow + { + get => _textDraw.GetShadow(); + set => _textDraw.SetShadow(value); + } + + /// Gets or sets the outline size of this text draw. + public virtual int Outline + { + get => _textDraw.GetOutline(); + set => _textDraw.SetOutline(value); + } + + /// Gets or sets the background color of this text draw. + public virtual Color BackColor + { + get => _textDraw.GetBackgroundColour(); + set => _textDraw.SetBackgroundColour(value); + } + + /// Gets or sets the font of this text draw. + public virtual TextDrawFont Font + { + get => (TextDrawFont)_textDraw.GetStyle(); + set => _textDraw.SetStyle((TextDrawStyle)value); + } + + /// Gets or sets a value indicating whether the font of this text draw is rendered as a monospaced font. + public virtual bool Proportional + { + get => _textDraw.IsProportional(); + set => _textDraw.SetProportional(value); + } + + /// Gets or sets a value indicating whether this text draw is selectable by the player. + public virtual bool Selectable + { + get => _textDraw.IsSelectable(); + set => _textDraw.SetSelectable(value); + } + + /// Gets or sets the text of this text draw. + public virtual string Text + { + get => _textDraw.GetText(); + set => _textDraw.SetText(string.IsNullOrEmpty(value) ? "_" : value); + } + + /// Gets or sets the preview model of this text draw. + public virtual int PreviewModel + { + get => _textDraw.GetPreviewModel(); + set => _textDraw.SetPreviewModel(value); + } + + /// Gets the position of this text draw. + public virtual Vector2 Position => _textDraw.GetPosition(); + + + /// Sets the preview object rotation and zoom of this text draw. + /// The rotation of the preview object. + /// The zoom of the preview object. + public virtual void SetPreviewRotation(Vector3 rotation, float zoom = 1.0f) + { + _textDraw.SetPreviewRotation(rotation); + _textDraw.SetPreviewZoom(zoom); + } + + /// Sets the color of the preview vehicle of this text draw. + /// The primary color of the vehicle. + /// The secondary color of the vehicle. + public virtual void SetPreviewVehicleColor(int color1, int color2) + { + _textDraw.SetPreviewVehicleColour(color1, color2); + } + + /// Shows this text draw to all players. + public virtual void Show() + { + foreach (var player in Manager.GetComponents()) + { + Show(player); + } + } + + /// Shows this text draw to the specified . + /// The player to show this text draw to. + public virtual void Show(Player player) + { + _textDraw.ShowForPlayer(player); + } + + /// Hides this text draw for all players. + public virtual void Hide() + { + foreach (var player in Manager.GetComponents()) + { + Hide(player); + } + } + + /// Hides this text draw for the specified . + /// The player to hide this text draw from. + public virtual void Hide(Player player) + { + _textDraw.HideForPlayer(player); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _textDraws.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Text: {Text})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator ITextDraw(TextDraw textDraw) + { + return textDraw._textDraw; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/TextLabel.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/TextLabel.cs new file mode 100644 index 00000000..db8e7fa1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/TextLabel.cs @@ -0,0 +1,111 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a 3D text label. +public class TextLabel : WorldEntity +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly ITextLabelsComponent _textLabels; + private readonly ITextLabel _textLabel; + + /// Constructs an instance of TextLabel, should be used internally. + protected TextLabel(IOmpEntityProvider entityProvider, ITextLabelsComponent textLabels, ITextLabel textLabel) : base((IEntity)textLabel) + { + _entityProvider = entityProvider; + _textLabels = textLabels; + _textLabel = textLabel; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _textLabel.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets or sets the color of this text label. + public virtual Color Color + { + get + { + _textLabel.GetColour(out var colour); + return colour; + } + set => _textLabel.SetColour(value); + } + + /// Gets or sets the text of this text label. + public virtual string Text + { + get => _textLabel.GetText(); + set => _textLabel.SetText(value); + } + + /// Gets the draw distance of this text label. + public virtual float DrawDistance => _textLabel.GetDrawDistance(); + + /// Gets a value indicating whether to test the line of sight. + public virtual bool TestLos => _textLabel.GetTestLOS(); + + /// Gets or sets the attached entity (player or vehicle). + public virtual Component? AttachedEntity + { + get + { + var attachmentData = _textLabel.GetAttachmentData(); + + if (attachmentData.PlayerId != OpenMpConstants.INVALID_PLAYER_ID) + { + return _entityProvider.GetPlayer(attachmentData.PlayerId); + } + + if (attachmentData.VehicleId != OpenMpConstants.INVALID_VEHICLE_ID) + { + return _entityProvider.GetVehicle(attachmentData.VehicleId); + } + + return null; + } + } + + /// + /// Attaches this text label to the specified player. + /// + /// The player to attach this text label to. + /// The offset from the player's position to attach this text label to. + public virtual void Attach(Player player, Vector3 offset = default) + { + _textLabel.AttachToPlayer(player, offset); + } + + /// + /// Attaches this text label to the specified vehicle. + /// + /// The vehicle to attach this player text label to. + /// The offset from the vehicle's position to attach this player text label to. + public virtual void Attach(Vehicle vehicle, Vector3 offset = default) + { + _textLabel.AttachToVehicle(vehicle, offset); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _textLabels.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Text: {Text})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator ITextLabel(TextLabel textLabel) + { + return textLabel._textLabel; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs new file mode 100644 index 00000000..b8c2d9ad --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/Vehicle.cs @@ -0,0 +1,483 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a component which provides the data and functionality of a vehicle. +public class Vehicle : WorldEntity +{ + private readonly IVehiclesComponent _vehicles; + private readonly IVehicle _vehicle; + + /// Constructs an instance of Vehicle, should be used internally. + protected Vehicle(IVehiclesComponent vehicles, IVehicle vehicle) : base((IEntity)vehicle) + { + _vehicles = vehicles; + _vehicle = vehicle; + } + + /// + /// Gets a value indicating whether the open.mp entity counterpart has been destroyed. + /// + protected bool IsOmpEntityDestroyed => _vehicle.TryGetExtension()?.IsOmpEntityDestroyed ?? true; + + /// Gets or sets the Z angle of this vehicle. + public virtual float Angle + { + get => _vehicle.GetZAngle(); + set => _vehicle.SetZAngle(value); + } + + /// Gets the model ID of this vehicle. + public virtual VehicleModelType Model => (VehicleModelType)_vehicle + .GetModel(); + + /// Gets whether this vehicle has a trailer attached to it. + public virtual bool HasTrailer => _vehicle.GetTrailer() != null; + + /// Gets or sets the the trailer attached to this vehicle. + /// The trailer attached. + public virtual Vehicle? Trailer + { + get => _vehicle.GetTrailer().TryGetExtension()?.Component as Vehicle; + set + { + if (value) + { + _vehicle.AttachTrailer(value!); + } + else + { + _vehicle.DetachTrailer(); + } + } + } + + /// Gets or sets the velocity at which this vehicle is moving. + public virtual Vector3 Velocity + { + get => _vehicle.GetVelocity(); + set => _vehicle.SetVelocity(value); + } + + /// + /// Gets or sets the parameters of this vehicle. This includes the engine, lights, alarm, doors, bonnet, boot and + /// objective status. + /// + public virtual VehicleParameters Parameters + { + get + { + var parameters = _vehicle.GetParams(); + return VehicleParameters.FromParams(ref parameters); + } + set + { + var p = value.ToParams(); + _vehicle.SetParams(ref p); + } + } + + /// Gets or sets this vehicle's engine status. If True, the engine is running. + public virtual bool Engine + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.engine == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Engine = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets this vehicle's lights' state. If True the lights are on. + public virtual bool Lights + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.lights == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Lights = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets this vehicle's alarm state. If True the alarm is (or was) sounding. + public virtual bool Alarm + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.alarm == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Alarm = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets the lock status of the doors of this vehicle. If True the doors are locked. + public virtual bool Doors + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.doors == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Doors = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets the bonnet/hood status of this vehicle. If True, it's open. + public virtual bool Bonnet + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.bonnet == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Bonnet = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets the boot/trunk status of this vehicle. True means it is open. + public virtual bool Boot + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.boot == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Boot = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets the objective status of this vehicle. True means the objective is on. + public virtual bool Objective + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.objective == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + Objective = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver door is open. + public virtual bool IsDriverDoorOpen + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.doorDriver == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + DoorDriver = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the passenger door is open. + public virtual bool IsPassengerDoorOpen + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.doorPassenger == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + DoorPassenger = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver door is open. + public virtual bool IsBackLeftDoorOpen + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.doorBackLeft == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + DoorBackLeft = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver door is open. + public virtual bool IsBackRightDoorOpen + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.doorBackRight == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + DoorBackRight = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver window is closed. + public virtual bool IsDriverWindowClosed + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.windowDriver == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + WindowDriver = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the passenger window is closed. + public virtual bool IsPassengerWindowClosed + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.windowPassenger == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + WindowPassenger = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver window is closed. + public virtual bool IsBackLeftWindowClosed + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.windowBackLeft == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + WindowBackLeft = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets or sets a value indicating whether the driver window is closed. + public virtual bool IsBackRightWindowClosed + { + get + { + var parameters = _vehicle.GetParams(); + return (VehicleParameterValue)parameters.windowBackRight == VehicleParameterValue.On; + } + set => + Parameters = Parameters with + { + WindowBackRight = value ? VehicleParameterValue.On : VehicleParameterValue.Off + }; + } + + /// Gets a value indicating whether this Vehicle's siren is on. + public virtual bool IsSirenOn => _vehicle.GetSirenState() == 1; + + /// Gets or sets the health of this vehicle. + public virtual float Health + { + get => _vehicle.GetHealth(); + set => _vehicle.SetHealth(value); + } + + /// Gets this vehicle's rotation on all axis as a quaternion. + [Obsolete("Deprecated. Use Rotation instead.")] + public virtual Quaternion RotationQuaternion => Rotation; + + /// Gets the first color of this vehicle. + public virtual int Color1 + { + get + { + _vehicle.GetColour(out var pair); + return pair.First; + } + } + + /// Gets the second color of this vehicle. + public virtual int Color2 + { + get + { + _vehicle.GetColour(out var pair); + return pair.Second; + } + } + + /// + /// This function can be used to calculate the distance (as a float) between this vehicle and another map + /// coordinate. This can be useful to detect how far a vehicle away is from a location. + /// + /// The point. + /// A float containing the distance from the point specified in the coordinates. + public virtual float GetDistanceFromPoint(Vector3 point) + { + var offset = point - Position; + return offset.Length(); + } + + /// Checks if this vehicle is streamed in for the specified . + /// The player to check. + /// if this vehicle is streamed in for the specified vehicle; otherwise. + public virtual bool IsStreamedIn(Player player) + { + return _vehicle.IsStreamedInForPlayer(player); + } + + /// Set the parameters of this vehicle for a player. + /// The player to set this vehicle's parameters for. + /// The vehicle parameters + public virtual void SetParametersForPlayer(Player player, in VehicleParameters parameters) + { + var p = parameters.ToParams(); + _vehicle.SetParamsForPlayer(player, ref p); + } + + /// Sets this vehicle back to the position at where it was created. + public virtual void Respawn() + { + _vehicle.Respawn(); + } + + /// Links this vehicle to the interior. This can be used for example for an arena/stadium. + /// Interior ID. + public virtual void LinkToInterior(int interiorId) + { + _vehicle.SetInterior(interiorId); + } + + /// Adds a 'component' (often referred to as a 'mod' (modification)) to this Vehicle. + /// The ID of the component to add to the vehicle. + public virtual void AddComponent(int componentId) + { + _vehicle.AddComponent(componentId); + } + + /// Remove a component from the vehicle. + /// ID of the component to remove. + public virtual void RemoveComponent(int componentId) + { + _vehicle.RemoveComponent(componentId); + } + + /// Change this vehicle's primary and secondary colors. + /// The new vehicle's primary Color ID. + /// The new vehicle's secondary Color ID. + public virtual void ChangeColor(int color1, int color2) + { + _vehicle.SetColour(color1, color2); + } + + /// Change this vehicle's paintjob (for plain colors see ). + /// The ID of the paintjob to apply. Use 3 to remove a paintjob. + public virtual void ChangePaintjob(int paintjobId) + { + _vehicle.SetPaintJob(paintjobId); + } + + /// Set this vehicle's numberplate, which supports color embedding. + /// The text that should be displayed on the numberplate. Color Embedding> is + /// supported. + public virtual void SetNumberPlate(string numberplate) + { + _vehicle.SetPlate(numberplate); + } + + /// Retrieves the installed component ID from this vehicle in a specific slot. + /// The component slot to check for components. + /// The ID of the component installed in the specified slot. + public virtual int GetComponentInSlot(CarModType slot) + { + return _vehicle.GetComponentInSlot((int)slot); + } + + /// Fully repairs this vehicle, including visual damage (bumps, dents, scratches, popped tires + /// etc.). + public virtual void Repair() + { + _vehicle.Repair(); + } + + /// Sets the angular velocity of this vehicle. + /// The amount of velocity in the angular directions. + public virtual void SetAngularVelocity(Vector3 velocity) + { + _vehicle.SetAngularVelocity(velocity); + } + + /// Retrieve the damage statuses of this vehicle. + /// A variable to store the panel damage data in, passed by reference. + /// A variable to store the door damage data in, passed by reference. + /// A variable to store the light damage data in, passed by reference. + /// A variable to store the tire damage data in, passed by reference. + public virtual void GetDamageStatus(out int panels, out int doors, out int lights, out int tires) + { + _vehicle.GetDamageStatus(out panels, out doors, out lights, out tires); + } + + /// Sets the various visual damage statuses of this vehicle, such as popped tires, broken lights and + /// damaged panels. + /// A set of bits containing the panel damage status. + /// A set of bits containing the door damage status. + /// A set of bits containing the light damage status. + /// A set of bits containing the tire damage status. + /// TODO: document parameter + public virtual void UpdateDamageStatus(int panels, int doors, int lights, int tires, Player? updater = null) + { + _vehicle.SetDamageStatus(panels, doors, (byte)lights, (byte)tires, updater ?? default(IPlayer)); + } + + /// + protected override void OnDestroyComponent() + { + if (!IsOmpEntityDestroyed) + { + _vehicles.AsPool().Release(Id); + } + } + + /// + public override string ToString() + { + return $"(Id: {Id}, Model: {Model})"; + } + + /// Performs an implicit conversion from to . + public static implicit operator IVehicle(Vehicle vehicle) + { + return vehicle._vehicle; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Components/WorldEntity.cs b/src/SampSharp.OpenMp.Entities/SAMP/Components/WorldEntity.cs new file mode 100644 index 00000000..e3f56348 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Components/WorldEntity.cs @@ -0,0 +1,51 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// +/// Represents a component which exists in the 3D world. +/// +/// +/// Initializes a new instance of the class. +/// +/// The open.mp entity this component represents. +public abstract class WorldEntity(IEntity entity) : IdProvider((IIDProvider)entity) +{ + /// + /// Gets or sets the position of this component. + /// + public virtual Vector3 Position + { + get => entity.GetPosition(); + set => entity.SetPosition(value); + } + + /// + /// Gets or sets the rotation of this component. + /// + public virtual Quaternion Rotation + { + get => entity.GetRotation(); + set => entity.SetRotation(value); + } + + /// + /// Gets or sets the rotation of this component in euler angles. Note: this is less accurate than the quaternion + /// representation available through the property. + /// + public virtual Vector3 RotationEuler + { + get => Vector3.RadiansToDegrees(MathHelper.CreateYawPitchRollFromQuaternion(Rotation)); + set => Rotation = MathHelper.CreateQuaternionFromYawPitchRoll(Vector3.DegreesToRadians(value)); + } + + /// + /// Gets or sets the virtual world of this component. + /// + public virtual int VirtualWorld + { + get => entity.GetVirtualWorld(); + set => entity.SetVirtualWorld(value); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandCollection.cs b/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandCollection.cs new file mode 100644 index 00000000..d2dcf416 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandCollection.cs @@ -0,0 +1,39 @@ +using System.Collections; +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.Entities.SAMP; + +/// +/// Represents a collection of console commands. +/// +[EventParameter] +public class ConsoleCommandCollection(FlatHashSetStringView set) : IReadOnlyCollection +{ + /// + /// Gets the number of commands in this collection. + /// + public int Count => set.Count; + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return set.Select(item => item ?? string.Empty).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Adds a command to this collection. + /// + /// The command to add to the collection. + public void Add(string command) + { + set.Emplace(command); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandSender.cs b/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandSender.cs new file mode 100644 index 00000000..3525e939 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/ConsoleCommandSender.cs @@ -0,0 +1,10 @@ +namespace SampSharp.Entities.SAMP; + +/// +/// Represents a console command sender. +/// +/// The player that sent the command. +/// A value indicating whether the command was sent by the console or a script. +/// A value indicating whether the command was sent by as custom component. +[EventParameter] +public record ConsoleCommandSender(Player? Player, bool IsConsole, bool IsCustom); \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BodyPart.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BodyPart.cs new file mode 100644 index 00000000..3e694368 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BodyPart.cs @@ -0,0 +1,42 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all available body parts. +/// See https://www.open.mp/docs/scripting/resources/bodyparts. +public enum BodyPart +{ + /// The chest. + Chest = 3, + + /// The crotch. + Crotch = 4, + + /// The left arm. + LeftArm = 5, + + /// The right arm. + RightArm = 6, + + /// The left leg. + LeftLeg = 7, + + /// The right leg. + RightLeg = 8, + + /// The head. + Head = 9 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Bone.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Bone.cs new file mode 100644 index 00000000..0342ab90 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Bone.cs @@ -0,0 +1,75 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains every bone in a player's body. +/// See https://www.open.mp/docs/scripting/resources/boneid. +public enum Bone +{ + /// The spine. + Spine = 1, + + /// The head. + Head = 2, + + /// The left upper arm. + LeftUpperArm = 3, + + /// The right upper arm. + RightUpperArm = 4, + + /// The left hand. + LeftHand = 5, + + /// The right hand. + RightHand = 6, + + /// The left thigh. + LeftThigh = 7, + + /// The right thigh. + RightThigh = 8, + + /// The left foot. + LeftFoot = 9, + + /// The right foot. + RightFoot = 10, + + /// The right calf. + RightCalf = 11, + + /// The left calf. + LeftCalf = 12, + + /// The left forearm. + LeftForearm = 13, + + /// The right forearm. + RightForearm = 14, + + /// The left clavicle. + LeftClavicle = 15, + + /// The right clavicle. + RightClavicle = 16, + + /// The neck. + Neck = 17, + + /// The jaw. + Jaw = 18 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BulletHitType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BulletHitType.cs new file mode 100644 index 00000000..9cac3503 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/BulletHitType.cs @@ -0,0 +1,36 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all types of things bullets can hit. +/// See . +public enum BulletHitType +{ + /// Hit nothing. + None = 0, + + /// Hit a player. + Player = 1, + + /// Hit a vehicle. + Vehicle = 2, + + /// Hit an GlobalObject. + Object = 3, + + /// Hit a PlayerObject. + PlayerObject = 4 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraCut.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraCut.cs new file mode 100644 index 00000000..f9f95962 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraCut.cs @@ -0,0 +1,26 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all camera cut styles. +public enum CameraCut +{ + /// Move the camera from one point to another. + Move = 1, + + /// Teleport the camera from one point to another. + Cut = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraMode.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraMode.cs new file mode 100644 index 00000000..3e59ea82 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CameraMode.cs @@ -0,0 +1,63 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all camera modes. +/// See https://www.open.mp/docs/scripting/resources/cameramodes. +public enum CameraMode +{ + /// Invalid mode. + Invalid = -1, + + /// Camera is behind a car. + BehindCar = 3, + + /// Camera is behind a Ped. + FollowPed = 4, + + /// Sniper view. + SniperAiming = 7, + + /// Rocket launcher view. + RocketLauncherAiming = 8, + + /// Camera is set to a fixed point (e.g. after setting the player's camera position) + Fixed = 15, + + /// Camera is in first person mode (e.g. when looking from inside the vehicle) + FirstPerson = 16, + + /// Camera 'normally' behind a car. + NormalCar = 18, + + /// Camera behind a boat. + BehindBoat = 22, + + /// Camera when aiming. + CameraWeaponAiming = 46, + + /// Heat-seeking rocket launcher view. + HeatSeekingRocketLauncher = 51, + + /// Aiming a weapon. + AimingWeapon = 53, + + /// Drive by view. + VehicleDriveBy = 55, + + /// Helicopter chase view. + HelicopterChaseCam = 56 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CarModType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CarModType.cs new file mode 100644 index 00000000..bab3b8a2 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CarModType.cs @@ -0,0 +1,67 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all modification types of vehicles. +/// See https://www.open.mp/docs/scripting/resources/carcomponentid. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum CarModType +{ + /// Car spoiler. + Spoiler = 0, + + /// Car hood. + Hood = 1, + + /// Car roof. + Roof = 2, + + /// Car sideskirts. + Sideskirt = 3, + + /// Car lamps. + Lamps = 4, + + /// Nitrogen. + Nitro = 5, + + /// Car exhaust. + Exhaust = 6, + + /// Car wheels. + Wheels = 7, + + /// Car stereo. + Stereo = 8, + + /// Car hydraulics. + Hydraulics = 9, + + /// Front car bumper. + FrontBumper = 10, + + /// Rear car bumper. + RearBumper = 11, + + /// Right car vent. + VentRight = 12, + + /// Left car vent. + VentLeft = 13 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CheckpointType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CheckpointType.cs new file mode 100644 index 00000000..9667716f --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/CheckpointType.cs @@ -0,0 +1,50 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all race checkpoint types. +/// +/// See +/// https://www.open.mp/docs/scripting/functions/SetPlayerRaceCheckpoint . +/// +public enum CheckpointType +{ + /// Normal race checkpoint. (Normal red cylinder) + Normal = 0, + + /// Finish race checkpoint. (Finish flag in red cylinder) + Finish = 1, + + /// No checkpoint. + Nothing = 2, + + /// Air race checkpoint. (normal red circle in the air) + Air = 3, + + /// Finish air race checkpoint. (Finish flag in red circle in the air) + AirFinish = 4, + + /// Checkpoint one. + One = 5, + /// Checkpoint two. + Two = 6, + /// Checkpoint three. + Three = 7, + /// Checkpoint four. + Four = 8, + /// No checkpoint. + None = 9 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Color.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Color.cs new file mode 100644 index 00000000..d7ecd4ed --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Color.cs @@ -0,0 +1,982 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Text.RegularExpressions; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// Represents a hexadecimal color. +[SuppressMessage("ReSharper", "CommentTypo")] +public partial struct Color +{ + private static readonly Regex _hex6Regex = Hex6Regex(); + private static readonly Regex _hex8Regex = Hex8Regex(); + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + /// The alpha value of this Color. + public Color(byte r, byte g, byte b, byte a) : this() + { + R = r; + G = g; + B = b; + A = a; + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + public Color(byte r, byte g, byte b) : this() + { + R = r; + G = g; + B = b; + A = 255; + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + /// The alpha value of this Color. + public Color(byte r, byte g, byte b, float a) : this(r, g, b, (byte)float.Clamp(a * byte.MaxValue, byte.MinValue, byte.MaxValue)) + { + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + /// The alpha value of this Color. + public Color(int r, int g, int b, int a) : this((byte)int.Clamp(r, byte.MinValue, byte.MaxValue), + (byte)int.Clamp(g, byte.MinValue, byte.MaxValue), (byte)int.Clamp(b, byte.MinValue, byte.MaxValue), + (byte)int.Clamp(a, byte.MinValue, byte.MaxValue)) + { + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + public Color(int r, int g, int b) : this(r, g, b, 255) + { + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + /// The alpha value of this Color. + public Color(float r, float g, float b, float a) : this((byte)float.Clamp(r * byte.MaxValue, byte.MinValue, byte.MaxValue), + (byte)float.Clamp(g * byte.MaxValue, byte.MinValue, byte.MaxValue), (byte)float.Clamp(b * byte.MaxValue, byte.MinValue, byte.MaxValue), + (byte)float.Clamp(a * byte.MaxValue, byte.MinValue, byte.MaxValue)) + { + } + + /// Initializes a new instance of the Color struct. + /// The red value of this Color. + /// The green value of this Color. + /// The blue value of this Color. + public Color(float r, float g, float b) : this(r, g, b, 1.0f) + { + } + + /// Initializes a new instance of the Color struct. + /// The Color values to use for this Color. + public Color(int color) : this() + { + var c = FromInteger(color, ColorFormat.RGBA); //Default format + R = c.R; + G = c.G; + B = c.B; + A = c.A; + } + + /// Initializes a new instance of the Color struct. + /// The Color values to use for this Color. + public Color(uint color) : this() + { + var c = FromInteger(color, ColorFormat.RGBA); //Default format + R = c.R; + G = c.G; + B = c.B; + A = c.A; + } + + /// Gets a system-defined color that has an ARGB value of #FFF0F8FF. + public static Color AliceBlue { get; } = new(0xF0, 0xF8, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFAEBD7. + public static Color AntiqueWhite { get; } = new(0xFA, 0xEB, 0xD7, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00FFFF. + public static Color Aqua { get; } = new(0x00, 0xFF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF7FFFD4. + public static Color Aquamarine { get; } = new(0x7F, 0xFF, 0xD4, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF0FFFF. + public static Color Azure { get; } = new(0xF0, 0xFF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF5F5DC. + public static Color Beige { get; } = new(0xF5, 0xF5, 0xDC, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFE4C4. + public static Color Bisque { get; } = new(0xFF, 0xE4, 0xC4, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF000000. + public static Color Black { get; } = new(0x00, 0x00, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFEBCD. + public static Color BlanchedAlmond { get; } = new(0xFF, 0xEB, 0xCD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF0000FF. + public static Color Blue { get; } = new(0x00, 0x00, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF8A2BE2. + public static Color BlueViolet { get; } = new(0x8A, 0x2B, 0xE2, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFA52A2A. + public static Color Brown { get; } = new(0xA5, 0x2A, 0x2A, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDEB887. + public static Color BurlyWood { get; } = new(0xDE, 0xB8, 0x87, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF5F9EA0. + public static Color CadetBlue { get; } = new(0x5F, 0x9E, 0xA0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF7FFF00. + public static Color Chartreuse { get; } = new(0x7F, 0xFF, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFD2691E. + public static Color Chocolate { get; } = new(0xD2, 0x69, 0x1E, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF7F50. + public static Color Coral { get; } = new(0xFF, 0x7F, 0x50, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF6495ED. + public static Color CornflowerBlue { get; } = new(0x64, 0x95, 0xED, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFF8DC. + public static Color Cornsilk { get; } = new(0xFF, 0xF8, 0xDC, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDC143C. + public static Color Crimson { get; } = new(0xDC, 0x14, 0x3C, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00FFFF. + public static Color Cyan { get; } = new(0x00, 0xFF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00008B. + public static Color DarkBlue { get; } = new(0x00, 0x00, 0x8B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF008B8B. + public static Color DarkCyan { get; } = new(0x00, 0x8B, 0x8B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFB8860B. + public static Color DarkGoldenrod { get; } = new(0xB8, 0x86, 0x0B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFA9A9A9. + public static Color DarkGray { get; } = new(0xA9, 0xA9, 0xA9, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF006400. + public static Color DarkGreen { get; } = new(0x00, 0x64, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFBDB76B. + public static Color DarkKhaki { get; } = new(0xBD, 0xB7, 0x6B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF8B008B. + public static Color DarkMagenta { get; } = new(0x8B, 0x00, 0x8B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF556B2F. + public static Color DarkOliveGreen { get; } = new(0x55, 0x6B, 0x2F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF8C00. + public static Color DarkOrange { get; } = new(0xFF, 0x8C, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF9932CC. + public static Color DarkOrchid { get; } = new(0x99, 0x32, 0xCC, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF8B0000. + public static Color DarkRed { get; } = new(0x8B, 0x00, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFE9967A. + public static Color DarkSalmon { get; } = new(0xE9, 0x96, 0x7A, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF8FBC8F. + public static Color DarkSeaGreen { get; } = new(0x8F, 0xBC, 0x8F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF483D8B. + public static Color DarkSlateBlue { get; } = new(0x48, 0x3D, 0x8B, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF2F4F4F. + public static Color DarkSlateGray { get; } = new(0x2F, 0x4F, 0x4F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00CED1. + public static Color DarkTurquoise { get; } = new(0x00, 0xCE, 0xD1, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF9400D3. + public static Color DarkViolet { get; } = new(0x94, 0x00, 0xD3, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF1493. + public static Color DeepPink { get; } = new(0xFF, 0x14, 0x93, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00BFFF. + public static Color DeepSkyBlue { get; } = new(0x00, 0xBF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF696969. + public static Color DimGray { get; } = new(0x69, 0x69, 0x69, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF1E90FF. + public static Color DodgerBlue { get; } = new(0x1E, 0x90, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFB22222. + public static Color Firebrick { get; } = new(0xB2, 0x22, 0x22, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFAF0. + public static Color FloralWhite { get; } = new(0xFF, 0xFA, 0xF0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF228B22. + public static Color ForestGreen { get; } = new(0x22, 0x8B, 0x22, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF00FF. + public static Color Fuchsia { get; } = new(0xFF, 0x00, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDCDCDC. + public static Color Gainsboro { get; } = new(0xDC, 0xDC, 0xDC, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF8F8FF. + public static Color GhostWhite { get; } = new(0xF8, 0xF8, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFD700. + public static Color Gold { get; } = new(0xFF, 0xD7, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDAA520. + public static Color Goldenrod { get; } = new(0xDA, 0xA5, 0x20, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF808080. + public static Color Gray { get; } = new(0x80, 0x80, 0x80, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF008000. + public static Color Green { get; } = new(0x00, 0x80, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFADFF2F. + public static Color GreenYellow { get; } = new(0xAD, 0xFF, 0x2F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF0FFF0. + public static Color Honeydew { get; } = new(0xF0, 0xFF, 0xF0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF69B4. + public static Color HotPink { get; } = new(0xFF, 0x69, 0xB4, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFCD5C5C. + public static Color IndianRed { get; } = new(0xCD, 0x5C, 0x5C, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF4B0082. + public static Color Indigo { get; } = new(0x4B, 0x00, 0x82, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFFF0. + public static Color Ivory { get; } = new(0xFF, 0xFF, 0xF0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF0E68C. + public static Color Khaki { get; } = new(0xF0, 0xE6, 0x8C, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFE6E6FA. + public static Color Lavender { get; } = new(0xE6, 0xE6, 0xFA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFF0F5. + public static Color LavenderBlush { get; } = new(0xFF, 0xF0, 0xF5, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF7CFC00. + public static Color LawnGreen { get; } = new(0x7C, 0xFC, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFACD. + public static Color LemonChiffon { get; } = new(0xFF, 0xFA, 0xCD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFADD8E6. + public static Color LightBlue { get; } = new(0xAD, 0xD8, 0xE6, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF08080. + public static Color LightCoral { get; } = new(0xF0, 0x80, 0x80, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFE0FFFF. + public static Color LightCyan { get; } = new(0xE0, 0xFF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFAFAD2. + public static Color LightGoldenrodYellow { get; } = new(0xFA, 0xFA, 0xD2, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFD3D3D3. + public static Color LightGray { get; } = new(0xD3, 0xD3, 0xD3, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF90EE90. + public static Color LightGreen { get; } = new(0x90, 0xEE, 0x90, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFB6C1. + public static Color LightPink { get; } = new(0xFF, 0xB6, 0xC1, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFA07A. + public static Color LightSalmon { get; } = new(0xFF, 0xA0, 0x7A, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF20B2AA. + public static Color LightSeaGreen { get; } = new(0x20, 0xB2, 0xAA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF87CEFA. + public static Color LightSkyBlue { get; } = new(0x87, 0xCE, 0xFA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF778899. + public static Color LightSlateGray { get; } = new(0x77, 0x88, 0x99, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFB0C4DE. + public static Color LightSteelBlue { get; } = new(0xB0, 0xC4, 0xDE, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFFE0. + public static Color LightYellow { get; } = new(0xFF, 0xFF, 0xE0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00FF00. + public static Color Lime { get; } = new(0x00, 0xFF, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF32CD32. + public static Color LimeGreen { get; } = new(0x32, 0xCD, 0x32, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFAF0E6. + public static Color Linen { get; } = new(0xFA, 0xF0, 0xE6, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF00FF. + public static Color Magenta { get; } = new(0xFF, 0x00, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF800000. + public static Color Maroon { get; } = new(0x80, 0x00, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF66CDAA. + public static Color MediumAquamarine { get; } = new(0x66, 0xCD, 0xAA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF0000CD. + public static Color MediumBlue { get; } = new(0x00, 0x00, 0xCD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFBA55D3. + public static Color MediumOrchid { get; } = new(0xBA, 0x55, 0xD3, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF9370DB. + public static Color MediumPurple { get; } = new(0x93, 0x70, 0xDB, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF3CB371. + public static Color MediumSeaGreen { get; } = new(0x3C, 0xB3, 0x71, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF7B68EE. + public static Color MediumSlateBlue { get; } = new(0x7B, 0x68, 0xEE, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00FA9A. + public static Color MediumSpringGreen { get; } = new(0x00, 0xFA, 0x9A, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF48D1CC. + public static Color MediumTurquoise { get; } = new(0x48, 0xD1, 0xCC, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFC71585. + public static Color MediumVioletRed { get; } = new(0xC7, 0x15, 0x85, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF191970. + public static Color MidnightBlue { get; } = new(0x19, 0x19, 0x70, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF5FFFA. + public static Color MintCream { get; } = new(0xF5, 0xFF, 0xFA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFE4E1. + public static Color MistyRose { get; } = new(0xFF, 0xE4, 0xE1, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFE4B5. + public static Color Moccasin { get; } = new(0xFF, 0xE4, 0xB5, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFDEAD. + public static Color NavajoWhite { get; } = new(0xFF, 0xDE, 0xAD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF000080. + public static Color Navy { get; } = new(0x00, 0x00, 0x80, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFDF5E6. + public static Color OldLace { get; } = new(0xFD, 0xF5, 0xE6, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF808000. + public static Color Olive { get; } = new(0x80, 0x80, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF6B8E23. + public static Color OliveDrab { get; } = new(0x6B, 0x8E, 0x23, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFA500. + public static Color Orange { get; } = new(0xFF, 0xA5, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF4500. + public static Color OrangeRed { get; } = new(0xFF, 0x45, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDA70D6. + public static Color Orchid { get; } = new(0xDA, 0x70, 0xD6, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFEEE8AA. + public static Color PaleGoldenrod { get; } = new(0xEE, 0xE8, 0xAA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF98FB98. + public static Color PaleGreen { get; } = new(0x98, 0xFB, 0x98, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFAFEEEE. + public static Color PaleTurquoise { get; } = new(0xAF, 0xEE, 0xEE, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDB7093. + public static Color PaleVioletRed { get; } = new(0xDB, 0x70, 0x93, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFEFD5. + public static Color PapayaWhip { get; } = new(0xFF, 0xEF, 0xD5, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFDAB9. + public static Color PeachPuff { get; } = new(0xFF, 0xDA, 0xB9, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFCD853F. + public static Color Peru { get; } = new(0xCD, 0x85, 0x3F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFC0CB. + public static Color Pink { get; } = new(0xFF, 0xC0, 0xCB, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFDDA0DD. + public static Color Plum { get; } = new(0xDD, 0xA0, 0xDD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFB0E0E6. + public static Color PowderBlue { get; } = new(0xB0, 0xE0, 0xE6, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF800080. + public static Color Purple { get; } = new(0x80, 0x00, 0x80, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF0000. + public static Color Red { get; } = new(0xFF, 0x00, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFBC8F8F. + public static Color RosyBrown { get; } = new(0xBC, 0x8F, 0x8F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF4169E1. + public static Color RoyalBlue { get; } = new(0x41, 0x69, 0xE1, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF8B4513. + public static Color SaddleBrown { get; } = new(0x8B, 0x45, 0x13, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFA8072. + public static Color Salmon { get; } = new(0xFA, 0x80, 0x72, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF4A460. + public static Color SandyBrown { get; } = new(0xF4, 0xA4, 0x60, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF2E8B57. + public static Color SeaGreen { get; } = new(0x2E, 0x8B, 0x57, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFF5EE. + public static Color SeaShell { get; } = new(0xFF, 0xF5, 0xEE, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFA0522D. + public static Color Sienna { get; } = new(0xA0, 0x52, 0x2D, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFC0C0C0. + public static Color Silver { get; } = new(0xC0, 0xC0, 0xC0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF87CEEB. + public static Color SkyBlue { get; } = new(0x87, 0xCE, 0xEB, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF6A5ACD. + public static Color SlateBlue { get; } = new(0x6A, 0x5A, 0xCD, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF708090. + public static Color SlateGray { get; } = new(0x70, 0x80, 0x90, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFAFA. + public static Color Snow { get; } = new(0xFF, 0xFA, 0xFA, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF00FF7F. + public static Color SpringGreen { get; } = new(0x00, 0xFF, 0x7F, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF4682B4. + public static Color SteelBlue { get; } = new(0x46, 0x82, 0xB4, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFD2B48C. + public static Color Tan { get; } = new(0xD2, 0xB4, 0x8C, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF008080. + public static Color Teal { get; } = new(0x00, 0x80, 0x80, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFD8BFD8. + public static Color Thistle { get; } = new(0xD8, 0xBF, 0xD8, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFF6347. + public static Color Tomato { get; } = new(0xFF, 0x63, 0x47, 0xFF); + + /// Gets a system-defined color. + public static Color Transparent { get; } = new(0xff, 0xff, 0xff, 0x00); + + /// Gets a system-defined color that has an ARGB value of #FF40E0D0. + public static Color Turquoise { get; } = new(0x40, 0xE0, 0xD0, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFEE82EE. + public static Color Violet { get; } = new(0xEE, 0x82, 0xEE, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF5DEB3. + public static Color Wheat { get; } = new(0xF5, 0xDE, 0xB3, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFFFF. + public static Color White { get; } = new(0xFF, 0xFF, 0xFF, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFF5F5F5. + public static Color WhiteSmoke { get; } = new(0xF5, 0xF5, 0xF5, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FFFFFF00. + public static Color Yellow { get; } = new(0xFF, 0xFF, 0x00, 0xFF); + + + /// Gets a system-defined color that has an ARGB value of #FF9ACD32. + public static Color YellowGreen { get; } = new(0x9A, 0xCD, 0x32, 0xFF); + + /// Gets or sets the red value of this Color. + public byte R { get; set; } + + /// Gets or sets the green value of this Color. + public byte G { get; set; } + + /// Gets or sets the blue value of this Color. + public byte B { get; set; } + + /// Gets or sets the alpha value of this Color. + public byte A { get; set; } + + /// Gets the brightness of this Color. + public float Brightness => 0.212655f * R + 0.715158f * G + 0.072187f * B; + + /// Returns an Integer representation of this Color. + /// The ColorFormat to use in the conversion. + /// An Integer representation of this Color. + public int ToInteger(ColorFormat colorFormat) + { + unchecked + { + switch (colorFormat) + { + case ColorFormat.ARGB: + return (((((A << 8) + R) << 8) + G) << 8) + B; + case ColorFormat.RGBA: + return (((((R << 8) + G) << 8) + B) << 8) + A; + case ColorFormat.RGB: + return (((R << 8) + G) << 8) + B; + default: + return 0; + } + } + } + + /// Returns an Color representation of this Integer. + /// The color to convert. + /// The ColorFormat to use in the conversion. + /// An Color representation of this Integer. + public static Color FromInteger(uint color, ColorFormat colorFormat) + { + byte r = 0, g = 0, b = 0, a = 0; + + switch (colorFormat) + { + case ColorFormat.ARGB: + b = (byte)(color & 0xFF); + g = (byte)((color >> 8) & 0xFF); + r = (byte)((color >> 16) & 0xFF); + a = (byte)((color >> 24) & 0xFF); + break; + case ColorFormat.RGBA: + a = (byte)(color & 0xFF); + b = (byte)((color >> 8) & 0xFF); + g = (byte)((color >> 16) & 0xFF); + r = (byte)((color >> 24) & 0xFF); + break; + case ColorFormat.RGB: + b = (byte)(color & 0xFF); + g = (byte)((color >> 8) & 0xFF); + r = (byte)((color >> 16) & 0xFF); + a = 0xFF; + break; + } + + return new Color(r, g, b, a); + } + + /// Returns an Color representation of the specified integer. + /// The color to convert. + /// The ColorFormat to use in the conversion. + /// A Color representation of the input. + public static Color FromInteger(int color, ColorFormat colorFormat) + { + return FromInteger(unchecked((uint)color), colorFormat); + } + + /// Returns an Color representation of the specified string. + /// The color to convert. + /// The ColorFormat to use in the conversion. + /// A Color representation of the input. + public static Color FromString(string input, ColorFormat colorFormat) + { + var regex = colorFormat == ColorFormat.RGB ? _hex6Regex : _hex8Regex; + + var isValidHexNumber = regex.IsMatch(input); + return isValidHexNumber + ? FromInteger(Convert.ToUInt32(input, 16), colorFormat) + : White; + } + + /// Performs linear interpolation of . + /// Source . + /// Destination . + /// Interpolation factor. + /// Whether it also blends alpha. + /// Interpolated . + public static Color Lerp(Color value1, Color value2, float amount, bool blendAlpha = false) + { + amount = float.Clamp(amount, 0, 1); + return new Color((int)float.Lerp(value1.R, value2.R, amount), (int)float.Lerp(value1.G, value2.G, amount), + (int)float.Lerp(value1.B, value2.B, amount), blendAlpha + ? (int)float.Lerp(value1.A, value2.A, amount) + : value1.A); + } + + /// Returns this color darkened specified . + /// The amount. + /// Whether it also blends alpha. + /// The darkened color. + public Color Darken(float amount, bool blendAlpha = false) + { + return Lerp(this, Black, amount, blendAlpha); + } + + /// Returns this color lightened specified . + /// The amount. + /// Whether it also blends alpha. + /// The lightened color. + public Color Lighten(float amount, bool blendAlpha = false) + { + return Lerp(this, White, amount, blendAlpha); + } + + /// Returns this color with sRGB gamma correction added to it. + /// The gamma corrected version of this color. + public Color AddGammaCorrection() + { + var r = R / (float)byte.MaxValue; + var g = G / (float)byte.MaxValue; + var b = B / (float)byte.MaxValue; + var a = A / (float)byte.MaxValue; + const float power = 1.0f / 2.4f; + + return new Color(r <= 0.0031308f + ? r * 12.92f + : (float)Math.Pow(r, power) * 1.055f - 0.055f, g <= 0.0031308f + ? g * 12.92f + : (float)Math.Pow(g, power) * 1.055f - 0.055f, b <= 0.0031308f + ? b * 12.92f + : (float)Math.Pow(b, power) * 1.055f - 0.055f, a <= 0.0031308f + ? a * 12.92f + : (float)Math.Pow(a, power) * 1.055f - 0.055f); + } + + /// Returns this color with sRGB gamma correction removed from it. + /// The non-gamma corrected version of this color. + public Color RemoveGammaCorrection() + { + var r = R / (float)byte.MaxValue; + var g = G / (float)byte.MaxValue; + var b = B / (float)byte.MaxValue; + var a = A / (float)byte.MaxValue; + + return new Color(r <= 0.04045f + ? r / 12.92f + : (float)Math.Pow((r + 0.055f) / 1.055f, 2.4f), g <= 0.04045f + ? g / 12.92f + : (float)Math.Pow((g + 0.055f) / 1.055f, 2.4f), b <= 0.04045f + ? b / 12.92f + : (float)Math.Pow((b + 0.055f) / 1.055f, 2.4f), a <= 0.04045f + ? a / 12.92f + : (float)Math.Pow((a + 0.055f) / 1.055f, 2.4f)); + } + + /// Returns the grayscaled version of this color. + /// The grayscaled version of this color. + public Color Grayscale() + { + return new Color(Brightness, Brightness, Brightness, A / (float)byte.MaxValue); + } + + /// Returns a representation of this Color. + /// The format to use to convert the color to a string. + /// A representation of this Color. + public string ToString(ColorFormat colorFormat) + { + switch (colorFormat) + { + case ColorFormat.RGB: + return "{" + ToInteger(colorFormat) + .ToString("X6", CultureInfo.InvariantCulture) + "}"; + default: + return "{" + ToInteger(colorFormat) + .ToString("X8", CultureInfo.InvariantCulture) + "}"; + } + } + + /// Returns a representation of this Color. + /// A representation of this Color. + public override string ToString() + { + return ToString(ColorFormat.RGB); + } + + /// Cast a Color to an integer. + /// The Color to cast to an integer. + /// The resulting integer. + public static implicit operator int(Color value) + { + return value.ToInteger(ColorFormat.RGBA); //Default format + } + + /// Cast an integer to a Color. + /// The integer to cast to a Color. + /// The resulting Color. + public static implicit operator Color(int value) + { + return new Color(value); + } + + /// Cast an unsigned integer to a Color. + /// The unsigned integer to cast to a Color. + /// The resulting Color. + public static implicit operator Color(uint value) + { + return new Color(value); + } + + /// + /// Casts a to a . + /// + /// The color to cast. + public static implicit operator Color(Colour value) + { + return new Color(value.R, value.G, value.B, value.A); + } + + /// + /// Casts a to a . + /// + /// The color to cast. + public static implicit operator Colour(Color value) + { + return new Colour(value.R, value.G, value.B, value.A); + } + + /// Implements the operator ==. + /// The left color. + /// The right color. + /// The result of the operator. + public static bool operator ==(Color a, Color b) + { + return a.Equals(b); + } + + /// Implements the operator !=. + /// The left color. + /// The right color. + /// The result of the operator. + public static bool operator !=(Color a, Color b) + { + return !(a == b); + } + + /// Implements the operator *. + /// The value. + /// The scale. + /// The result of the operator. + public static Color operator *(Color value, float scale) + { + return new Color((int)(value.R * scale), (int)(value.G * scale), (int)(value.B * scale), (int)(value.A * scale)); + } + + /// Performs an explicit conversion from to . + /// The value. + /// The result of the conversion. + public static explicit operator Vector3(Color value) + { + return new Vector3((float)value.R / byte.MaxValue, (float)value.G / byte.MaxValue, (float)value.B / byte.MaxValue); + } + + /// Indicates whether this instance and a specified object are equal. + /// true if and this instance represent the same value; otherwise, false. + /// Another object to compare to. + public bool Equals(Color other) + { + return R == other.R && G == other.G && B == other.B && A == other.A; + } + + /// Indicates whether this instance and a specified object are equal. + /// true if and this instance are the same type and represent the same value; otherwise, false. + /// Another object to compare to. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is Color color && Equals(color); + } + + /// Returns a hash code for this instance. + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + public override int GetHashCode() + { + unchecked + { + // ReSharper disable NonReadonlyMemberInGetHashCode + var hashCode = R.GetHashCode(); + hashCode = (hashCode * 397) ^ G.GetHashCode(); + hashCode = (hashCode * 397) ^ B.GetHashCode(); + hashCode = (hashCode * 397) ^ A.GetHashCode(); + // ReSharper restore NonReadonlyMemberInGetHashCode + return hashCode; + } + } + + [GeneratedRegex(@"^(?:0x)?[0-9A-F]{6}$", RegexOptions.IgnoreCase)] + private static partial Regex Hex6Regex(); + + [GeneratedRegex(@"^(?:0x)?[0-9A-F]{8}$", RegexOptions.IgnoreCase)] + private static partial Regex Hex8Regex(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ColorFormat.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ColorFormat.cs new file mode 100644 index 00000000..f3b6e693 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ColorFormat.cs @@ -0,0 +1,14 @@ +namespace SampSharp.Entities.SAMP; + +/// Contains different formats of String representations of Color instances. +public enum ColorFormat +{ + /// {RRGGBBAA} + RGBA, + + /// {AARRGGBB} + ARGB, + + /// {RRGGBB} + RGB +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ConnectionStatus.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ConnectionStatus.cs new file mode 100644 index 00000000..9741b721 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ConnectionStatus.cs @@ -0,0 +1,47 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains the connection status values possible for player connections. +public enum ConnectionStatus +{ + /// NO_ACTION + NoAction = 0, + + /// DISCONNECT_ASAP + DisconnectASAP = 1, + + /// DISCONNECT_ASAP_SILENTLY + DisconnectASAPSilently = 2, + + /// DISCONNECT_ON_NO_ACK + DisconnectOnNoAck = 3, + + /// REQUESTED_CONNECTION + RequestedConnection = 4, + + /// HANDLING_CONNECTION_REQUEST + HandlingConnectionRequest = 5, + + /// UNVERIFIED_SENDER + UnverifiedSender = 6, + + /// SET_ENCRYPTION_ON_MULTIPLE_16_BYTE_PACKET + SetEncryptionOnMultiple16BytePacket = 7, + + /// CONNECTED + Connected = 8 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DialogStyle.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DialogStyle.cs new file mode 100644 index 00000000..2d183bee --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DialogStyle.cs @@ -0,0 +1,38 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all dialog styles. +public enum DialogStyle +{ + /// A box with a caption, text and one or two buttons. + MessageBox = 0, + + /// A box with a caption, text, an input box and one or two buttons. + Input = 1, + + /// A box with a caption, a bunch of selectable items and one or two buttons. + List = 2, + + /// A box with a caption, text, an password input box and one or two buttons. + Password = 3, + + /// A box with a caption, a bunch of selectable rows which contain a number of columns and one or two buttons. + Tablist = 4, + + /// A box with a caption, a bunch of selectable rows which contain a number of columns with a header and one or two buttons. + TablistHeaders = 5 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DisconnectReason.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DisconnectReason.cs new file mode 100644 index 00000000..ceaab935 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/DisconnectReason.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all reasons for a player to disconnect. +public enum DisconnectReason +{ + /// The Player timed out. + TimedOut = 0, + + /// The Player left. (/q(uit) or trough the menu) + Left = 1, + + /// The Player was kicked or banned. + Kicked = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EditObjectResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EditObjectResponse.cs new file mode 100644 index 00000000..3059d985 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EditObjectResponse.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all object editing responses. +public enum EditObjectResponse +{ + /// Editing has been canceled. + Cancel = 0, + + /// The current is the final edit sate. + Final = 1, + + /// The current is a updated edit state. + Update = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EnterExit.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EnterExit.cs new file mode 100644 index 00000000..1f45895b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/EnterExit.cs @@ -0,0 +1,26 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all enter/exit garage states. +public enum EnterExit +{ + /// Has exited garage. + Exited = 0, + + /// Has entered garage. + Entered = 1 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ExplosionType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ExplosionType.cs new file mode 100644 index 00000000..cd1ae43e --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ExplosionType.cs @@ -0,0 +1,75 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all types of explosions with description +public enum ExplosionType +{ + /// Size large. Visible. Damage. + LargeVisibleDamage, + + + /// Size normal. Visible. Creates a fire. + NormalVisibleFire, + + + /// Size large. Visible. Damage. Creates a fire. + LargeVisibleDamageFire, + + + /// Size large. Visible. Damage. Sometimes it does not create a fire. + LargeVisibleDamageFire2, + + + /// Size normal. Visible. Damage. It represents a vanishing flash. No sound. + NormalVisibleDamageFlash, + + + /// Size normal. Visible. Damage. It represents a vanishing flash. No sound. + NormalVisibleDamageFlash2, + + + /// Size very large. Visible. Damage. Additional reddish explosion after-glow. + VeryLargeVisibleDamage, + + + /// Size huge. Visible. Damage. Additional reddish explosion after-glow. + HugeVisibleDamage, + + + /// Size normal. Invisible. Damage. + NormalInvisibleDamage, + + + /// Size normal. Damage. Creates a fire at ground level, otherwise explosion is heard but invisible. + NormalInvisibleDamageFire, + + + /// Size large. Visible. Damage. Compared to the LargeVisibleDamage, the explosion seems great. + LargeVisibleDamage2, + + + /// Size small. Visible. Damage. + SmallVisibleDamage, + + + /// Size very small. Visible. Damage. + VerySmallVisibleDamage, + + + /// Size large. Invisible. Produces no special effects other than black burn effects on the ground. + LargeInvisible +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/FightStyle.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/FightStyle.cs new file mode 100644 index 00000000..f598d1ae --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/FightStyle.cs @@ -0,0 +1,43 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all fighting styles. +/// See https://www.open.mp/docs/scripting/resources/fightingstyles. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum FightStyle : uint +{ + /// Normal fighting style. + Normal = 4, + + /// Boxing fighting style. + Boxing = 5, + + /// Kung fu fighting style. + Kungfu = 6, + + /// Kneehead fighting style. + Kneehead = 7, + + /// Grabkick fighting style. + Grabkick = 15, + + /// Elbow fighting style. + Elbow = 16 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Keys.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Keys.cs new file mode 100644 index 00000000..25e9e230 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Keys.cs @@ -0,0 +1,87 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable CA1069 + +namespace SampSharp.Entities.SAMP; + +/// Contains all detectable keys. +/// See https://www.open.mp/docs/scripting/resources/keys. +[Flags] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum Keys +{ + /// The action key. (Default: Tab on foot; ALT GR / LCTRL / NUM 0 in vehicle) + Action = 1, + + /// The crouch key. (Default: C on foot; H / CAPSLOCK in vehicle) + Crouch = 2, + + /// The fire key. (Default: LCTRL / LMB on foot; LALT in vehicle) + Fire = 4, + + /// The sprint key. (Default: SPACE on foot; W in vehicle) + Sprint = 8, + + /// Secondary attack key. (Default: ENTER on foot; ENTER in vehicle) + SecondaryAttack = 16, + + /// Jump key. (Default: LSHIFT on foot) + Jump = 32, + + /// Look right key. (Default: E in vehicle) + LookRight = 64, + + /// Handbrake key. (Default: RMB on foot; SPACE in vehicle) + Handbrake = 128, + + /// Aim key. (Default: RMB on foot; SPACE in vehicle) + Aim = 128, + + /// Look left key. (Default: Q in vehicle) + LookLeft = 256, + + /// Submission key. (Default: NUM 1 / MMB on foot; 2 / NUM + in vehicle) + Submission = 512, + + /// Look behind key, look left + look right combined. (Default: NUM 1 / MMB on foot; 2 in vehicle) + LookBehind = 512, + + /// Walk key. (Default: LALT on foot) + Walk = 1024, + + /// Analog up key. (Default: NUM 8) + AnalogUp = 2048, + + /// Analog down key. (Default: NUM 2) + AnalogDown = 4096, + + /// Analog left key. (Default: NUM 4) + AnalogLeft = 8192, + + /// Analog right key. (Default: NUM 6) + AnalogRight = 16384, + + /// Yes key. (Default: Y) + Yes = 65536, + + /// No key. (Default: N) + No = 131072, + + /// Controll back key. (Default: H) + CtrlBack = 262144 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIcon.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIcon.cs new file mode 100644 index 00000000..734f0ca8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIcon.cs @@ -0,0 +1,220 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all map icons. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum MapIcon +{ + /// Can be used in any colour. Used for Single Player objectives. + ColoredSquareTriangleDynamic = 0, + + /// 2 times bigger than ID 0 and without the border. + [Obsolete("This element will cause your game to crash if you have map legends enabled while viewing the map", false)] + WhiteSquare = 1, + + /// Will be used on the minimap by default. + [Obsolete("This element will cause your game to crash if you have map legends enabled while viewing the map", false)] + PlayerPosition = 2, + + /// Your position when on the large map. + PlayerMenuMap = 3, + + /// Always appears on the radar toward the north. + [Obsolete("This element will cause your game to crash if you have map legends enabled while viewing the map", false)] + North = 4, + + /// Air Yard + AirYard = 5, + + /// Ammunation + Ammunation = 6, + + /// Barber + Barber = 7, + + /// Big Smoke + BigSmoke = 8, + + /// Boat Yard + BoatYard = 9, + + /// Burger Shot + BurgerShot = 10, + + /// Quarry + Quarry = 11, + + /// Catalina + Catalina = 12, + + /// Cesar + Cesar = 13, + + /// Cluckin' Bell + CluckinBell = 14, + + /// Carl Johnson + CarlJohnson = 15, + + /// C.R.A.S.H + Crash = 16, + + /// Diner + Diner = 17, + + /// Emmet + Emmet = 18, + + /// Enemy Attack + EnemyAttack = 19, + + /// Fire + Fire = 20, + + /// Girlfriend + Girlfriend = 21, + + /// Hospital + Hospital = 22, + + /// Loco + Loco = 23, + + /// Madd Dogg + MaddDogg = 24, + + /// Caligulas + Caligulas = 25, + + /// MC Loc + McLoc = 26, + + /// Mod garage + ModGarage = 27, + + /// OG Loc + OgLoc = 28, + + /// Well Stacked Pizza Co + WellStackedPizzaCo = 29, + + /// Police + Police = 30, + + /// A property you're free to purchase. + FreeProperty = 31, + + /// A property that isn't available for purchase. + Property = 32, + + /// Race + Race = 33, + + /// Ryder + Ryder = 34, + + /// Used for safehouses where you save the game in singleplayer. + SaveGame = 35, + + /// School + School = 36, + + /// Unknown + Unknown = 37, + + /// Sweet + Sweet = 38, + + /// Tattoo + Tattoo = 39, + + /// The Truth + TheTruth = 40, + + /// Can be placed by players on the pause menu map by right-clicking + Waypoint = 41, + + /// Toreno + Toreno = 42, + + /// Triads + Triads = 43, + + /// Triads Casino + TriadsCasino = 44, + + /// Clothes + Clothes = 45, + + /// Woozie + Woozie = 46, + + /// Zero + Zero = 47, + + /// Club + Club = 48, + + /// Bar + Bar = 49, + + /// Restaurant + Restaurant = 50, + + /// Truck + Truck = 51, + + /// Frequently used for banks. + Robbery = 52, + + /// Race + RaceFlag = 53, + + /// Gym + Gym = 54, + + /// Car + Car = 55, + + /// Light + [Obsolete("This element will cause your game to crash if you have map legends enabled while viewing the map", false)] + Light = 56, + + /// Closest airport + ClosestAirport = 57, + + /// Varrios Los Aztecas + VarriosLosAztecas = 58, + + /// Ballas + Ballas = 59, + + /// Los Santos Vagos + LosSantosVagos = 60, + + /// San Fierro Rifa + SanFierroRifa = 61, + + /// Grove street + GroveStreet = 62, + + /// Pay 'n' Spray + PayNSpray = 63 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIconType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIconType.cs new file mode 100644 index 00000000..77b0dd29 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/MapIconType.cs @@ -0,0 +1,33 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all map icon styles. +/// See https://www.open.mp/docs/scripting/resources/mapiconstyles. +public enum MapIconType +{ + /// Displays in the player's local are. + Local = 0, + + /// Displays always. + Global = 1, + + /// Displays in the player's local area and has a checkpoint marker. + LocalCheckPoint = 2, + + /// Displays always and has a checkpoint marker. + GlobalCheckPoint = 3 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/NetworkStats.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/NetworkStats.cs new file mode 100644 index 00000000..febf1156 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/NetworkStats.cs @@ -0,0 +1,113 @@ +namespace SampSharp.Entities.SAMP; + +/// +/// Represents the network statistics of a player. +/// +/// The open.mp stats this value represents. +public readonly struct NetworkStats(OpenMp.Core.Api.NetworkStats stats) +{ + /// + /// Gets the connection start time. + /// + public int ConnectionStartTime => (int)stats.ConnectionStartTime; + + /// + /// Gets the number of messages in the send buffer. + /// + public int MessageSendBuffer => (int)stats.MessageSendBuffer; + + /// + /// Gets the number of messages sent. + /// + public int MessagesSent => (int)stats.MessagesSent; + + /// + /// Gets the total number of bytes sent. + /// + public int TotalBytesSent => (int)stats.TotalBytesSent; + + /// + /// Gets the number of acknowledgements sent. + /// + public int AcknowlegementsSent => (int)stats.AcknowlegementsSent; + + /// + /// Gets the number of acknowledgements pending. + /// + public int AcknowlegementsPending => (int)stats.AcknowlegementsPending; + + /// + /// Gets the number of messages on the resend queue. + /// + public int MessagesOnResendQueue => (int)stats.MessagesOnResendQueue; + + /// + /// Gets the number of message resends. + /// + public int MessageResends => (int)stats.MessageResends; + + /// + /// Gets the total number of bytes of messages resent. + /// + public int MessagesTotalBytesResent => (int)stats.MessagesTotalBytesResent; + + /// + /// Gets the packet loss percentage. + /// + public float Packetloss => stats.Packetloss; + + /// + /// Gets the number of messages received. + /// + public int MessagesReceived => (int)stats.MessagesReceived; + + /// + /// Gets the number of messages received per second. + /// + public int MessagesReceivedPerSecond => (int)stats.MessagesReceivedPerSecond; + + /// + /// Gets the total number of bytes received. + /// + public int BytesReceived => (int)stats.BytesReceived; + + /// + /// Gets the number of acknowledgements received. + /// + public int AcknowlegementsReceived => (int)stats.AcknowlegementsReceived; + + /// + /// Gets the number of duplicate acknowledgements received. + /// + public int DuplicateAcknowlegementsReceived => (int)stats.DuplicateAcknowlegementsReceived; + + /// + /// Gets the number of bits per second sent and received. + /// + public double BitsPerSecond => stats.BitsPerSecond; + + /// + /// Gets the number of bits per second sent. + /// + public double BpsSent => stats.BpsSent; + + /// + /// Gets the number of bits per second received. + /// + public double BpsReceived => stats.BpsReceived; + + /// + /// Gets a value indicating whether the connection is active. + /// + public bool IsActive => stats.IsActive; + + /// + /// Gets the connection mode/status. + /// + public ConnectionStatus ConnectMode => (ConnectionStatus)stats.ConnectMode; + + /// + /// Gets the connection elapsed time. + /// + public uint ConnectionElapsedTime => stats.ConnectionElapsedTime; +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialSize.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialSize.cs new file mode 100644 index 00000000..8bbc4dec --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialSize.cs @@ -0,0 +1,62 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all object material sizes. +public enum ObjectMaterialSize +{ + /// 32 x 32 + X32X32 = 10, + + /// 64 x 32 + X64X32 = 20, + + /// 64 x 64 + X64X64 = 30, + + /// 128 x 32 + X128X32 = 40, + + /// 128 x 64 + X128X64 = 50, + + /// 128 x 128 + X128X128 = 60, + + /// 256 x 32 + X256X32 = 70, + + /// 256 x 64 + X256X64 = 80, + + /// 256 x 128 + X256X128 = 90, + + /// 256 x 256 + X256X256 = 100, + + /// 512 x 64 + X512X64 = 110, + + /// 512 x 128 + X512X128 = 120, + + /// 512 x 256 + X512X256 = 130, + + /// 512 x 512 + X512X512 = 140 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialTextAlign.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialTextAlign.cs new file mode 100644 index 00000000..c15ed694 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectMaterialTextAlign.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all object material alignments. +public enum ObjectMaterialTextAlign +{ + /// Align left. + Left = 0, + + /// Align center. + Center = 1, + + /// Align right. + Right = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectType.cs new file mode 100644 index 00000000..56bf943c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ObjectType.cs @@ -0,0 +1,26 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all object types +public enum ObjectType +{ + /// Global object. + GlobalObject = 1, + + /// Player object. + PlayerObject = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PickupType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PickupType.cs new file mode 100644 index 00000000..5b68c9f0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PickupType.cs @@ -0,0 +1,74 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all pickup types +public enum PickupType +{ + /// + /// The pickup does not always display. If displayed, it can't be picked up and does not trigger OnPlayerPickUpPickup and it will stay after server + /// shutdown. + /// + ShowButNotPickupable = 0, + + /// + /// Exists always. Disables pickup scripts such as horseshoes and oysters to allow for scripted actions ONLY. Will trigger OnPlayerPickUpPickup every few + /// seconds. + /// + ScriptedActionsOnlyEveryFewSeconds = 1, + + /// Disappears after pickup, respawns after 30 seconds if the player is at a distance of at least 15 meters. + ShowNearAndRespawnWhenPickup = 2, + + /// Disappears after pickup, respawns after death. + ShowAndRespawnWhenDeath = 3, + + /// Disappears after 15 to 20 seconds. Respawns after death. + ShowTemporary1520AndRespawnWhenDeath = 4, + + /// Disappears after pickup, but has no effect. + ShowTillPickedUp = 8, + + /// Blows up a few seconds after being created (bombs?) + ShowAndExplode = 11, + + /// Blows up a few seconds after being created. + ShowAndExplode2 = 12, + + /// Invisible. Triggers checkpoint sound when picked up with a vehicle, but doesn't trigger OnPlayerPickUpPickup. + HiddenPlaySoundButNotPickupable = 13, + + /// Disappears after pickup, can only be picked up with a vehicle. Triggers checkpoint sound. + ShowAndPickupableWithVehicleWithSound = 14, + + /// Disappears after pickup, respawns after 30 seconds if the player is at a distance of at least 15 meters. + ShowNearAndRespawnWhenPickup2 = 15, + + /// Similar to type 1. Pressing Tab (KEY_ACTION) makes it disappear but the key press doesn't trigger OnPlayerPickUpPickup. + ScriptedActionsOnlyEveryFewSecondsButTabDisappear = 18, + + /// Disappears after pickup, but doesn't respawn. Makes "cash pickup" sound if picked up. + ShowAndNoRespawnAfterPickupWithSound = 19, + + /// + /// Similar to type 1. Disappears when you take a picture of it with the Camera weapon, which triggers "Snapshot # out of 0" message. Taking a picture + /// doesn't trigger OnPlayerPickUpPickup. + /// + ScriptedActionsOnlyEveryFewSecondsButCameraWillDestroy = 20, + + /// Disappears after pickup, respawns after death. + ShowAndRespawnWhenDeath2 = 22 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerClickSource.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerClickSource.cs new file mode 100644 index 00000000..f0783c52 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerClickSource.cs @@ -0,0 +1,23 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all player click sources. +public enum PlayerClickSource +{ + /// Clicked the player on the scoreboard. + Scoreboard = 0 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerMarkersMode.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerMarkersMode.cs new file mode 100644 index 00000000..cb9111de --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerMarkersMode.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all PlayerMarker modes. +public enum PlayerMarkersMode +{ + /// No makers. + Off = 0, + + /// All markers. + Global = 1, + + /// All markers within the streamed area. + Streamed = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerRecordingType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerRecordingType.cs new file mode 100644 index 00000000..093d6207 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerRecordingType.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all PlayerRecording types. +public enum PlayerRecordingType +{ + /// Nothing. + None = 0, + + /// As a driver. + Driver = 1, + + /// As a pedestrian + OnFoot = 2 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerState.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerState.cs new file mode 100644 index 00000000..7a4e4dd7 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/PlayerState.cs @@ -0,0 +1,50 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all player states. +public enum PlayerState +{ + /// No state. + None = 0, + + /// Player is on foot. + OnFoot = 1, + + /// Player is driving a vehicle. + Driving = 2, + + /// Player is in a vehicle as passenger. + Passenger = 3, + + /// Player is exiting a vehicle. + ExitVehicle = 4, + + /// Player is entering a vehicle as driver. + EnterVehicleDriver = 5, + + /// Player is entering a vehicle as passenger. + EnterVehiclePassenger = 6, + + /// Player is dead. + Wasted = 7, + + /// Player has spawned. + Spawned = 8, + + /// Player is spectating. + Spectating = 9 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ServerVarType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ServerVarType.cs new file mode 100644 index 00000000..988baf50 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ServerVarType.cs @@ -0,0 +1,32 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all server variable types. +public enum ServerVarType +{ + /// Var does not exist. + None = 0, + + /// Var as an integer. + Int = 1, + + /// Var is a string. + String = 2, + + /// Var is a float. + Float = 3 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ShopName.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ShopName.cs new file mode 100644 index 00000000..a93f7af2 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/ShopName.cs @@ -0,0 +1,48 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all shop names. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public static class ShopName +{ + /// The Well Stacked Pizza Co. + public const string PizzaStack = "FDPIZA"; + + /// Burger Shot. + public const string BurgerShot = "FDBURG"; + + /// Cluckin' Bell. + public const string CluckinBell = "FDCHICK"; + + /// Ammunation 1. + public const string Ammunation1 = "AMMUN1"; + + /// Ammunation 2. + public const string Ammunation2 = "AMMUN2"; + + /// Ammunation 3. + public const string Ammunation3 = "AMMUN3"; + + /// Ammunation 4. + public const string Ammunation4 = "AMMUN4"; + + /// Ammunation 5. + public const string Ammunation5 = "AMMUN5"; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpecialAction.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpecialAction.cs new file mode 100644 index 00000000..aca35007 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpecialAction.cs @@ -0,0 +1,84 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all special actions. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum SpecialAction +{ + /// Nothing. + None = 0, + + /// Player is ducking. + Duck = 1, + + /// Player is using a jetpack. + UseJetpack = 2, + + /// Player is entering a vehicle. + EnterVehicle = 3, + + /// Player is leaving a vehicle. + ExitVehicle = 4, + + /// Player is dancing. (Style 1) + Dance1 = 5, + + /// Player is dancing. (Style 2) + Dance2 = 6, + + /// Player is dancing. (Style 3) + Dance3 = 7, + + /// Player is dancing. (Style 4) + Dance4 = 8, + + /// Player is holding his hands up. + HandsUp = 10, + + /// Player is using a cellphone. + UseCellphone = 11, + + /// Player is sitting. + Sitting = 12, + + /// Player stops using a cellphone. + StopUseCellphone = 13, + + /// Player is drinking a beer. + DrinkBeer = 20, + + /// Player is smoking a cigarette. + SmokeCiggy = 21, + + /// Player is drinking whine. + DrinkWine = 22, + + /// Player is drinking sprunk. + DrinkSprunk = 23, + + /// Player is cuffed. + Cuffed = 24, + + /// PLayer is carrying. + Carry = 25, + + /// Player is pissing. + Pissing = 68 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpectateMode.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpectateMode.cs new file mode 100644 index 00000000..29a5a292 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/SpectateMode.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all spectating modes. +public enum SpectateMode +{ + /// Normal spectating mode. + Normal = 1, + + /// Player is looking from a fixed point. + Fixed = 2, + + /// Attached to the side. + Side = 3 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawAlignment.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawAlignment.cs new file mode 100644 index 00000000..bab18109 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawAlignment.cs @@ -0,0 +1,34 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all text draw alignments. +public enum TextDrawAlignment +{ + /// + /// Default alignment. + /// + Default = 0, + + /// Align left. + Left = 1, + + /// Align center. + Center = 2, + + /// Align right. + Right = 3 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawFont.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawFont.cs new file mode 100644 index 00000000..3045f196 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/TextDrawFont.cs @@ -0,0 +1,42 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all fonts. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum TextDrawFont +{ + /// Font 0, also known as Diploma. + Diploma = 0, + + /// Font 1, also known as Normal. + Normal = 1, + + /// Font 2, also known as Slim. + Slim = 2, + + /// Font 3, also known as Pricedown. + Pricedown = 3, + + /// Font used to draw sprites. + DrawSprite = 4, + + /// Font used to draw model previews. + PreviewModel = 5 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleCategory.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleCategory.cs new file mode 100644 index 00000000..193e6ac0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleCategory.cs @@ -0,0 +1,72 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all vehicle categories. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum VehicleCategory +{ + /// Airplanes. + Airplane = 1, + + /// Helicopters. + Helicopter = 2, + + /// Bikes. + Bike = 3, + + /// Convertibles. + Convertible = 4, + + /// Industrials. + Industrial = 5, + + /// Lowriders. + Lowrider = 6, + + /// Off Road. + OffRoad = 7, + + /// Public Service Vehicles. + PublicService = 8, + + /// Saloons. + Saloon = 9, + + /// Sport Vehicles. + Sport = 10, + + /// Station Wagons. + Station = 11, + + /// Boats. + Boat = 12, + + /// Trailers. + Trailer = 13, + + /// Unique Vehicles. + Unique = 14, + + /// RC Vehicles. + RemoteControl = 15, + + /// Train trailers. + TrainTrailer = 16 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelInfoType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelInfoType.cs new file mode 100644 index 00000000..5636895d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelInfoType.cs @@ -0,0 +1,47 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all vehicle model information types. +public enum VehicleModelInfoType +{ + /// Vehicle size + Size = 1, + + /// Position of the front seat. (calculated from the center of the vehicle) + FrontSeat = 2, + + /// Position of the rear seat. (calculated from the center of the vehicle) + RearSeat = 3, + + /// Position of the fuel cap. (calculated from the center of the vehicle) + PetrolCap = 4, + + /// Position of the front wheels. (calculated from the center of the vehicle) + WheelsFront = 5, + + /// Position of the rear wheels. (calculated from the center of the vehicle) + WheelsRear = 6, + + /// Position of the middle wheels, applies to vehicles with 3 axes. (calculated from the center of the vehicle) + WheelsMiddle = 7, + + /// Height of the front bumper. (calculated from the center of the vehicle) + FrontBumperZ = 8, + + /// Height of the rear bumper. (calculated from the center of the vehicle) + RearBumperZ = 9 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelType.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelType.cs new file mode 100644 index 00000000..d34ef88d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleModelType.cs @@ -0,0 +1,662 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all vehicle models. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum VehicleModelType +{ + // ReSharper disable InconsistentNaming + /// Model of a Landstalker. + Landstalker = 400, + + /// Model of a Bravura. + Bravura = 401, + + /// Model of a Buffalo. + Buffalo = 402, + + /// Model of a Linerunner. + Linerunner, + + /// Model of a Perenniel. + Perenniel, + + /// Model of a Sentinel. + Sentinel, + + /// Model of a Dumper. + Dumper, + + /// Model of a Firetruck. + Firetruck, + + /// Model of a Trashmaster. + Trashmaster, + + /// Model of a Stretch. + Stretch, + + /// Model of a Manana. + Manana, + + /// Model of a Infernus. + Infernus, + + /// Model of a Voodoo. + Voodoo, + + /// Model of a Pony. + Pony, + + /// Model of a Mule. + Mule, + + /// Model of a Cheetah. + Cheetah, + + /// Model of a Ambulance. + Ambulance, + + /// Model of a Leviathan. + Leviathan, + + /// Model of a Moonbeam. + Moonbeam, + + /// Model of a Esperanto. + Esperanto, + + /// Model of a Taxi. + Taxi, + + /// Model of a Washington. + Washington, + + /// Model of a Bobcat. + Bobcat, + + /// Model of a Mr Whoopee. + MrWhoopee, + + /// Model of a BF Injection. + BFInjection, + + /// Model of a Hunter. + Hunter, + + /// Model of a Premier. + Premier, + + /// Model of a Enforcer. + Enforcer, + + /// Model of a Securicar. + Securicar, + + /// Model of a Banshee. + Banshee, + + /// Model of a Predator. + Predator, + + /// Model of a Bus. + Bus, + + /// Model of a Rhino. + Rhino, + + /// Model of a Barracks. + Barracks, + + /// Model of a Hotknife. + Hotknife, + + /// Model of a Article Trailer. + ArticleTrailer, + + /// Model of a Previon. + Previon, + + /// Model of a Coach. + Coach, + + /// Model of a Cabbie. + Cabbie, + + /// Model of a Stallion. + Stallion, + + /// Model of a Rumpo. + Rumpo, + + /// Model of a RC Bandit. + RCBandit, + + /// Model of a Romero. + Romero, + + /// Model of a Packer. + Packer, + + /// Model of a Monster. + Monster, + + /// Model of a Admiral. + Admiral, + + /// Model of a Squallo. + Squallo, + + /// Model of a Seasparrow. + Seasparrow, + + /// Model of a Pizzaboy. + Pizzaboy, + + /// Model of a Tram. + Tram, + + /// Model of a Article Trailer 2. + ArticleTrailer2, + + /// Model of a Turismo. + Turismo, + + /// Model of a Speeder. + Speeder, + + /// Model of a Reefer. + Reefer, + + /// Model of a Tropic. + Tropic, + + /// Model of a Flatbed. + Flatbed, + + /// Model of a Yankee. + Yankee, + + /// Model of a Caddy. + Caddy, + + /// Model of a Solair. + Solair, + + /// Model of a Topfun Van Berkleys RC. + TopfunVanBerkleysRC, + + /// Model of a Skimmer. + Skimmer, + + /// Model of a PCJ-600. + PCJ600, + + /// Model of a Faggio. + Faggio, + + /// Model of a Freeway. + Freeway, + + /// Model of a RC Baron. + RCBaron, + + /// Model of a RC Raider. + RCRaider, + + /// Model of a Glendale. + Glendale, + + /// Model of a Oceanic. + Oceanic, + + /// Model of a Sanchez. + Sanchez, + + /// Model of a Sparrow. + Sparrow, + + /// Model of a Patriot. + Patriot, + + /// Model of a Quad. + Quad, + + /// Model of a Coastguard. + Coastguard, + + /// Model of a Dinghy. + Dinghy, + + /// Model of a Hermes. + Hermes, + + /// Model of a Sabre. + Sabre, + + /// Model of a Rustler. + Rustler, + + /// Model of a ZR350. + ZR350, + + /// Model of a Walton. + Walton, + + /// Model of a Regina. + Regina, + + /// Model of a Comet. + Comet, + + /// Model of a BMX. + BMX, + + /// Model of a Burrito. + Burrito, + + /// Model of a Camper. + Camper, + + /// Model of a Marquis. + Marquis, + + /// Model of a Baggage. + Baggage, + + /// Model of a Dozer. + Dozer, + + /// Model of a Maverick. + Maverick, + + /// Model of a SAN News Maverick. + SANNewsMaverick, + + /// Model of a Rancher. + Rancher, + + /// Model of a FBI Rancher. + FBIRancher, + + /// Model of a Virgo. + Virgo, + + /// Model of a Greenwood. + Greenwood, + + /// Model of a Jetmax. + Jetmax, + + /// Model of a Hotring Racer. + HotringRacer, + + /// Model of a Sandking. + Sandking, + + /// Model of a Blista Compact. + BlistaCompact, + + /// Model of a Police Maverick. + PoliceMaverick, + + /// Model of a Boxville. + Boxville, + + /// Model of a Benson. + Benson, + + /// Model of a Mesa. + Mesa, + + /// Model of a RC Goblin. + RCGoblin, + + /// Model of a Hotring Racer 2. + HotringRacer2, + + /// Model of a Hotring Racer 3. + HotringRacer3, + + /// Model of a Bloodring Banger. + BloodringBanger, + + /// Model of a Rancher 2. + Rancher2, + + /// Model of a Super GT. + SuperGT, + + /// Model of a Elegant. + Elegant, + + /// Model of a Journey. + Journey, + + /// Model of a Bike. + Bike, + + /// Model of a Mountain Bike. + MountainBike, + + /// Model of a Beagle. + Beagle, + + /// Model of a Cropduster. + Cropduster, + + /// Model of a Stuntplane. + Stuntplane, + + /// Model of a Tanker. + Tanker, + + /// Model of a Roadtrain. + Roadtrain, + + /// Model of a Nebula. + Nebula, + + /// Model of a Majestic. + Majestic, + + /// Model of a Buccaneer. + Buccaneer, + + /// Model of a Shamal. + Shamal, + + /// Model of a Hydra. + Hydra, + + /// Model of a FCR-900. + FCR900, + + /// Model of a NRG-500. + NRG500, + + /// Model of a HPV1000. + HPV1000, + + /// Model of a Cement Truck. + CementTruck, + + /// Model of a Towtruck. + Towtruck, + + /// Model of a Fortune. + Fortune, + + /// Model of a Cadrona. + Cadrona, + + /// Model of a FBITruck. + FBITruck, + + /// Model of a Willard. + Willard, + + /// Model of a Forklift. + Forklift, + + /// Model of a Tractor. + Tractor, + + /// Model of a Combine Harvester. + CombineHarvester, + + /// Model of a Feltzer. + Feltzer, + + /// Model of a Remington. + Remington, + + /// Model of a Slamvan. + Slamvan, + + /// Model of a Blade. + Blade, + + /// Model of a Freight Train. + FreightTrain, + + /// Model of a Brownstreak Train. + BrownstreakTrain, + + /// Model of a Vortex. + Vortex, + + /// Model of a Vincent. + Vincent, + + /// Model of a Bullet. + Bullet, + + /// Model of a Clover. + Clover, + + /// Model of a Sadler. + Sadler, + + /// Model of a Firetruck LA. + FiretruckLA, + + /// Model of a Hustler. + Hustler, + + /// Model of a Intruder. + Intruder, + + /// Model of a Primo. + Primo, + + /// Model of a Cargobob. + Cargobob, + + /// Model of a Tampa. + Tampa, + + /// Model of a Sunrise. + Sunrise, + + /// Model of a Merit. + Merit, + + /// Model of a Utility Van. + UtilityVan, + + /// Model of a Nevada. + Nevada, + + /// Model of a Yosemite. + Yosemite, + + /// Model of a Windsor. + Windsor, + + /// Model of a Monster A. + MonsterA, + + /// Model of a Monster B. + MonsterB, + + /// Model of a Uranus. + Uranus, + + /// Model of a Jester. + Jester, + + /// Model of a Sultan. + Sultan, + + /// Model of a Stratum. + Stratum, + + /// Model of a Elegy. + Elegy, + + /// Model of a Raindance. + Raindance, + + /// Model of a RC Tiger. + RCTiger, + + /// Model of a Flash. + Flash, + + /// Model of a Tahoma. + Tahoma, + + /// Model of a Savanna. + Savanna, + + /// Model of a Bandito. + Bandito, + + /// Model of a Freight Flat Trailer Train. + FreightFlatTrailerTrain, + + /// Model of a Streak Trailer Train. + StreakTrailerTrain, + + /// Model of a Kart. + Kart, + + /// Model of a Mower. + Mower, + + /// Model of a Dune. + Dune, + + /// Model of a Sweeper. + Sweeper, + + /// Model of a Broadway. + Broadway, + + /// Model of a Tornado. + Tornado, + + /// Model of a AT400. + AT400, + + /// Model of a DFT-30. + DFT30, + + /// Model of a Huntley. + Huntley, + + /// Model of a Stafford. + Stafford, + + /// Model of a BF400. + BF400, + + /// Model of a Newsvan. + Newsvan, + + /// Model of a Tug. + Tug, + + /// Model of a Petrol Trailer. + PetrolTrailer, + + /// Model of a Emperor. + Emperor, + + /// Model of a Wayfarer. + Wayfarer, + + /// Model of a Euros. + Euros, + + /// Model of a Hotdog. + Hotdog, + + /// Model of a Club. + Club, + + /// Model of a Freight Box Trailer Train. + FreightBoxTrailerTrain, + + /// Model of a Article Trailer 3. + ArticleTrailer3, + + /// Model of a Andromada. + Andromada, + + /// Model of a Dodo. + Dodo, + + /// Model of a RC Cam. + RCCam, + + /// Model of a Launch. + Launch, + + /// Model of a Police Car LSPD. + PoliceCarLSPD, + + /// Model of a Police Car SFPD. + PoliceCarSFPD, + + /// Model of a Police Car LVPD. + PoliceCarLVPD, + + /// Model of a Police Ranger. + PoliceRanger, + + /// Model of a Picador. + Picador, + + /// Model of a SWAT Truck. + SWAT, + + /// Model of a Alpha. + Alpha, + + /// Model of a Phoenix. + Phoenix, + + /// Model of a Damaged Glendale. + GlendaleShit, + + /// Model of a Damaged Sadler. + SadlerShit, + + /// Model of a Baggage Trailer A. + BaggageTrailerA, + + /// Model of a Baggage Trailer B. + BaggageTrailerB, + + /// Model of a Tug Stairs Trailer. + TugStairsTrailer, + + /// Model of a Boxville 2. + Boxville2, + + /// Model of a Farm Trailer. + FarmTrailer, + + /// Model of a Utility Trailer. + UtilityTrailer + // ReSharper restore InconsistentNaming +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameterValue.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameterValue.cs new file mode 100644 index 00000000..6eca9902 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameterValue.cs @@ -0,0 +1,14 @@ +namespace SampSharp.Entities.SAMP; + +/// Contains all vehicle param values. +public enum VehicleParameterValue : sbyte +{ + /// Value has not been set. + Unset = -1, + + /// Value is off. + Off = 0, + + /// Value is on. + On = 1 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameters.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameters.cs new file mode 100644 index 00000000..defe0482 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/VehicleParameters.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// +/// Represents a set of vehicle parameters. +/// +/// A value indicating whether the vehicle engine is on. +/// A value indicating whether the vehicle lights are on. +/// A value indicating whether the vehicle alarm is on. +/// A value indicating whether the vehicle doors are locked. +/// A value indicating whether the vehicle bonnet is open. +/// A value indicating whether the vehicle boot is open. +/// A value indicating whether the vehicle has an objective indicator. +/// A value indicating whether the vehicle siren is on. +/// A value indicating whether the driver's door is open. +/// A value indicating whether the passenger's door is open. +/// A value indicating whether the back left door is open. +/// A value indicating whether the back right door is open. +/// A value indicating whether the driver's window is open. +/// A value indicating whether the passenger's window is open. +/// A value indicating whether the back left window is open. +/// A value indicating whether the back right window is open. +[StructLayout(LayoutKind.Sequential)] +public record struct VehicleParameters( + VehicleParameterValue Engine, + VehicleParameterValue Lights, + VehicleParameterValue Alarm, + VehicleParameterValue Doors, + VehicleParameterValue Bonnet, + VehicleParameterValue Boot, + VehicleParameterValue Objective, + VehicleParameterValue Siren, + VehicleParameterValue DoorDriver, + VehicleParameterValue DoorPassenger, + VehicleParameterValue DoorBackLeft, + VehicleParameterValue DoorBackRight, + VehicleParameterValue WindowDriver, + VehicleParameterValue WindowPassenger, + VehicleParameterValue WindowBackLeft, + VehicleParameterValue WindowBackRight) +{ + internal static VehicleParameters FromParams(ref VehicleParams value) + { + return Unsafe.As(ref value); + } + + [Pure] + internal VehicleParams ToParams() + { + return Unsafe.As(ref this); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Weapon.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Weapon.cs new file mode 100644 index 00000000..862573c3 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/Weapon.cs @@ -0,0 +1,183 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all weapons. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum Weapon +{ + /// No weapon. + None = 0, + + /// Brask knuckles. + Brassknuckle = 1, + + /// Golf Club. + Golfclub = 2, + + /// Nitestick. + Nitestick = 3, + + /// Knife. + Knife = 4, + + /// Baseball Bat. + Bat = 5, + + /// Shovel. + Shovel = 6, + + /// Pool Cue. + Poolstick = 7, + + /// Katana (sword). + Katana = 8, + + /// Chainsaw + Chainsaw = 9, + + /// Double-ended Dildo. + DoubleEndedDildo = 10, + + /// Dildo. + Dildo = 11, + + /// Vibrator. + Vibrator = 12, + + /// Silver Vibrator. + SilverVibrator = 13, + + /// Flowers. + Flower = 14, + + /// Cane. + Cane = 15, + + /// Grenade. + Grenade = 16, + + /// Tear Gas. + Teargas = 17, + + /// Molotov Cockail. + Moltov = 18, + + /// 9mm Colt45 pistol. + Colt45 = 22, + + /// Silenced 9mm Colt45 pistol. + Silenced = 23, + + /// DesertEagle pistol. + Deagle = 24, + + /// Shotgun. + Shotgun = 25, + + /// Sawnoff Shotgun. + Sawedoff = 26, + + /// Combat Shotgun. + CombatShotgun = 27, + + /// Micro SMG/Uzi. + Uzi = 28, + + /// MP5 + MP5 = 29, + + /// AK-47. + AK47 = 30, + + /// M4. + M4 = 31, + + /// Tec-9. + Tec9 = 32, + + /// Country Rifle. + Rifle = 33, + + /// Sniper Rifle. + Sniper = 34, + + /// RPG. + RocketLauncher = 35, + + /// Heat Seeking Rocket. + HeatSeeker = 36, + + /// Flamethrower. + FlameThrower = 37, + + /// Minigun. + Minigun = 38, + + /// Satchel Charge. + SatchelCharge = 39, + + /// Detonator. + Detonator = 40, + + /// Spraycan. + Spraycan = 41, + + /// Fire Extinguisher. + FireExtinguisher = 42, + + /// Camera. + Camera = 43, + + /// Night Vision Goggles. + NightVisionGoggles = 44, + + /// Thermal Goggles. + ThermalGoggles = 45, + + /// Parachute. + Parachute = 46, + + /// ??? + FakePistol = 47, + + /// Vehicle + Vehicle = 49, + + /// Helicopter Blades. + HelicopterBlades = 50, + + /// Explosion. + Explosion = 51, + + /// Drowned. + Drown = 53, + + /// Collision. + Collision = 54, + + /// Connected. (use with deathmessages) + Connect = 200, + + /// Disconnected. (use with deathmessages) + Disconnect = 201, + + /// Self inflicted death. + Suicide = 255 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponSkill.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponSkill.cs new file mode 100644 index 00000000..af953fc1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponSkill.cs @@ -0,0 +1,57 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics.CodeAnalysis; + +namespace SampSharp.Entities.SAMP; + +/// Contains all weapon skills types. +[SuppressMessage("ReSharper", "IdentifierTypo")] +[SuppressMessage("ReSharper", "CommentTypo")] +public enum WeaponSkill +{ + /// Pistol skills. + Pistol = 0, + + /// Silenced pistol skills. + PistolSilenced = 1, + + /// Desert eagle skills. + DesertEagle = 2, + + /// Shotgun skills. + Shotgun = 3, + + /// Sawn-off shotgun skills. + SawnoffShotgun = 4, + + /// Combat shotgun skills. + Spas12Shotgun = 5, + + /// Micro uzi skills. + MicroUzi = 6, + + /// MP5 skills. + MP5 = 7, + + /// AK47 skills. + AK47 = 8, + + /// M4 skills. + M4 = 9, + + /// Sniper rifle skills. + SniperRifle = 10 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponState.cs b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponState.cs new file mode 100644 index 00000000..14de6a42 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Definitions/WeaponState.cs @@ -0,0 +1,35 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains all weapon states. +public enum WeaponState +{ + /// Unknown state. + Unknown = -1, + + /// Weapon is out of bullets. + NoBullets = 0, + + /// Last bullet in gun. + LastBullet = 1, + + /// More bullets in gun. + MoreBullets = 2, + + /// Weapon is reloading. + Reloading = 3 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResponse.cs new file mode 100644 index 00000000..ab7edd39 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResponse.cs @@ -0,0 +1,29 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Contains types of responses to a dialog. +public enum DialogResponse +{ + /// The player responded by selecting the left button. + LeftButton = 1, + + /// The player responded by selecting the right button or closing the dialog. + RightButtonOrCancel = 0, + + /// The player disconnected while the dialog was still open. + Disconnected = -1 +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResult.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResult.cs new file mode 100644 index 00000000..4bee6636 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogResult.cs @@ -0,0 +1,40 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a raw response value to a dialog. +public struct DialogResult +{ + /// Initializes a new instance of the struct. + /// The way in which the player has responded to the dialog. + /// The index of item selected by the player in the dialog. + /// The text entered by the player in the dialog. + public DialogResult(DialogResponse response, int listItem, string? inputText) + { + Response = response; + ListItem = listItem; + InputText = inputText; + } + + /// Gets the way in which the player has responded to the dialog. + public DialogResponse Response { get; } + + /// Gets the index of item selected by the player in the dialog. + public int ListItem { get; } + + /// Gets the text entered by the player in the dialog. + public string? InputText { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogRowCollection.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogRowCollection.cs new file mode 100644 index 00000000..8c18df3c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogRowCollection.cs @@ -0,0 +1,81 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; + +namespace SampSharp.Entities.SAMP; + +/// Represents a collection of dialog rows. +/// The type of the dialog rows. +public class DialogRowCollection : IEnumerable where T : IDialogRow +{ + private readonly List _rows = []; + + /// + public string RawText => string.Join("\n", _rows.Select(r => r.RawText)); + + /// Gets the number of rows in the list. + public int Count => _rows.Count; + + /// + public IEnumerator GetEnumerator() + { + return _rows.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// Adds the specified row to the list. + /// The row to add. + /// Thrown if is null. + public virtual void Add(T row) + { + if (row == null) + { + throw new ArgumentNullException(nameof(row)); + } + + _rows.Add(row); + } + + /// Gets the row at the specified . + /// The index. + /// The row at the specified index. + /// Thrown if the is out of the range of the list. + public virtual T Get(int index) + { + if (index < 0 || index >= _rows.Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + return _rows[index]; + } + + /// Removes all rows from the list. + public virtual void Clear() + { + _rows.Clear(); + } + + /// Removes the first occurrence of the specified row to the list. + public virtual bool Remove(T row) + { + return _rows.Remove(row); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogService.cs new file mode 100644 index 00000000..8d0d8d5f --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogService.cs @@ -0,0 +1,73 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class DialogService : IDialogService +{ + private readonly IEntityManager _entityManager; + + /// The dialog ID used by the dialog service. + public const int DialogId = 10000; + + public DialogService(IEntityManager entityManager) + { + _entityManager = entityManager; + } + + public void Show(Player player, IDialog dialog, Action responseHandler) where TResponse : struct + { + ArgumentNullException.ThrowIfNull(player); + ArgumentNullException.ThrowIfNull(dialog); + ArgumentNullException.ThrowIfNull(responseHandler); + + _entityManager.Destroy(player); + + IPlayer native = player; + if (!native.TryQueryExtension(out var dialogData)) + { + throw new InvalidOperationException("Missing dialog data"); + } + + dialogData.Show(native, DialogId, (SampSharp.OpenMp.Core.Api.DialogStyle)dialog.Style, dialog.Caption ?? string.Empty, dialog.Content ?? string.Empty, + dialog.Button1 ?? string.Empty, dialog.Button2 ?? string.Empty); + + _entityManager.AddComponent(player, dialog, (Action)Handler); + return; + + void Handler(DialogResult result) + { + // Destroy the visible dialog component before the dialog handler might replace it with a new dialog. + _entityManager.Destroy(player); + + var translated = dialog.Translate(result); + responseHandler?.Invoke(translated); + } + } + + public Task ShowAsync(Player player, IDialog dialog) where TResponse : struct + { + ArgumentNullException.ThrowIfNull(player); + ArgumentNullException.ThrowIfNull(dialog); + + var taskCompletionSource = new TaskCompletionSource(); + + Show(player, dialog, taskCompletionSource.SetResult); + + return taskCompletionSource.Task; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogSystem.cs new file mode 100644 index 00000000..fb667eb6 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/DialogSystem.cs @@ -0,0 +1,63 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class DialogSystem : DisposableSystem, IPlayerDialogEventHandler +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IEventDispatcher _eventDispatcher; + + public DialogSystem(IOmpEntityProvider entityProvider, IEventDispatcher eventDispatcher, SampSharpEnvironment environment) + { + _entityProvider = entityProvider; + _eventDispatcher = eventDispatcher; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + [Event] + public void OnPlayerDisconnect(VisibleDialog player, DisconnectReason _) + { + player.ResponseReceived = true; + player.Handler(new DialogResult(DialogResponse.Disconnected, 0, null)); + player.Destroy(); + } + + [Event] + public void OnDialogResponse(VisibleDialog player, int dialogId, DialogResponse response, int listItem, string inputText) + { + if (dialogId != DialogService.DialogId) + { + return; // Prevent dialog hacks + } + + player.ResponseReceived = true; + player.Handler(new DialogResult(response, listItem, inputText)); + + if (!player.IsComponentAlive) + { + return; + } + + player.Destroy(); + } + + public void OnDialogResponse(IPlayer player, int dialogId, SampSharp.OpenMp.Core.Api.DialogResponse response, int listItem, string inputText) + { + _eventDispatcher.Invoke("OnDialogResponse", _entityProvider.GetEntity(player), dialogId, response, listItem, inputText); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialog.cs new file mode 100644 index 00000000..5e700114 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialog.cs @@ -0,0 +1,45 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Provides the functionality of a dialog definition with a specialized dialog response struct of type . +/// The type of the specialized response struct. +public interface IDialog : IDialog where TResponse : struct +{ + /// Translates the specified to a specialized response of type . + /// The dialog result to translate. + /// The translated dialog result. + TResponse Translate(DialogResult dialogResult); +} + +/// Provides the functionality of a dialog definition. +public interface IDialog +{ + /// Gets the dialog style. + DialogStyle Style { get; } + + /// Gets the caption of the dialog. + string? Caption { get; } + + /// Gets the content of the dialog. + string? Content { get; } + + /// Gets the text on the left button. + string? Button1 { get; } + + /// Gets the text on the right button. + string? Button2 { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogRow.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogRow.cs new file mode 100644 index 00000000..2dffd79b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogRow.cs @@ -0,0 +1,23 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Provides the functionality of a dialog row +public interface IDialogRow +{ + /// Gets the raw text as it is sent to SA:MP. + string RawText { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogService.cs new file mode 100644 index 00000000..6edd1ca4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/IDialogService.cs @@ -0,0 +1,34 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Provides the functionality for showing dialogs to players. +public interface IDialogService +{ + /// Shows the specified to the . + /// The type of the response returned by the dialog. + /// The player to show the dialog to. + /// The dialog to show to the player. + /// A handler for the dialog response. + void Show(Player player, IDialog dialog, Action responseHandler) where TResponse : struct; + + /// Shows the specified to the . + /// The type of the response. + /// The player to show the dialog to. + /// The dialog to show to the player. + /// The dialog response. + Task ShowAsync(Player player, IDialog dialog) where TResponse : struct; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialog.cs new file mode 100644 index 00000000..7fe8a81a --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialog.cs @@ -0,0 +1,67 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a dialog with an input field. +public class InputDialog : IDialog +{ + /// + /// Initializes a new instance of the class. + /// + /// The caption of the input dialog. + /// The content of the input dialog. + /// The left button text of the input dialog. + /// The right button text of the input dialog. + public InputDialog(string? caption, string? content, string? button1, string? button2 = null) + { + Caption = caption; + Content = content; + Button1 = button1; + Button2 = button2; + } + + /// + /// Initializes a new instance of the class. + /// + public InputDialog() + { + } + + /// + public InputDialogResponse Translate(DialogResult dialogResult) + { + return new InputDialogResponse(dialogResult.Response, dialogResult.InputText); + } + + DialogStyle IDialog.Style => IsPassword + ? DialogStyle.Password + : DialogStyle.Input; + + /// Gets or sets a value indicating whether the input is a password. + public bool IsPassword { get; set; } + + /// Gets or sets the text above the input field in this dialog. + public string? Content { get; set; } + + /// Gets or sets the caption of this input dialog. + public string? Caption { get; set; } + + /// Gets or sets the text on the left button of this input dialog. + public string? Button1 { get; set; } + + /// Gets or sets the text on the right button of this input dialog. If the value is , the right button is hidden. + public string? Button2 { get; set; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialogResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialogResponse.cs new file mode 100644 index 00000000..7bd5d4d8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/InputDialogResponse.cs @@ -0,0 +1,35 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a response to a . +public struct InputDialogResponse +{ + /// Initializes a new instance of the struct. + /// The way in which the player has responded to the dialog. + /// The text the player has entered into the input field. + public InputDialogResponse(DialogResponse response, string? inputText) + { + Response = response; + InputText = inputText; + } + + /// Gets the way in which the player has responded to the dialog. + public DialogResponse Response { get; } + + /// Gets the text the player has entered into the input field. + public string? InputText { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialog.cs new file mode 100644 index 00000000..0af004e1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialog.cs @@ -0,0 +1,105 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; + +namespace SampSharp.Entities.SAMP; + +/// Represents a dialog with a list of selectable rows. +/// The caption. +/// The text on the left button. +/// The text on the right button. If the value is , the right button is hidden. +public class ListDialog(string caption, string? button1, string? button2 = null) : IDialog, IEnumerable +{ + + /// Gets the rows of this dialog. + public ListDialogRowCollection Rows { get; } = []; + + DialogStyle IDialog.Style => DialogStyle.List; + + string IDialog.Content => Rows.RawText; + + /// Gets or sets the caption of this list dialog. + public string? Caption { get; set; } = caption; + + /// Gets or sets the text on the left button of this list dialog. + public string? Button1 { get; set; } = button1; + + /// Gets or sets the text on the right button of this list dialog. If the value is , the right button is hidden. + public string? Button2 { get; set; } = button2; + + ListDialogResponse IDialog.Translate(DialogResult dialogResult) + { + var index = dialogResult.ListItem; + ListDialogRow? item; + if (dialogResult.Response == DialogResponse.Disconnected || Rows.Count <= index || index < 0) + { + index = -1; + item = null; + } + else + { + item = Rows.Get(index); + } + + return new ListDialogResponse(dialogResult.Response, index, item); + } + + /// + /// Returns an enumerator that iterates through the rows of this list dialog. + /// + /// The enumerator that can be used to iterate through the rows of this list dialog. + public IEnumerator GetEnumerator() + { + return Rows.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// Adds a row to the list with the specified . + /// The text of the row to add. + /// Thrown if is null. + public void Add(string text) + { + ArgumentNullException.ThrowIfNull(text); + + Rows.Add(text); + } + + /// Adds a row to the list with the specified and . + /// The text of the row to add. + /// + /// The tag of the row to add. The tag can be used so associate data with this row which can be used retrieved when the user responds to the + /// dialog. + /// + /// Thrown if is null. + public void Add(string text, object? tag) + { + ArgumentNullException.ThrowIfNull(text); + + Rows.Add(new ListDialogRow(text) { Tag = tag }); + } + + /// Adds the specified row to the list. + /// The row to add. + /// Thrown if is null. + public void Add(ListDialogRow row) + { + Rows.Add(row); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogResponse.cs new file mode 100644 index 00000000..2cb7dd5e --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogResponse.cs @@ -0,0 +1,40 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a response to a . +public struct ListDialogResponse +{ + /// Initializes a new instance of the struct. + /// The way in which the player has responded to the dialog. + /// The index of the item the player selected in the dialog. + /// The item the player selected in the dialog. + public ListDialogResponse(DialogResponse response, int itemIndex, ListDialogRow? item) + { + Response = response; + ItemIndex = itemIndex; + Item = item; + } + + /// Gets the way in which the player has responded to the dialog. + public DialogResponse Response { get; } + + /// Gets the index of the item the player selected in the dialog. + public int ItemIndex { get; } + + /// Gets the item the player selected in the dialog. + public ListDialogRow? Item { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRow.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRow.cs new file mode 100644 index 00000000..4abfab0f --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRow.cs @@ -0,0 +1,35 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a row in a +public class ListDialogRow : IDialogRow +{ + /// Initializes a new instance of the class. + /// The text. + public ListDialogRow(string text) + { + Text = text ?? throw new ArgumentNullException(nameof(text)); + } + + /// Gets the text. + public string Text { get; } + + /// Gets or sets the tag. The tag can be used so associate data with this row which can be used retrieved when the user responds to the dialog. + public object? Tag { get; set; } + + string IDialogRow.RawText => Text; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRowCollection.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRowCollection.cs new file mode 100644 index 00000000..aa7b0c30 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/ListDialogRowCollection.cs @@ -0,0 +1,30 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a collection of dialog rows of type . +public class ListDialogRowCollection : DialogRowCollection +{ + /// Adds a row to the list with the specified . + /// The text of the row to add. + /// Thrown if is null. + public void Add(string text) + { + ArgumentNullException.ThrowIfNull(text); + + Add(new ListDialogRow(text)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialog.cs new file mode 100644 index 00000000..0291a106 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialog.cs @@ -0,0 +1,53 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a dialog with a message. +public class MessageDialog : IDialog +{ + /// Initializes a new instance of the class. + /// The caption. + /// The content. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + public MessageDialog(string? caption, string? content, string? button1, string? button2 = null) + { + Caption = caption; + Content = content; + Button1 = button1; + Button2 = button2; + } + + DialogStyle IDialog.Style => DialogStyle.MessageBox; + + + /// Gets or sets the caption of this message dialog. + public string? Caption { get; set; } + + /// Gets or sets the content of this message dialog. + public string? Content { get; set; } + + /// Gets or sets the text on the left button of this message dialog. + public string? Button1 { get; set; } + + /// Gets or sets the text on the right button of this message dialog. If the value is , the right button is hidden. + public string? Button2 { get; set; } + + MessageDialogResponse IDialog.Translate(DialogResult dialogResult) + { + return new MessageDialogResponse(dialogResult.Response); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialogResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialogResponse.cs new file mode 100644 index 00000000..ea92f8a3 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/MessageDialogResponse.cs @@ -0,0 +1,30 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a response to a . +public struct MessageDialogResponse +{ + /// Initializes a new instance of the struct. + /// The way in which the player has responded to the dialog. + public MessageDialogResponse(DialogResponse response) + { + Response = response; + } + + /// Gets the way in which the player has responded to the dialog. + public DialogResponse Response { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialog.cs new file mode 100644 index 00000000..80ad06e2 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialog.cs @@ -0,0 +1,211 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections; + +namespace SampSharp.Entities.SAMP; + +/// Represents a dialog with a list of selectable rows with columns. +public class TablistDialog : IDialog, IEnumerable +{ + private TablistDialogRow? _header; + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The number of columns in this dialog. + public TablistDialog(string? caption, string? button1, string? button2, int columnCount) + { + if (columnCount < 1) + { + throw new ArgumentException("Dialog must contain at least 1 column.", nameof(columnCount)); + } + + Caption = caption; + Button1 = button1; + Button2 = button2; + ColumnCount = columnCount; + Rows = new TablistDialogRowCollection(columnCount); + } + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The column headers. + public TablistDialog(string? caption, string? button1, string? button2, string[] columnHeaders) + { + ArgumentNullException.ThrowIfNull(columnHeaders); + + if (columnHeaders.Length < 1) + { + throw new ArgumentException("Dialog must contain at least 1 column.", nameof(columnHeaders)); + } + + Caption = caption; + Button1 = button1; + Button2 = button2; + ColumnCount = columnHeaders.Length; + Rows = new TablistDialogRowCollection(columnHeaders.Length); + Header = new TablistDialogRow(columnHeaders); + } + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The first column header. + public TablistDialog(string? caption, string? button1, string? button2, string columnHeader1) : this(caption, button1, button2, [columnHeader1]) + { + } + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The first column header. + /// The second column header. + public TablistDialog(string? caption, string? button1, string? button2, string columnHeader1, string columnHeader2) : this(caption, button1, button2, [columnHeader1, columnHeader2 + ]) + { + } + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The first column header. + /// The second column header. + /// The third column header. + public TablistDialog(string? caption, string? button1, string? button2, string columnHeader1, string columnHeader2, string columnHeader3) : this(caption, + button1, button2, [columnHeader1, columnHeader2, columnHeader3]) + { + } + + /// Initializes a new instance of the class. + /// The caption. + /// The text on the left button. + /// The text on the right button. If the value is , the right button is hidden. + /// The first column header. + /// The second column header. + /// The third column header. + /// The third column header. + public TablistDialog(string caption, string button1, string button2, string columnHeader1, string columnHeader2, string columnHeader3, string columnHeader4) + : this(caption, button1, button2, [columnHeader1, columnHeader2, columnHeader3, columnHeader4]) + { + } + + /// Gets the rows of this dialog. + public TablistDialogRowCollection Rows { get; } + + /// Gets the number of columns in this tablist dialog. + public int ColumnCount { get; } + + /// Gets or sets the header of this tablist dialog. + public TablistDialogRow? Header + { + get => _header; + set + { + if (value != null && value.ColumnCount != ColumnCount) + { + throw new ArgumentException($"The row must contain exactly {ColumnCount} columns.", nameof(value)); + } + + _header = value; + } + } + + DialogStyle IDialog.Style => _header == null + ? DialogStyle.Tablist + : DialogStyle.TablistHeaders; + + string IDialog.Content => _header == null + ? Rows.RawText + : $"{((IDialogRow)_header).RawText}\n{Rows.RawText}"; + + /// Gets or sets the caption of this tablist dialog. + public string? Caption { get; set; } + + /// Gets or sets the text on the left button of this tablist dialog. + public string? Button1 { get; set; } + + /// Gets or sets the text on the right button of this tablist dialog. If the value is , the right button is hidden. + public string? Button2 { get; set; } + + TablistDialogResponse IDialog.Translate(DialogResult dialogResult) + { + var index = dialogResult.ListItem; + TablistDialogRow? item; + if (dialogResult.Response == DialogResponse.Disconnected || Rows.Count <= index || index < 0) + { + index = -1; + item = null; + } + else + { + item = Rows.Get(index); + } + + return new TablistDialogResponse(dialogResult.Response, index, item); + } + + /// + /// Returns an enumerator that iterates through the rows of this tablist dialog. + /// + /// The enumerator that can be used to iterate through the rows of this tablist dialog. + public IEnumerator GetEnumerator() + { + return Rows.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// Adds a row to the list with the specified . + /// The columns of the row to add. + /// Thrown if is null. + public void Add(params string[] columns) + { + ArgumentNullException.ThrowIfNull(columns); + + Rows.Add(columns); + } + + /// Adds a row to the list with the specified and . + /// The columns of the row to add. + /// + /// The tag of the row to add. The tag can be used so associate data with this row which can be used retrieved when the user responds to the + /// dialog. + /// + /// Thrown if is null. + public void Add(string[] columns, object tag) + { + ArgumentNullException.ThrowIfNull(columns); + + Rows.Add(new TablistDialogRow(columns) { Tag = tag }); + } + + /// Adds the specified row to the list. + /// The row to add. + /// Thrown if is null. + public void Add(TablistDialogRow row) + { + Rows.Add(row); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogResponse.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogResponse.cs new file mode 100644 index 00000000..4fe93e0d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogResponse.cs @@ -0,0 +1,40 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a response to a . +public struct TablistDialogResponse +{ + /// Initializes a new instance of the struct. + /// The way in which the player has responded to the dialog. + /// The index of the item the player selected in the dialog. + /// The item the player selected in the dialog. + public TablistDialogResponse(DialogResponse response, int itemIndex, TablistDialogRow? item) + { + Response = response; + ItemIndex = itemIndex; + Item = item; + } + + /// Gets the way in which the player has responded to the dialog. + public DialogResponse Response { get; } + + /// Gets the index of the item the player selected in the dialog. + public int ItemIndex { get; } + + /// Gets the item the player selected in the dialog. + public TablistDialogRow? Item { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRow.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRow.cs new file mode 100644 index 00000000..464f40cd --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRow.cs @@ -0,0 +1,38 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a row in a +public class TablistDialogRow : IDialogRow +{ + /// Initializes a new instance of the class. + /// The columns of the row. + public TablistDialogRow(params string[] columns) + { + Columns = columns ?? throw new ArgumentNullException(nameof(columns)); + } + + /// Gets the columns of this tablist dialog row. + public string[] Columns { get; } + + /// Gets the number of columns in this row. + public int ColumnCount => Columns.Length; + + /// Gets or sets the tag. The tag can be used so associate data with this row which can be used retrieved when the user responds to the dialog. + public object? Tag { get; set; } + + string IDialogRow.RawText => string.Join("\t", Columns); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRowCollection.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRowCollection.cs new file mode 100644 index 00000000..ab2d7f9c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/TablistDialogRowCollection.cs @@ -0,0 +1,55 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities.SAMP; + +/// Represents a collection of dialog rows of type . +public class TablistDialogRowCollection : DialogRowCollection +{ + private readonly int _columnCount; + + /// Initializes a new instance of the class. + /// The required number of columns. + public TablistDialogRowCollection(int columnCount) + { + _columnCount = columnCount; + } + + /// Adds a row to the list with the specified . + /// The columns of the row to add. + /// Thrown if is null. + public void Add(params string[] columns) + { + ArgumentNullException.ThrowIfNull(columns); + + Add(new TablistDialogRow(columns)); + } + + /// Adds the specified row to the list. + /// The row to add. + /// Thrown if is null. + /// Thrown if the row does not contain the required number of columns. + public override void Add(TablistDialogRow row) + { + ArgumentNullException.ThrowIfNull(row); + + if (row.ColumnCount != _columnCount) + { + throw new ArgumentException($"The row must contain exactly {_columnCount} columns.", nameof(row)); + } + + base.Add(row); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/VisibleDialog.cs b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/VisibleDialog.cs new file mode 100644 index 00000000..eb2846e7 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Dialogs/VisibleDialog.cs @@ -0,0 +1,60 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +/// A component which contains the data of the currently visible dialog. +/// +/// Initializes a new instance of the class. +/// The open.mp dialog shown to the player. +/// The response handler. +public class VisibleDialog(IDialog dialog, Action handler) : Component +{ + + /// Gets the visible dialog. + public IDialog Dialog { get; } = dialog ?? throw new ArgumentNullException(nameof(dialog)); + + /// Gets the response handler for the dialog. + public Action Handler { get; } = handler ?? throw new ArgumentNullException(nameof(handler)); + + /// Gets or sets a value indicating whether a response has been received. + public bool ResponseReceived { get; set; } + + /// + protected override void OnDestroyComponent() + { + if (ResponseReceived) + { + return; + } + + var player = GetComponent(); + + if (player == null) + { + return; + } + ResponseReceived = true; + Handler(new DialogResult(DialogResponse.RightButtonOrCancel, 0, null)); + + IPlayer native = player; + if (native.TryQueryExtension(out var dialogData)) + { + dialogData.Hide(native); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/ServiceCollectionSampExtensions.cs b/src/SampSharp.OpenMp.Entities/SAMP/ServiceCollectionSampExtensions.cs new file mode 100644 index 00000000..f9a9c455 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/ServiceCollectionSampExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities.SAMP; + +internal static class ServiceCollectionSampExtensions +{ + public static IServiceCollection AddSamp(this IServiceCollection services) + { + return services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem() + .AddSystem(); + } +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/IOmpEntityProvider.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/IOmpEntityProvider.cs new file mode 100644 index 00000000..96bc45af --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/IOmpEntityProvider.cs @@ -0,0 +1,304 @@ +using SampSharp.OpenMp.Core.Api; +using INPC = SampSharp.OpenMp.Core.Api.INPC; + +namespace SampSharp.Entities.SAMP; + +/// +/// Provides methods for getting ECS entities/components for open.mp entities. For entities created through +/// SampSharp.Entities, the existing entities and components are returned. For foreign entities (entities +/// created +/// through other scripts or open.mp components) new ECS entities and components are created and returned where +/// possible. +/// +public interface IOmpEntityProvider +{ + /// + /// Gets the entity for the specified actor. + /// + /// The actor to get the entity for. + /// The actor entity. + EntityId GetEntity(IActor actor); + + /// + /// Gets the entity for the specified NPC. + /// + /// The NPC to get the entity for. + /// The NPC entity. + EntityId GetEntity(INPC npc); + + /// + /// Gets the entity for the specified gang zone. + /// + /// The gang zone to get the entity for. + /// The gang zone entity. + EntityId GetEntity(IGangZone gangZone); + + /// + /// Gets the entity for the specified menu. + /// + /// The menu to get the entity for. + /// The menu entity. + EntityId GetEntity(IMenu menu); + + /// + /// Gets the entity for the specified object. + /// + /// The object to get the entity for. + /// The object entity. + EntityId GetEntity(IObject @object); + + /// + /// Gets the entity for the specified pickup. + /// + /// The pickup to get the entity for. + /// The pickup entity. + EntityId GetEntity(IPickup pickup); + + /// + /// Gets the entity for the specified player. + /// + /// The player to get the entity for. + /// The player entity. + EntityId GetEntity(IPlayer player); + + /// + /// Gets the entity for the specified player object. + /// + /// The owner of the player object. + /// The player object to get the entity for. + /// The player object entity. + EntityId GetEntity(IPlayerObject playerObject, IPlayer player = default); + + /// + /// Gets the entity for the specified player text draw. + /// + /// The owner of the player text draw. + /// The player text draw to get the entity for. + /// The player text draw entity. + EntityId GetEntity(IPlayerTextDraw playerTextDraw, IPlayer player = default); + + /// + /// Gets the entity for the specified player text label. + /// + /// The owner of the player text label. + /// The player text label to get the entity for. + /// The player text label entity. + EntityId GetEntity(IPlayerTextLabel playerTextLabel, IPlayer player = default); + + /// + /// Gets the entity for the specified text draw. + /// + /// The text draw to get the entity for. + /// The text draw entity. + EntityId GetEntity(ITextDraw textDraw); + + /// + /// Gets the entity for the specified text label. + /// + /// The text label to get the entity for. + /// The text label entity. + EntityId GetEntity(ITextLabel textLabel); + + /// + /// Gets the entity for the specified vehicle. + /// + /// The vehicle to get the entity for. + /// The vehicle entity. + EntityId GetEntity(IVehicle vehicle); + + /// + /// Gets the component for the specified actor. + /// + /// The actor to get the component for. + /// The actor component. + Actor? GetComponent(IActor actor); + + /// + /// Gets the component for the specified NPC. + /// + /// The NPC to get the component for. + /// The NPC component. + Npc? GetComponent(INPC npc); + + /// + /// Gets the component for the specified gang zone. + /// + /// The gang zone to get the component for. + /// The gang zone component. + GangZone? GetComponent(IGangZone gangZone); + + /// + /// Gets the component for the specified menu. + /// + /// The menu to get the component for. + /// The menu component. + Menu? GetComponent(IMenu menu); + + /// + /// Gets the component for the specified object. + /// + /// The object to get the component for. + /// The object component. + GlobalObject? GetComponent(IObject @object); + + /// + /// Gets the component for the specified pickup. + /// + /// The pickup to get the component for. + /// The pickup component. + Pickup? GetComponent(IPickup pickup); + + /// + /// Gets the component for the specified player. + /// + /// The player to get the component for. + /// The player component. + Player? GetComponent(IPlayer player); + + /// + /// Gets the component for the specified player object. + /// + /// The owner of the player object. + /// The player object to get the component for. + /// The player object component. + PlayerObject? GetComponent(IPlayerObject playerObject, IPlayer player = default); + + /// + /// Gets the component for the specified player text draw. + /// + /// The owner of the player text draw. + /// The player text draw to get the component for. + /// The player text draw component. + PlayerTextDraw? GetComponent(IPlayerTextDraw playerTextDraw, IPlayer player = default); + + /// + /// Gets the component for the specified player text label. + /// + /// The owner of the player text label. + /// The player text label to get the component for. + /// The player text label component. + PlayerTextLabel? GetComponent(IPlayerTextLabel playerTextLabel, IPlayer player = default); + + /// + /// Gets the component for the specified text draw. + /// + /// The text draw to get the component for. + /// The text draw component. + TextDraw? GetComponent(ITextDraw textDraw); + + /// + /// Gets the component for the specified text label. + /// + /// The text label to get the component for. + /// The text label component. + TextLabel? GetComponent(ITextLabel textLabel); + + /// + /// Gets the component for the specified vehicle. + /// + /// The vehicle to get the component for. + /// The vehicle component. + Vehicle? GetComponent(IVehicle vehicle); + + /// + /// Gets the actor with the specified identifier. + /// + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + Actor? GetActor(int id); + + /// + /// Gets the NPC with the specified identifier. + /// + /// The identifier of the NPC. + /// The NPC with the specified identifier or if no NPC could be found. + Npc? GetNpc(int id); + + /// + /// Creates a new server-controlled NPC with the given name and returns its ECS component. + /// You still need to call for it to appear in the world. + /// + /// The NPC name (must follow the same rules as normal player names). + /// The created NPC component, or if creation failed. + Npc? CreateNpc(string name); + + /// + /// Gets the gang zone with the specified identifier. + /// + /// The identifier of the gang zone. + /// The gang zone with the specified identifier or if no gang zone could be found. + GangZone? GetGangZone(int id); + + /// + /// Gets the menu with the specified identifier. + /// + /// The identifier of the menu. + /// The menu with the specified identifier or if no menu could be found. + Menu? GetMenu(int id); + + /// + /// Gets the object with the specified identifier. + /// + /// The identifier of the object. + /// The object with the specified identifier or if no object could be found. + GlobalObject? GetObject(int id); + + /// + /// Gets the pickup with the specified identifier. + /// + /// The identifier of the pickup. + /// The pickup with the specified identifier or if no pickup could be found. + Pickup? GetPickup(int id); + + /// + /// Gets the player with the specified identifier. + /// + /// The identifier of the player. + /// The player with the specified identifier or if no player could be found. + Player? GetPlayer(int id); + + /// + /// Gets the player object with the specified identifier. + /// + /// The owner of the player object. + /// The identifier of the player object. + /// The player object with the specified identifier or if no player object could be found. + PlayerObject? GetPlayerObject(IPlayer player, int id); + + /// + /// Gets the actor with the specified identifier. + /// + /// The owner of the player text draw. + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + PlayerTextDraw? GetPlayerTextDraw(IPlayer player, int id); + + /// + /// Gets the actor with the specified identifier. + /// + /// The owner of the player text label. + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + PlayerTextLabel? GetPlayerTextLabel(IPlayer player, int id); + + /// + /// Gets the actor with the specified identifier. + /// + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + TextDraw? GetTextDraw(int id); + + /// + /// Gets the actor with the specified identifier. + /// + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + TextLabel? GetTextLabel(int id); + + /// + /// Gets the actor with the specified identifier. + /// + /// The identifier of the actor. + /// The actor with the specified identifier or if no actor could be found. + Vehicle? GetVehicle(int id); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/IServerService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/IServerService.cs new file mode 100644 index 00000000..34f284a4 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/IServerService.cs @@ -0,0 +1,187 @@ +using System.Numerics; + +namespace SampSharp.Entities.SAMP; + +/// Provides functionality for controlling the SA:MP server. +public interface IServerService +{ + /// Gets the highest actor identifier created on the server. + int ActorPoolSize { get; } + + /// Gets the maximum number of players that can join the server, as set by the server variable 'maxplayers' in server.cfg. + int MaxPlayers { get; } + + /// Gets the player actor identifier created on the server. + int PlayerPoolSize { get; } + + /// Returns the up time of the actual server (not the SA-MP server) in milliseconds. + /// The tick count will eventually overflow after roughly 24 days. + int TickCount { get; } + + /// Gets the tick rate of the server. + /// The tick rate is 0 if the server just started. + int TickRate { get; } + + /// Gets the vehicle actor identifier created on the server. + int VehiclePoolSize { get; } + + /// Adds a class to class selection. Classes are used so players may spawn with a skin of their choice. + /// The team for the player to spawn with. + /// The skin for the player to spawn with. + /// The position for the player to spawn at. + /// The angle of the player to spawn at. + /// The first weapon for the player to spawn with. + /// The amount of ammunition of the first weapon for the player to spawn with. + /// The second weapon for the player to spawn with. + /// The amount of ammunition of the second weapon for the player to spawn with. + /// The third weapon for the player to spawn with. + /// The amount of ammunition of the third weapon for the player to spawn with. + /// The identifier of the class which was added. + /// + /// The maximum class ID is 319 (starting from 0, so a total of 320 classes). When this limit is reached, any more classes that are added will replace ID + /// 319. + /// + int AddPlayerClass(int teamId, int modelId, Vector3 spawnPosition, float angle, Weapon weapon1 = Weapon.None, int weapon1Ammo = 0, + Weapon weapon2 = Weapon.None, int weapon2Ammo = 0, Weapon weapon3 = Weapon.None, int weapon3Ammo = 0); + + /// Adds a class to class selection. Classes are used so players may spawn with a skin of their choice. + /// The skin for the player to spawn with. + /// The position for the player to spawn at. + /// The angle of the player to spawn at. + /// The first weapon for the player to spawn with. + /// The amount of ammunition of the first weapon for the player to spawn with. + /// The second weapon for the player to spawn with. + /// The amount of ammunition of the second weapon for the player to spawn with. + /// The third weapon for the player to spawn with. + /// The amount of ammunition of the third weapon for the player to spawn with. + /// The identifier of the class which was added. + /// + /// The maximum class ID is 319 (starting from 0, so a total of 320 classes). When this limit is reached, any more classes that are added will replace ID + /// 319. + /// + int AddPlayerClass(int modelId, Vector3 spawnPosition, float angle, Weapon weapon1 = Weapon.None, int weapon1Ammo = 0, Weapon weapon2 = Weapon.None, + int weapon2Ammo = 0, Weapon weapon3 = Weapon.None, int weapon3Ammo = 0); + + /// + /// Blocks an IP address from further communication with the server for a set amount of time (with wildcards allowed). Players trying to connect to the + /// server with a blocked IP address will receive the generic "You are banned from this server." message. Players that are online on the specified IP before the + /// block will timeout after specific amount of seconds and, upon reconnect, will receive the same message. Effect takes place only when server is running (it is + /// not persistent). + /// + /// The ip address to block. + /// The time that the connection will be blocked for. Use a 0-length timespan for an indefinite block. + void BlockIpAddress(string ipAddress, TimeSpan time = default); + + /// Connect an NPC to the server. + /// The name the NPC should connect as. Must follow the same rules as normal player names. + /// The NPC script name that is located in the "npcmodes" folder (without the .amx extension). + void ConnectNpc(string name, string script); + + /// Disable all the interior entrances and exits in the game (the yellow arrows at doors). + /// + /// This function will only work if it has been used BEFORE a player connects (it is recommended to use it in OnGameModeInit). It will not remove a + /// connected player's markers. + /// + void DisableInteriorEnterExits(); + + /// + /// Enables or disables stunt bonuses for all players. If enabled, players will receive monetary rewards when performing a stunt in a vehicle (e.g. a + /// wheelie). + /// + /// if set to stunt bonuses are enabled. + void EnableStuntBonus(bool enable); + + /// Enable friendly fire for team vehicles. Players will be unable to damage teammates' vehicles. + void EnableVehicleFriendlyFire(); + + /// Ends the current game mode. + void GameModeExit(); + + /// Get the boolean value of a console variable. + /// The name of the variable. + /// The value of the specified console variable. + bool GetConsoleVarAsBool(string variableName); + + /// Gets the integer value of a console variable. + /// The name of the variable. + /// The value of the specified console variable. + int GetConsoleVarAsInt(string variableName); + + /// Gets the string value of a console variable. + /// The name of the variable. + /// The value of the specified console variable. + string? GetConsoleVarAsString(string variableName); + + /// + /// Set a radius limitation for the chat. Only players at a certain distance from the player will see their message in the chat. Also changes the distance + /// at which a player can see other players on the map at the same distance. + /// + /// The range in which players will be able to see chat. + void LimitGlobalChatRadius(float chatRadius); + + /// Set the player marker radius. + /// The radius that markers will show at. + void LimitPlayerMarkerRadius(float markerRadius); + + /// + /// Use this function before any player connects (OnGameModeInit) to tell all clients that the script will control vehicle engines and lights. This + /// prevents the game automatically turning the engine on/off when players enter/exit vehicles and headlights automatically coming on when it is dark. + /// + void ManualVehicleEngineAndLights(); + + /// Sends an RCON (Remote Console) command. + /// The RCON command to be executed. + void SendRconCommand(string command); + + /// Set the name of the game mode which appears in the server browser. + /// The game mode name to display. + void SetGameModeText(string text); + + /// Set the server name (as shown in the SA:MP server browser). + /// The new server name. + void SetServerName(string name); + + /// Set the map name (displayed in the query response's "map" field). + /// The new map name. + void SetMapName(string name); + + /// Set the server language (displayed in the query response's "lan" field). + /// The language identifier. + void SetLanguage(string language); + + /// Set the server website URL. + /// The website URL. + void SetWebsiteUrl(string url); + + /// Set the connect password. Pass or empty string to clear. + /// The password, or null/empty to disable password auth. + void SetServerPassword(string? password); + + /// Set the RCON admin password. Pass or empty string to clear. + /// The RCON password, or null/empty to disable. + void SetAdminPassword(string? password); + + /// Set the maximum distance to display the names of players. The default draw distance is 70.0. + /// The distance to set. + void SetNameTagDrawDistance(float distance = 70); + + /// Sets the world time (for all players) to a specific hour. + /// The hour to set. + void SetWorldTime(int hour); + + /// Shows the name tags. This function can only be used in OnGameModeInit. + /// if set to name tags are enabled; otherwise the tags are disabled. + void ShowNameTags(bool show); + + /// Toggles player markers (blips on the radar). This function can only be used in OnGameModeInit.. + /// The mode used for the markers. + void ShowPlayerMarkers(PlayerMarkersMode mode); + + /// Unblock an IP address that was previously blocked using . + /// The ip address to unblock. + void UnBlockIpAddress(string ipAddress); + + /// Uses standard player walking animation (animation of the CJ skin) instead of custom animations for every skin (e.g. skating for skater skins). + /// Only works when placed under OnGameModeInit. + void UsePlayerPedAnims(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/IVehicleInfoService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/IVehicleInfoService.cs new file mode 100644 index 00000000..3c6279fc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/IVehicleInfoService.cs @@ -0,0 +1,49 @@ +using System.Numerics; + +namespace SampSharp.Entities.SAMP; + +/// Provides functionality for getting information about vehicle models and components. +public interface IVehicleInfoService +{ + /// Gets the car mod type of the specified . + /// The identifier of the component. + /// The car mod type of the component. + /// Thrown if the specified is invalid. + CarModType GetComponentType(int componentId); + + /// Gets information of type specified by for the specified . + /// The model of the vehicle. + /// The type of information to get. + /// The information about the vehicle model. + Vector3 GetModelInfo(VehicleModelType vehicleModel, VehicleModelInfoType infoType); + + /// + /// Returns a value indicating whether the specified is valid for the specified . + /// + /// The model of the vehicle. + /// The component to check. + /// if the component is valid for the vehicle model; otherwise. + public bool IsValidComponentForVehicle(VehicleModelType vehicleModel, int componentId); + + /// + /// Gets a set of random colors for the specified as they would naturally appear in the game. + /// + /// The model of the vehicle. + /// The vehicle colors (color 1 - 4) + public (int, int, int, int) GetRandomVehicleColor(VehicleModelType vehicleModel); + + /// + /// Gets the representation of the specified . + /// + /// The vehicle color. + /// The alpha value of the result. + /// A value representing the specified vehicle color. + public Color GetColorFromVehicleColor(int vehicleColor, uint alpha = 0xff); + + /// + /// Gets the number of passenger seats in the specified . + /// + /// The model of the vehicle. + /// The number of passenger seats excluding the driver seat. + public int GetPassengerSeatCount(VehicleModelType vehicleModel); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/IWorldService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/IWorldService.cs new file mode 100644 index 00000000..78d96db9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/IWorldService.cs @@ -0,0 +1,228 @@ +using System.Numerics; +using JetBrains.Annotations; + +namespace SampSharp.Entities.SAMP; + + +/// Provides functionality for adding entities to and controlling the SA:MP world. +public interface IWorldService +{ + /// Gets or sets the gravity. + /// Thrown if value is not between -50.0 and 50.0. + float Gravity { get; set; } + + /// Creates a new actor in the world. + /// The model identifier. + /// The position of the actor. + /// The rotation of the actor. + /// The parent of the entity to be created. + /// The actor component of the newly created entity. + Actor CreateActor(int modelId, Vector3 position, float rotation, EntityId parent = default); + + /// Creates a vehicle in the world. + /// The model for the vehicle. + /// The coordinates for the vehicle. + /// The facing angle for the vehicle. + /// The primary color ID. + /// The secondary color ID. + /// The delay until the car is respawned without a driver in seconds. Using -1 will + /// prevent the vehicle from respawning. + /// If true, enables the vehicle to have a siren, providing the vehicle has a horn. + /// The parent of the entity to be created. + /// The created vehicle. + Vehicle CreateVehicle(VehicleModelType type, Vector3 position, float rotation, int color1, int color2, int respawnDelay = -1, bool addSiren = false, + EntityId parent = default); + + /// Creates a static vehicle in the world. + /// The model for the vehicle. + /// The coordinates for the vehicle. + /// The facing angle for the vehicle. + /// The primary color ID. + /// The secondary color ID. + /// The delay until the car is respawned without a driver in seconds. Using -1 will + /// prevent the vehicle from respawning. + /// If true, enables the vehicle to have a siren, providing the vehicle has a horn. + /// The parent of the entity to be created. + /// The created vehicle. + Vehicle CreateStaticVehicle(VehicleModelType type, Vector3 position, float rotation, int color1, int color2, int respawnDelay = -1, bool addSiren = false, + EntityId parent = default); + + /// Creates a gang zone in the world. + /// The minimum x. + /// The minimum y. + /// The maximum x. + /// The maximum y. + /// The parent of the entity to be created. + /// The created gang zone. + [Obsolete("Deprecated. Use CreateGangZone(Vector2, Vector2, EntityId) instead.")] + GangZone CreateGangZone(float minX, float minY, float maxX, float maxY, EntityId parent = default); + + /// Creates a gang zone in the world. + /// The minimum position. + /// The maximum position. + /// The parent of the entity to be created. + /// The created gang zone. + GangZone CreateGangZone(Vector2 min, Vector2 max, EntityId parent = default); + + /// Creates a pickup in the world. + /// The model of the pickup. + /// The pickup spawn type. + /// The position where the pickup should be spawned. + /// The virtual world ID of the pickup. Use -1 for all worlds. + /// The parent of the entity to be created. + /// The created pickup. + Pickup CreatePickup(int model, PickupType type, Vector3 position, int virtualWorld = -1, EntityId parent = default); + + /// + /// Adds a 'static' pickup to the world. These pickups support weapons, health, armor etc., with the ability to + /// function without scripting them (weapons/health/armor will be given automatically). + /// + /// The model of the pickup. + /// The pickup spawn type. + /// The position where the pickup should be spawned. + /// The virtual world ID of the pickup. Use -1 for all worlds. + /// The parent of the entity to be created. + /// The created pickup. + Pickup CreateStaticPickup(int model, PickupType type, Vector3 position, int virtualWorld = -1, EntityId parent = default); + + /// Creates an object in the world. + /// The model ID. + /// The position. + /// The rotation. + /// The draw distance. + /// The parent of the entity to be created. + /// The created object. + GlobalObject CreateObject(int modelId, Vector3 position, Vector3 rotation, float drawDistance = 0, EntityId parent = default); + + /// Creates a player object in the world. + /// The player. + /// The model ID. + /// The position. + /// The rotation. + /// The draw distance. + /// The parent of the entity to be created. + /// The created player object. + PlayerObject CreatePlayerObject(Player player, int modelId, Vector3 position, Vector3 rotation, float drawDistance = 0, EntityId parent = default); + + /// Creates a text label in the world. + /// The text. + /// The color. + /// The position. + /// The draw distance. + /// The virtual world. + /// if set to the line of sight is tested to decide whether the label is drawn. + /// The parent of the entity to be created. + /// The created text label. + TextLabel CreateTextLabel(string text, Color color, Vector3 position, float drawDistance, int virtualWorld = 0, bool testLos = true, + EntityId parent = default); + + /// Creates a player text label in the world. + /// The player. + /// The text. + /// The color. + /// The position. + /// The draw distance. + /// if set to the line of sight is tested to decide whether the label is drawn. + /// The parent of the entity to be created. + /// The created text label. + PlayerTextLabel CreatePlayerTextLabel(Player player, string text, Color color, Vector3 position, float drawDistance, bool testLos = true, EntityId parent = default); + + /// Creates a text draw in the world. + /// The position of the text draw. + /// The text of the text draw. + /// The parent of the entity to be created. + /// The created text draw. + TextDraw CreateTextDraw(Vector2 position, string text, EntityId parent = default); + + /// Creates the player text draw in the world. + /// The player. + /// The position of the text draw. + /// The text of the text draw. + /// The parent of the entity to be created. + /// The created player text draw. + PlayerTextDraw CreatePlayerTextDraw(Player player, Vector2 position, string text, EntityId parent = default); + + /// Creates the menu in this world. + /// The title of the menu. + /// The position of the menu. + /// Width of the left column. + /// Width of the right column or null if the menu should only have one column. + /// The parent of the entity to be created. + /// The created menu. + Menu CreateMenu(string title, Vector2 position, float col0Width, float? col1Width = null, EntityId parent = default); + + /// Allows camera collisions with newly created objects to be disabled by default. + /// A value indicating whether camera collision with new objects should be disabled. + void SetObjectsDefaultCameraCollision(bool disable); + + /// + /// This function sends a message to all players with a chosen color in the chat. The whole line in the chat box will be in the set color unless color + /// embedding is used. + /// + /// The color of the message. + /// The text that will be displayed. + void SendClientMessage(Color color, string message); + + /// + /// This function sends a message to all players with a chosen color in the chat. The whole line in the chat box will be in the set color unless color + /// embedding is used. + /// + /// The color of the message. + /// The composite format string of the text that will be displayed (max 144 characters). + /// An object array that contains zero or more objects to format. + [StringFormatMethod("messageFormat")] + void SendClientMessage(Color color, string messageFormat, params object[] args); + + /// + /// This function sends a message to all players in white in the chat. The whole line in the chat box will be in the set color unless color embedding is + /// used. + /// + /// The text that will be displayed. + void SendClientMessage(string message); + + /// + /// This function sends a message to all players in white in the chat. The whole line in the chat box will be in the set color unless color embedding is + /// used. + /// + /// The composite format string of the text that will be displayed (max 144 characters). + /// An object array that contains zero or more objects to format. + [StringFormatMethod("messageFormat")] + void SendClientMessage(string messageFormat, params object[] args); + + /// + /// Sends a message in the name the specified to all players. The message will appear in the chat box and can be seen by all + /// player. The line will start with the the sender's name in their color, followed by the in white. + /// + /// The player which has sent the message. + /// The message that will be sent. + void SendPlayerMessageToPlayer(Player sender, string message); + + /// Adds a death to the kill feed on the right-hand side of the screen of all players. + /// The player that killer the . + /// The player that has been killed. + /// The reason for this player's death. + void SendDeathMessage(Player killer, Player killee, Weapon weapon); + + /// Shows 'game text' (on-screen text) for a certain length of time for all players. + /// The text to be displayed. + /// The duration of the text being shown in milliseconds. + /// The style of text to be displayed. + [Obsolete("Use GameText(string, TimeSpan, int) instead.")] + void GameText(string text, int time, int style); + + /// Shows 'game text' (on-screen text) for a certain length of time for all players. + /// The text to be displayed. + /// The duration of the text being shown. + /// The style of text to be displayed. + void GameText(string text, TimeSpan time, int style); + + /// Creates an explosion for all players. + /// The position of the explosion. + /// The explosion type. + /// The radius of the explosion. + void CreateExplosion(Vector3 position, ExplosionType type, float radius); + + /// Set the weather for all players. + /// The weather to set. + void SetWeather(int weather); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/OmpEntityProvider.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/OmpEntityProvider.cs new file mode 100644 index 00000000..10728ec1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/OmpEntityProvider.cs @@ -0,0 +1,463 @@ +using SampSharp.OpenMp.Core.Api; +using INPC = SampSharp.OpenMp.Core.Api.INPC; +using INPCComponent = SampSharp.OpenMp.Core.Api.INPCComponent; + +namespace SampSharp.Entities.SAMP; + +internal class OmpEntityProvider(SampSharpEnvironment environment, IEntityManager entityManager) : IOmpEntityProvider +{ + private readonly IActorsComponent _actors = environment.Components.QueryComponent(); + private readonly IGangZonesComponent _gangZones = environment.Components.QueryComponent(); + private readonly IMenusComponent _menus = environment.Components.QueryComponent(); + private readonly INPCComponent _npcs = environment.Components.QueryComponent(); + private readonly IObjectsComponent _objects = environment.Components.QueryComponent(); + private readonly IPickupsComponent _pickups = environment.Components.QueryComponent(); + private readonly IPlayerPool _players = environment.Core.GetPlayers(); + private readonly ITextDrawsComponent _textDraws = environment.Components.QueryComponent(); + private readonly ITextLabelsComponent _textLabels = environment.Components.QueryComponent(); + private readonly IVehiclesComponent _vehicles = environment.Components.QueryComponent(); + + public EntityId GetEntity(IActor actor) + { + return GetComponent(actor)?.Entity ?? default; + } + + public EntityId GetEntity(INPC npc) + { + return GetComponent(npc)?.Entity ?? default; + } + + public EntityId GetEntity(IGangZone gangZone) + { + return GetComponent(gangZone)?.Entity ?? default; + } + + public EntityId GetEntity(IMenu menu) + { + return GetComponent(menu)?.Entity ?? default; + } + + public EntityId GetEntity(IObject @object) + { + return GetComponent(@object)?.Entity ?? default; + } + + public EntityId GetEntity(IPickup pickup) + { + return GetComponent(pickup)?.Entity ?? default; + } + + public EntityId GetEntity(IPlayer player) + { + return GetComponent(player)?.Entity ?? default; + } + + public EntityId GetEntity(IPlayerObject playerObject, IPlayer player = default) + { + return GetComponent(playerObject, player)?.Entity ?? default; + } + + public EntityId GetEntity(IPlayerTextDraw playerTextDraw, IPlayer player = default) + { + return GetComponent(playerTextDraw, player)?.Entity ?? default; + } + + public EntityId GetEntity(IPlayerTextLabel playerTextLabel, IPlayer player = default) + { + return GetComponent(playerTextLabel, player)?.Entity ?? default; + } + + public EntityId GetEntity(ITextDraw textDraw) + { + return GetComponent(textDraw)?.Entity ?? default; + } + + public EntityId GetEntity(ITextLabel textLabel) + { + return GetComponent(textLabel)?.Entity ?? default; + } + + public EntityId GetEntity(IVehicle vehicle) + { + return GetComponent(vehicle)?.Entity ?? default; + } + + public Actor? GetComponent(IActor actor) + { + if (actor == null) + { + return null; + } + + var ext = actor.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _actors, actor); + ext = new ComponentExtension(component); + actor.AddExtension(ext); + + return component; + } + + return (Actor)ext.Component; + } + + public Npc? GetComponent(INPC npc) + { + if (npc == null) + { + return null; + } + + var ext = npc.TryGetExtension(); + if (ext != null) + { + return (Npc)ext.Component; + } + + var component = entityManager.AddComponent(EntityId.NewEntityId(), _npcs, npc); + ext = new ComponentExtension(component); + npc.AddExtension(ext); + return component; + } + + public GangZone? GetComponent(IGangZone gangZone) + { + if (gangZone == null) + { + return null; + } + + var ext = gangZone.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _gangZones, gangZone); + ext = new ComponentExtension(component); + gangZone.AddExtension(ext); + + return component; + } + + return (GangZone)ext.Component; + } + + public Menu? GetComponent(IMenu menu) + { + if (menu == null) + { + return null; + } + + var ext = menu.TryGetExtension(); + if (ext == null) + { + // don't know the title of the menu (which cannot be retrieved through open.mp api) - cannot create a component for the foreign entity. + return null; + } + + return (Menu)ext.Component; + } + + public GlobalObject? GetComponent(IObject @object) + { + if (@object == null) + { + return null; + } + + var ext = @object.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _objects, @object); + ext = new ComponentExtension(component); + @object.AddExtension(ext); + + return component; + } + + return (GlobalObject)ext.Component; + } + + public Pickup? GetComponent(IPickup pickup) + { + if (pickup == null) + { + return null; + } + + var ext = pickup.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _pickups, pickup); + ext = new ComponentExtension(component); + pickup.AddExtension(ext); + + return component; + } + + return (Pickup)ext.Component; + } + + public Player? GetComponent(IPlayer player) + { + if (player == null) + { + return null; + } + + var ext = player.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), this, player); + ext = new ComponentExtension(component); + player.AddExtension(ext); + return component; + } + + return (Player)ext.Component; + } + + public PlayerObject? GetComponent(IPlayerObject playerObject, IPlayer player = default) + { + if (playerObject == null) + { + return null; + } + + var ext = playerObject.TryGetExtension(); + if (ext == null) + { + if (player == null) + { + // don't know for which player this object is created - cannot create a component for the foreign entity. + return null; + } + + if (!player.TryQueryExtension(out var data)) + { + return null; + } + + var component = entityManager.AddComponent(EntityId.NewEntityId(), data, playerObject); + ext = new ComponentExtension(component); + playerObject.AddExtension(ext); + return component; + } + + return (PlayerObject)ext.Component; + } + + public PlayerTextDraw? GetComponent(IPlayerTextDraw playerTextDraw, IPlayer player = default) + { + if (playerTextDraw == null) + { + return null; + } + + var ext = playerTextDraw.TryGetExtension(); + if (ext == null) + { + if (player == null) + { + // don't know for which player this text draw is created - cannot create a component for the foreign entity. + return null; + } + + if (!player.TryQueryExtension(out var data)) + { + return null; + } + + var component = entityManager.AddComponent(EntityId.NewEntityId(), data, playerTextDraw); + ext = new ComponentExtension(component); + playerTextDraw.AddExtension(ext); + return component; + } + + return (PlayerTextDraw)ext.Component; + } + + public PlayerTextLabel? GetComponent(IPlayerTextLabel playerTextLabel, IPlayer player = default) + { + if (playerTextLabel == null) + { + return null; + } + + var ext = playerTextLabel.TryGetExtension(); + if (ext == null) + { + if (player == null) + { + // don't know for which player this text label is created - cannot create a component for the foreign entity. + return null; + } + + if (!player.TryQueryExtension(out var data)) + { + return null; + } + + var component = + entityManager.AddComponent(EntityId.NewEntityId(), this, data, playerTextLabel); + ext = new ComponentExtension(component); + playerTextLabel.AddExtension(ext); + return component; + } + + return (PlayerTextLabel)ext.Component; + } + + public TextDraw? GetComponent(ITextDraw textDraw) + { + if (textDraw == null) + { + return null; + } + + var ext = textDraw.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _textDraws, textDraw); + ext = new ComponentExtension(component); + textDraw.AddExtension(ext); + + return component; + } + + return (TextDraw)ext.Component; + } + + public TextLabel? GetComponent(ITextLabel textLabel) + { + if (textLabel == null) + { + return null; + } + + var ext = textLabel.TryGetExtension(); + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), this, _textLabels, textLabel); + ext = new ComponentExtension(component); + textLabel.AddExtension(ext); + + return component; + } + + return (TextLabel)ext.Component; + } + + public Vehicle? GetComponent(IVehicle vehicle) + { + if (vehicle == null) + { + return null; + } + + var ext = vehicle.TryGetExtension(); + + if (ext == null) + { + var component = entityManager.AddComponent(EntityId.NewEntityId(), _vehicles, vehicle); + ext = new ComponentExtension(component); + vehicle.AddExtension(ext); + + return component; + } + + return (Vehicle)ext.Component; + } + + public Actor? GetActor(int id) + { + return GetComponent(_actors.AsPool().Get(id)); + } + + public Npc? GetNpc(int id) + { + if (!_npcs.HasValue) + { + return null; + } + + return GetComponent(_npcs.Get(id)); + } + + public Npc? CreateNpc(string name) + { + if (!_npcs.HasValue) + { + return null; + } + + return GetComponent(_npcs.Create(name)); + } + + public GangZone? GetGangZone(int id) + { + return GetComponent(_gangZones.AsPool().Get(id)); + } + + public Pickup? GetPickup(int id) + { + return GetComponent(_pickups.AsPool().Get(id)); + } + + public Player? GetPlayer(int id) + { + return GetComponent(_players.Get(id)); + } + + public PlayerObject? GetPlayerObject(IPlayer player, int id) + { + if (!player.TryQueryExtension(out var data)) + { + return null; + } + return GetComponent(data.Get(id), player); + } + + public PlayerTextDraw? GetPlayerTextDraw(IPlayer player, int id) + { + if (!player.TryQueryExtension(out var data)) + { + return null; + } + return GetComponent(data.Get(id), player); + } + + public PlayerTextLabel? GetPlayerTextLabel(IPlayer player, int id) + { + if (!player.TryQueryExtension(out var data)) + { + return null; + } + return GetComponent(data.Get(id), player); + } + + public TextDraw? GetTextDraw(int id) + { + return GetComponent(_textDraws.AsPool().Get(id)); + } + + public TextLabel? GetTextLabel(int id) + { + return GetComponent(_textLabels.AsPool().Get(id)); + } + + public Vehicle? GetVehicle(int id) + { + return GetComponent(_vehicles.AsPool().Get(id)); + } + + public GlobalObject? GetObject(int id) + { + return GetComponent(_objects.AsPool().Get(id)); + } + + public Menu? GetMenu(int id) + { + return GetComponent(_menus.AsPool().Get(id)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/ServerService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/ServerService.cs new file mode 100644 index 00000000..353ae864 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/ServerService.cs @@ -0,0 +1,392 @@ +using System.Numerics; +using Microsoft.Extensions.Logging; +using SampSharp.OpenMp.Core; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class ServerService : IServerService +{ + private readonly ILogger _logger; + private readonly IActorsComponent _actors; + private readonly IPlayerPool _players; + private readonly IConfig _config; + private readonly ICore _core; + private readonly IVehiclesComponent _vehicles; + private readonly IClassesComponent _classes; + private readonly IConsoleComponent _console; + + public ServerService(SampSharpEnvironment environment, ILogger logger) + { + _logger = logger; + _actors = environment.Components.QueryComponent(); + _config = environment.Core.GetConfig(); + _players = environment.Core.GetPlayers(); + _vehicles = environment.Components.QueryComponent(); + _classes = environment.Components.QueryComponent(); + _console = environment.Components.QueryComponent(); + _core = environment.Core; + } + + public int ActorPoolSize + { + get + { + var max = -1; + + foreach (var actor in _actors.AsPool()) + { + var id = actor.GetID(); + if (id > max) + { + max = id; + } + } + + return max; + } + } + + public int MaxPlayers => _config.GetInt("max_players").Value; + + public int PlayerPoolSize + { + get + { + var max = -1; + + foreach (var player in _players.Entries()) + { + var id = player.GetID(); + if (id > max) + { + max = id; + } + } + + return max; + } + } + + public int TickCount => (int)_core.GetTickCount(); + public int TickRate => (int)_core.TickRate(); + + public int VehiclePoolSize + { + get + { + var max = -1; + + foreach (var vehicle in _vehicles.AsPool()) + { + var id = vehicle.GetID(); + if (id > max) + { + max = id; + } + } + + return max; + } + } + + // TODO: convert classes to components + + public int AddPlayerClass(int teamId, int modelId, Vector3 spawnPosition, float angle, Weapon weapon1 = Weapon.None, int weapon1Ammo = 0, Weapon weapon2 = Weapon.None, + int weapon2Ammo = 0, Weapon weapon3 = Weapon.None, int weapon3Ammo = 0) + { + var slots = new WeaponSlotData[WeaponSlots.MAX_WEAPON_SLOTS]; + + slots[0] = new WeaponSlotData((byte)weapon1, weapon1Ammo); + slots[1] = new WeaponSlotData((byte)weapon2, weapon2Ammo); + slots[2] = new WeaponSlotData((byte)weapon3, weapon3Ammo); + + var weapons = new WeaponSlots(slots); + + var @class = _classes.Create(modelId, teamId, spawnPosition, angle, ref weapons); + + return @class.GetID(); + } + + public int AddPlayerClass(int modelId, Vector3 spawnPosition, float angle, Weapon weapon1 = Weapon.None, int weapon1Ammo = 0, Weapon weapon2 = Weapon.None, int weapon2Ammo = 0, + Weapon weapon3 = Weapon.None, int weapon3Ammo = 0) + { + return AddPlayerClass(OpenMpConstants.TEAM_NONE, modelId, spawnPosition, angle, weapon1, weapon1Ammo, weapon2, weapon2Ammo, weapon3, weapon3Ammo); + } + + public void BlockIpAddress(string ipAddress, TimeSpan time = default) + { + ArgumentNullException.ThrowIfNull(ipAddress); + + var entry = new BanEntry(ipAddress); + foreach (var network in _core.GetNetworks()) + { + network.Ban(entry, time); + } + } + + public void ConnectNpc(string name, string script) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(script); + + _core.ConnectBot(name, script); + } + + public void DisableInteriorEnterExits() + { + ref var fld = ref _core.GetConfig().GetBool("game.use_entry_exit_markers").Value; + fld = false; + } + + public void EnableStuntBonus(bool enable) + { + _core.UseStuntBonuses(enable); + } + + public void EnableVehicleFriendlyFire() + { + ref var fld = ref _core.GetConfig().GetBool("game.use_vehicle_friendly_fire").Value; + fld = false; + } + + public void GameModeExit() + { + SendRconCommand("gmx"); + } + + public bool GetConsoleVarAsBool(string variableName) + { + ArgumentNullException.ThrowIfNull(variableName); + + var res = _config.GetNameFromAlias(variableName); + + BlittableRef v0; + BlittableRef v1 = default; + if (!string.IsNullOrEmpty(res.Item2)) + { + if (res.Item1) + { + _logger.LogWarning("Deprecated console variable \"{old}\", use \"{new}\" instead.", variableName, res.Item2); + } + + v0 = _config.GetBool(res.Item2); + + if (!v0.HasValue) + { + v1 = _config.GetInt(res.Item2); + } + } + else + { + v0 = _config.GetBool(variableName); + + if (!v0.HasValue) + { + v1 = _config.GetInt(variableName); + } + } + + if (v0.HasValue) + { + return v0.Value; + } + + if (v1.HasValue) + { + _logger.LogWarning( "Integer console variable \"{name}\" retrieved as boolean.", variableName); + return v1.Value != 0; + } + + return false; + } + + public int GetConsoleVarAsInt(string variableName) + { + ArgumentNullException.ThrowIfNull(variableName); + + var res = _config.GetNameFromAlias(variableName); + + BlittableRef v0 = default; + BlittableRef v1; + if (!string.IsNullOrEmpty(res.Item2)) + { + if (res.Item1) + { + _logger.LogWarning("Deprecated console variable \"{old}\", use \"{new}\" instead.", variableName, res.Item2); + } + + v1 = _config.GetInt(res.Item2); + + + if (!v1.HasValue) + { + v0 = _config.GetBool(res.Item2); + } + } + else + { + v1 = _config.GetInt(variableName); + + if (!v1.HasValue) + { + v0 = _config.GetBool(variableName); + } + } + + if (v1.HasValue) + { + return v1.Value; + } + + if (v0.HasValue) + { + _logger.LogWarning( "Boolean console variable \"{name}\" retrieved as integer.", variableName); + return v0.Value ? 1 : 0; + } + + return 0; + } + + public string? GetConsoleVarAsString(string variableName) + { + ArgumentNullException.ThrowIfNull(variableName); + + var gm = variableName.StartsWith("gamemode"); + var res = _config.GetNameFromAlias(gm ? "gamemode" : variableName); + + if (!string.IsNullOrEmpty(res.Item2)) + { + if (res.Item1) + { + _logger.LogWarning("Deprecated console variable \"{old}\", use \"{new}\" instead.", variableName, res.Item2); + } + + if (gm) + { + if (int.TryParse(variableName[8..], out var num)) + { + var mainScripts = _config.GetStrings(res.Item2); + if (num < mainScripts.Length) + { + return mainScripts[num]; + } + } + } + else + { + return _config.GetString(res.Item2); + } + } + + return _config.GetString(variableName); + } + + public void LimitGlobalChatRadius(float chatRadius) + { + ref var use = ref _config.GetBool("game.use_chat_radius").Value; + use = true; + ref var radius = ref _config.GetFloat("game.chat_radius").Value; + radius = chatRadius; + } + + public void LimitPlayerMarkerRadius(float markerRadius) + { + ref var use = ref _config.GetBool("game.use_player_marker_draw_radius").Value; + use = true; + ref var radius = ref _config.GetFloat("game.player_marker_draw_radius").Value; + radius = markerRadius; + } + + public void ManualVehicleEngineAndLights() + { + ref var use = ref _config.GetBool("game.use_manual_engine_and_lights").Value; + use = true; + } + + public void SendRconCommand(string command) + { + ArgumentNullException.ThrowIfNull(command); + + var snd = new ConsoleCommandSenderData(SampSharp.OpenMp.Core.Api.ConsoleCommandSender.Console, 0); + _console.Send(command, ref snd); + } + + public void SetGameModeText(string text) + { + ArgumentNullException.ThrowIfNull(text); + _core.SetData(SettableCoreDataType.ModeText, text); + } + + public void SetServerName(string name) + { + ArgumentNullException.ThrowIfNull(name); + _core.SetData(SettableCoreDataType.ServerName, name); + } + + public void SetMapName(string name) + { + ArgumentNullException.ThrowIfNull(name); + _core.SetData(SettableCoreDataType.MapName, name); + } + + public void SetLanguage(string language) + { + ArgumentNullException.ThrowIfNull(language); + _core.SetData(SettableCoreDataType.Language, language); + } + + public void SetWebsiteUrl(string url) + { + ArgumentNullException.ThrowIfNull(url); + _core.SetData(SettableCoreDataType.URL, url); + } + + public void SetServerPassword(string? password) + { + _core.SetData(SettableCoreDataType.Password, password ?? string.Empty); + } + + public void SetAdminPassword(string? password) + { + _core.SetData(SettableCoreDataType.AdminPassword, password ?? string.Empty); + } + + public void SetNameTagDrawDistance(float distance = 70) + { + ref var fld = ref _config.GetFloat("game.nametag_draw_radius").Value; + fld = distance; + } + + public void SetWorldTime(int hour) + { + _core.SetWorldTime(TimeSpan.FromHours(hour)); + } + + public void ShowNameTags(bool show) + { + ref var fld = ref _config.GetBool("game.use_nametags").Value; + fld = show; + } + + public void ShowPlayerMarkers(PlayerMarkersMode mode) + { + ref var fld = ref _config.GetInt("game.player_marker_mode").Value; + fld = (int)mode; + } + + public void UnBlockIpAddress(string ipAddress) + { + var entry = new BanEntry(ipAddress); + foreach (var network in _core.GetNetworks()) + { + network.Unban(entry); + } + } + + public void UsePlayerPedAnims() + { + ref var fld = ref _config.GetBool("game.use_player_ped_anims").Value; + fld = true; + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/VehicleInfoService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/VehicleInfoService.cs new file mode 100644 index 00000000..dde0a576 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/VehicleInfoService.cs @@ -0,0 +1,39 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class VehicleInfoService : IVehicleInfoService +{ + public CarModType GetComponentType(int componentId) + { + return (CarModType)VehicleData.GetVehicleComponentSlot(componentId); + } + + public bool IsValidComponentForVehicle(VehicleModelType vehicleModel, int componentId) + { + return VehicleData.IsValidComponentForVehicleModel((int)vehicleModel, componentId); + } + + public Vector3 GetModelInfo(VehicleModelType vehicleModel, VehicleModelInfoType infoType) + { + VehicleData.GetVehicleModelInfo((int)vehicleModel, (SampSharp.OpenMp.Core.Api.VehicleModelInfoType)infoType, out var outInfo); + return outInfo; + } + + public (int, int, int, int) GetRandomVehicleColor(VehicleModelType vehicleModel) + { + VehicleData.GetRandomVehicleColour((int)vehicleModel, out var a, out var b, out var c, out var d); + return (a, b, c, d); + } + + public Color GetColorFromVehicleColor(int vehicleColor, uint alpha = 0xff) + { + return VehicleData.CarColourIndexToColour((int)vehicleColor, alpha); + } + + public int GetPassengerSeatCount(VehicleModelType vehicleModel) + { + return VehicleData.GetVehiclePassengerSeats((int)vehicleModel); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Services/WorldService.cs b/src/SampSharp.OpenMp.Entities/SAMP/Services/WorldService.cs new file mode 100644 index 00000000..421f16d1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Services/WorldService.cs @@ -0,0 +1,282 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class WorldService(SampSharpEnvironment environment, IEntityManager entityManager, IOmpEntityProvider entityProvider) : IWorldService +{ + private readonly ICore _core = environment.Core; + private readonly IPlayerPool _players = environment.Core.GetPlayers(); + private readonly IVehiclesComponent _vehicles = environment.Components.QueryComponent(); + private readonly IObjectsComponent _objects = environment.Components.QueryComponent(); + private readonly IActorsComponent _actors = environment.Components.QueryComponent(); + private readonly IGangZonesComponent _gangZones = environment.Components.QueryComponent(); + private readonly IMenusComponent _menus = environment.Components.QueryComponent(); + private readonly IPickupsComponent _pickups = environment.Components.QueryComponent(); + private readonly ITextDrawsComponent _textDraws = environment.Components.QueryComponent(); + private readonly ITextLabelsComponent _textLabels = environment.Components.QueryComponent(); + + public float Gravity + { + get => _core.GetGravity(); + set => _core.SetGravity(value); + } + + public Actor CreateActor(int modelId, Vector3 position, float rotation, EntityId parent = default) + { + var native = _actors.Create(modelId, position, rotation); + + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _actors, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public Vehicle CreateVehicle(VehicleModelType type, Vector3 position, float rotation, int color1, int color2, int respawnDelay = -1, bool addSiren = false, EntityId parent = default) + { + return CreateVehicle(false, type, position, rotation, color1, color2, respawnDelay, addSiren, parent); + } + + public Vehicle CreateStaticVehicle(VehicleModelType type, Vector3 position, float rotation, int color1, int color2, int respawnDelay = -1, bool addSiren = false, + EntityId parent = default) + { + return CreateVehicle(true, type, position, rotation, color1, color2, respawnDelay, addSiren, parent); + } + + public GangZone CreateGangZone(float minX, float minY, float maxX, float maxY, EntityId parent = default) + { + return CreateGangZone(new Vector2(minX, minY), new Vector2(maxX, maxY), parent); + } + + public GangZone CreateGangZone(Vector2 min, Vector2 max, EntityId parent = default) + { + var native = _gangZones.Create(new GangZonePos(min, max)); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _gangZones, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public Pickup CreatePickup(int model, PickupType type, Vector3 position, int virtualWorld = -1, EntityId parent = default) + { + var native = _pickups.Create(model, (byte)type, position, (uint)virtualWorld, false); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _pickups, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public Pickup CreateStaticPickup(int model, PickupType type, Vector3 position, int virtualWorld = -1, EntityId parent = default) + { + var native = _pickups.Create(model, (byte)type, position, (uint)virtualWorld, true); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _objects, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public GlobalObject CreateObject(int modelId, Vector3 position, Vector3 rotation, float drawDistance = 0, EntityId parent = default) + { + var native = _objects.Create(modelId, position, rotation, drawDistance); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _objects, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public PlayerObject CreatePlayerObject(Player player, int modelId, Vector3 position, Vector3 rotation, float drawDistance = 0, EntityId parent = default) + { + IPlayer nativePlayer = player; + if (!nativePlayer.TryQueryExtension(out var playerObjectData)) + { + throw new InvalidOperationException("Missing object data"); + } + + var native = playerObjectData.Create(modelId, position, rotation, drawDistance); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, playerObjectData, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public TextLabel CreateTextLabel(string text, Color color, Vector3 position, float drawDistance, int virtualWorld = 0, bool testLos = true, EntityId parent = default) + { + ArgumentNullException.ThrowIfNull(text); + + var native = _textLabels.Create(text, color, position, drawDistance, virtualWorld, testLos); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, entityProvider, _textLabels, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public PlayerTextLabel CreatePlayerTextLabel(Player player, string text, Color color, Vector3 position, float drawDistance, bool testLos = true, + EntityId parent = default) + { + ArgumentNullException.ThrowIfNull(player); + ArgumentNullException.ThrowIfNull(text); + + IPlayer nativePlayer = player; + if (!nativePlayer.TryQueryExtension(out var playerTextLabels)) + { + throw new InvalidOperationException("Missing text label data"); + } + + var native = playerTextLabels.Create(text, color, position, drawDistance, testLos); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, entityProvider, playerTextLabels, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public TextDraw CreateTextDraw(Vector2 position, string text, EntityId parent = default) + { + ArgumentNullException.ThrowIfNull(text); + + var native = _textDraws.Create(position, text); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _textDraws, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public PlayerTextDraw CreatePlayerTextDraw(Player player, Vector2 position, string text, EntityId parent = default) + { + ArgumentNullException.ThrowIfNull(player); + ArgumentNullException.ThrowIfNull(text); + + IPlayer nativePlayer = player; + if (!nativePlayer.TryQueryExtension(out var playerTextDrawData)) + { + throw new InvalidOperationException("Missing text draw data"); + } + + var native = playerTextDrawData.Create(position, text); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, playerTextDrawData, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public Menu CreateMenu(string title, Vector2 position, float col0Width, float? col1Width = null, EntityId parent = default) + { + ArgumentNullException.ThrowIfNull(title); + + var native = _menus.Create(title, position, col1Width.HasValue ? (byte)2 : (byte)1, col0Width, col1Width ?? 0); + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _menus, native, title); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + private Vehicle CreateVehicle(bool isStatic, VehicleModelType type, Vector3 position, float rotation, int color1, int color2, int respawnDelay = -1, bool addSiren = false, + EntityId parent = default) + { + var respawnDelaySpan = respawnDelay < 0 ? TimeSpan.Zero : TimeSpan.FromSeconds(respawnDelay); + var native = _vehicles.Create(isStatic, (int)type, position, rotation, color1, color2, respawnDelaySpan, addSiren); + + var entityId = EntityId.NewEntityId(); + var component = entityManager.AddComponent(entityId, parent, _vehicles, native); + + var extension = new ComponentExtension(component); + native.AddExtension(extension); + + return component; + } + + public void SetObjectsDefaultCameraCollision(bool disable) + { + _objects.SetDefaultCameraCollision(disable); + } + + public void SendClientMessage(Color color, string message) + { + ArgumentNullException.ThrowIfNull(message); + + Colour clr = color; + _players.SendClientMessageToAll(ref clr, message); + } + + public void SendClientMessage(Color color, string messageFormat, params object[] args) + { + var message = string.Format(messageFormat, args); + SendClientMessage(color, message); + } + + public void SendClientMessage(string message) + { + SendClientMessage(Color.White, message); + } + + public void SendClientMessage(string messageFormat, params object[] args) + { + var message = string.Format(messageFormat, args); + SendClientMessage(message); + } + + public void SendPlayerMessageToPlayer(Player sender, string message) + { + ArgumentNullException.ThrowIfNull(sender); + ArgumentNullException.ThrowIfNull(message); + _players.SendChatMessageToAll(sender, message); + } + + public void SendDeathMessage(Player killer, Player killee, Weapon weapon) + { + _players.SendDeathMessageToAll(killer, killee, (int)weapon); + } + + public void GameText(string text, int time, int style) + { + GameText(text, TimeSpan.FromMilliseconds(time), style); + } + + public void GameText(string text, TimeSpan time, int style) + { + // TODO: style enum? + _players.SendGameTextToAll(text, time, style); + } + + public void CreateExplosion(Vector3 position, ExplosionType type, float radius) + { + _players.CreateExplosionForAll(position, (int)type, radius); + } + + public void SetWeather(int weather) + { + _core.SetWeather(weather); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/ActorSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ActorSystem.cs new file mode 100644 index 00000000..a0301386 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ActorSystem.cs @@ -0,0 +1,37 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class ActorSystem : DisposableSystem, IActorEventHandler +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IEventDispatcher _eventDispatcher; + + public ActorSystem(IOmpEntityProvider entityProvider, IEventDispatcher eventDispatcher, SampSharpEnvironment environment) + { + _entityProvider = entityProvider; + _eventDispatcher = eventDispatcher; + + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerGiveDamageActor(IPlayer player, IActor actor, float amount, uint weapon, SampSharp.OpenMp.Core.Api.BodyPart part) + { + _eventDispatcher.Invoke("OnPlayerGiveDamageActor", + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(actor), + amount, + weapon, + part); + } + + public void OnActorStreamOut(IActor actor, IPlayer forPlayer) + { + _eventDispatcher.Invoke("OnActorStreamOut", _entityProvider.GetEntity(actor), _entityProvider.GetEntity(forPlayer)); + } + + public void OnActorStreamIn(IActor actor, IPlayer forPlayer) + { + _eventDispatcher.Invoke("OnActorStreamIn", _entityProvider.GetEntity(actor), _entityProvider.GetEntity(forPlayer)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/CheckpointSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/CheckpointSystem.cs new file mode 100644 index 00000000..04bbf014 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/CheckpointSystem.cs @@ -0,0 +1,30 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class CheckpointSystem : DisposableSystem, IPlayerCheckpointEventHandler +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IEventDispatcher _eventDispatcher; + + public CheckpointSystem(IOmpEntityProvider entityProvider, IEventDispatcher eventDispatcher, + SampSharpEnvironment environment) + { + _entityProvider = entityProvider; + _eventDispatcher = eventDispatcher; + + AddDisposable(environment.TryAddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerEnterCheckpoint(IPlayer player) => + _eventDispatcher.Invoke("OnPlayerEnterCheckpoint", _entityProvider.GetEntity(player)); + + public void OnPlayerLeaveCheckpoint(IPlayer player) => + _eventDispatcher.Invoke("OnPlayerLeaveCheckpoint", _entityProvider.GetEntity(player)); + + public void OnPlayerEnterRaceCheckpoint(IPlayer player) => + _eventDispatcher.Invoke("OnPlayerEnterRaceCheckpoint", _entityProvider.GetEntity(player)); + + public void OnPlayerLeaveRaceCheckpoint(IPlayer player) => + _eventDispatcher.Invoke("OnPlayerLeaveRaceCheckpoint", _entityProvider.GetEntity(player)); +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/ClassSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ClassSystem.cs new file mode 100644 index 00000000..c5ba598b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ClassSystem.cs @@ -0,0 +1,23 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class ClassSystem : DisposableSystem, IClassEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public ClassSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + + AddDisposable(environment.TryAddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public bool OnPlayerRequestClass(IPlayer player, uint classId) + { + return _eventDispatcher.InvokeAs("OnPlayerRequestClass", true, + _entityProvider.GetEntity(player), (int)classId); + } +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/ConsoleSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ConsoleSystem.cs new file mode 100644 index 00000000..15b79b82 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ConsoleSystem.cs @@ -0,0 +1,40 @@ +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.RobinHood; + +namespace SampSharp.Entities.SAMP; + +internal class ConsoleSystem : DisposableSystem, IConsoleEventHandler +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IEventDispatcher _eventDispatcher; + + public ConsoleSystem(IOmpEntityProvider entityProvider, IEventDispatcher eventDispatcher, SampSharpEnvironment environment) + { + _entityProvider = entityProvider; + _eventDispatcher = eventDispatcher; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public bool OnConsoleText(string command, string parameters, ref ConsoleCommandSenderData sender) + { + var player = sender.Player.HasValue + ? _entityProvider.GetComponent(sender.Player.Value) + : null; + var isCustom = sender.Sender == SampSharp.OpenMp.Core.Api.ConsoleCommandSender.Custom; + var isConsole = sender.Sender == SampSharp.OpenMp.Core.Api.ConsoleCommandSender.Console; + + return _eventDispatcher.InvokeAs("OnConsoleText", false, command, parameters, new ConsoleCommandSender(player, isConsole, isCustom)); + } + + public void OnRconLoginAttempt(IPlayer player, string password, bool success) + { + _eventDispatcher.Invoke("OnRconLoginAttempt", _entityProvider.GetEntity(player), password, success); + } + + public void OnConsoleCommandListRequest(FlatHashSetStringView commands) + { + var collection = new ConsoleCommandCollection(commands); + + _eventDispatcher.Invoke("OnConsoleCommandListRequest", collection); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/CustomModelsSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/CustomModelsSystem.cs new file mode 100644 index 00000000..b1c9a9f9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/CustomModelsSystem.cs @@ -0,0 +1,29 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class CustomModelsSystem : DisposableSystem, IPlayerModelsEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public CustomModelsSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, + SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + + AddDisposable(environment.TryAddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerFinishedDownloading(IPlayer player) + { + _eventDispatcher.Invoke("OnPlayerFinishedDownloading", _entityProvider.GetEntity(player)); + } + + public bool OnPlayerRequestDownload(IPlayer player, ModelDownloadType type, uint checksum) + { + return _eventDispatcher.InvokeAs("OnPlayerRequestDownload", true, + _entityProvider.GetEntity(player), (int)type, (int)checksum); + } +} diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/GangZoneSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/GangZoneSystem.cs new file mode 100644 index 00000000..02daffdc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/GangZoneSystem.cs @@ -0,0 +1,31 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class GangZoneSystem : DisposableSystem, IGangZoneEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public GangZoneSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerEnterGangZone(IPlayer player, IGangZone zone) => + Dispatch("OnPlayerEnterGangZone", "OnPlayerEnterPlayerGangZone", player, zone); + + public void OnPlayerLeaveGangZone(IPlayer player, IGangZone zone) => + Dispatch("OnPlayerLeaveGangZone", "OnPlayerLeavePlayerGangZone", player, zone); + + public void OnPlayerClickGangZone(IPlayer player, IGangZone zone) => + Dispatch("OnPlayerClickGangZone", "OnPlayerClickPlayerGangZone", player, zone); + + private void Dispatch(string globalName, string perPlayerName, IPlayer player, IGangZone zone) + { + var name = zone.GetLegacyPlayer().HasValue ? perPlayerName : globalName; + _eventDispatcher.Invoke(name, _entityProvider.GetEntity(player), _entityProvider.GetEntity(zone)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/MenuSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/MenuSystem.cs new file mode 100644 index 00000000..0fdf2a34 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/MenuSystem.cs @@ -0,0 +1,29 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class MenuSystem : DisposableSystem, IMenuEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public MenuSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerSelectedMenuRow(IPlayer player, byte row) + { + _eventDispatcher.Invoke("OnPlayerSelectedMenuRow", + _entityProvider.GetEntity(player), + row); + } + + public void OnPlayerExitedMenu(IPlayer player) + { + _eventDispatcher.Invoke("OnPlayerExitedMenu", + _entityProvider.GetEntity(player)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/NpcSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/NpcSystem.cs new file mode 100644 index 00000000..aa515dbd --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/NpcSystem.cs @@ -0,0 +1,53 @@ +using SampSharp.OpenMp.Core.Api; +using INPC = SampSharp.OpenMp.Core.Api.INPC; +using INPCComponent = SampSharp.OpenMp.Core.Api.INPCComponent; +using INPCEventHandler = SampSharp.OpenMp.Core.Api.INPCEventHandler; + +namespace SampSharp.Entities.SAMP; + +internal class NpcSystem : DisposableSystem, INPCEventHandler +{ + private readonly IOmpEntityProvider _entityProvider; + private readonly IEventDispatcher _eventDispatcher; + + public NpcSystem(IOmpEntityProvider entityProvider, IEventDispatcher eventDispatcher, SampSharpEnvironment environment) + { + _entityProvider = entityProvider; + _eventDispatcher = eventDispatcher; + + AddDisposable(environment.TryAddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnNPCFinishMove(INPC npc) + { + _eventDispatcher.Invoke("OnNPCFinishMove", _entityProvider.GetEntity(npc)); + } + + public void OnNPCCreate(INPC npc) + { + _eventDispatcher.Invoke("OnNPCCreate", _entityProvider.GetEntity(npc)); + } + + public void OnNPCDestroy(INPC npc) + { + _eventDispatcher.Invoke("OnNPCDestroy", _entityProvider.GetEntity(npc)); + } + + public void OnNPCSpawn(INPC npc) + { + _eventDispatcher.Invoke("OnNPCSpawn", _entityProvider.GetEntity(npc)); + } + + public void OnNPCRespawn(INPC npc) + { + _eventDispatcher.Invoke("OnNPCRespawn", _entityProvider.GetEntity(npc)); + } + + public void OnNPCDeath(INPC npc, IPlayer killer, int reason) + { + _eventDispatcher.Invoke("OnNPCDeath", + _entityProvider.GetEntity(npc), + _entityProvider.GetEntity(killer), + reason); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/ObjectSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ObjectSystem.cs new file mode 100644 index 00000000..6653ca02 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/ObjectSystem.cs @@ -0,0 +1,52 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class ObjectSystem : DisposableSystem, IObjectEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public ObjectSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnMoved(IObject objekt) + { + _eventDispatcher.Invoke("OnObjectMoved", _entityProvider.GetEntity(objekt)); + } + + public void OnPlayerObjectMoved(IPlayer player, IPlayerObject objekt) + { + _eventDispatcher.Invoke("OnPlayerObjectMoved", _entityProvider.GetEntity(player), _entityProvider.GetEntity(objekt, player)); + } + + public void OnObjectSelected(IPlayer player, IObject objekt, int model, Vector3 position) + { + _eventDispatcher.Invoke("OnObjectSelected", _entityProvider.GetEntity(player), _entityProvider.GetEntity(objekt), model, position); + } + + public void OnPlayerObjectSelected(IPlayer player, IPlayerObject objekt, int model, Vector3 position) + { + _eventDispatcher.Invoke("OnPlayerObjectSelected", _entityProvider.GetEntity(player), _entityProvider.GetEntity(objekt, player), model, position); + } + + public void OnObjectEdited(IPlayer player, IObject objekt, ObjectEditResponse response, Vector3 offset, Vector3 rotation) + { + _eventDispatcher.Invoke("OnObjectEdited", _entityProvider.GetEntity(player), _entityProvider.GetEntity(objekt), response, offset, rotation); + } + + public void OnPlayerObjectEdited(IPlayer player, IPlayerObject objekt, ObjectEditResponse response, Vector3 offset, Vector3 rotation) + { + _eventDispatcher.Invoke("OnPlayerObjectEdited", _entityProvider.GetEntity(player), _entityProvider.GetEntity(objekt, player), response, offset, rotation); + } + + public void OnPlayerAttachedObjectEdited(IPlayer player, int index, bool saved, ref ObjectAttachmentSlotData data) + { + _eventDispatcher.Invoke("OnPlayerAttachedObjectEdited", _entityProvider.GetEntity(player), index, saved, data); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PickupSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PickupSystem.cs new file mode 100644 index 00000000..5ef2c9f9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PickupSystem.cs @@ -0,0 +1,26 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PickupSystem : DisposableSystem, IPickupEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PickupSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerPickUpPickup(IPlayer player, IPickup pickup) + { + var name = pickup.GetLegacyPlayer().HasValue + ? "OnPlayerPickUpPlayerPickup" + : "OnPlayerPickUpPickup"; + _eventDispatcher.Invoke(name, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(pickup)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerChangeSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerChangeSystem.cs new file mode 100644 index 00000000..8f92c6d7 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerChangeSystem.cs @@ -0,0 +1,41 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerChangeSystem : DisposableSystem, IPlayerChangeEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerChangeSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerChangeDispatcher(), this)); + } + + public void OnPlayerScoreChange(IPlayer player, int score) + { + _eventDispatcher.Invoke("OnPlayerScoreChange", _entityProvider.GetEntity(player), score); + } + + public void OnPlayerNameChange(IPlayer player, string oldName) + { + _eventDispatcher.Invoke("OnPlayerNameChange", _entityProvider.GetEntity(player), oldName); + } + + public void OnPlayerInteriorChange(IPlayer player, uint newInterior, uint oldInterior) + { + _eventDispatcher.Invoke("OnPlayerInteriorChange", _entityProvider.GetEntity(player), newInterior, oldInterior); + } + + public void OnPlayerStateChange(IPlayer player, SampSharp.OpenMp.Core.Api.PlayerState newState, SampSharp.OpenMp.Core.Api.PlayerState oldState) + { + _eventDispatcher.Invoke("OnPlayerStateChange", _entityProvider.GetEntity(player), newState, oldState); + } + + public void OnPlayerKeyStateChange(IPlayer player, uint newKeys, uint oldKeys) + { + _eventDispatcher.Invoke("OnPlayerKeyStateChange", _entityProvider.GetEntity(player), newKeys, oldKeys); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerCheckSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerCheckSystem.cs new file mode 100644 index 00000000..f306f04b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerCheckSystem.cs @@ -0,0 +1,21 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerCheckSystem : DisposableSystem, IPlayerCheckEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerCheckSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerCheckDispatcher(), this)); + } + + public void OnClientCheckResponse(IPlayer player, int actionType, int address, int results) + { + _eventDispatcher.Invoke("OnClientCheckResponse", _entityProvider.GetEntity(player), actionType, address, results); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerClickSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerClickSystem.cs new file mode 100644 index 00000000..da7cc794 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerClickSystem.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerClickSystem : DisposableSystem, IPlayerClickEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerClickSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerClickDispatcher(), this)); + } + + public void OnPlayerClickMap(IPlayer player, Vector3 pos) + { + _eventDispatcher.Invoke("OnPlayerClickMap", _entityProvider.GetEntity(player), pos); + } + + public void OnPlayerClickPlayer(IPlayer player, IPlayer clicked, SampSharp.OpenMp.Core.Api.PlayerClickSource source) + { + _eventDispatcher.Invoke("OnPlayerClickPlayer", _entityProvider.GetEntity(player), _entityProvider.GetEntity(clicked), source); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerConnectSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerConnectSystem.cs new file mode 100644 index 00000000..afcfbfc1 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerConnectSystem.cs @@ -0,0 +1,36 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerConnectSystem : DisposableSystem, IPlayerConnectEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerConnectSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerConnectDispatcher(), this)); + } + + public void OnIncomingConnection(IPlayer player, string ipAddress, ushort port) + { + _eventDispatcher.Invoke("OnIncomingConnection", _entityProvider.GetEntity(player), ipAddress, port); + } + + public void OnPlayerConnect(IPlayer player) + { + _eventDispatcher.Invoke("OnPlayerConnect", _entityProvider.GetEntity(player)); + } + + public void OnPlayerDisconnect(IPlayer player, PeerDisconnectReason reason) + { + _eventDispatcher.Invoke("OnPlayerDisconnect", _entityProvider.GetEntity(player), reason); + } + + public void OnPlayerClientInit(IPlayer player) + { + _eventDispatcher.Invoke("OnPlayerClientInit", _entityProvider.GetEntity(player)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerDamageSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerDamageSystem.cs new file mode 100644 index 00000000..b19114dd --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerDamageSystem.cs @@ -0,0 +1,31 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerDamageSystem : DisposableSystem, IPlayerDamageEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerDamageSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerDamageDispatcher(), this)); + } + + public void OnPlayerDeath(IPlayer player, IPlayer killer, int reason) + { + _eventDispatcher.Invoke("OnPlayerDeath", _entityProvider.GetEntity(player), _entityProvider.GetEntity(killer), reason); + } + + public void OnPlayerTakeDamage(IPlayer player, IPlayer from, float amount, uint weapon, SampSharp.OpenMp.Core.Api.BodyPart part) + { + _eventDispatcher.Invoke("OnPlayerTakeDamage", _entityProvider.GetEntity(player), _entityProvider.GetEntity(from), amount, weapon, part); + } + + public void OnPlayerGiveDamage(IPlayer player, IPlayer to, float amount, uint weapon, SampSharp.OpenMp.Core.Api.BodyPart part) + { + _eventDispatcher.Invoke("OnPlayerGiveDamage", _entityProvider.GetEntity(player), _entityProvider.GetEntity(to), amount, weapon, part); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerShotSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerShotSystem.cs new file mode 100644 index 00000000..308ff1e8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerShotSystem.cs @@ -0,0 +1,41 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerShotSystem : DisposableSystem, IPlayerShotEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerShotSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerShotDispatcher(), this)); + } + + public bool OnPlayerShotMissed(IPlayer player, ref PlayerBulletData bulletData) + { + return _eventDispatcher.InvokeAs("OnPlayerShotMissed", true, _entityProvider.GetEntity(player), bulletData); + } + + public bool OnPlayerShotPlayer(IPlayer player, IPlayer target, ref PlayerBulletData bulletData) + { + return _eventDispatcher.InvokeAs("OnPlayerShotPlayer", true, _entityProvider.GetEntity(player), _entityProvider.GetEntity(target), bulletData); + } + + public bool OnPlayerShotVehicle(IPlayer player, IVehicle target, ref PlayerBulletData bulletData) + { + return _eventDispatcher.InvokeAs("OnPlayerShotVehicle", true, _entityProvider.GetEntity(player), _entityProvider.GetEntity(target), bulletData); + } + + public bool OnPlayerShotObject(IPlayer player, IObject target, ref PlayerBulletData bulletData) + { + return _eventDispatcher.InvokeAs("OnPlayerShotObject", true, _entityProvider.GetEntity(player), _entityProvider.GetEntity(target), bulletData); + } + + public bool OnPlayerShotPlayerObject(IPlayer player, IPlayerObject target, ref PlayerBulletData bulletData) + { + return _eventDispatcher.InvokeAs("OnPlayerShotPlayerObject", true, _entityProvider.GetEntity(player), _entityProvider.GetEntity(target, player), bulletData); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerSpawnSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerSpawnSystem.cs new file mode 100644 index 00000000..5549911c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerSpawnSystem.cs @@ -0,0 +1,26 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerSpawnSystem : DisposableSystem, IPlayerSpawnEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerSpawnSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerSpawnDispatcher(), this)); + } + + public bool OnPlayerRequestSpawn(IPlayer player) + { + return _eventDispatcher.InvokeAs("OnPlayerRequestSpawn", true, _entityProvider.GetEntity(player)); + } + + public void OnPlayerSpawn(IPlayer player) + { + _eventDispatcher.Invoke("OnPlayerSpawn", _entityProvider.GetEntity(player)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerStreamSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerStreamSystem.cs new file mode 100644 index 00000000..6cddae12 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerStreamSystem.cs @@ -0,0 +1,26 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerStreamSystem : DisposableSystem, IPlayerStreamEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerStreamSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerStreamDispatcher(), this)); + } + + public void OnPlayerStreamIn(IPlayer player, IPlayer forPlayer) + { + _eventDispatcher.Invoke("OnPlayerStreamIn", _entityProvider.GetEntity(player), _entityProvider.GetEntity(forPlayer)); + } + + public void OnPlayerStreamOut(IPlayer player, IPlayer forPlayer) + { + _eventDispatcher.Invoke("OnPlayerStreamOut", _entityProvider.GetEntity(player), _entityProvider.GetEntity(forPlayer)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerTextSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerTextSystem.cs new file mode 100644 index 00000000..4389e5a6 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerTextSystem.cs @@ -0,0 +1,26 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerTextSystem : DisposableSystem, IPlayerTextEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerTextSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerTextDispatcher(), this)); + } + + public bool OnPlayerText(IPlayer player, string message) + { + return _eventDispatcher.InvokeAs("OnPlayerText", true, _entityProvider.GetEntity(player), message); + } + + public bool OnPlayerCommandText(IPlayer player, string message) + { + return _eventDispatcher.InvokeAs("OnPlayerCommandText", false, _entityProvider.GetEntity(player), message); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerUpdateSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerUpdateSystem.cs new file mode 100644 index 00000000..1e6e7b87 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/PlayerUpdateSystem.cs @@ -0,0 +1,22 @@ +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.Entities.SAMP; + +internal class PlayerUpdateSystem : DisposableSystem, IPlayerUpdateEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public PlayerUpdateSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetPlayers().GetPlayerUpdateDispatcher(), this)); + } + + public bool OnPlayerUpdate(IPlayer player, TimePoint now) + { + return _eventDispatcher.InvokeAs("OnPlayerUpdate", true, _entityProvider.GetEntity(player), now); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/TextDrawSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/TextDrawSystem.cs new file mode 100644 index 00000000..811ddc9c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/TextDrawSystem.cs @@ -0,0 +1,42 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class TextDrawSystem : DisposableSystem, ITextDrawEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public TextDrawSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnPlayerClickTextDraw(IPlayer player, ITextDraw td) + { + _eventDispatcher.Invoke("OnPlayerClickTextDraw", + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(td)); + } + + public void OnPlayerClickPlayerTextDraw(IPlayer player, IPlayerTextDraw td) + { + _eventDispatcher.Invoke("OnPlayerClickPlayerTextDraw", + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(td, player)); + } + + public bool OnPlayerCancelTextDrawSelection(IPlayer player) + { + return _eventDispatcher.InvokeAs("OnPlayerCancelTextDrawSelection", true, + _entityProvider.GetEntity(player)); + } + + public bool OnPlayerCancelPlayerTextDrawSelection(IPlayer player) + { + return _eventDispatcher.InvokeAs("OnPlayerCancelPlayerTextDrawSelection", true, + _entityProvider.GetEntity(player)); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SAMP/Systems/VehicleSystem.cs b/src/SampSharp.OpenMp.Entities/SAMP/Systems/VehicleSystem.cs new file mode 100644 index 00000000..9c5ba3a8 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SAMP/Systems/VehicleSystem.cs @@ -0,0 +1,122 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities.SAMP; + +internal class VehicleSystem : DisposableSystem, IVehicleEventHandler +{ + private readonly IEventDispatcher _eventDispatcher; + private readonly IOmpEntityProvider _entityProvider; + + public VehicleSystem(IEventDispatcher eventDispatcher, IOmpEntityProvider entityProvider, SampSharpEnvironment environment) + { + _eventDispatcher = eventDispatcher; + _entityProvider = entityProvider; + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnVehicleStreamIn(IVehicle vehicle, IPlayer player) + { + _eventDispatcher.Invoke("OnVehicleStreamIn", + _entityProvider.GetEntity(vehicle), + _entityProvider.GetEntity(player)); + } + + public void OnVehicleStreamOut(IVehicle vehicle, IPlayer player) + { + _eventDispatcher.Invoke("OnVehicleStreamOut", + _entityProvider.GetEntity(vehicle), + _entityProvider.GetEntity(player)); + } + + public void OnVehicleDeath(IVehicle vehicle, IPlayer player) + { + _eventDispatcher.Invoke("OnVehicleDeath", + _entityProvider.GetEntity(vehicle), + _entityProvider.GetEntity(player)); + } + + public void OnPlayerEnterVehicle(IPlayer player, IVehicle vehicle, bool passenger) + { + _eventDispatcher.Invoke("OnPlayerEnterVehicle", + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle), + passenger); + } + + public void OnPlayerExitVehicle(IPlayer player, IVehicle vehicle) + { + _eventDispatcher.Invoke("OnPlayerExitVehicle", + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle)); + } + + public void OnVehicleDamageStatusUpdate(IVehicle vehicle, IPlayer player) + { + _eventDispatcher.Invoke("OnVehicleDamageStatusUpdate", + _entityProvider.GetEntity(vehicle), + _entityProvider.GetEntity(player)); + } + + public bool OnVehiclePaintJob(IPlayer player, IVehicle vehicle, int paintJob) + { + return _eventDispatcher.InvokeAs("OnVehiclePaintJob", true, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle), + paintJob); + } + + public bool OnVehicleMod(IPlayer player, IVehicle vehicle, int component) + { + return _eventDispatcher.InvokeAs("OnVehicleMod", true, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle), + component); + } + + public bool OnVehicleRespray(IPlayer player, IVehicle vehicle, int colour1, int colour2) + { + return _eventDispatcher.InvokeAs("OnVehicleRespray", true, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle), + colour1, + colour2); + } + + public void OnEnterExitModShop(IPlayer player, bool enterexit, int interiorId) + { + _eventDispatcher.Invoke("OnEnterExitModShop", + _entityProvider.GetEntity(player), + enterexit, + interiorId); + } + + public void OnVehicleSpawn(IVehicle vehicle) + { + _eventDispatcher.Invoke("OnVehicleSpawn", _entityProvider.GetEntity(vehicle)); + } + + public bool OnUnoccupiedVehicleUpdate(IVehicle vehicle, IPlayer player, UnoccupiedVehicleUpdate updateData) + { + return _eventDispatcher.InvokeAs("OnUnoccupiedVehicleUpdate", true, + _entityProvider.GetEntity(vehicle), + _entityProvider.GetEntity(player), + updateData.position, + updateData.velocity, + (int)updateData.seat); + } + + public bool OnTrailerUpdate(IPlayer player, IVehicle trailer) + { + return _eventDispatcher.InvokeAs("OnTrailerUpdate", true, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(trailer)); + } + + public bool OnVehicleSirenStateChange(IPlayer player, IVehicle vehicle, byte sirenState) + { + return _eventDispatcher.InvokeAs("OnVehicleSirenStateChange", true, + _entityProvider.GetEntity(player), + _entityProvider.GetEntity(vehicle), + sirenState); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj b/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj new file mode 100644 index 00000000..d9429138 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + SampSharp.Entities + True + + + + + + + + + + + + + + + + + + + diff --git a/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj.DotSettings b/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj.DotSettings new file mode 100644 index 00000000..adcfb393 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/SampSharp.OpenMp.Entities.csproj.DotSettings @@ -0,0 +1,14 @@ + + True + True + True + True + True + True + True + True + True + True + True + True + True \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/DisposableSystem.cs b/src/SampSharp.OpenMp.Entities/Systems/DisposableSystem.cs new file mode 100644 index 00000000..89bd125a --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/DisposableSystem.cs @@ -0,0 +1,70 @@ +namespace SampSharp.Entities; + +/// +/// Represents a base type for systems that contain disposable objects. +/// +public abstract class DisposableSystem : ISystem, IDisposable +{ + private readonly List _disposables = []; + private bool _disposed; + + /// + /// Adds a disposable object to the list of objects to dispose when this system is disposed. + /// + /// The disposable object to add. + protected void AddDisposable(IDisposable? disposable) + { + ObjectDisposedException.ThrowIf(_disposed, this); + + if (disposable is not null) + { + _disposables.Add(disposable); + } + } + + /// + /// A method that is called when this system is disposed. + /// + protected virtual void OnDispose() + { + List? errors = null; + foreach (var disposable in _disposables) + { + try + { + disposable.Dispose(); + } + catch (Exception ex) + { + errors ??= []; + errors.Add(ex); + } + } + + _disposables.Clear(); + + if (errors?.Count > 0) + { + throw new AggregateException(errors); + } + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + try + { + OnDispose(); + } + finally + { + _disposed = true; + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/ISystem.cs b/src/SampSharp.OpenMp.Entities/Systems/ISystem.cs new file mode 100644 index 00000000..ef97f83d --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/ISystem.cs @@ -0,0 +1,6 @@ +namespace SampSharp.Entities; + +/// Represents a marker interface for systems. All systems must implement this interface. +public interface ISystem +{ +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/ISystemRegistry.cs b/src/SampSharp.OpenMp.Entities/Systems/ISystemRegistry.cs new file mode 100644 index 00000000..3199ce07 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/ISystemRegistry.cs @@ -0,0 +1,27 @@ +namespace SampSharp.Entities; + +/// Provides the functionality for a registry of system types. +public interface ISystemRegistry +{ + /// Gets all systems of the specified . + /// The type of the systems to get. + /// A segment of memory containing instances of systems of the specified type. + ReadOnlyMemory Get(Type type); + + /// Gets all systems of the specified . + /// The type of the systems to get. + /// A segment of memory containing instances of systems of the specified type. + ReadOnlyMemory Get() where TSystem : ISystem; + + /// + /// Gets all implementation types of registered systems in this system registry. + /// + /// A segment of memory containing all implementation types of registered systems. + ReadOnlyMemory GetSystemTypes(); + + /// + /// Registers a handler to be called when the system registry has been loaded. The handler will be called immediately if the system registry has already been loaded. + /// + /// The handler to call when the system registry has been loaded. + void Register(Action handler); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/ITickingSystem.cs b/src/SampSharp.OpenMp.Entities/Systems/ITickingSystem.cs new file mode 100644 index 00000000..cbf6bf13 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/ITickingSystem.cs @@ -0,0 +1,9 @@ +namespace SampSharp.Entities; + +/// Contains methods which can be implemented by systems which handle server ticks. +/// +public interface ITickingSystem : ISystem +{ + /// Occurs every server tick. + void Tick(); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs b/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs new file mode 100644 index 00000000..8f8ac7dc --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/SafeEventHandlerRegistration.cs @@ -0,0 +1,68 @@ +using SampSharp.OpenMp.Core.Api; + +namespace SampSharp.Entities; + +internal class SafeEventHandlerRegistration(SampSharpEnvironment environment, TEventHandler handler, Func> dispatcherProvider) : IDisposable + where TComponent : unmanaged, IComponent.IManagedInterface + where TEventHandler : class, IEventHandler +{ + private bool _disposed; + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + var component = environment.Components.QueryComponent(); + + if (!component.HasValue) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + + var dispatcher = dispatcherProvider(component); + + if (!dispatcher.HasValue) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + + dispatcher.RemoveEventHandler(handler); + } +} + +internal class SafeEventHandlerRegistration(SampSharpEnvironment environment, TEventHandler handler, Func> dispatcherProvider) : IDisposable + where TEventHandler : class, IEventHandler +{ + private bool _disposed; + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + if (!environment.Core.HasValue) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + + var dispatcher = dispatcherProvider(environment.Core); + + if (!dispatcher.HasValue) + { + TEventHandler.Marshaller.Marshal(handler).Free(); + return; + } + + dispatcher.RemoveEventHandler(handler); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/ServiceCollectionSystemExtensions.cs b/src/SampSharp.OpenMp.Entities/Systems/ServiceCollectionSystemExtensions.cs new file mode 100644 index 00000000..a89ee6a0 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/ServiceCollectionSystemExtensions.cs @@ -0,0 +1,68 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.Utilities; + +namespace SampSharp.Entities; + +/// Extension methods for adding systems to an . +public static class ServiceCollectionSystemExtensions +{ + /// Adds the system of the specified as a singleton and enables the system in the system registry. + /// The service collection to add the system to. + /// The type of the system to add. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddSystem(this IServiceCollection services, Type type) + { + return services.AddSingleton(type) + .AddSingleton(new SystemEntry(type)); + } + + /// Adds the system of the specified type as a singleton and enables the system in the system registry. + /// The type of the system to add. + /// The service collection to add the system to. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddSystem(this IServiceCollection services) where T : class, ISystem + { + return services.AddSystem(typeof(T)); + } + + /// + /// Adds the all types which implement in the specified as singletons and enable the systems in the + /// system registry. + /// + /// The service collection to add the systems to. + /// The assembly of which to add its types as systems. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddSystemsInAssembly(this IServiceCollection services, Assembly assembly) + { + var types = ClassScanner.Create() + .IncludeAssembly(assembly) + .Implements() + .ScanTypes(); + + foreach (var type in types) + services.AddSystem(type); + + return services; + } + + /// + /// Adds the all types which implement in the assembly of the specified type as singletons + /// and enable the systems in the system registry. + /// + /// A type in the assembly of which to add its types as system. + /// The service collection to add the systems to. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddSystemsInAssembly(this IServiceCollection services) + { + return services.AddSystemsInAssembly(typeof(TTypeInAssembly).Assembly); + } + + /// Adds the all types which implement in the calling assembly as singletons and enable the systems in the system registry. + /// The service collection to add the systems to. + /// A reference to this instance after the operation has completed. + public static IServiceCollection AddSystemsInAssembly(this IServiceCollection services) + { + return services.AddSystemsInAssembly(Assembly.GetCallingAssembly()); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/SystemEntry.cs b/src/SampSharp.OpenMp.Entities/Systems/SystemEntry.cs new file mode 100644 index 00000000..47b9c1ef --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/SystemEntry.cs @@ -0,0 +1,11 @@ +namespace SampSharp.Entities; + +internal class SystemEntry +{ + public SystemEntry(Type type) + { + Type = type; + } + + public Type Type { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/SystemRegistry.cs b/src/SampSharp.OpenMp.Entities/Systems/SystemRegistry.cs new file mode 100644 index 00000000..0149a858 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/SystemRegistry.cs @@ -0,0 +1,107 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace SampSharp.Entities; + +internal sealed class SystemRegistry(IServiceProvider serviceProvider) : ISystemRegistry +{ + private Type[]? _systemTypes; + private Dictionary? _data; + + private List? _handlers = []; + + public void LoadSystems() + { + if (_data != null) + { + return; + } + + _systemTypes = serviceProvider.GetServices() + .Select(w => w.Type) + .ToArray(); + + _data = ExtractSystemTypeLookupTable(_systemTypes) + .ToDictionary(x => x.Key, x => x.Value.ToArray()); + + InvokeHandlers(); + } + + private Dictionary> ExtractSystemTypeLookupTable(Type[] systemTypes) + { + var data = new Dictionary>(); + foreach (var type in systemTypes) + { + if (serviceProvider.GetService(type) is not ISystem instance) + { + continue; + } + + // Add the system to all types and interfaces it implements. + var currentType = type; + while (currentType != null && currentType != typeof(object)) + { + if (!data.TryGetValue(currentType, out var set)) + { + data[currentType] = set = []; + } + + set.Add(instance); + + currentType = currentType.BaseType; + } + + foreach (var interfaceType in type.GetInterfaces() + .Where(t => typeof(ISystem).IsAssignableFrom(t))) + { + if (!data.TryGetValue(interfaceType, out var set)) + { + data[interfaceType] = set = []; + } + + set.Add(instance); + } + } + + return data; + } + + private void InvokeHandlers() + { + if (_handlers != null) + { + foreach (var handler in _handlers) + { + handler(); + } + + _handlers = null; + } + } + + public ReadOnlyMemory Get(Type type) + { + return _data?.TryGetValue(type, out var value) ?? false ? value : default(ReadOnlyMemory); + } + + public ReadOnlyMemory Get() where TSystem : ISystem + { + return Get(typeof(TSystem)); + } + + public ReadOnlyMemory GetSystemTypes() + { + return _systemTypes?.AsMemory() ?? default; + } + + public void Register(Action handler) + { + if (_handlers != null) + { + _handlers.Add(handler); + } + else + { + handler(); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/SystemRegistryException.cs b/src/SampSharp.OpenMp.Entities/Systems/SystemRegistryException.cs new file mode 100644 index 00000000..9a26929b --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/SystemRegistryException.cs @@ -0,0 +1,24 @@ +namespace SampSharp.Entities; + +/// Represents an error which occurs while registering a system. +/// +public class SystemRegistryException : Exception +{ + /// Initializes a new instance of the class. + public SystemRegistryException() + { + } + + /// Initializes a new instance of the class. + /// The message that describes the error. + public SystemRegistryException(string message) : base(message) + { + } + + /// Initializes a new instance of the class. + /// The message. + /// The inner. + public SystemRegistryException(string message, Exception inner) : base(message, inner) + { + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Systems/TickingSystem.cs b/src/SampSharp.OpenMp.Entities/Systems/TickingSystem.cs new file mode 100644 index 00000000..da107eca --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Systems/TickingSystem.cs @@ -0,0 +1,27 @@ +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace SampSharp.Entities; + +internal class TickingSystem : DisposableSystem, ICoreEventHandler +{ + private ITickingSystem[] _tickers = []; + + [Event] + public void OnGameModeInit(ISystemRegistry systemRegistry, SampSharpEnvironment environment) + { + var tickers = systemRegistry.Get().ToArray(); + _tickers = new ITickingSystem[tickers.Length]; + Array.Copy(tickers, _tickers, tickers.Length); + + AddDisposable(environment.AddEventHandler(x => x.GetEventDispatcher(), this)); + } + + public void OnTick(Microseconds elapsed, TimePoint now) + { + for (var i = 0; i < _tickers.Length; i++) + { + _tickers[i].Tick(); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/ITimerService.cs b/src/SampSharp.OpenMp.Entities/Timers/ITimerService.cs new file mode 100644 index 00000000..3f1211f9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/ITimerService.cs @@ -0,0 +1,44 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities; + +/// Provides methods for starting and stopping timers. +public interface ITimerService +{ + /// Starts a timer with the specified and . + /// The action to perform each timer tick. + /// The interval at which to tick. + /// A reference to the started timer. + TimerReference Start(Action action, TimeSpan interval); + + /// Starts a timer with the specified and . + /// The action to perform each timer tick. + /// The interval at which to tick. + /// A reference to the started timer. + TimerReference Start(Action action, TimeSpan interval); + + /// + /// Starts a timer with the specified which will tick only once after the specified . + /// + /// The action perform at the ticker + /// The delay after which the timer will tick. + /// A reference to the started timer + TimerReference Delay(Action action, TimeSpan delay); + + /// Stops the specified timer. + /// The timer to stop. + void Stop(TimerReference timer); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerAttribute.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerAttribute.cs new file mode 100644 index 00000000..2a6fe936 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/TimerAttribute.cs @@ -0,0 +1,36 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using JetBrains.Annotations; + +namespace SampSharp.Entities; + +/// An attribute which indicates the method should be invoked at a specified interval. +[AttributeUsage(AttributeTargets.Method)] +[MeansImplicitUse] +public class TimerAttribute : Attribute +{ + /// Initializes a new instance of the class. + /// The interval of the timer. + public TimerAttribute(double interval) + { + Interval = interval; + } + + /// Gets or sets the interval of the timer. + public double Interval { get; set; } + + internal TimeSpan IntervalTimeSpan => TimeSpan.FromMilliseconds(Interval); +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerInfo.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerInfo.cs new file mode 100644 index 00000000..37cbff6c --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/TimerInfo.cs @@ -0,0 +1,33 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace SampSharp.Entities; + +internal class TimerInfo +{ + public TimerInfo(long intervalTicks, long nextTick, Action invoke, bool isActive) + { + IntervalTicks = intervalTicks; + NextTick = nextTick; + Invoke = invoke; + IsActive = isActive; + } + + public long IntervalTicks; + public long NextTick; + public Action Invoke; + public bool IsActive; + public TimerReference? Reference; +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs new file mode 100644 index 00000000..e022e2d9 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/TimerReference.cs @@ -0,0 +1,44 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics; +using System.Reflection; + +namespace SampSharp.Entities; + +/// Represents a reference to an interval or timeout. +public class TimerReference +{ + internal TimerReference(TimerInfo info, object? target, MethodInfo? method) + { + Info = info; + Target = target; + Method = method; + } + + /// Gets the time span until the next tick of this timer. + public TimeSpan NextTick => new(Info.NextTick - Stopwatch.GetTimestamp()); + + internal TimerInfo Info { get; set; } + + /// Gets a value indicating whether the timer is active. + public bool IsActive => Info.IsActive; + + /// Gets the target on which the timer is invoked. + public object? Target { get; } + + /// Gets the method to be invoked with this timer. + public MethodInfo? Method { get; } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerServiceExtensions.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerServiceExtensions.cs new file mode 100644 index 00000000..63145654 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/TimerServiceExtensions.cs @@ -0,0 +1,55 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Reflection; + +namespace SampSharp.Entities; + +/// Provides extended methods for . +public static class TimerServiceExtensions +{ + /// + /// Starts a timer with the specified . The specified will be invoked on the specified + /// each timer tick. + /// + /// The timer service. + /// The target on which to tick. + /// The method to invoke each timer tick. + /// The interval at which to tick. + /// A reference to the started timer. + public static TimerReference Start(this ITimerService timerService, object target, MethodInfo method, TimeSpan interval) + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(method); + + if (!TimerSystem.IsValidInterval(interval)) + { + throw new ArgumentOutOfRangeException(nameof(interval), interval, "The interval should be a nonzero positive value."); + } + + if (!method.DeclaringType!.IsInstanceOfType(target)) + { + throw new ArgumentException("The specified method is not a member of the specified target", nameof(method)); + } + + var parameterInfos = method.GetParameters() + .Select(info => new MethodParameterSource(info) { IsService = true }) + .ToArray(); + + var compiled = MethodInvokerFactory.Compile(method, parameterInfos); + + return timerService.Start(serviceProvider => compiled(target, null, serviceProvider, null), interval); + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs b/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs new file mode 100644 index 00000000..f3811012 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Timers/TimerSystem.cs @@ -0,0 +1,186 @@ +// SampSharp +// Copyright 2022 Tim Potze +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using SampSharp.Entities.Utilities; +using SampSharp.OpenMp.Core; + +namespace SampSharp.Entities; + +internal class TimerSystem : ITickingSystem, ITimerService +{ + private static readonly TimeSpan _lowIntervalThreshold = TimeSpan.FromSeconds(1.0 / 50); // 50Hz + + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly ISystemRegistry _systemRegistry; + + private readonly List _timers = []; + private long _lastTick; + + public TimerSystem(IServiceProvider serviceProvider, ILogger logger, ISystemRegistry systemRegistry) + { + _serviceProvider = serviceProvider; + _logger = logger; + _systemRegistry = systemRegistry; + + _lastTick = Stopwatch.GetTimestamp(); + + systemRegistry.Register(CreateTimersFromAssemblies); + } + + public TimerReference Delay(Action action, TimeSpan delay) + { + return Start((sp, reference) => + { + Stop(reference); + action(sp); + }, delay); + } + + public void Stop(TimerReference timer) + { + ArgumentNullException.ThrowIfNull(timer); + timer.Info.IsActive = false; + _timers.Remove(timer.Info); + } + + public TimerReference Start(Action action, TimeSpan interval) + { + return Start((sp, _) => action(sp), interval); + } + + public TimerReference Start(Action action, TimeSpan interval) + { + ArgumentNullException.ThrowIfNull(action); + + if (!IsValidInterval(interval)) + { + throw new ArgumentOutOfRangeException(nameof(interval), interval, "The interval should be a nonzero positive value."); + } + + var invoker = new TimerInfo(intervalTicks: interval.Ticks, nextTick: Stopwatch.GetTimestamp() + interval.Ticks, invoke: null!, true); + + var reference = new TimerReference(invoker, action.Target, action.Method); + + invoker.Invoke = () => action(_serviceProvider, reference); + invoker.Reference = reference; + + _timers.Add(invoker); + + return reference; + } + + public void Tick() + { + if (_timers.Count == 0) + { + return; + } + + var timestamp = Stopwatch.GetTimestamp(); + + // Don't user foreach for performance reasons + // ReSharper disable once ForCanBeConvertedToForeach + for (var i = 0; i < _timers.Count; i++) + { + var timer = _timers[i]; + + while (timer.NextTick <= timestamp) + { + try + { + timer.Invoke(); + } + catch (Exception ex) + { + var context = "timer"; + + if (timer.Reference?.Method != null) + { + context = $"timer {timer.Reference.Method.DeclaringType}.{timer.Reference.Method.Name}"; + } + + SampSharpExceptionHandler.HandleException(context, ex); + } + + timer.NextTick += timer.IntervalTicks; + } + } + + _lastTick = timestamp; + } + + internal static bool IsValidInterval(TimeSpan interval) + { + return interval >= TimeSpan.FromTicks(1); + } + + private void CreateTimersFromAssemblies() + { + var tick = _lastTick; + // Find methods with TimerAttribute in any loaded system. + var events = ClassScanner.Create() + .IncludeTypes(_systemRegistry.GetSystemTypes().Span) + .IncludeNonPublicMembers() + .Implements() + .ScanMethods(); + + // Create timer invokers and store timer info in registry. + foreach (var (target, method, attribute) in events) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Adding timer on {type}.{method}.", target, method.Name); + } + + if (!IsValidInterval(attribute.IntervalTimeSpan)) + { + _logger.LogError("Timer {method} could not be registered the interval {interval} is invalid.", method, attribute.IntervalTimeSpan); + continue; + } + + var service = _serviceProvider.GetService(target); + + if (service == null) + { + _logger.LogDebug("Skipping timer registration because the service could not be loaded."); + continue; + } + + var parameterInfos = method.GetParameters() + .Select(info => new MethodParameterSource(info) { IsService = true }) + .ToArray(); + + var compiled = MethodInvokerFactory.Compile(method, parameterInfos); + + if (attribute.IntervalTimeSpan < _lowIntervalThreshold) + { + _logger.LogWarning("Timer {type}.{method} has a low interval of {interval}.", target, method.Name, attribute.IntervalTimeSpan); + } + + var timer = new TimerInfo( + intervalTicks: attribute.IntervalTimeSpan.Ticks, + nextTick: tick + attribute.IntervalTimeSpan.Ticks, + invoke: () => compiled(service, null, _serviceProvider, null), + isActive: true); + + timer.Reference = new TimerReference(timer, service, method); + + _timers.Add(timer); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.OpenMp.Entities/Utilities/ClassScanner.cs b/src/SampSharp.OpenMp.Entities/Utilities/ClassScanner.cs new file mode 100644 index 00000000..4cb625c3 --- /dev/null +++ b/src/SampSharp.OpenMp.Entities/Utilities/ClassScanner.cs @@ -0,0 +1,207 @@ +using System.Reflection; + +namespace SampSharp.Entities.Utilities; + +/// Represents a utility for scanning for classes and members with specific attributes in loaded assemblies. +public sealed class ClassScanner +{ + private List _assemblies = []; + private List _classAttributes = []; + private List _classImplements = []; + private List _classTypes = []; + private bool _includeNonPublicMembers; + private List _memberAttributes = []; + private bool _includeAbstract; + + private BindingFlags MemberBindingFlags => + BindingFlags.Instance | + BindingFlags.Public | + (_includeNonPublicMembers ? BindingFlags.NonPublic : BindingFlags.Default); + + private ClassScanner() + { + } + + /// + /// Creates a new instance. + /// + /// A new scanner instance. + public static ClassScanner Create() + { + return new ClassScanner(); + } + /// Includes the specified in the scan. + /// The assembly to include. + /// An updated scanner. + public ClassScanner IncludeAssembly(Assembly assembly) + { + if (_assemblies.Contains(assembly)) + { + return this; + } + + var result = Clone(); + result._assemblies.Add(assembly); + return result; + } + + /// Includes the referenced assemblies of the previously included assemblies in the scan. + /// An updated scanner. + public ClassScanner IncludeReferencedAssemblies() + { + var assemblies = new List(); + + foreach (var a in _assemblies) + { + AddToScan(a); + } + + var result = Clone(); + result._assemblies = assemblies; + return result; + + void AddToScan(Assembly asm) + { + if (assemblies.Contains(asm)) + { + return; + } + + assemblies.Add(asm); + + foreach (var assemblyRef in asm.GetReferencedAssemblies()) + { + if (IsSystemAssembly(assemblyRef)) + { + continue; + } + + AddToScan(Assembly.Load(assemblyRef)); + } + } + } + + /// + /// Includes the specified in the scan. This can be used to include types from assemblies which are not directly referenced by the loaded assemblies, e.g. plugin assemblies. + /// + /// The types to include. + /// An updated scanner. + public ClassScanner IncludeTypes(ReadOnlySpan types) + { + var result = Clone(); + result._classTypes.AddRange(types); + return result; + } + + private static bool IsSystemAssembly(AssemblyName assemblyRef) + { + return (assemblyRef.Name!.StartsWith("System", StringComparison.InvariantCulture) || + assemblyRef.Name.StartsWith("Microsoft", StringComparison.InvariantCulture) || + assemblyRef.Name.StartsWith("netstandard", StringComparison.InvariantCulture)); + } + + /// Includes non-public members in the scan. + /// An updated scanner. + public ClassScanner IncludeNonPublicMembers() + { + var result = Clone(); + result._includeNonPublicMembers = true; + return result; + } + + /// Includes members of abstract classes in the scan. + /// An updated scanner. + public ClassScanner IncludeAbstractClasses() + { + var result = Clone(); + result._includeAbstract = true; + return result; + } + + /// Includes only members of classes which implement in the scan. + /// The class or interface the results of the scan should implement. + /// An updated scanner. + public ClassScanner Implements() + { + var result = Clone(); + result._classImplements.Add(typeof(T)); + return result; + } + + /// Includes only members of classes which have an attribute . + /// The type of the attribute the class should have. + /// An updated scanner. + public ClassScanner HasClassAttribute() where T : Attribute + { + var result = Clone(); + result._classAttributes.Add(typeof(T)); + return result; + } + + /// Includes only members which have an attribute . + /// The type of the attribute the member should have. + /// An updated scanner. + public ClassScanner HasMemberAttribute() where T : Attribute + { + var result = Clone(); + result._memberAttributes.Add(typeof(T)); + return result; + } + + private bool ApplyTypeFilter(Type type) + { + return type.IsClass && + (_includeAbstract || !type.IsAbstract) && + _classImplements.All(i => i.IsAssignableFrom(type)) && + _classAttributes.All(a => type.GetCustomAttribute(a) != null); + } + + private bool ApplyMemberFilter(MemberInfo memberInfo) + { + return _memberAttributes.All(a => memberInfo.GetCustomAttribute(a) != null); + } + + /// Runs the scan for methods. + /// The found methods. + public IEnumerable ScanTypes() + { + return _assemblies.SelectMany(a => a.GetTypes()) + .Concat(_classTypes) + .Where(ApplyTypeFilter) + .Distinct(); + } + + /// Runs the scan for methods and provides the attribute in the + /// results. + /// The type of the attribute. + /// The found methods with their attribute of type . + public IEnumerable<(Type target, MethodInfo method, TAttribute attribute)> ScanMethods() where TAttribute : Attribute + { + return HasMemberAttribute() + .ScanMethods() + .Select(x => (x.target, x.method, attribute: x.method.GetCustomAttribute()!)); + } + + /// Runs the scan for methods. + /// The found methods. + public IEnumerable<(Type target, MethodInfo method)> ScanMethods() + { + return ScanTypes() + .SelectMany(t => t.GetMethods(MemberBindingFlags).Select(m => (t, m))) + .Where(x => ApplyMemberFilter(x.m)); + } + + private ClassScanner Clone() + { + return new ClassScanner + { + _assemblies = [.. _assemblies], + _classTypes = [.. _classTypes], + _includeNonPublicMembers = _includeNonPublicMembers, + _classImplements = [.. _classImplements], + _classAttributes = [.. _classAttributes], + _memberAttributes = [.. _memberAttributes], + _includeAbstract = _includeAbstract + }; + } +} \ No newline at end of file diff --git a/src/SampSharp.Sdk/SampSharp.Sdk.csproj b/src/SampSharp.Sdk/SampSharp.Sdk.csproj new file mode 100644 index 00000000..3fbe7790 --- /dev/null +++ b/src/SampSharp.Sdk/SampSharp.Sdk.csproj @@ -0,0 +1,23 @@ + + + + net10.0 + SampSharp package for development of open.mp gamemodes and libraries. + false + true + false + false + + + + + + + + + + + + + + diff --git a/src/SampSharp.SourceGenerator/CompilerServices.cs b/src/SampSharp.SourceGenerator/CompilerServices.cs new file mode 100644 index 00000000..41124927 --- /dev/null +++ b/src/SampSharp.SourceGenerator/CompilerServices.cs @@ -0,0 +1,6 @@ + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices; + +// Enable record support +internal class IsExternalInit; diff --git a/src/SampSharp.SourceGenerator/Constants.cs b/src/SampSharp.SourceGenerator/Constants.cs new file mode 100644 index 00000000..e5bebb62 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Constants.cs @@ -0,0 +1,130 @@ +using System; +using System.Runtime.InteropServices; + +namespace SampSharp.SourceGenerator; + +public static class Constants +{ + // System + + /// + /// System.Span<byte> + /// + public const string SpanOfBytesFQN = "System.Span"; + + /// + /// System.Delegate + /// + public static readonly string DelegateFQN = typeof(Delegate).FullName!; + + /// + /// System.Runtime.InteropServices.Marshal + /// + public static readonly string MarshalFQN = typeof(Marshal).FullName!; + + /// + /// System.IEquatable{T} + /// + public static readonly string IEquatableFQN = typeof(IEquatable<>).FullName!.Split('`')[0]; + + /// + /// System.Runtime.InteropServices.Marshalling.MarshalUsingAttribute + /// + public const string MarshalUsingAttributeFQN = "System.Runtime.InteropServices.Marshalling.MarshalUsingAttribute"; + + /// + /// System.Runtime.InteropServices.Marshalling.MarshalUsingAttribute + /// + public const string NativeMarshallingAttributeFQN = "System.Runtime.InteropServices.Marshalling.NativeMarshallingAttribute"; + + /// + /// System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute + /// + public const string CustomMarshallerAttributeFQN = "System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute"; + + /// + /// System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute.GenericPlaceholder + /// + public const string GenericPlaceholderFQN = "System.Runtime.InteropServices.Marshalling.CustomMarshallerAttribute.GenericPlaceholder"; + + // SampSharp.OpenMp.Core + + /// + /// SampSharp.OpenMp.Core.OpenMpApiAttribute + /// + public const string ApiAttributeFQN = "SampSharp.OpenMp.Core.OpenMpApiAttribute"; + + /// + /// SampSharp.OpenMp.Core.OpenMpApiPartialAttribute + /// + public const string ApiPartialAttributeFQN = "SampSharp.OpenMp.Core.OpenMpApiPartialAttribute"; + + /// + /// SampSharp.OpenMp.Core.OpenMpEventHandlerAttribute + /// + public const string EventHandlerAttributeFQN = "SampSharp.OpenMp.Core.OpenMpEventHandlerAttribute"; + + /// + /// SampSharp.OpenMp.Core.OpenMpApiOverloadAttribute + /// + public const string OverloadAttributeFQN = "SampSharp.OpenMp.Core.OpenMpApiOverloadAttribute"; + + /// + /// SampSharp.OpenMp.Core.OpenMpApiFunctionAttribute + /// + public const string FunctionAttributeFQN = "SampSharp.OpenMp.Core.OpenMpApiFunctionAttribute"; + + /// + /// SampSharp.OpenMp.Core.NumberedTypeGeneratorAttribute + /// + public const string NumberedTypeGeneratorAttributeFQN = "SampSharp.OpenMp.Core.NumberedTypeGeneratorAttribute"; + + /// + /// SampSharp.OpenMp.Core.StartupContext + /// + public const string StartupContextFQN = "SampSharp.OpenMp.Core.StartupContext"; + + /// + /// SampSharp.OpenMp.Core.SampSharpInitParams + /// + public const string InitParamsFQN = "SampSharp.OpenMp.Core.SampSharpInitParams"; + + /// + /// SampSharp.OpenMp.Core.IUnmanagedInterface + /// + public const string UnmanagedInterfaceFQN = "SampSharp.OpenMp.Core.IUnmanagedInterface"; + + // SampSharp.OpenMp.Core.Std + + /// + /// SampSharp.OpenMp.Core.Std.StringViewMarshaller + /// + public const string StringViewMarshallerFQN = "SampSharp.OpenMp.Core.Std.StringViewMarshaller"; + + /// + /// SampSharp.OpenMp.Core.Std.BooleanMarshaller + /// + public const string BooleanMarshallerFQN = "SampSharp.OpenMp.Core.Std.BooleanMarshaller"; + + // SampSharp.OpenMp.Core.Api + + /// + /// SampSharp.OpenMp.Core.Api.IComponent + /// + public const string IComponentFQN = "SampSharp.OpenMp.Core.Api.IComponent"; + + /// + /// SampSharp.OpenMp.Core.Api.IEventHandler{TEventHandler} + /// + public const string EventHandlerFQN = "SampSharp.OpenMp.Core.Api.IEventHandler"; + + /// + /// SampSharp.OpenMp.Core.Api.EventHandlerMarshaller{TEventHandler} + /// + public const string EventHandlerMarshallerFQN = "SampSharp.OpenMp.Core.Api.EventHandlerMarshaller"; + + /// + /// SampSharp.OpenMp.Core.Api.IEventHandlerMarshaller{TEventHandler} + /// + public const string IEventHandlerMarshallerFQN = "SampSharp.OpenMp.Core.Api.IEventHandlerMarshaller"; +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/FullyQualifiedTypeRewriter.cs b/src/SampSharp.SourceGenerator/FullyQualifiedTypeRewriter.cs new file mode 100644 index 00000000..879f433d --- /dev/null +++ b/src/SampSharp.SourceGenerator/FullyQualifiedTypeRewriter.cs @@ -0,0 +1,142 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator; + +public class FullyQualifiedTypeRewriter : CSharpSyntaxRewriter +{ + private readonly SemanticModel _semanticModel; + + public FullyQualifiedTypeRewriter(SemanticModel semanticModel) + { + _semanticModel = semanticModel; + } + + public override SyntaxNode VisitMemberAccessExpression(MemberAccessExpressionSyntax node) + { + return ResolveMemberAccessSyntaxTree(node); + } + + private MemberAccessExpressionSyntax ResolveMemberAccessSyntaxTree(MemberAccessExpressionSyntax node) + { + switch (node.Expression) + { + case MemberAccessExpressionSyntax access: + return node.WithExpression(ResolveMemberAccessSyntaxTree(access)); + case IdentifierNameSyntax name: + { + var symbol = _semanticModel.GetSymbolInfo(name); + if (symbol.Symbol is INamedTypeSymbol type) + { + return node.WithExpression(name.WithIdentifier(Identifier(TypeSyntaxFactory.ToGlobalTypeString(type)))); + } + + break; + } + } + + return node; + } + + public override SyntaxNode VisitAttribute(AttributeSyntax node) + { + var symbol = _semanticModel.GetSymbolInfo(node.Name).Symbol; + + if (symbol != null && !node.Name.ToString().StartsWith("global::")) + { + node = node + .WithName(ParseName($"global::{symbol.ContainingNamespace}.{node.Name}")) + .WithArgumentList((AttributeArgumentListSyntax?)Visit(node.ArgumentList)); + + } + + return node; + } + + public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + var symbol = symbolInfo.Symbol; + + if (symbol is ITypeSymbol { SpecialType: SpecialType.None } typeSymbol) + { + if (typeSymbol.TypeKind != TypeKind.TypeParameter) + { + return IdentifierName(TypeSyntaxFactory.ToGlobalTypeString(typeSymbol)); + } + } + + return base.VisitIdentifierName(node); + } + + public override SyntaxNode? VisitQualifiedName(QualifiedNameSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + var symbol = symbolInfo.Symbol; + + if (symbol is ITypeSymbol typeSymbol) + { + return IdentifierName(TypeSyntaxFactory.ToGlobalTypeString(typeSymbol)); + } + + return base.VisitQualifiedName(node); + } + + public override SyntaxNode? VisitGenericName(GenericNameSyntax node) + { + var symbolInfo = _semanticModel.GetSymbolInfo(node); + var symbol = symbolInfo.Symbol; + + if (symbol is ITypeSymbol typeSymbol) + { + var fullyQualifiedName = TypeSyntaxFactory.ToGlobalTypeString(typeSymbol); + var identifier = fullyQualifiedName.Split('<')[0]; // Get the identifier part + var typeArguments = node.TypeArgumentList.Arguments.Select(arg => (TypeSyntax)Visit(arg)).ToArray(); + + return GenericName(identifier) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList(typeArguments))); + } + + return base.VisitGenericName(node); + } + public override SyntaxNode? VisitInvocationExpression(InvocationExpressionSyntax node) + { + if (node.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax memberGenericName } memberAccess) + { + var typeArguments = memberGenericName.TypeArgumentList.Arguments.Select(arg => + { + var symbolInfo = _semanticModel.GetSymbolInfo(arg); + var symbol = symbolInfo.Symbol; + + if (symbol is ITypeSymbol typeSymbol) + { + return IdentifierName(TypeSyntaxFactory.ToGlobalTypeString(typeSymbol)); + } + + return arg; + }) + .ToArray(); + + var newExpression = memberAccess + .WithName(memberGenericName + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList(typeArguments)))) + .WithExpression( + (ExpressionSyntax)Visit(memberAccess.Expression)); + + return node + .WithExpression(newExpression) + .WithArgumentList( + (ArgumentListSyntax)Visit(node.ArgumentList)); + } + + return base.VisitInvocationExpression(node); + } +} diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/CastMembersGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/CastMembersGenerator.cs new file mode 100644 index 00000000..557f7547 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/CastMembersGenerator.cs @@ -0,0 +1,257 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Helpers; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class CastMembersGenerator +{ + public static IEnumerable GenerateCastMembers(StructStubGenerationContext ctx) + { + var isFirst = true; + var generatedFromComponentHandle = false; + + foreach (var impl in ctx.ImplementingTypes) + { + var block = CreateCastBlock(ctx, isFirst, impl); + + yield return ConversionOperatorDeclaration( + Token(SyntaxKind.ExplicitKeyword), + impl.Type.Syntax) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("value")) + .WithType( + ctx.Type)))) + .WithLeadingTrivia( + TriviaFactory.DocsOpCast(ctx.Type, impl.Type.Syntax)) + .WithBody(block); + + // Generate reverse cast (impl.Type -> ctx.Type) + var reverseBlock = CreateReverseCastBlock(ctx, isFirst, impl); + yield return ConversionOperatorDeclaration( + Token(SyntaxKind.ExplicitKeyword), + ctx.Type) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("value")) + .WithType( + impl.Type.Syntax)))) + .WithLeadingTrivia( + TriviaFactory.DocsOpCast(impl.Type.Syntax, ctx.Type)) + .WithBody(reverseBlock); + + isFirst = false; + + if (!generatedFromComponentHandle && impl.Type.Symbol.IsSame(Constants.IComponentFQN)) + { + yield return GenerateFromComponentHandle(ctx); + generatedFromComponentHandle = true; + } + } + } + + private static BlockSyntax CreateCastBlock(StructStubGenerationContext ctx, bool isFirst, ImplementingType impl) + { + BlockSyntax block; + + if (isFirst) + { + block = Block(SingletonList( + ReturnStatement( + ObjectCreationExpression(impl.Type.Syntax) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("value"), + IdentifierName("Handle"))))))))); + } + else + { + if (impl.CastPath.Length == 1) + { + var func = GenerateExternFunctionCast(ctx, impl.Type.Symbol); + + var invoke = InvocationExpression( + IdentifierName("__PInvoke")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("value"), + IdentifierName("Handle")))))); + + var ret = ReturnStatement( + ObjectCreationExpression(impl.Type.Syntax) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(invoke))))); + + block = Block( + List([ + ret, + func + ])); + } + else + { + var cast = impl.CastPath.Aggregate((ExpressionSyntax)IdentifierName("value"), (current, c) => CastExpression(c.Syntax, current)); + + block = Block(SingletonList( + ReturnStatement(cast))); + } + } + + return block; + } + + private static BlockSyntax CreateReverseCastBlock(StructStubGenerationContext ctx, bool isFirst, ImplementingType impl) + { + BlockSyntax block; + + if (isFirst) + { + // First implementing type: direct handle access + block = Block(SingletonList( + ReturnStatement( + ObjectCreationExpression(ctx.Type) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("value"), + IdentifierName("Handle"))))))))); + } + else + { + if (impl.CastPath.Length == 1) + { + // Use extern function for the reverse cast + var func = GenerateExternFunctionCastReverse(ctx, impl.Type.Symbol); + + var invoke = InvocationExpression( + IdentifierName("__PInvoke")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("value"), + IdentifierName("Handle")))))); + + var ret = ReturnStatement( + ObjectCreationExpression(ctx.Type) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument(invoke))))); + + block = Block( + List([ + ret, + func + ])); + } + else + { + // For multi-step cast paths, apply the reverse cast chain directly + var cast = impl.CastPath.Reverse().Aggregate( + (ExpressionSyntax)IdentifierName("value"), + (current, c) => CastExpression(c.Syntax, current)); + + // Wrap the result in a cast to ctx.Type + var finalCast = CastExpression(ctx.Type, cast); + + block = Block(SingletonList( + ReturnStatement(finalCast))); + } + } + + return block; + } + + private static MethodDeclarationSyntax GenerateFromComponentHandle(StructStubGenerationContext ctx) + { + return MethodDeclaration( + ParseName("nint"), + "FromComponentHandle") + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("handle")) + .WithType( + ParseName("nint"))))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithBody( + Block( + SingletonList( + ReturnStatement( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParenthesizedExpression( + CastExpression( + ctx.Type, + ObjectCreationExpression( + TypeNameGlobal(Constants.IComponentFQN)) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + ParseName("handle"))))))), + IdentifierName("Handle")))))); + } + + + private static LocalFunctionStatementSyntax GenerateExternFunctionCast(StructStubGenerationContext ctx, ITypeSymbol target) + { + return HelperSyntaxFactory.GenerateExternFunction( + library: ctx.Library, + externName: $"cast_{ctx.Symbol.Name}_to_{target.Name}", + externReturnType: IntPtrType, + parameters: [new HelperSyntaxFactory.ParamForwardInfo("ptr", IntPtrType, RefKind.None)]); + } + + private static LocalFunctionStatementSyntax GenerateExternFunctionCastReverse(StructStubGenerationContext ctx, ITypeSymbol source) + { + return HelperSyntaxFactory.GenerateExternFunction( + library: ctx.Library, + externName: $"cast_{source.Name}_to_{ctx.Symbol.Name}", + externReturnType: IntPtrType, + parameters: [new HelperSyntaxFactory.ParamForwardInfo("ptr", IntPtrType, RefKind.None)]); + } + +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/CreationMembersGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/CreationMembersGenerator.cs new file mode 100644 index 00000000..d0f29ab6 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/CreationMembersGenerator.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TriviaFactory; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class CreationMembersGenerator +{ + /// + /// Returns members for the creation of values. This includes the handle field and property, the constructor, and + /// implicit/explicit conversion operators. + /// + public static IEnumerable GenerateCreationMembers(StructStubGenerationContext ctx) + { + // private readonly field _handle; + yield return GenerateHandleField(); + + // .ctor(nint handle) + yield return GenerateConstructor(ctx); + + // public nint Handle => _handle; + yield return GenerateHandleProperty(); + } + + private static PropertyDeclarationSyntax GenerateHandleProperty() + { + return PropertyDeclaration( + ParseTypeName("nint"), + "Handle") + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword))) + .WithExpressionBody( + ArrowExpressionClause( + IdentifierName("_handle"))) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia( + InheritDoc()); + } + + private static FieldDeclarationSyntax GenerateHandleField() + { + return FieldDeclaration( + VariableDeclaration(ParseTypeName("nint"), + SingletonSeparatedList( + VariableDeclarator("_handle")))) + .WithModifiers( + TokenList( + Token(SyntaxKind.PrivateKeyword), + Token(SyntaxKind.ReadOnlyKeyword))); + } + + private static ConstructorDeclarationSyntax GenerateConstructor(StructStubGenerationContext ctx) + { + return ConstructorDeclaration(Identifier(ctx.Symbol.Name)) + .WithParameterList(ParameterList( + SingletonSeparatedList( + Parameter(Identifier("handle")).WithType(ParseTypeName("nint")) + ))) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithLeadingTrivia( + DocsStructConstructor( + ctx.Type, + new ParameterDoc( + "handle", + "A pointer to the unmanaged interface."))) + .WithBody(Block( + SingletonList( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("_handle"), + IdentifierName("handle")))))); + } +} diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/EqualityMembersGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/EqualityMembersGenerator.cs new file mode 100644 index 00000000..e16aeb78 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/EqualityMembersGenerator.cs @@ -0,0 +1,260 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class EqualityMembersGenerator +{ + public static IEnumerable GenerateEqualityMembers(StructStubGenerationContext ctx) + { + var self = ParseTypeName(ctx.Symbol.Name); + + if (ctx.Syntax.TypeParameterList != null) + { + self = GenericName( + Identifier(ctx.Symbol.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + ctx.Syntax.TypeParameterList.Parameters.Select(x => IdentifierName(x.Identifier))))); + } + + // public readonly bool HasValue => _handle != default; + yield return PropertyDeclaration( + PredefinedType( + Token(SyntaxKind.BoolKeyword)), + Identifier("HasValue")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.ReadOnlyKeyword))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithExpressionBody( + ArrowExpressionClause( + BinaryExpression( + SyntaxKind.NotEqualsExpression, + IdentifierName("_handle"), + LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + Token(SyntaxKind.DefaultKeyword))))) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)); + + // public override int GetHashCode() + yield return MethodDeclaration( + PredefinedType( + Token(SyntaxKind.IntKeyword)), + Identifier("GetHashCode")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.OverrideKeyword) + )) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithBody( + Block( + SingletonList( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("_handle"), + IdentifierName("GetHashCode"))))))); + + // public bool Equals(MyType other) + yield return MethodDeclaration( + PredefinedType( + Token(SyntaxKind.BoolKeyword)), + Identifier("Equals")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("other")) + .WithType(ctx.Type)))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithBody( + Block( + SingletonList( + ReturnStatement( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("_handle"), + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("other"), + IdentifierName("_handle"))))))); + + // public override bool Equals(object? other) + yield return MethodDeclaration( + PredefinedType( + Token(SyntaxKind.BoolKeyword)), + Identifier("Equals")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.OverrideKeyword))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("other")) + .WithType( + NullableType( + PredefinedType( + Token(SyntaxKind.ObjectKeyword))))))) + .WithBody( + Block( + IfStatement( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("_handle"), + LiteralExpression( + SyntaxKind.DefaultLiteralExpression, + Token(SyntaxKind.DefaultKeyword))), + ReturnStatement( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("other"), + LiteralExpression( + SyntaxKind.NullLiteralExpression)))), + IfStatement( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("other"), + LiteralExpression( + SyntaxKind.NullLiteralExpression)), + ReturnStatement( + LiteralExpression( + SyntaxKind.FalseLiteralExpression))), + IfStatement( + IsPatternExpression( + IdentifierName("other"), + DeclarationPattern( + self, + SingleVariableDesignation( + Identifier("otherValue")))), + Block( + SingletonList( + ReturnStatement( + InvocationExpression( + IdentifierName("Equals")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName("otherValue"))))))))), + ReturnStatement( + LiteralExpression( + SyntaxKind.FalseLiteralExpression)))); + + // public static bool operator ==(MyType lhs, object? rhs) + yield return OperatorDeclaration( + PredefinedType( + Token(SyntaxKind.BoolKeyword)), + Token(SyntaxKind.EqualsEqualsToken)) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SeparatedList([ + Parameter( + Identifier("lhs")) + .WithType(self), + Parameter( + Identifier("rhs")) + .WithType( + NullableType( + PredefinedType( + Token(SyntaxKind.ObjectKeyword))))]))) + .WithLeadingTrivia( + TriviaFactory.DocsOpEqual()) + .WithBody( + Block( + IfStatement( + IsPatternExpression( + IdentifierName("rhs"), + ConstantPattern( + LiteralExpression( + SyntaxKind.NullLiteralExpression))), + ReturnStatement( + PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("lhs"), + IdentifierName("HasValue"))))), + IfStatement( + IsPatternExpression( + IdentifierName("rhs"), + DeclarationPattern( + self, + SingleVariableDesignation( + Identifier("other")))), + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("lhs"), + IdentifierName("Equals"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName("other"))))))), + ReturnStatement( + LiteralExpression( + SyntaxKind.FalseLiteralExpression)))); + + // public static bool operator !=(MyType lhs, object? rhs) + yield return OperatorDeclaration( + PredefinedType( + Token(SyntaxKind.BoolKeyword)), + Token(SyntaxKind.ExclamationEqualsToken)) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SeparatedList([ + Parameter( + Identifier("lhs")) + .WithType(self), + Parameter( + Identifier("rhs")) + .WithType( + NullableType( + PredefinedType( + Token(SyntaxKind.ObjectKeyword))))]))) + .WithLeadingTrivia( + TriviaFactory.DocsOpNotEqual()) + .WithExpressionBody( + ArrowExpressionClause( + PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + ParenthesizedExpression( + BinaryExpression( + SyntaxKind.EqualsExpression, + IdentifierName("lhs"), + IdentifierName("rhs")))))) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/ForwardingMembersGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/ForwardingMembersGenerator.cs new file mode 100644 index 00000000..0c85658b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/ForwardingMembersGenerator.cs @@ -0,0 +1,168 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.HelperSyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class ForwardingMembersGenerator +{ + /// + /// Returns members for implementing the types specified in the CodeGen attribute. + /// + public static SyntaxList GenerateImplementingTypeMembers(StructStubGenerationContext ctx) + { + var result = List(); + + foreach (var impl in ctx.ImplementingTypes) + { + var implementingType = impl.Type; + // only forward public ordinary methods, excluding Equals + var implementingMethods = implementingType.Symbol.GetMembers() + .OfType() + .Where(x => !x.IsStatic && x is + { + MethodKind: MethodKind.Ordinary, + Name: not "Equals" and not "GetHashCode", + DeclaredAccessibility: Accessibility.Public + }) + .ToList(); + + foreach (var implementingMethod in implementingMethods) + { + SyntaxTriviaList leadingTrivia = default; + foreach (var reference in implementingMethod.DeclaringSyntaxReferences) + { + var syntax = reference.GetSyntax(); + + if (syntax is MethodDeclarationSyntax { HasLeadingTrivia: true }) + { + leadingTrivia = syntax.GetLeadingTrivia(); + break; + } + } + + var method = MethodDeclaration( + TypeNameGlobal(implementingMethod, true), + implementingMethod.Name) + .WithParameterList(ToParameterListSyntax(implementingMethod.Parameters)) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword))) + .WithLeadingTrivia(leadingTrivia); + + SimpleNameSyntax memberName = IdentifierName(implementingMethod.Name); + if (implementingMethod.TypeParameters.Length > 0) + { + method = method.WithTypeParameterList( + TypeParameterList( + SeparatedList( + implementingMethod.TypeParameters.Select(x => TypeParameter(x.Name))))); + + memberName = GenericName( + Identifier(implementingMethod.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + implementingMethod.TypeParameters.Select(x => IdentifierName(x.Name))))); + } + + method = method.WithConstraintClauses( + List( + implementingMethod.TypeParameters + .Select(x => TypeParameterConstraintClause(IdentifierName(x.Name)) + .WithConstraints( + ToConstraintList(x))))); + + var target = ParenthesizedExpression( + CastExpression( + implementingType.Syntax, + ThisExpression())); + + var invocation = + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + target, + memberName)) + .WithArgumentList( + ArgumentList( + SeparatedList( + implementingMethod.Parameters.Select(symbol => WithPInvokeParameterRefToken(Argument(IdentifierName(symbol.Name)), symbol.RefKind))))); + + if (implementingMethod.ReturnsVoid) + { + method = method.WithBody( + Block( + SingletonList( + ExpressionStatement(invocation)))); + } + else if (implementingMethod.ReturnsByRef || implementingMethod.ReturnsByRefReadonly) + { + method = method.WithBody( + Block( + SingletonList( + ReturnStatement( + RefExpression(invocation))))); + } + else + { + method = method.WithBody( + Block( + SingletonList( + ReturnStatement(invocation)))); + } + + result = result.Add(method); + } + } + + return result; + } + + private static SeparatedSyntaxList ToConstraintList(ITypeParameterSymbol x) + { + var result = SeparatedList(); + + if (x.HasReferenceTypeConstraint) + { + result = result.Add(ClassOrStructConstraint(SyntaxKind.ClassConstraint)); + } + + if (x.HasUnmanagedTypeConstraint) + { + result = result.Add(TypeConstraint(IdentifierName("unmanaged"))); + } + else if (x.HasValueTypeConstraint) + { + result = result.Add(ClassOrStructConstraint(SyntaxKind.StructConstraint)); + } + + if (x.HasNotNullConstraint) + { + result = result.Add(TypeConstraint(IdentifierName("notnull"))); + } + + + result = result.AddRange(x.ConstraintTypes.Select(ToTypeConstraint)); + + if (x.HasConstructorConstraint) + { + result = result.Add(ClassOrStructConstraint(SyntaxKind.ConstructorConstraint)); + } + + return result; + } + + private static TypeParameterConstraintSyntax ToTypeConstraint(ITypeSymbol typeSymbol) + { + + var typeSyntax = ParseTypeName(typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + + return TypeConstraint(typeSyntax); + } +} diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/InterfaceMemberGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/InterfaceMemberGenerator.cs new file mode 100644 index 00000000..f720b4d8 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/InterfaceMemberGenerator.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Helpers; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class InterfaceMemberGenerator +{ + public const string InterfaceName = "IManagedInterface"; + + public static IEnumerable GenerateNativeMethods(StructStubGenerationContext ctx) + { + var publicMembers = ctx.PublicMembers + .Where(x => !x.HasModifier(SyntaxKind.StaticKeyword)) + .Where(x => x is MethodDeclarationSyntax) + .Select(MemberDeclarationSyntax (member) => + { + if (member is MethodDeclarationSyntax method) + { + method = method.WithBody(null).WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); + member = method.WithParameterList(ParameterList(SeparatedList(method.ParameterList.Parameters.Select(x => x.WithAttributeLists([]))))); + } + + var trivia = member.GetLeadingTrivia(); + + var modifiers = member.Modifiers; + var partialIdx = modifiers.IndexOf(SyntaxKind.PartialKeyword); + + if (partialIdx >= 0) + { + modifiers = modifiers.RemoveAt(partialIdx); + } + + member = member + .WithAttributeLists([]) + .WithModifiers(modifiers) + .WithLeadingTrivia(trivia); + + return member; + }) + .ToList(); + + + yield return InterfaceDeclaration(InterfaceName) + .WithModifiers(TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.PartialKeyword) + )) + .WithMembers(List(publicMembers)) + .WithBaseList(BaseList( + SingletonSeparatedList( + SimpleBaseType( + TypeSyntaxFactory.TypeNameGlobal(Constants.UnmanagedInterfaceFQN))))) + .WithLeadingTrivia( + TriviaFactory.Docs("Represents the managed interface implemented by its unmanaged counterpart.", [])); + } +} + diff --git a/src/SampSharp.SourceGenerator/Generators/ApiStructs/NativeMembersGenerator.cs b/src/SampSharp.SourceGenerator/Generators/ApiStructs/NativeMembersGenerator.cs new file mode 100644 index 00000000..cd7b019e --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/ApiStructs/NativeMembersGenerator.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Generators.Marshalling; +using SampSharp.SourceGenerator.Models; + +namespace SampSharp.SourceGenerator.Generators.ApiStructs; + +public static class NativeMembersGenerator +{ + public static IEnumerable GenerateNativeMethods(StructStubGenerationContext ctx) + { + return ctx.Methods + .Select(GenerateNativeMethod) + .Where(x => x != null); + } + + /// + /// Returns a method declaration for a native method including marshalling of parameters and return value. + /// + private static MemberDeclarationSyntax GenerateNativeMethod(ApiMethodStubGenerationContext ctx) + { + var xx = new ApiMethodMarshallingGenerator(); + return xx.GenerateNativeMethod(ctx); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/EntryPointSourceGenerator.cs b/src/SampSharp.SourceGenerator/Generators/EntryPointSourceGenerator.cs new file mode 100644 index 00000000..8790e13e --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/EntryPointSourceGenerator.cs @@ -0,0 +1,194 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators; + +[Generator] +public class EntryPointSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.CreateSyntaxProvider( + (node, _) => node is ClassDeclarationSyntax { BaseList: not null }, + (ctx, ct) => + { + var classDeclaration = (ClassDeclarationSyntax)ctx.Node; + var semanticModel = ctx.SemanticModel; + + if (semanticModel.GetDeclaredSymbol(classDeclaration, ct) is { } classSymbol) + { + var interf = semanticModel.Compilation.GetTypeByMetadataName("SampSharp.OpenMp.Core.IStartup"); + if (interf != null && classSymbol.AllInterfaces.Contains(interf)) + { + return classDeclaration; + } + } + + return null; + }) + .Where(cls => cls != null) + .Select((cls, _) => cls); + + context.RegisterSourceOutput(provider, Execute); + } + + private static string GetFQN(ClassDeclarationSyntax classDeclaration) + { + // Start with the class name + var className = classDeclaration.Identifier.Text; + + // Traverse upwards to find all namespaces + var currentNode = classDeclaration.Parent; + while (currentNode != null) + { + className = currentNode switch + { + BaseNamespaceDeclarationSyntax ns => ns.Name + "." + className, + _ => className + }; + + currentNode = currentNode.Parent; + } + + return className; + } + + private void Execute(SourceProductionContext ctx, ClassDeclarationSyntax? syntax) + { + var source = Generate(syntax!); + + ctx.AddSource("EntryPoint.g.cs", source); + } + + private static SourceText Generate(ClassDeclarationSyntax syntax) + { + var startup = TypeNameGlobal(GetFQN(syntax)); + + var unit = CompilationUnit() + .WithMembers( + SingletonList( + NamespaceDeclaration(ParseName("SampSharp")) + .WithMembers( + SingletonList( + ClassDeclaration("Entrypoint") + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithMembers( + List([ + FieldDeclaration( + VariableDeclaration( + startup) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier("_startup")) + .WithInitializer( + EqualsValueClause( + ImplicitObjectCreationExpression()))))) + .WithModifiers( + TokenList( + Token(SyntaxKind.PrivateKeyword), + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.ReadOnlyKeyword))), + FieldDeclaration( + VariableDeclaration( + TypeNameGlobal(Constants.StartupContextFQN)) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier("_context"))))) + .WithModifiers( + TokenList( + Token(SyntaxKind.PrivateKeyword), + Token(SyntaxKind.StaticKeyword))), + MethodDeclaration( + PredefinedType( + Token(SyntaxKind.VoidKeyword)), + Identifier("Cleanup")) + .WithAttributeLists( + SingletonList( + AttributeFactory.UnmanagedCallersOnly())) + .WithModifiers( + TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword))) + .WithBody( + Block( + SingletonList( + ExpressionStatement( + ConditionalAccessExpression( + IdentifierName("_context"), + InvocationExpression( + MemberBindingExpression( + IdentifierName("InvokeCleanup")))))))), + MethodDeclaration( + PredefinedType( + Token(SyntaxKind.VoidKeyword)), + Identifier("Initialize")) + .WithAttributeLists( + SingletonList( + AttributeFactory.UnmanagedCallersOnly())) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("inf")) + .WithType( + TypeNameGlobal(Constants.InitParamsFQN))))) + .WithBody( + Block( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("_context"), + ObjectCreationExpression( + TypeNameGlobal(Constants.StartupContextFQN)) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName( + Identifier("inf")))))))), + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("_context"), + IdentifierName("InitializeUsing"))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName("_startup")))))))), + MethodDeclaration( + PredefinedType( + Token(SyntaxKind.VoidKeyword)), + Identifier("Main")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithBody( + Block( + SingletonList( + StatementFactory.Invoke(Constants.StartupContextFQN, "MainInfoProvider")))) + ])))))); + + var sourceText = unit.NormalizeWhitespace(elasticTrivia: true) + .GetText(Encoding.UTF8); + + return sourceText; + } + + +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiEventDelegateMarshallingGenerator.cs b/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiEventDelegateMarshallingGenerator.cs new file mode 100644 index 00000000..0e6b0fc6 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiEventDelegateMarshallingGenerator.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Marshalling; +using SampSharp.SourceGenerator.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.HelperSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.Marshalling; + +public class ApiEventDelegateMarshallingGenerator() : MarshallingGeneratorBase(MarshalDirection.UnmanagedToManaged) +{ + private const string LocalHandler = "handler"; + public ExpressionSyntax GenerateDelegateExpression(MarshallingStubGenerationContext ctx) + { + ExpressionSyntax expr; + if (!ctx.RequiresMarshalling) + { + // (DelegateType_) MethodName; + expr = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(LocalHandler), + IdentifierName(ctx.Symbol.Name)); + } + else + { + // (DelegateType_) ((TypeOne one, TypeTwoNative __two_native) => { ... }); + var parameters = ToParameterListSyntax([], ctx.Parameters.Select(x => ToForwardInfo(x))); + + expr = ParenthesizedExpression( + ParenthesizedLambdaExpression() + .WithParameterList( + parameters) + .WithBlock( + GetMarshallingBlock(ctx))); + } + + return CastExpression( + IdentifierName($"{ctx.Symbol.Name}_"), + expr); + } + + protected override ExpressionSyntax GetInvocation(MarshallingStubGenerationContext ctx) + { + return InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(LocalHandler), + IdentifierName(ctx.Symbol.Name))) + .WithArgumentList( + ArgumentList( + SeparatedList( + GetInvocationArguments(ctx)))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiMethodMarshallingGenerator.cs b/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiMethodMarshallingGenerator.cs new file mode 100644 index 00000000..79e7f55c --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/Marshalling/ApiMethodMarshallingGenerator.cs @@ -0,0 +1,79 @@ +using System.Linq; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Helpers; +using SampSharp.SourceGenerator.Marshalling; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators.Marshalling; + +public class ApiMethodMarshallingGenerator() : MarshallingGeneratorBase(MarshalDirection.ManagedToUnmanaged) +{ + private const string MethodPInvoke = "__PInvoke"; + private const string FieldHandle = "_handle"; + + public MemberDeclarationSyntax GenerateNativeMethod(ApiMethodStubGenerationContext ctx) + { + return MethodDeclaration(TypeSyntaxFactory.TypeNameGlobal(ctx.Symbol), ctx.Declaration.Identifier) + .WithModifiers(ctx.Declaration.Modifiers) + .WithParameterList(HelperSyntaxFactory.ToParameterListSyntax(ctx.Symbol.Parameters)) + .WithBody(GetMarshallingBlock(ctx)); + } + + protected override BlockSyntax GetMarshallingBlock(MarshallingStubGenerationContext ctx) + { + var block = base.GetMarshallingBlock(ctx); + + block = block.WithStatements( + block.Statements.Add( + GenerateExternFunction((ApiMethodStubGenerationContext)ctx))); + + return block; + } + + protected override ExpressionSyntax GetInvocation(MarshallingStubGenerationContext ctx) + { + return InvocationExpression(IdentifierName(MethodPInvoke)) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList(Argument(IdentifierName(FieldHandle))) + .AddRange( + GetInvocationArguments(ctx)) + ) + ); + } + + private static LocalFunctionStatementSyntax GenerateExternFunction(ApiMethodStubGenerationContext ctx) + { + // Extern P/Invoke + + var externReturnType = ctx.ReturnValue.Generator.GetNativeType(ctx.ReturnValue); + + if(ctx.ReturnsByRef) + { + externReturnType = ctx.RequiresMarshalling + ? PointerType(externReturnType) + : RefType(externReturnType); + } + + + var handleParam = Parameter(Identifier("handle_")).WithType(TypeSyntaxFactory.IntPtrType); + + return HelperSyntaxFactory.GenerateExternFunction( + library: ctx.Library, + externName: ToExternName(ctx), + externReturnType: externReturnType, + parameters: ctx.Parameters.Select(x => HelperSyntaxFactory.ToForwardInfo(x, true)), + parametersPrefix: handleParam); + } + + private static string ToExternName(ApiMethodStubGenerationContext ctx) + { + var overload = ctx.Symbol.GetAttribute(Constants.OverloadAttributeFQN)?.ConstructorArguments[0].Value as string; + + return ctx.Symbol.GetAttribute(Constants.FunctionAttributeFQN)?.ConstructorArguments[0].Value is string functionName + ? $"{ctx.NativeTypeName}_{functionName}" + : $"{ctx.NativeTypeName}_{StringUtil.FirstCharToLower(ctx.Symbol.Name)}{overload}"; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/Marshalling/MarshallingGeneratorBase.cs b/src/SampSharp.SourceGenerator/Generators/Marshalling/MarshallingGeneratorBase.cs new file mode 100644 index 00000000..49e4daf0 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/Marshalling/MarshallingGeneratorBase.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Marshalling; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Generators.Marshalling; + +public abstract class MarshallingGeneratorBase(MarshalDirection direction) +{ + private const string LocalInvokeSucceeded = "__invokeSucceeded"; + + /// + /// Returns a block with the invocation of the native method including marshalling of parameters and return value. + /// + protected virtual BlockSyntax GetMarshallingBlock(MarshallingStubGenerationContext ctx) + { + return ctx.RequiresMarshalling + ? GenerateInvocationWithMarshalling(ctx) + : GenerateInvocationWithoutMarshalling(ctx); + } + + protected abstract ExpressionSyntax GetInvocation(MarshallingStubGenerationContext ctx); + + private BlockSyntax GenerateInvocationWithoutMarshalling(MarshallingStubGenerationContext ctx) + { + var invoke = GetInvocation(ctx); + + if (ctx.Symbol.ReturnsVoid) + { + return Block(ExpressionStatement(invoke)); + } + + if (ctx.Symbol.ReturnsByRef || ctx.Symbol.ReturnsByRefReadonly) + { + invoke = RefExpression(invoke); + } + + return Block(ReturnStatement(invoke)); + } + + protected static IEnumerable GetInvocationArguments(MarshallingStubGenerationContext ctx) + { + return ctx.Parameters.Select(GetArgumentForPInvokeParameter); + } + + private BlockSyntax GenerateInvocationWithMarshalling(MarshallingStubGenerationContext ctx) + { + return direction == MarshalDirection.ManagedToUnmanaged + ? GenerateInvocationWithMarshallingManagedToUnmanaged(ctx) + : GenerateInvocationWithMarshallingUnmanagedToManaged(ctx); + } + + private BlockSyntax GenerateInvocationWithMarshallingManagedToUnmanaged(MarshallingStubGenerationContext ctx) + { + // The generated method consists of the following content: + // + // LocalsInit + // Setup + // try + // { + // Marshal + // { + // PinnedMarshal + // Invoke + // } + // [[__invokeSucceeded = true;]] + // NotifyForSuccessfulInvoke + // UnmarshalCapture + // Unmarshal + // } + // finally + // { + // if (__invokeSucceeded) + // { + // GuaranteedUnmarshal + // CleanupCalleeAllocated + // } + // CleanupCallerAllocated + // } + // + // return: retVal + + var steps = CollectPhases(ctx); + + var initLocals = List(GenerateInitLocals(ctx)); + + // if callee cleanup or guaranteed unmarshalling is required, we need to keep track of invocation success + var guaranteedStatements = steps.GuaranteedUnmarshal.AddRange(steps.CleanupCallee); + var notify = steps.Notify; + + GenerateGuaranteedBlock(ref guaranteedStatements, ref initLocals, ref notify); + + // chain all pin statements + var invoke = AddReturnValueAssignmentToInvoke(ctx, GetInvocation(ctx)); + var pinnedBlock = ChainPins(steps, invoke); + + // wire up steps + var statements = initLocals + .AddRange(steps.Setup) + .AddRange( + TryCatch( + tryBlock: steps.Marshal + .Add(pinnedBlock) + .AddRange(notify) + .AddRange(steps.UnmarshalCapture) + .AddRange(steps.Unmarshal), + finallyBlock: guaranteedStatements + .AddRange(steps.CleanupCaller))); + + // add return statement if the method returns a value + if (!ctx.Symbol.ReturnsVoid) + { + ExpressionSyntax returnExpression = IdentifierName(MarshallerConstants.LocalReturnValue); + + if (ctx.ReturnsByRef) + { + returnExpression = RefExpression( + PrefixUnaryExpression( + SyntaxKind.PointerIndirectionExpression, + returnExpression)); + } + else if (ctx.Symbol.ReturnType is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.NotAnnotated }) + { + returnExpression = PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, returnExpression); + } + + statements = statements.Add(ReturnStatement(returnExpression)); + } + + return Block(statements); + } + + private BlockSyntax GenerateInvocationWithMarshallingUnmanagedToManaged(MarshallingStubGenerationContext ctx) + { + // NOTE: We support only unmarshalling options for unmanaged to managed marshalling. + // + // LocalsInit + // Setup + // try + // { + // GuaranteedUnmarshal + // UnmarshalCapture + // Unmarshal + // { + // p/invoke + // } + // [[__invokeSucceeded = true;]] + // NotifyForSuccessfulInvoke + // Marshal + // PinnedMarshal + // } + // finally + // { + // CleanupCallerAllocated + // } + // + // return: retVal + + var steps = CollectPhases(ctx); + + if (steps.CleanupCallee.Count > 0) + { + throw new InvalidOperationException("cannot cleanup callee allocated"); + } + + var initLocals = List(GenerateInitLocals(ctx)); + + // if callee cleanup or guaranteed unmarshalling is required, we need to keep track of invocation success + var notify = steps.Notify; + + var invoke = ExpressionStatement(AddReturnValueAssignmentToInvoke(ctx, GetInvocation(ctx))); + + // wire up steps + var statements = initLocals + .AddRange(steps.Setup) + .AddRange( + TryCatch( + tryBlock: steps.UnmarshalCapture + .AddRange(steps.Unmarshal) + .AddRange(steps.GuaranteedUnmarshal) + .Add(invoke) + .AddRange(notify), + finallyBlock: steps.CleanupCaller)); + + // add return statement if the method returns a value + if (!ctx.Symbol.ReturnsVoid) + { + var returnIdentifier = ctx.ReturnValue.Generator.UsesNativeIdentifier + ? ctx.ReturnValue.GetNativeId() + : MarshallerConstants.LocalReturnValue; + + ExpressionSyntax returnExpression = IdentifierName(returnIdentifier); + + if (ctx.ReturnsByRef) + { + returnExpression = RefExpression(returnExpression); + } + else if (ctx.Symbol.ReturnType is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.NotAnnotated }) + { + returnExpression = PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, returnExpression); + } + + statements = statements.Add(ReturnStatement(returnExpression)); + } + + return Block(statements); + } + + private static void GenerateGuaranteedBlock( + ref SyntaxList guarannteedStatements, + ref SyntaxList initLocals, + ref SyntaxList notify) + { + if (guarannteedStatements.Count > 0) + { + // var __invokeSucceeded = false; to the initializer block + initLocals = initLocals.Add(GenerateInvokeSucceededLocal()); + + // insert __invokeSucceeded = true; at the beginning of the notify step + notify = notify.Insert(0, GenerateSetInvokeSucceeded()); + + guarannteedStatements = SingletonList( + IfStatement( + IdentifierName(LocalInvokeSucceeded), + Block(guarannteedStatements))); + } + } + + private static IEnumerable GenerateInitLocals(MarshallingStubGenerationContext ctx) + { + foreach(var p in ctx.Parameters) + { + if (!p.Generator.UsesNativeIdentifier) + { + continue; + } + + if (p.Direction == MarshalDirection.ManagedToUnmanaged) + { + yield return DeclareLocal(p.Generator.GetNativeType(p), p.GetNativeId()); + } + else + { + // UnmanagedToManaged + yield return DeclareLocal(p.ManagedType.TypeName, p.GetManagedId()); + } + } + + if (!ctx.Symbol.ReturnsVoid) + { + var managedReturnType = ctx.ReturnValue.ManagedType.TypeName; + if (ctx.ReturnsByRef) + { + // when marshalling ref-return types are marshalled as pointers + managedReturnType = PointerType(managedReturnType); + } + else if (ctx.Symbol.ReturnType is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.NotAnnotated }) + { + managedReturnType = NullableType(managedReturnType); + } + + yield return + DeclareLocal( + managedReturnType, + ctx.ReturnValue.GetManagedId()); + + if (ctx.ReturnValue.Generator.UsesNativeIdentifier) + { + yield return + DeclareLocal( + ctx.ReturnValue.Generator.GetNativeType(ctx.ReturnValue), + ctx.ReturnValue.GetNativeId()); + } + } + } + + private static LocalDeclarationStatementSyntax GenerateInvokeSucceededLocal() + { + return DeclareLocal(PredefinedType(Token(SyntaxKind.BoolKeyword)), LocalInvokeSucceeded); + } + + private static ExpressionStatementSyntax GenerateSetInvokeSucceeded() + { + return ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(LocalInvokeSucceeded), + LiteralExpression(SyntaxKind.TrueLiteralExpression))); + } + + private static StatementSyntax ChainPins(MarshallingPhases phases, ExpressionSyntax invoke) + { + StatementSyntax pinnedBlock = Block(phases.PinnedMarshal.Add(ExpressionStatement(invoke))); + for (var i = phases.Pin.Count - 1; i >= 0; i--) + { + if (phases.Pin[i] is not FixedStatementSyntax fixedStatement) + { + throw new InvalidOperationException("Pinned statement is not fixed statement"); + } + + pinnedBlock = fixedStatement.WithStatement(pinnedBlock); + } + + return pinnedBlock; + } + + private static SyntaxList TryCatch(SyntaxList tryBlock, SyntaxList finallyBlock) + { + if (finallyBlock.Count == 0) + { + return tryBlock; + } + + return SingletonList( + TryStatement() + .WithBlock(Block(tryBlock)) + .WithFinally( + FinallyClause( + Block(finallyBlock)))); + } + + private static ExpressionSyntax AddReturnValueAssignmentToInvoke(MarshallingStubGenerationContext ctx, ExpressionSyntax invoke) + { + if (!ctx.Symbol.ReturnsVoid) + { + string assignedLocal; + if (ctx.ReturnValue.Direction == MarshalDirection.ManagedToUnmanaged) + { + assignedLocal = ctx.ReturnValue.Generator.UsesNativeIdentifier ? ctx.ReturnValue.GetNativeId() : ctx.ReturnValue.GetManagedId(); + } + else + { + // UnmanagedToManaged + assignedLocal = ctx.ReturnValue.Generator.UsesNativeIdentifier ? ctx.ReturnValue.GetManagedId() : ctx.ReturnValue.GetNativeId(); + } + + invoke = + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(assignedLocal), + invoke); + } + + return invoke; + } + + private static MarshallingPhases CollectPhases(MarshallingStubGenerationContext ctx) + { + var setup = Phase(ctx, MarshalPhase.Setup, MarshallingCodeGenDocumentation.COMMENT_SETUP); + var marshal = Phase(ctx, MarshalPhase.Marshal, MarshallingCodeGenDocumentation.COMMENT_MARSHAL); + var pinnedMarshal = Phase(ctx, MarshalPhase.PinnedMarshal, MarshallingCodeGenDocumentation.COMMENT_PINNED_MARSHAL); + var pin = Phase(ctx, MarshalPhase.Pin, MarshallingCodeGenDocumentation.COMMENT_PIN); + var notify = Phase(ctx, MarshalPhase.NotifyForSuccessfulInvoke, MarshallingCodeGenDocumentation.COMMENT_NOTIFY); + var unmarshalCapture = Phase(ctx, MarshalPhase.UnmarshalCapture, MarshallingCodeGenDocumentation.COMMENT_UNMARSHAL_CAPTURE); + var unmarshal = Phase(ctx, MarshalPhase.Unmarshal, MarshallingCodeGenDocumentation.COMMENT_UNMARSHAL); + var guaranteedUnmarshal = Phase(ctx, MarshalPhase.GuaranteedUnmarshal, MarshallingCodeGenDocumentation.COMMENT_GUARANTEED_UNMARSHAL); + var cleanupCallee = Phase(ctx, MarshalPhase.CleanupCalleeAllocated, MarshallingCodeGenDocumentation.COMMENT_CLEANUP_CALLEE); + var cleanupCaller = Phase(ctx, MarshalPhase.CleanupCallerAllocated, MarshallingCodeGenDocumentation.COMMENT_CLEANUP_CALLER); + + return new MarshallingPhases(setup, marshal, pinnedMarshal, pin, notify, unmarshalCapture, unmarshal, guaranteedUnmarshal, cleanupCallee, cleanupCaller); + } + + private static ArgumentSyntax GetArgumentForPInvokeParameter(IdentifierStubContext ctx) + { + var arg = ctx.GetManagedId(); + if (ctx.Generator.UsesNativeIdentifier) + { + arg = ctx.GetNativeId(); + } + + ExpressionSyntax expr = IdentifierName(arg); + + + if (ctx.RefKind is RefKind.In or RefKind.RefReadOnlyParameter) + { + expr = PrefixUnaryExpression(SyntaxKind.AddressOfExpression, expr); + } + + return HelperSyntaxFactory.WithPInvokeParameterRefToken(Argument(expr), ctx.RefKind); + } + + private static SyntaxList Phase( + MarshallingStubGenerationContext ctx, + MarshalPhase phase, + string? comment) + { + var elements = ctx.Parameters + .SelectMany(x => x.Generator.Generate(phase, x)) + .Concat(ctx.ReturnValue.Generator.Generate(phase, ctx.ReturnValue)); + + var result = List(elements); + + if (comment != null && result.Count > 0) + { + result = result.Replace(result[0], + result[0] + .WithLeadingTrivia(Comment(comment))); + } + + return result; + } + + private record MarshallingPhases( + SyntaxList Setup, + SyntaxList Marshal, + SyntaxList PinnedMarshal, + SyntaxList Pin, + SyntaxList Notify, + SyntaxList UnmarshalCapture, + SyntaxList Unmarshal, + SyntaxList GuaranteedUnmarshal, + SyntaxList CleanupCallee, + SyntaxList CleanupCaller); + +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/NumberedTypeSourceGenerator.cs b/src/SampSharp.SourceGenerator/Generators/NumberedTypeSourceGenerator.cs new file mode 100644 index 00000000..d91686f9 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/NumberedTypeSourceGenerator.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators; + +[Generator] +public class NumberedTypeSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var structDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName(Constants.NumberedTypeGeneratorAttributeFQN, + predicate: static (s, _) => s is StructDeclarationSyntax, transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)); + + var compilationAndStructs = context.CompilationProvider.Combine(structDeclarations.Collect()); + + context.RegisterSourceOutput(compilationAndStructs, static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static StructData GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context) + { + var attributes = new AttributeData[context.Attributes.Length]; + + for (var i = 0; i < context.Attributes.Length; i++) + { + var attribute = context.Attributes[i]; + var fieldName = attribute.ConstructorArguments[0].Value as string ?? "unknown"; + var value = (int)attribute.ConstructorArguments[1].Value!; + + attributes[i] = new AttributeData(fieldName, value); + } + + return new StructData((StructDeclarationSyntax)context.TargetNode, attributes); + } + + private static void Execute(Compilation compilation, ImmutableArray datas, SourceProductionContext context) + { + foreach (var data in datas) + { + foreach (var attribute in data.Attributes) + { + + var model = compilation.GetSemanticModel(data.Declaration.SyntaxTree); + var symbol = model.GetDeclaredSymbol(data.Declaration)!; + + var newStructName = GetNewStructName(symbol.Name, attribute.Value); + var source = GenerateNewStructSource(symbol, newStructName, attribute.Field, attribute.Value, compilation); + + context.AddSource($"{newStructName}.g.cs", source); + } + } + } + + private static string GetNewStructName(string originalName, int value) + { + var numberIndex = originalName.Length - 1; + while (numberIndex >= 0 && char.IsDigit(originalName[numberIndex])) + { + numberIndex--; + } + + var baseName = originalName.Substring(0, numberIndex + 1); + return $"{baseName}{value}"; + } + + private static SourceText GenerateNewStructSource(INamedTypeSymbol symbol, string newStructName, string fieldName, int value, Compilation compilation) + { + // Get the original struct declaration + if (symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not StructDeclarationSyntax structDeclarationSyntax) + { + throw new InvalidOperationException("Unable to retrieve the struct declaration syntax."); + } + + // Get the struct with fully qualified types + var qualifiedStruct = FullyQualifyTypes(structDeclarationSyntax, compilation); + + // Rename the struct + var updatedStruct = qualifiedStruct.WithIdentifier(Identifier(newStructName)); + + // Enable nullable if required + if (compilation.Options.NullableContextOptions == NullableContextOptions.Enable) + { + + updatedStruct = updatedStruct.WithOpenBraceToken( + updatedStruct.OpenBraceToken.WithTrailingTrivia(TriviaList( + Trivia( + NullableDirectiveTrivia( + Token(SyntaxKind.EnableKeyword), + true))))); + } + + // Replace the constant field value + var members = updatedStruct.Members.Select(member => + { + if (member is FieldDeclarationSyntax fieldDeclaration && fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword) && + fieldDeclaration.Declaration.Variables.Any(v => v.Identifier.Text == fieldName)) + { + // Update the constant value + var updatedVariables = fieldDeclaration.Declaration.Variables.Select(variable => + { + if (variable.Identifier.Text == fieldName) + { + return variable.WithInitializer( + EqualsValueClause(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(value)))); + } + + return variable; + }); + + var updatedDeclaration = fieldDeclaration.Declaration.WithVariables(SeparatedList(updatedVariables)); + return fieldDeclaration.WithDeclaration(updatedDeclaration); + } + + if (member is ConstructorDeclarationSyntax constructor) + { + // Replace the type name in constructors + var updatedConstructor = constructor.WithIdentifier(Identifier(newStructName)); + return updatedConstructor; + } + + return member; // Keep other members unchanged + }); + + // Update the struct with modified members + updatedStruct = updatedStruct.WithMembers(List(members)); + + // Ensure attributes have fully qualified names + var updatedAttributes = updatedStruct.AttributeLists.Select(attributeList => + AttributeList(SeparatedList( + attributeList.Attributes.Where(attribute => !attribute.Name.ToString().Contains("NumberedTypeGenerator"))))) + .Where(x => x.Attributes.Count > 0); + + updatedAttributes = updatedAttributes.Append(AttributeFactory.GeneratedCode()); + + updatedStruct = updatedStruct + .WithAttributeLists( + List(updatedAttributes)) + .WithLeadingTrivia( + structDeclarationSyntax.GetLeadingTrivia()); + + // Generate the updated namespace and code + var namespaceDeclaration = + NamespaceDeclaration( + ParseName( + symbol.ContainingNamespace.ToDisplayString())) + .AddMembers(updatedStruct); + + var unit = CompilationUnit() + .AddMembers(namespaceDeclaration) + .WithLeadingTrivia( + TriviaFactory.AutoGeneratedComment(), + TriviaFactory.NullableEnable()); + + var sourceText = unit.NormalizeWhitespace(elasticTrivia: true) + .GetText(Encoding.UTF8); + + return sourceText; + } + + private static StructDeclarationSyntax FullyQualifyTypes(StructDeclarationSyntax structDeclaration, Compilation compilation) + { + // Traverse the original tree and resolve symbols + var semanticModel = compilation.GetSemanticModel(structDeclaration.SyntaxTree); + + // Create a new rewriter that works on symbols tied to the original syntax tree + var rewriter = new FullyQualifiedTypeRewriter(semanticModel); + return (StructDeclarationSyntax)rewriter.Visit(structDeclaration); + } + + + private readonly record struct StructData(StructDeclarationSyntax Declaration, AttributeData[] Attributes); + + private readonly record struct AttributeData(string Field, int Value); +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/OpenMpApiSourceGenerator.cs b/src/SampSharp.SourceGenerator/Generators/OpenMpApiSourceGenerator.cs new file mode 100644 index 00000000..5a7f5ac0 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/OpenMpApiSourceGenerator.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Generators.ApiStructs; +using SampSharp.SourceGenerator.Helpers; +using SampSharp.SourceGenerator.Marshalling; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators; + +/// +/// This source generator generates the marshalling interop methods for open.mp API structs. The generator generates the +/// following: +/// +/// P/Invoke every partial method in the interface with marshalling of every parameter and return value +/// For every "implementing" interface specified in the CodeGen attribute generate the following: +/// +/// Pass-through implementations of all methods in the implementing interface +/// +/// +/// +/// +[Generator] +public class OpenMpApiSourceGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributedStructs = context.SyntaxProvider.ForAttributeWithMetadataName( + Constants.ApiAttributeFQN, + static (s, _) => s is StructDeclarationSyntax str && str.IsPartial(), + static (ctx, ct) => GetStructDeclaration(ctx, ct)) + .Where(x => x is not null); + + context.RegisterSourceOutput(attributedStructs, (ctx, info) => + { + var unit = GenerateUnit(info); + + var sourceText = unit.NormalizeWhitespace(elasticTrivia: true) + .GetText(Encoding.UTF8); + + ctx.AddSource($"{info!.Symbol.Name}.g.cs", sourceText); + }); + } + + private static CompilationUnitSyntax GenerateUnit(StructStubGenerationContext? info) + { + var modifiers = info!.Syntax.Modifiers; + + if (!info.Syntax.HasModifier(SyntaxKind.UnsafeKeyword)) + { + modifiers = modifiers.Insert(modifiers.IndexOf(SyntaxKind.PartialKeyword), Token(SyntaxKind.UnsafeKeyword)); + } + + var structDeclaration = StructDeclaration(info.Syntax.Identifier) + .WithModifiers(modifiers) + .WithMembers(GenerateStructMembers(info)) + .WithBaseList( + BaseList( + SeparatedList( + GenerateBaseTypes(info)))) + .WithAttributeLists( + List([ + AttributeFactory.GeneratedCode(), + AttributeFactory.SkipLocalsInit() + ])); + + if(info.Syntax.TypeParameterList != null) + { + structDeclaration = structDeclaration.WithTypeParameterList(info.Syntax.TypeParameterList); + } + + var namespaceDeclaration = NamespaceDeclaration( + ParseName(info.Symbol.ContainingNamespace.ToDisplayString())) + .AddMembers(structDeclaration); + + var unit = CompilationUnit() + .AddMembers(namespaceDeclaration) + .WithLeadingTrivia( + TriviaFactory.AutoGeneratedComment(), + TriviaFactory.NullableEnable()); + return unit; + } + + /// + /// Returns the base types for the generated struct implementation. + /// + private static IEnumerable GenerateBaseTypes(StructStubGenerationContext ctx) + { + var self = ParseTypeName(ctx.Symbol.Name); + + if (ctx.Syntax.TypeParameterList != null) + { + self = GenericName( + Identifier(ctx.Symbol.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + ctx.Syntax.TypeParameterList.Parameters.Select(x => IdentifierName(x.Identifier))))); + } + + + IEnumerable result = [ + SimpleBaseType( + GenericType( + Constants.IEquatableFQN, + self)) + ]; + + if(!ctx.IsPartial) + { + result = result + .Append( + SimpleBaseType( + ParseTypeName($"{TypeNameGlobal((ITypeSymbol)ctx.Symbol)}.{InterfaceMemberGenerator.InterfaceName}"))) + .Concat( + ctx.ImplementingTypes.Select( + x => SimpleBaseType( + ParseTypeName( + $"{TypeNameGlobal(x.Type.Symbol)}.{InterfaceMemberGenerator.InterfaceName}")))); + } + else + { + result = result.Append(SimpleBaseType(ParseTypeName($"{TypeNameGlobal(Constants.UnmanagedInterfaceFQN)}"))); + } + return result; + } + + /// + /// Returns the members for the struct implementation. + /// + private static SyntaxList GenerateStructMembers(StructStubGenerationContext ctx) + { + return List([ + ..CreationMembersGenerator.GenerateCreationMembers(ctx), + ..CastMembersGenerator.GenerateCastMembers(ctx), + ..EqualityMembersGenerator.GenerateEqualityMembers(ctx), + ..ForwardingMembersGenerator.GenerateImplementingTypeMembers(ctx), + ..NativeMembersGenerator.GenerateNativeMethods(ctx), + ..InterfaceMemberGenerator.GenerateNativeMethods(ctx) + ]); + } + + /// + /// Returns the struct generation context for a code gen context. + /// + private static StructStubGenerationContext? GetStructDeclaration(GeneratorAttributeSyntaxContext ctx, CancellationToken cancellationToken) + { + var targetNode = (StructDeclarationSyntax)ctx.TargetNode; + if (ctx.TargetSymbol is not INamedTypeSymbol symbol) + { + return null; + } + + var attribute = ctx.Attributes.Single(); + + var isPartial = symbol.GetAttribute(Constants.ApiPartialAttributeFQN) != null; + + var library = attribute.NamedArguments.FirstOrDefault(x => x.Key == "Library") + .Value.Value as string ?? "SampSharp"; + + var nativeTypeName = attribute.NamedArguments.FirstOrDefault(x => x.Key == "NativeTypeName") + .Value.Value as string ?? symbol.Name; + + var wellKnownMarshallerTypes = WellKnownMarshallerTypes.Create(ctx.SemanticModel.Compilation); + var ctxFactory = new IdentifierStubContextFactory(wellKnownMarshallerTypes); + + + var implementingTypes = new List(); + AddImplementingTypes(implementingTypes, attribute, [], [], targetNode); + + // filter methods: partial, non-static, non-generic + var methods = targetNode.Members + .OfType() + .Where(x => x.IsPartial() && !x.HasModifier(SyntaxKind.StaticKeyword) && x.TypeParameterList == null) + .Select(methodDeclaration => ctx.SemanticModel.GetDeclaredSymbol(methodDeclaration, cancellationToken) is { } methodSymbol + ? (methodDeclaration, methodSymbol) + : (null, null)) + .Where(x => x.methodSymbol != null) + .Select(method => + { + var parameters = method.methodSymbol!.Parameters + .Select(parameter => ctxFactory.Create(parameter, MarshalDirection.ManagedToUnmanaged)) + .ToArray(); + + var returnValueContext = ctxFactory.Create(method.methodSymbol, MarshalDirection.ManagedToUnmanaged); + + if (returnValueContext.Shape != MarshallerShape.None && (method.methodSymbol.ReturnsByRef || method.methodSymbol.ReturnsByRefReadonly)) + { + // marshalling return-by-ref not supported. + return null; + } + + return new ApiMethodStubGenerationContext( + method.methodDeclaration!, + method.methodSymbol, + parameters, + returnValueContext, + library, + nativeTypeName); + }) + .Where(x => x != null) + .ToArray(); + + var rewriter = new FullyQualifiedTypeRewriter(ctx.SemanticModel); + + var publicMembers = targetNode.Members.Where(x => x.HasModifier(SyntaxKind.PublicKeyword)) + .Select(MemberDeclarationSyntax (member) => (MemberDeclarationSyntax)rewriter.Visit(member)) + .ToList(); + + + return new StructStubGenerationContext(symbol, targetNode, methods!, implementingTypes.ToArray(), isPartial, library, publicMembers); + } + + private static void AddImplementingTypes(List implementingTypes, AttributeData attribute, DefiniteType[] castPath, List trace, + StructDeclarationSyntax structDecl) + { + var typesImplementedHere = attribute.ConstructorArguments[0].Values + .Select(x => (INamedTypeSymbol)x.Value!) + .ToArray(); + + foreach (var type in typesImplementedHere) + { + TypeSyntax syntax; + if (type.IsGenericType && structDecl.TypeParameterList != null) + { + syntax = GenericName( + Identifier(type.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + structDecl.TypeParameterList.Parameters.Select(x => IdentifierName(x.Identifier))))); + } + else + { + syntax = TypeNameGlobal(type); + } + + var def = new DefiniteType(type, syntax); + + if (implementingTypes.Any(x => x.Type.Symbol.IsSame(type))) + { + continue; + } + + implementingTypes.Add(new ImplementingType(def, [..castPath, def])); + } + + foreach (var type in typesImplementedHere) + { + var nestedAttribute = type.GetAttribute(Constants.ApiAttributeFQN); + + if (nestedAttribute == null) + { + continue; + } + + if (trace.Any(x => x.IsSame(type))) + { + continue; + } + + trace.Add(type); + + var syntax = TypeNameGlobal(type); + var def = new DefiniteType(type, syntax); + + AddImplementingTypes(implementingTypes, nestedAttribute!, [..castPath, def], trace, structDecl); + + } + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generators/OpenMpEventHandlerSourceGenerator.cs b/src/SampSharp.SourceGenerator/Generators/OpenMpEventHandlerSourceGenerator.cs new file mode 100644 index 00000000..6edc7bb5 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generators/OpenMpEventHandlerSourceGenerator.cs @@ -0,0 +1,438 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Generators.Marshalling; +using SampSharp.SourceGenerator.Generics; +using SampSharp.SourceGenerator.Helpers; +using SampSharp.SourceGenerator.Marshalling; +using SampSharp.SourceGenerator.Models; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.HelperSyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.Generators; + +/// +/// This source generator generates event handler interfaces for open.mp events. The generated interface contains a +/// default implementation for IncreaseReference/DecreaseReference methods, which are used to creating an unmanaged +/// event handler for the managed event handler. The implementation invokes the native function +/// `{EventHandlerName}Impl_create`/_delete to create the unmanaged event handler. The create call will include native +/// handles of delegate functions for every event method in the interface. +/// +[Generator] +public class OpenMpEventHandlerSourceGenerator : IIncrementalGenerator +{ + private const string ClassEventHandlerMarshaller = "EventHandlerMarshaller"; + private const string LocalHandle = "handle"; + private const string ParamHandle = "handle"; + private const string MethodPInvoke = "__PInvoke"; + private static readonly SyntaxToken _idToken = Identifier("Marshaller"); + private static readonly SyntaxToken _idInstance = Identifier("Instance"); + + private static readonly ApiEventDelegateMarshallingGenerator _marshallingGenerator = new(); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributedInterfaces = context.SyntaxProvider + .ForAttributeWithMetadataName( + Constants.EventHandlerAttributeFQN, + static (s, _) => s is InterfaceDeclarationSyntax str && str.IsPartial(), + static(ctx, ct) => GetInterfaceDeclaration(ctx, ct)) + .Where(x => x is not null); + + + context.RegisterSourceOutput(attributedInterfaces, (ctx, info) => + { + var interfaceDeclaration = InterfaceDeclaration(info!.Syntax.Identifier) + .WithModifiers(info.Syntax.Modifiers) + .WithMembers(GenerateInterfaceMembers(info)) + .WithBaseList( + BaseList(SingletonSeparatedList( + SimpleBaseType( + GenericType( + Constants.EventHandlerFQN, + ParseTypeName(info.Syntax.Identifier.ToString())))))) + .WithAttributeLists(List([ + AttributeFactory.GeneratedCode() + ])); + + if (info.Syntax.TypeParameterList != null) + { + interfaceDeclaration = interfaceDeclaration.WithTypeParameterList(info.Syntax.TypeParameterList); + } + + var namespaceDeclaration = NamespaceDeclaration( + ParseName( + info.Symbol.ContainingNamespace.ToDisplayString())) + .AddMembers(interfaceDeclaration); + + var unit = CompilationUnit() + .AddMembers(namespaceDeclaration) + .WithLeadingTrivia( + TriviaFactory.AutoGeneratedComment()); + + var sourceText = unit.NormalizeWhitespace() + .GetText(Encoding.UTF8); + + ctx.AddSource($"{info.Symbol.Name}.g.cs", sourceText); + }); + } + + private static SyntaxList GenerateInterfaceMembers(EventInterfaceStubGenerationContext ctx) + { + return List([ + GenerateManagerPropertyMember(ctx), + GenerateManagerClass(ctx) + ]); + } + + private static PropertyDeclarationSyntax GenerateManagerPropertyMember(EventInterfaceStubGenerationContext ctx) + { + return PropertyDeclaration( + AliasQualifiedName( + IdentifierName( + Token(SyntaxKind.GlobalKeyword)), + GenericName( + Identifier(Constants.IEventHandlerMarshallerFQN)) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList(ctx.Type)))), + _idToken) + .WithModifiers( + TokenList( + Token(SyntaxKind.StaticKeyword))) + .WithExplicitInterfaceSpecifier( + ExplicitInterfaceSpecifier( + AliasQualifiedName( + IdentifierName( + Token(SyntaxKind.GlobalKeyword)), + GenericName( + Identifier(Constants.EventHandlerFQN)) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList(ctx.Type)))))) + .WithExpressionBody( + ArrowExpressionClause( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(ClassEventHandlerMarshaller), + IdentifierName("Instance")))) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)); + } + + private static ClassDeclarationSyntax GenerateManagerClass(EventInterfaceStubGenerationContext ctx) + { + return ClassDeclaration(Identifier(ClassEventHandlerMarshaller)) + .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))) + .WithBaseList(BaseList( + SingletonSeparatedList( + SimpleBaseType( + GenericType( + Constants.EventHandlerMarshallerFQN, + ParseTypeName(ctx.Symbol.Name)))))) + .WithLeadingTrivia(TriviaFactory.Docs([ + XmlText("Manages the marshalling of native event handlers for "), + TriviaFactory.SeeElement( + ParseTypeName(ctx.Symbol.Name)), + XmlText(".") + ], [])) + .WithMembers(GenerateManagerMembers(ctx)); + } + + private static SyntaxList GenerateManagerMembers(EventInterfaceStubGenerationContext ctx) + { + return SingletonList(GenerateInstancePropertyMember()) + .AddRange(GenerateDelegateMembers(ctx)) + .AddRange([ + GenerateCreateMember(ctx), + GenerateFreeMember(ctx) + ]); + } + + private static PropertyDeclarationSyntax GenerateInstancePropertyMember() + { + return PropertyDeclaration( + IdentifierName(ClassEventHandlerMarshaller), + _idInstance) + .WithModifiers( + TokenList( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword))) + .WithAccessorList( + AccessorList( + SingletonList( + AccessorDeclaration( + SyntaxKind.GetAccessorDeclaration) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken))))) + .WithInitializer( + EqualsValueClause( + ImplicitObjectCreationExpression())) + .WithLeadingTrivia( + TriviaFactory.Docs([ + XmlText("Gets the singleton instance of the "), + TriviaFactory.SeeElement( + IdentifierName(ClassEventHandlerMarshaller)), + XmlText("."), + ], [])) + .WithSemicolonToken( + Token(SyntaxKind.SemicolonToken)); + } + + private static MethodDeclarationSyntax GenerateCreateMember(EventInterfaceStubGenerationContext ctx) + { + var delegateVars = ctx.Methods.Select(method => + VariableDeclarator( + Identifier($"__{method.Symbol.Name}_delegate")) + .WithInitializer( + EqualsValueClause( + GenerateDelegateExpression(method)))); + + + var functionPointerVars = ctx.Methods.Select(method => + VariableDeclarator( + Identifier($"__{method.Symbol.Name}_ptr")) + .WithInitializer( + EqualsValueClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + TypeNameGlobal(Constants.MarshalFQN), + IdentifierName(nameof(Marshal.GetFunctionPointerForDelegate)))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName($"__{method.Symbol.Name}_delegate")))))))); + + // protected override (nint, object) Create(HandlerType handler) + return MethodDeclaration( + TupleType( + SeparatedList([ + TupleElement(IntPtrType), + TupleElement(ObjectType) + ])), + Identifier("Create")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ProtectedKeyword), + Token(SyntaxKind.OverrideKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier("handler")) + .WithType(ParseTypeName(ctx.Symbol.Name))))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithBody( + Block(List([ + // Delegate __x_delegate = (Method_)handler.Method, ...; + LocalDeclarationStatement( + VariableDeclaration( + TypeNameGlobal(Constants.DelegateFQN)) + .WithVariables( + SeparatedList(delegateVars))), + + // nint __x_ptr = Marshal.GetFunctionPointerForDelegate(__x_delegate), ...; + LocalDeclarationStatement( + VariableDeclaration( + IntPtrType) + .WithVariables( + SeparatedList(functionPointerVars))), + + // object[] data == [ __x_delegate, ... ] ; + LocalDeclarationStatement( + VariableDeclaration( + ArrayType( + PredefinedType( + Token(SyntaxKind.ObjectKeyword))) + .WithRankSpecifiers( + SingletonList( + ArrayRankSpecifier( + SingletonSeparatedList( + OmittedArraySizeExpression()))))) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier("data")) + .WithInitializer( + EqualsValueClause( + CollectionExpression( + SeparatedList( + ctx.Methods.Select(method => + ExpressionElement( + IdentifierName($"__{method.Symbol.Name}_delegate"))) + ))))))), + + // nint handle = __PInvoke(__x_ptr, ...); + LocalDeclarationStatement( + VariableDeclaration( + IntPtrType) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier(LocalHandle)) + .WithInitializer( + EqualsValueClause( + InvocationExpression( + IdentifierName(MethodPInvoke)) + .WithArgumentList( + ArgumentList( + SeparatedList(ctx.Methods.Select(method => + Argument( + IdentifierName($"__{method.Symbol.Name}_ptr"))))))))))), + + // return (handle, data); + ReturnStatement( + TupleExpression( + SeparatedList([ + Argument( + IdentifierName(LocalHandle)), + Argument( + IdentifierName("data"))]))), + + // static extern nint __PInvoke(...); + GenerateExternFunctionCreate(ctx) + ]))); + } + + private static ExpressionSyntax GenerateDelegateExpression(MarshallingStubGenerationContext method) + { + return _marshallingGenerator.GenerateDelegateExpression(method); + } + + private static MethodDeclarationSyntax GenerateFreeMember(EventInterfaceStubGenerationContext ctx) + { + return MethodDeclaration( + PredefinedType( + Token(SyntaxKind.VoidKeyword)), + Identifier("Free")) + .WithModifiers( + TokenList( + Token(SyntaxKind.ProtectedKeyword), + Token(SyntaxKind.OverrideKeyword))) + .WithParameterList( + ParameterList( + SingletonSeparatedList( + Parameter( + Identifier(ParamHandle)) + .WithType(IntPtrType)))) + .WithLeadingTrivia( + TriviaFactory.InheritDoc()) + .WithBody( + Block( + ExpressionStatement( + InvocationExpression( + IdentifierName(MethodPInvoke)) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName(ParamHandle)))))), + GenerateExternFunctionDelete(ctx) + )); + } + + private static IEnumerable GenerateDelegateMembers(EventInterfaceStubGenerationContext ctx) + { + return ctx.Methods.Select(method => + { + var parameters = ToParameterListSyntax([], method.Parameters.Select(x => ToForwardInfo(x))); + var returnType = method.ReturnValue.Generator.GetNativeType(method.ReturnValue); + + return DelegateDeclaration( + returnType, + Identifier($"{method.Symbol.Name}_")) + .WithModifiers( + TokenList( + Token(SyntaxKind.PrivateKeyword))) + .WithParameterList(parameters); + }); + } + + private static LocalFunctionStatementSyntax GenerateExternFunctionCreate(EventInterfaceStubGenerationContext ctx) + { + var parameters = ctx.Methods.Select(x => new ParamForwardInfo($"_{x.Symbol.Name}", IntPtrType, RefKind.None)); + + return GenerateExternFunction( + library: ctx.Library, + externName: $"{ctx.NativeTypeName}Impl_create", + externReturnType: IntPtrType, + parameters: parameters); + } + + private static LocalFunctionStatementSyntax GenerateExternFunctionDelete(EventInterfaceStubGenerationContext ctx) + { + return GenerateExternFunction( + library: ctx.Library, + externName: $"{ctx.NativeTypeName}Impl_delete", + externReturnType: PredefinedType(Token(SyntaxKind.VoidKeyword)), + parameters: [new ParamForwardInfo("ptr", IntPtrType, RefKind.None)]); + } + + /// + /// Returns a context for the generation of the interface declaration. + /// + private static EventInterfaceStubGenerationContext? GetInterfaceDeclaration(GeneratorAttributeSyntaxContext ctx, CancellationToken cancellationToken) + { + var targetNode = (InterfaceDeclarationSyntax)ctx.TargetNode; + + if (ctx.TargetSymbol is not INamedTypeSymbol symbol) + { + return null; + } + + var attribute = ctx.Attributes.Single(); + + var library = attribute.NamedArguments.FirstOrDefault(x => x.Key == "Library") + .Value.Value as string ?? "SampSharp"; + + var nativeTypeName = attribute.NamedArguments.FirstOrDefault(x => x.Key == "NativeTypeName") + .Value.Value as string ?? (symbol.Name.StartsWith("I") ? symbol.Name.Substring(1) : symbol.Name); + + var wellKnownMarshallerTypes = WellKnownMarshallerTypes.Create(ctx.SemanticModel.Compilation); + var ctxFactory = new IdentifierStubContextFactory(wellKnownMarshallerTypes); + + + // filter methods: non-static + var methods = InterfaceResolver.GetInterfaces(ctx.SemanticModel, targetNode) + .SelectMany(iface => iface.GetInstanceMethods()) + .Select(x => + { + var model = ctx.SemanticModel; + if (x.Interface.Syntax.SyntaxTree != model.SyntaxTree) + { + model = model.Compilation.GetSemanticModel(x.Interface.Syntax.SyntaxTree); + } + + return model.GetDeclaredSymbol(x.Method, cancellationToken) is { } methodSymbol ? (method: x, methodSymbol) : default; + }) + .Where(x => x.methodSymbol != null) + .Select(x => + { + var parameters = x.methodSymbol!.Parameters.Select(parameter => ctxFactory.Create(parameter, x.method.Interface.Resolve(parameter.Type), MarshalDirection.UnmanagedToManaged)) + .ToArray(); + + var returnValueContext = ctxFactory.Create(x.methodSymbol, x.method.Interface.Resolve(x.methodSymbol.ReturnType), MarshalDirection.ManagedToUnmanaged); + if (returnValueContext.Shape != MarshallerShape.None && (x.methodSymbol.ReturnsByRef || x.methodSymbol.ReturnsByRefReadonly)) + { + // marshalling return-by-ref not supported. + return null; + } + + return new MarshallingStubGenerationContext(x.methodSymbol, parameters, returnValueContext); + }) + .Where(x => x != null) + .ToArray(); + + return new EventInterfaceStubGenerationContext(symbol, targetNode, methods!, library, nativeTypeName); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generics/InterfaceResolver.cs b/src/SampSharp.SourceGenerator/Generics/InterfaceResolver.cs new file mode 100644 index 00000000..bbd143f1 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generics/InterfaceResolver.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Generics; + +public class InterfaceResolver +{ + public static List GetInterfaces(SemanticModel semanticModel, InterfaceDeclarationSyntax first) + { + var result = new List(); + GetInterfaces(semanticModel, first, result, null); + return result; + } + + private static void GetInterfaces(SemanticModel semanticModel, InterfaceDeclarationSyntax current, List result, Dictionary? typeSymbols) + { + result.Add(new ResolvedInterface(current, typeSymbols)); + + if (current.BaseList == null) + { + return; + } + + foreach (var baseType in current.BaseList.Types) + { + var interfaceTypeSyntax = baseType.Type; + + var typeSymbol = semanticModel.GetSymbolInfo(interfaceTypeSyntax).Symbol; + if (typeSymbol is INamedTypeSymbol { TypeKind: TypeKind.Interface } namedTypeSymbol) + { + foreach (var reference in namedTypeSymbol.DeclaringSyntaxReferences) + { + if (reference.GetSyntax() is InterfaceDeclarationSyntax iface) + { + Dictionary? dict = null; + + if (namedTypeSymbol.TypeParameters.Length > 0) + { + dict = new Dictionary(); + var index = 0; + foreach (var sym in namedTypeSymbol.TypeParameters) + { + dict[sym.Name] = namedTypeSymbol.TypeArguments[index++]; + } + + } + + GetInterfaces(semanticModel, iface, result, dict); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generics/ResolvedInterface.cs b/src/SampSharp.SourceGenerator/Generics/ResolvedInterface.cs new file mode 100644 index 00000000..df5c3068 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generics/ResolvedInterface.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Helpers; + +namespace SampSharp.SourceGenerator.Generics; + +public readonly record struct ResolvedInterface(InterfaceDeclarationSyntax Syntax, Dictionary? TypeArguments) +{ + public ITypeSymbol Resolve(ITypeSymbol type) + { + if (type.TypeKind == TypeKind.TypeParameter && TypeArguments != null) + { + if (TypeArguments.TryGetValue(type.Name, out var result)) + { + return result; + } + } + + return type; + } + + public IEnumerable GetInstanceMethods() + { + var iface = this; + return Syntax.Members.OfType() + .Select(m => new ResolvedInterfaceMethod(m, iface)) + .Where(x => !x.Method.HasModifier(SyntaxKind.StaticKeyword)); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Generics/ResolvedInterfaceMethod.cs b/src/SampSharp.SourceGenerator/Generics/ResolvedInterfaceMethod.cs new file mode 100644 index 00000000..f3a25ebf --- /dev/null +++ b/src/SampSharp.SourceGenerator/Generics/ResolvedInterfaceMethod.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Generics; + +public readonly record struct ResolvedInterfaceMethod(MethodDeclarationSyntax Method, ResolvedInterface Interface); \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Helpers/EnumerableExtensions.cs b/src/SampSharp.SourceGenerator/Helpers/EnumerableExtensions.cs new file mode 100644 index 00000000..73ab8579 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Helpers/EnumerableExtensions.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace SampSharp.SourceGenerator.Helpers; + +public static class EnumerableExtensions +{ + public static IEnumerable WhereNotNull(this IEnumerable source) where T : class + { + foreach (var item in source) + { + if (item != null) + { + yield return item; + } + } + } +} diff --git a/src/SampSharp.SourceGenerator/Helpers/StringUtil.cs b/src/SampSharp.SourceGenerator/Helpers/StringUtil.cs new file mode 100644 index 00000000..91cb049c --- /dev/null +++ b/src/SampSharp.SourceGenerator/Helpers/StringUtil.cs @@ -0,0 +1,9 @@ +namespace SampSharp.SourceGenerator.Helpers; + +public static class StringUtil +{ + public static string FirstCharToLower(string value) + { + return $"{char.ToLowerInvariant(value[0])}{value.Substring(1)}"; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Helpers/SymbolExtensions.cs b/src/SampSharp.SourceGenerator/Helpers/SymbolExtensions.cs new file mode 100644 index 00000000..bcaaa03f --- /dev/null +++ b/src/SampSharp.SourceGenerator/Helpers/SymbolExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Helpers; + +public static class SymbolExtensions +{ + private static readonly SymbolDisplayFormat _fullyQualifiedFormatWithoutGlobal = + SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining); + + public static bool IsPartial(this MemberDeclarationSyntax syntax) + { + return HasModifier(syntax, SyntaxKind.PartialKeyword); + } + + public static bool HasModifier(this MemberDeclarationSyntax syntax, SyntaxKind modifier) + { + return syntax.Modifiers.Any(m => m.IsKind(modifier)); + } + + public static IEnumerable GetAttributes(this ISymbol symbol, string attributeName) + { + return symbol.GetAttributes().GetAttributes(attributeName); + } + + public static AttributeData? GetAttribute(this ISymbol symbol, string attributeName) + { + return symbol.GetAttributes(attributeName) + .FirstOrDefault(); + } + + public static AttributeData? GetReturnTypeAttribute(this IMethodSymbol symbol, string attributeName) + { + return symbol.GetReturnTypeAttributes() + .GetAttributes(attributeName) + .FirstOrDefault(); + } + + public static bool IsSame(this ITypeSymbol symbol, string typeFQN) + { + return string.Equals(symbol.ToDisplayString(_fullyQualifiedFormatWithoutGlobal), typeFQN, StringComparison.Ordinal); + } + + public static bool IsSame(this ISymbol symbol, ISymbol other) + { + return SymbolEqualityComparer.Default.Equals(symbol, other); + } + + private static IEnumerable GetAttributes(this ImmutableArray attribute, string attributeName) + { + return attribute + .Where(x => + string.Equals( + x.AttributeClass?.ToDisplayString(_fullyQualifiedFormatWithoutGlobal), + attributeName, + StringComparison.Ordinal + ) + ); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/CustomMarshalGeneratorFactory.cs b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshalGeneratorFactory.cs new file mode 100644 index 00000000..5a8d51a5 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshalGeneratorFactory.cs @@ -0,0 +1,20 @@ +using SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +namespace SampSharp.SourceGenerator.Marshalling; + +public static class CustomMarshalGeneratorFactory +{ + public static IMarshalShapeGenerator Create(MarshallerShape shape, bool? stateful) + { + return !stateful.HasValue || shape == MarshallerShape.None // fast path + ? EmptyMarshalShapeGenerator.Instance + : GetFactory(stateful.Value).Create(shape); + } + + private static ICustomMarshalGeneratorFactoryStrategy GetFactory(bool stateful) + { + return stateful + ? StatefulCustomMarshalGeneratorFactoryStrategy.Instance + : StatelessCustomMarshalGeneratorFactoryStrategy.Instance; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerInfo.cs b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerInfo.cs new file mode 100644 index 00000000..e3192a2f --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerInfo.cs @@ -0,0 +1,15 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +/// +/// Information about a marshaller mode as provided by the CustomMarshallerAttribute on the marshaller entry point. +/// +/// The managed type to marshal. +/// The marshalling mode this applies to. +/// The type used for marshalling. +public record CustomMarshallerInfo(ManagedType ManagedType, MarshalMode MarshalMode, ManagedType MarshallerType) +{ + public bool IsStateful => MarshallerType.Symbol is { IsStatic: false, IsValueType: true }; + public bool IsStateless => MarshallerType.Symbol.IsStatic; + + public bool IsValid => IsStateful || IsStateless; +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerTypeDetector.cs b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerTypeDetector.cs new file mode 100644 index 00000000..8b1cde46 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/CustomMarshallerTypeDetector.cs @@ -0,0 +1,196 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using SampSharp.SourceGenerator.Helpers; + +namespace SampSharp.SourceGenerator.Marshalling; + +/// +/// Provides methods for finding the best fitting custom marshaller. +/// +public class CustomMarshallerTypeDetector +{ + private readonly WellKnownMarshallerTypes _wellKnownMarshallerTypes; + + public CustomMarshallerTypeDetector(WellKnownMarshallerTypes wellKnownMarshallerTypes) + { + _wellKnownMarshallerTypes = wellKnownMarshallerTypes; + } + + /// + /// Gets the best fitting custom marshaller for the specified parameter. + /// + public CustomMarshallerInfo? GetCustomMarshaller(IParameterSymbol parameter, ITypeSymbol type, MarshalDirection direction) + { + var marshalUsing = parameter.GetAttribute(Constants.MarshalUsingAttributeFQN); + var typeMarshaller = type.GetAttribute(Constants.NativeMarshallingAttributeFQN); + + var entryPoint = GetEntryPoint(typeMarshaller, marshalUsing, type); + + return entryPoint == null + ? null + : GetCustomMarshaller(entryPoint, type, direction, parameter.RefKind); + } + + /// + /// Gets the best fitting custom marshaller for the return value of the specified method. + /// + public CustomMarshallerInfo? GetCustomMarshaller(IMethodSymbol method, ITypeSymbol type, MarshalDirection direction) + { + if (method.ReturnsVoid) + { + return null; + } + + var marshalUsing = method.GetReturnTypeAttribute(Constants.MarshalUsingAttributeFQN); + var typeMarshaller = type.GetAttribute(Constants.NativeMarshallingAttributeFQN); + + var entryPoint = GetEntryPoint(typeMarshaller, marshalUsing, type); + + return entryPoint == null + ? null + : GetCustomMarshaller(entryPoint, type, direction, RefKind.Out); + } + + private static CustomMarshallerInfo? GetCustomMarshaller( + ITypeSymbol entryPoint, + ITypeSymbol managedType, + MarshalDirection direction, + RefKind refKind) + { + var dir = GetDirectionInfo(direction); + + var filteredModes = GetModesFromEntryPoint(entryPoint, managedType).Where(x => managedType.IsSame(x.ManagedType.Symbol)).ToList(); + + if (filteredModes.Count == 0) + { + return null; + } + + // Select best fitting marshaller mode for the parameter + var marshallerMode = refKind switch + { + RefKind.In or RefKind.RefReadOnlyParameter or RefKind.None => filteredModes.FirstOrDefault(x => x.MarshalMode == dir.In), + RefKind.Out => filteredModes.FirstOrDefault(x => x.MarshalMode == dir.Out), + RefKind.Ref => filteredModes.FirstOrDefault(x => x.MarshalMode == dir.Ref), + _ => null + }; + + var defaultInfo = filteredModes.FirstOrDefault(x => x.MarshalMode == MarshalMode.Default); + + return marshallerMode ?? defaultInfo; + } + + private static CustomMarshallerInfo[] GetModesFromEntryPoint(ITypeSymbol entryPoint, ITypeSymbol forType) + { + return entryPoint.GetAttributes(Constants.CustomMarshallerAttributeFQN) + .Select(x => GetModeFromAttribute(x, forType)) + .WhereNotNull() + .ToArray(); + } + + private static CustomMarshallerInfo? GetModeFromAttribute(AttributeData attributeData, ITypeSymbol forType) + { + var managedType = (ITypeSymbol)attributeData.ConstructorArguments[0].Value!; + var mode = ModeForValue(attributeData.ConstructorArguments[1].Value!); + + if (attributeData.ConstructorArguments[2].Value is not INamedTypeSymbol marshallerType) + { + return null; + } + + if (managedType.IsSame(Constants.GenericPlaceholderFQN)) + { + managedType = forType; + } + + // Replace generic placeholders with the actual type + if (marshallerType is { IsGenericType: true }) + { + marshallerType = ReplacePlaceholderWithType(marshallerType, forType); + } + + if (marshallerType.ContainingType is { IsGenericType: true }) + { + var containing = ReplacePlaceholderWithType(marshallerType.ContainingType, forType); + + // TODO: might not work properly for nested generic types + marshallerType = containing.GetMembers(marshallerType.Name) + .OfType() + .First(); + } + + + return new CustomMarshallerInfo(new ManagedType(managedType), mode, new ManagedType(marshallerType)); + } + + private static INamedTypeSymbol ReplacePlaceholderWithType(INamedTypeSymbol namedType, ITypeSymbol type) + { + var replacements = 0; + var typeArguments = namedType.TypeArguments; + while (typeArguments.Any(x => x.IsSame(Constants.GenericPlaceholderFQN))) + { + var placeholder = namedType.TypeArguments.First(x => x.IsSame(Constants.GenericPlaceholderFQN)); + + typeArguments = typeArguments.Replace(placeholder, type); + replacements++; + } + + return replacements > 0 + ? namedType.ConstructedFrom.Construct(typeArguments.ToArray()) + : namedType; + } + + private static MarshalMode ModeForValue(object constant) + { + return constant is int number + ? (MarshalMode)number + : MarshalMode.Other; + } + + private static MarshalDirectionInfo GetDirectionInfo(MarshalDirection direction) + { + return direction switch + { + MarshalDirection.ManagedToUnmanaged => MarshalDirectionInfo.ManagedToUnmanaged, + MarshalDirection.UnmanagedToManaged => MarshalDirectionInfo.UnmanagedToManaged, + _ => throw new ArgumentOutOfRangeException(nameof(direction)) + }; + } + + private ITypeSymbol? GetEntryPoint(AttributeData? typeMarshallerAttrib, AttributeData? marshalUsingAttrib, + ITypeSymbol managedType) + { + var marshaller = typeMarshallerAttrib?.ConstructorArguments[0].Value as ITypeSymbol; + if (marshalUsingAttrib?.ConstructorArguments.Length > 0) + { + if (marshalUsingAttrib.ConstructorArguments[0].Value is ITypeSymbol marshallerOverride) + { + marshaller = marshallerOverride; + } + } + + // If no marshaller specified, look at a matching well known marshaller + if (marshaller != null) + { + return marshaller; + } + + return _wellKnownMarshallerTypes.Marshallers + .FirstOrDefault(x => x.matcher(managedType) && x.marshaller != null) + .marshaller; + } + + private record MarshalDirectionInfo(MarshalMode In, MarshalMode Out, MarshalMode Ref) + { + public static readonly MarshalDirectionInfo ManagedToUnmanaged = + new(MarshalMode.ManagedToUnmanagedIn, + MarshalMode.ManagedToUnmanagedOut, + MarshalMode.ManagedToUnmanagedRef); + + public static readonly MarshalDirectionInfo UnmanagedToManaged = + new(MarshalMode.UnmanagedToManagedIn, + MarshalMode.UnmanagedToManagedOut, + MarshalMode.UnmanagedToManagedRef); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ICustomMarshalGeneratorFactoryStrategy.cs b/src/SampSharp.SourceGenerator/Marshalling/ICustomMarshalGeneratorFactoryStrategy.cs new file mode 100644 index 00000000..b838394c --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ICustomMarshalGeneratorFactoryStrategy.cs @@ -0,0 +1,6 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public interface ICustomMarshalGeneratorFactoryStrategy +{ + IMarshalShapeGenerator Create(MarshallerShape shape); +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/IMarshalShapeGenerator.cs b/src/SampSharp.SourceGenerator/Marshalling/IMarshalShapeGenerator.cs new file mode 100644 index 00000000..66a1b5d2 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/IMarshalShapeGenerator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Marshalling; + +public interface IMarshalShapeGenerator +{ + bool UsesNativeIdentifier { get; } + + TypeSyntax GetNativeType(IdentifierStubContext context); + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context); +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContext.cs b/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContext.cs new file mode 100644 index 00000000..bcf44e08 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContext.cs @@ -0,0 +1,49 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Marshalling; + +public record IdentifierStubContext( + MarshalDirection Direction, + ManagedType ManagedType, + ManagedType? MarshallerType, + ManagedType? NativeType, + NullableAnnotation ManagedTypeNullableAnnotation, + MarshallerShape Shape, + IMarshalShapeGenerator Generator, + RefKind RefKind, + string ManagedIdentifier) +{ + public string GetManagedId() + { + return ManagedIdentifier; + } + + public string GetMarshallerId() + { + return GetNativeExtraId("marshaller"); + } + + public string GetNativeId() + { + return ManagedIdentifier == MarshallerConstants.LocalReturnValue + ? $"{MarshallerConstants.LocalReturnValue}_native" + : $"__{ManagedIdentifier}_native"; + } + + public string GetNativeExtraId(string extra) + { + return $"{GetNativeId()}__{extra}"; + } + + public ExpressionSyntax PostfixManagedNullableSuppression(ExpressionSyntax expr) + { + if (ManagedTypeNullableAnnotation == NullableAnnotation.NotAnnotated) + { + expr = SyntaxFactory.PostfixUnaryExpression(SyntaxKind.SuppressNullableWarningExpression, expr); + } + + return expr; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContextFactory.cs b/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContextFactory.cs new file mode 100644 index 00000000..baf7d033 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/IdentifierStubContextFactory.cs @@ -0,0 +1,62 @@ +using Microsoft.CodeAnalysis; + +namespace SampSharp.SourceGenerator.Marshalling; + +public class IdentifierStubContextFactory +{ + // + // stages: + // during context building: + // 1. Decide which marshaller implementation to use based on entry point of the specified custom marshaller ( CustomMarshallerTypeFinder ) + // 2. Deduce shape based on the implementation ( ShapeTool ) + // 3. Activate ShapeGenerator based on shape (CustomMarshalGeneratorFactory.Create) + // during generation: + // 4. Generate marshaling code and combine with invocation code + // + + private readonly CustomMarshallerTypeDetector _customMarshallerTypeDetector; + + public IdentifierStubContextFactory(WellKnownMarshallerTypes wellKnownMarshallerTypes) + { + _customMarshallerTypeDetector = new CustomMarshallerTypeDetector(wellKnownMarshallerTypes); + } + + public IdentifierStubContext Create(IParameterSymbol parameter, MarshalDirection marshalDirection) + { + return Create(parameter, parameter.Type, marshalDirection); + } + + public IdentifierStubContext Create(IParameterSymbol parameter, ITypeSymbol type, MarshalDirection marshalDirection) + { + var customMarshaller = _customMarshallerTypeDetector.GetCustomMarshaller(parameter, type, marshalDirection); + var (shape, nativeType) = customMarshaller == null + ? (MarshallerShape.None, new ManagedType(type)) + : ShapeDetector.GetShapeOfMarshaller(customMarshaller); + + var generator = CustomMarshalGeneratorFactory.Create(shape, customMarshaller?.IsStateful); + + return new IdentifierStubContext(marshalDirection, new ManagedType(type), customMarshaller?.MarshallerType, nativeType, parameter.NullableAnnotation, shape, generator, parameter.RefKind, parameter.Name); + } + + public IdentifierStubContext Create(IMethodSymbol method, MarshalDirection marshalDirection) + { + return Create(method, method.ReturnType, marshalDirection); + } + + public IdentifierStubContext Create(IMethodSymbol method, ITypeSymbol type, MarshalDirection marshalDirection) + { + var customMarshaller = _customMarshallerTypeDetector.GetCustomMarshaller(method, type, marshalDirection); + var (shape, nativeType) = customMarshaller == null + ? (MarshallerShape.None, new ManagedType(type)) + : ShapeDetector.GetShapeOfMarshaller(customMarshaller); + + var generator = CustomMarshalGeneratorFactory.Create(shape, customMarshaller?.IsStateful); + + // will have been annotated by the MarshallingGeneratorBase + var annotation = method.ReturnType.IsReferenceType + ? NullableAnnotation.Annotated + : NullableAnnotation.NotAnnotated; + + return new IdentifierStubContext(marshalDirection, new ManagedType(type), customMarshaller?.MarshallerType, nativeType, annotation, shape, generator, RefKind.Out, MarshallerConstants.LocalReturnValue); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ManagedType.cs b/src/SampSharp.SourceGenerator/Marshalling/ManagedType.cs new file mode 100644 index 00000000..9398545d --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ManagedType.cs @@ -0,0 +1,10 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.SyntaxFactories; + +namespace SampSharp.SourceGenerator.Marshalling; + +public record ManagedType(ITypeSymbol Symbol, string Name, TypeSyntax TypeName) +{ + public ManagedType(ITypeSymbol symbol) : this(symbol, TypeSyntaxFactory.ToGlobalTypeString(symbol), TypeSyntaxFactory.TypeNameGlobal(symbol)) { } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshalDirection.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshalDirection.cs new file mode 100644 index 00000000..d569e13b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshalDirection.cs @@ -0,0 +1,17 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +/// +/// Represents the direction of marshal code, unmanaged-to-managed or managed-to-unmanaged. +/// +public enum MarshalDirection +{ + /// + /// managed-to-unmanaged. + /// + ManagedToUnmanaged, + + /// + /// unmanaged-to-managed. + /// + UnmanagedToManaged +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshalInspector.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshalInspector.cs new file mode 100644 index 00000000..5bb20e3e --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshalInspector.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using SampSharp.SourceGenerator.Helpers; +using static SampSharp.SourceGenerator.Marshalling.ShapeConstants; + +namespace SampSharp.SourceGenerator.Marshalling; + +public static class MarshalInspector +{ + public static MarshalMembers GetMembers(CustomMarshallerInfo info) + { + var type = info.MarshallerType.Symbol; + if (info.IsStateful) + { + return new MarshalMembers( + StatefulFreeMethod: GetMethod(type, true, MethodFree), + StatefulFromManagedMethod: GetMethod(type, true, MethodFromManaged, false, info.ManagedType.Symbol), + StatefulFromManagedWithBufferMethod: GetMethod(type, true, MethodFromManaged, false, x => x.Type.IsSame(info.ManagedType.Symbol), x => IsSpanByte(x.Type)), + StatefulFromUnmanagedMethod: GetMethod(type, true, MethodFromUnmanaged, parameterCount: 1), + + StatefulToManagedMethod: GetMethod(type, true, MethodToManaged), + StatefulToManagedFinallyMethod: GetMethod(type, true, MethodToManagedFinally), + StatefulToUnmanagedMethod: GetMethod(type, true, MethodToUnmanaged), + + StatefulOnInvokedMethod: GetMethod(type, true, MethodOnInvoked), + StatefulGetPinnableReferenceMethod: GetMethod(type, true, MethodGetPinnableReference, true), + StatelessGetPinnableReferenceMethod: GetMethod(type, false, MethodGetPinnableReference, true, info.ManagedType.Symbol), + + StatelessConvertToUnmanagedMethod: null, + StatelessConvertToUnmanagedWithBufferMethod: null, + StatelessConvertToManagedMethod: null, + StatelessConvertToManagedFinallyMethod: null, + StatelessFreeMethod: null, + + BufferSizeProperty: GetStaticProperty(type, PropertyBufferSize, x => x.SpecialType == SpecialType.System_Int32) + ); + } + + return new MarshalMembers( + StatefulFreeMethod: null, + StatefulFromManagedMethod: null, + StatefulFromManagedWithBufferMethod: null, + StatefulFromUnmanagedMethod: null, + + StatefulToManagedMethod: null, + StatefulToManagedFinallyMethod: null, + StatefulToUnmanagedMethod: null, + + StatefulOnInvokedMethod: null, + StatefulGetPinnableReferenceMethod: null, + StatelessGetPinnableReferenceMethod: GetMethod(type, false, MethodGetPinnableReference, true, info.ManagedType.Symbol), + + StatelessConvertToUnmanagedMethod: GetMethod(type, false, MethodConvertToUnmanaged, false, info.ManagedType.Symbol), + StatelessConvertToUnmanagedWithBufferMethod: GetMethod(type, false, MethodConvertToUnmanaged, false, x => x.Type.IsSame(info.ManagedType.Symbol), x => IsSpanByte(x.Type)), + StatelessConvertToManagedMethod: GetMethod(type, false, MethodConvertToManaged, parameterCount: 1), + StatelessConvertToManagedFinallyMethod: GetMethod(type, false, MethodConvertToManagedFinally, parameterCount: 1), + StatelessFreeMethod: GetMethod(type, false, MethodFree, parameterCount: 1), + + BufferSizeProperty: GetStaticProperty(type, PropertyBufferSize, x => x.SpecialType == SpecialType.System_Int32) + ); + } + + private static IMethodSymbol? GetMethod(ITypeSymbol type, bool stateful, string name, bool returnsByRef = false, int parameterCount = 0) + { + return type + .GetMembers(name) + .OfType() + .FirstOrDefault(x => + { + if (stateful == x.IsStatic || x.Parameters.Length != parameterCount || x.ReturnsByRef != returnsByRef) + { + return false; + } + + return !Array.Empty().Where((t, i) => !x.Parameters[i].Type.IsSame(t)) + .Any(); + }); + } + + + private static bool IsSpanByte(ITypeSymbol type) + { + return type is INamedTypeSymbol named && named.ToDisplayString() == Constants.SpanOfBytesFQN; + } + + private static IMethodSymbol? GetMethod(ITypeSymbol type, bool stateful, string name, bool returnsByRef = false, params ITypeSymbol[] paramTypes) + { + return type + .GetMembers(name) + .OfType() + .FirstOrDefault(x => + { + if (stateful == x.IsStatic || x.Parameters.Length != paramTypes.Length) + { + return false; + } + + if (x.ReturnsByRef != returnsByRef) + { + return false; + } + + return !paramTypes.Where((t, i) => !x.Parameters[i].Type.IsSame(t)) + .Any(); + }); + + } + + private static IMethodSymbol? GetMethod(ITypeSymbol type, bool stateful, string name, bool returnsByRef, params Func[] paramTypes) + { + return type + .GetMembers(name) + .OfType() + .FirstOrDefault(x => + { + if (stateful == x.IsStatic || x.Parameters.Length != paramTypes.Length) + { + return false; + } + + if (x.ReturnsByRef != returnsByRef) + { + return false; + } + + return !paramTypes.Where((check, i) => !check(x.Parameters[i])) + .Any(); + }); + + } + + private static IPropertySymbol? GetStaticProperty(ITypeSymbol type, string name, Func propertyType) + { + return type + .GetMembers(name) + .OfType() + .FirstOrDefault(x => x.IsStatic && propertyType(x.Type)); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshalMembers.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshalMembers.cs new file mode 100644 index 00000000..faa9d44f --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshalMembers.cs @@ -0,0 +1,23 @@ +using Microsoft.CodeAnalysis; + +namespace SampSharp.SourceGenerator.Marshalling; + +public record MarshalMembers( + IMethodSymbol? StatefulFreeMethod, + IMethodSymbol? StatefulFromManagedMethod, + IMethodSymbol? StatefulFromManagedWithBufferMethod, + IMethodSymbol? StatefulFromUnmanagedMethod, + IMethodSymbol? StatefulToManagedMethod, + IMethodSymbol? StatefulToManagedFinallyMethod, + IMethodSymbol? StatefulToUnmanagedMethod, + IMethodSymbol? StatefulOnInvokedMethod, + IMethodSymbol? StatefulGetPinnableReferenceMethod, + IMethodSymbol? StatelessGetPinnableReferenceMethod, + IMethodSymbol? StatelessConvertToUnmanagedMethod, + IMethodSymbol? StatelessConvertToUnmanagedWithBufferMethod, + IMethodSymbol? StatelessConvertToManagedMethod, + IMethodSymbol? StatelessConvertToManagedFinallyMethod, + IMethodSymbol? StatelessFreeMethod, + + IPropertySymbol? BufferSizeProperty +); \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshalMode.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshalMode.cs new file mode 100644 index 00000000..835661c1 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshalMode.cs @@ -0,0 +1,13 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public enum MarshalMode +{ + Default, + ManagedToUnmanagedIn, + ManagedToUnmanagedRef, + ManagedToUnmanagedOut, + UnmanagedToManagedIn, + UnmanagedToManagedRef, + UnmanagedToManagedOut, + Other +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshalPhase.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshalPhase.cs new file mode 100644 index 00000000..545c6494 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshalPhase.cs @@ -0,0 +1,15 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public enum MarshalPhase +{ + Setup, + Marshal, + PinnedMarshal, + Pin, + NotifyForSuccessfulInvoke, + UnmarshalCapture, + Unmarshal, + CleanupCalleeAllocated, + CleanupCallerAllocated, + GuaranteedUnmarshal +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshallerConstants.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshallerConstants.cs new file mode 100644 index 00000000..bf6cc5ee --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshallerConstants.cs @@ -0,0 +1,6 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public static class MarshallerConstants +{ + public const string LocalReturnValue = "__retVal"; +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshallerHelper.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshallerHelper.cs new file mode 100644 index 00000000..92c10815 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshallerHelper.cs @@ -0,0 +1,28 @@ +using Microsoft.CodeAnalysis; + +namespace SampSharp.SourceGenerator.Marshalling; + +public static class MarshallerHelper +{ + public static string GetVar(IParameterSymbol? parameterSymbol) + { + return parameterSymbol?.Name ?? MarshallerConstants.LocalReturnValue; + } + + public static string GetMarshallerVar(IParameterSymbol? parameterSymbol) + { + return GetNativeExtraVar(parameterSymbol, "marshaller"); + } + + public static string GetNativeVar(IParameterSymbol? parameterSymbol) + { + return parameterSymbol == null + ? $"{MarshallerConstants.LocalReturnValue}_native" + : $"__{parameterSymbol.Name}_native"; + } + + public static string GetNativeExtraVar(IParameterSymbol? parameterSymbol, string extra) + { + return $"{GetNativeVar(parameterSymbol)}__{extra}"; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshallerShape.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshallerShape.cs new file mode 100644 index 00000000..af200111 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshallerShape.cs @@ -0,0 +1,17 @@ +using System; + +namespace SampSharp.SourceGenerator.Marshalling; + +[Flags] +public enum MarshallerShape +{ + None = 0x0, + ToUnmanaged = 0x1, + CallerAllocatedBuffer = 0x2, + StatelessPinnableReference = 0x4, + StatefulPinnableReference = 0x8, + ToManaged = 0x10, + GuaranteedUnmarshal = 0x20, + Free = 0x40, + OnInvoked = 0x80, +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/MarshallingCodeGenDocumentation.cs b/src/SampSharp.SourceGenerator/Marshalling/MarshallingCodeGenDocumentation.cs new file mode 100644 index 00000000..21aa2a72 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/MarshallingCodeGenDocumentation.cs @@ -0,0 +1,17 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public static class MarshallingCodeGenDocumentation +{ + public const string COMMENT_SETUP = "// Setup - Perform required setup."; + public const string COMMENT_MARSHAL = "// Marshal - Convert managed data to native data."; + public const string COMMENT_PINNED_MARSHAL = "// PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned."; + public const string COMMENT_PIN = "// Pin - Pin data in preparation for calling the P/Invoke."; + public const string COMMENT_NOTIFY = "// NotifyForSuccessfulInvoke - Keep alive any managed objects that need to stay alive across the call."; + public const string COMMENT_UNMARSHAL_CAPTURE = + "// UnmarshalCapture - Capture the native data into marshaller instances in case conversion to managed data throws an exception."; + public const string COMMENT_UNMARSHAL = "// Unmarshal - Convert native data to managed data."; + public const string COMMENT_CLEANUP_CALLEE = "// CleanupCalleeAllocated - Perform cleanup of callee allocated resources."; + public const string COMMENT_CLEANUP_CALLER = "// CleanupCallerAllocated - Perform cleanup of caller allocated resources."; + public const string COMMENT_GUARANTEED_UNMARSHAL = "// GuaranteedUnmarshal - Convert native data to managed data even in the case of an exception during the non-cleanup phases."; + public const string COMMENT_P_INVOKE = "// Local P/Invoke"; +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeConstants.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeConstants.cs new file mode 100644 index 00000000..ecc92d41 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeConstants.cs @@ -0,0 +1,19 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +public static class ShapeConstants +{ + public const string MethodFree = "Free"; + public const string MethodFromManaged = "FromManaged"; + public const string MethodFromUnmanaged = "FromUnmanaged"; + public const string MethodToManaged = "ToManaged"; + public const string MethodToManagedFinally = "ToManagedFinally"; + public const string MethodToUnmanaged = "ToUnmanaged"; + public const string MethodOnInvoked = "OnInvoked"; + + public const string MethodConvertToUnmanaged = "ConvertToUnmanaged"; + public const string MethodConvertToManaged = "ConvertToManaged"; + public const string MethodConvertToManagedFinally = "ConvertToManagedFinally"; + public const string MethodGetPinnableReference = "GetPinnableReference"; + + public const string PropertyBufferSize = "BufferSize"; +} diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeDetector.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeDetector.cs new file mode 100644 index 00000000..db3fbd3b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeDetector.cs @@ -0,0 +1,91 @@ +namespace SampSharp.SourceGenerator.Marshalling; + +/// +/// Provides methods for determining the shape of a custom marshaller. +/// +public static class ShapeDetector +{ + public static (MarshallerShape shape, ManagedType nativeType) GetShapeOfMarshaller(CustomMarshallerInfo marshaller) + { + var members = MarshalInspector.GetMembers(marshaller); + var nativeType = marshaller.ManagedType; + var shape = MarshallerShape.None; + if (marshaller.IsStateless) + { + if (members.StatelessConvertToManagedFinallyMethod != null) + { + shape |= MarshallerShape.GuaranteedUnmarshal; + nativeType = new ManagedType(members.StatelessConvertToManagedFinallyMethod.Parameters[0].Type); + } + + if (members.StatelessConvertToManagedMethod != null) + { + shape |= MarshallerShape.ToManaged; + nativeType = new ManagedType(members.StatelessConvertToManagedMethod.Parameters[0].Type); + } + + if (members.StatelessConvertToUnmanagedWithBufferMethod != null) + { + shape |= MarshallerShape.CallerAllocatedBuffer | MarshallerShape.ToUnmanaged; + nativeType = new ManagedType(members.StatelessConvertToUnmanagedWithBufferMethod.ReturnType); + } + + if (members.StatelessConvertToUnmanagedMethod != null) + { + shape |= MarshallerShape.ToUnmanaged; + nativeType = new ManagedType(members.StatelessConvertToUnmanagedMethod.ReturnType); + } + + if (members.StatelessFreeMethod != null) + { + shape |= MarshallerShape.Free; + } + + if (members.StatelessGetPinnableReferenceMethod != null) + { + shape |= MarshallerShape.StatelessPinnableReference; + } + } + else if (marshaller.IsStateful) + { + if (members.StatefulToUnmanagedMethod != null) + { + shape |= MarshallerShape.ToUnmanaged; + nativeType = new ManagedType(members.StatefulToUnmanagedMethod.ReturnType); + + if (members.StatefulFromManagedWithBufferMethod != null) + { + shape |= MarshallerShape.CallerAllocatedBuffer; + } + } + + if (members.StatefulFromUnmanagedMethod != null) + { + nativeType = new ManagedType(members.StatefulFromUnmanagedMethod.Parameters[0].Type); + shape |= MarshallerShape.ToManaged; + } + + if (members.StatefulFreeMethod != null) + { + shape |= MarshallerShape.Free; + } + + if (members.StatefulOnInvokedMethod != null) + { + shape |= MarshallerShape.OnInvoked; + } + + if (members.StatefulGetPinnableReferenceMethod != null) + { + shape |= MarshallerShape.StatefulPinnableReference; + } + + if (members.StatelessGetPinnableReferenceMethod != null) + { + shape |= MarshallerShape.StatelessPinnableReference; + } + } + + return (shape, nativeType); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/CallerAllocatedBuffer.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/CallerAllocatedBuffer.cs new file mode 100644 index 00000000..cd9b36c8 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/CallerAllocatedBuffer.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.SyntaxFactories; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class CallerAllocatedBuffer(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Marshal => GenerateMarshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private IEnumerable GenerateMarshal(IdentifierStubContext context) + { + var bufferVar = context.GetNativeExtraId("buffer"); + + // global::System.Span __varName_native__buffer = stackalloc byte[MarshallerType.BufferSize]; + yield return DeclareLocal( + TypeSyntaxFactory.SpanOfBytes, + bufferVar, + StackAllocArrayCreationExpression( + ArrayType( + PredefinedType( + Token(SyntaxKind.ByteKeyword))) + .WithRankSpecifiers( + SingletonList( + ArrayRankSpecifier(SingletonSeparatedList( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + context.MarshallerType!.TypeName, + IdentifierName(ShapeConstants.PropertyBufferSize)))))))); + + // append argument to the method call + var rewriter = new InvocationRewriter(bufferVar); + foreach (var el in innerGenerator.Generate(MarshalPhase.Marshal, context)) + { + yield return (StatementSyntax)rewriter.Visit(el); + } + } + + private class InvocationRewriter(string bufferName) : CSharpSyntaxRewriter + { + public override SyntaxNode VisitInvocationExpression(InvocationExpressionSyntax node) + { + return node.WithArgumentList( + node.ArgumentList.AddArguments( + Argument( + IdentifierName(bufferName)))); + } + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/EmptyMarshalShapeGenerator.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/EmptyMarshalShapeGenerator.cs new file mode 100644 index 00000000..34eee931 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/EmptyMarshalShapeGenerator.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class EmptyMarshalShapeGenerator : IMarshalShapeGenerator +{ + private EmptyMarshalShapeGenerator() + { + } + + public static IMarshalShapeGenerator Instance { get; } = new EmptyMarshalShapeGenerator(); + + public bool UsesNativeIdentifier => false; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.ManagedType.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return []; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulFree.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulFree.cs new file mode 100644 index 00000000..fe4139c8 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulFree.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulFree(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.CleanupCallerAllocated => CleanupCallerAllocated(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable CleanupCallerAllocated(IdentifierStubContext context) + { + // marshaller.Free(); + yield return Invoke( + context.GetMarshallerId(), + ShapeConstants.MethodFree); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulGuaranteedUnmarshal.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulGuaranteedUnmarshal.cs new file mode 100644 index 00000000..8fde3e3b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulGuaranteedUnmarshal.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulGuaranteedUnmarshal(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Unmarshal => [], + MarshalPhase.GuaranteedUnmarshal => GuaranteedUnmarshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable GuaranteedUnmarshal(IdentifierStubContext context) + { + // managed = marshaller.ToManagedFinally(); + yield return Assign( + context.GetManagedId(), + context.PostfixManagedNullableSuppression( + InvocationExpression( + context.GetMarshallerId(), + ShapeConstants.MethodToManagedFinally))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulManagedToUnmanaged.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulManagedToUnmanaged.cs new file mode 100644 index 00000000..eaed29b9 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulManagedToUnmanaged.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulManagedToUnmanaged(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => true; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.NativeType!.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Setup => Setup(context), + MarshalPhase.Marshal => Marshal(context), + MarshalPhase.PinnedMarshal => PinnedMarshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable PinnedMarshal(IdentifierStubContext context) + { + // native = marshaller.ToUnmanaged(); + yield return Assign( + context.GetNativeId(), + InvocationExpression( + context.GetMarshallerId(), + ShapeConstants.MethodToUnmanaged) + ); + } + + private static IEnumerable Marshal(IdentifierStubContext context) + { + // marshaller.FromManaged(managed); + yield return Invoke( + context.GetMarshallerId(), + ShapeConstants.MethodFromManaged, + Argument( + IdentifierName( + context.GetManagedId()))); + } + + private static IEnumerable Setup(IdentifierStubContext context) + { + // scoped type marshaller = new(); + var local = DeclareLocal( + context.MarshallerType!.TypeName, + context.GetMarshallerId(), + ImplicitObjectCreationExpression()); + + if (context.MarshallerType.Symbol.IsRefLikeType) + { + local = local.WithModifiers( + TokenList( + Token( + SyntaxKind.ScopedKeyword))); + + } + + yield return local; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulNotifyForSuccesfulInvokeMarshaller.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulNotifyForSuccesfulInvokeMarshaller.cs new file mode 100644 index 00000000..bcc1b61e --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulNotifyForSuccesfulInvokeMarshaller.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulNotifyForSuccesfulInvokeMarshaller(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.NotifyForSuccessfulInvoke => GenerateNotifyForSuccessfulInvoke(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable GenerateNotifyForSuccessfulInvoke(IdentifierStubContext context) + { + // marshaller.OnInvoked(); + yield return Invoke( + context.GetMarshallerId(), + ShapeConstants.MethodOnInvoked); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulPinnableManagedValueMarshaller.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulPinnableManagedValueMarshaller.cs new file mode 100644 index 00000000..de717403 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulPinnableManagedValueMarshaller.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulPinnableManagedValueMarshaller(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Pin => GeneratePin(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable GeneratePin(IdentifierStubContext context) + { + // fixed(void* __managed_native__unused = managed) {} + yield return FixedStatement(VariableDeclaration( + PointerType( + PredefinedType( + Token(SyntaxKind.VoidKeyword)))) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier(context.GetNativeExtraId("unused"))) + .WithInitializer( + EqualsValueClause( + IdentifierName(context.GetMarshallerId()))))), Block()); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulUnmanagedToManaged.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulUnmanagedToManaged.cs new file mode 100644 index 00000000..190ea2a1 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatefulUnmanagedToManaged.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatefulUnmanagedToManaged(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => true; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.NativeType!.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Setup => Setup(context), + MarshalPhase.UnmarshalCapture => UnmarshalCapture(context), + MarshalPhase.Unmarshal => Unmarshal(context), + MarshalPhase.CleanupCalleeAllocated => CleanupCalleeAllocated(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable Setup(IdentifierStubContext context) + { + // scoped type marshaller = new(); + var local = DeclareLocal( + context.MarshallerType!.TypeName, + context.GetMarshallerId(), + ImplicitObjectCreationExpression()); + + if (context.MarshallerType.Symbol.IsRefLikeType) + { + local = local.WithModifiers( + TokenList( + Token( + SyntaxKind.ScopedKeyword))); + + } + + yield return local; + } + + private static IEnumerable CleanupCalleeAllocated(IdentifierStubContext context) + { + // marshaller.Free(); + yield return Invoke( + context.GetMarshallerId(), + ShapeConstants.MethodFree); + } + + private static IEnumerable Unmarshal(IdentifierStubContext context) + { + // managed = marshaller.ToManaged(); + yield return Assign( + context.GetManagedId(), + context.PostfixManagedNullableSuppression( + InvocationExpression( + context.GetMarshallerId(), + ShapeConstants.MethodToManaged))); + } + + private static IEnumerable UnmarshalCapture(IdentifierStubContext context) + { + // marshaller.FromUnmanaged(unmanaged); + yield return Invoke( + context.GetMarshallerId(), + ShapeConstants.MethodFromUnmanaged, + Argument( + IdentifierName(context.GetNativeId()))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessFree.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessFree.cs new file mode 100644 index 00000000..ea0073ef --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessFree.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatelessFree(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => innerGenerator.UsesNativeIdentifier; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return innerGenerator.GetNativeType(context); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.CleanupCallerAllocated => CleanupCallerAllocated(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable CleanupCallerAllocated(IdentifierStubContext context) + { + // Marshaller.Free(native); + yield return Invoke( + context.MarshallerType!.TypeName, + ShapeConstants.MethodFree, + Argument( + IdentifierName( + context.GetNativeId()))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessManagedToUnmanaged.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessManagedToUnmanaged.cs new file mode 100644 index 00000000..9b6b0aa7 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessManagedToUnmanaged.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatelessManagedToUnmanaged(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => true; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.NativeType!.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Marshal => Marshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable Marshal(IdentifierStubContext context) + { + // native = Marshaller.ConvertToUnmanaged(managed); + yield return Assign( + context.GetNativeId(), + InvocationExpression( + context.MarshallerType!.TypeName, + ShapeConstants.MethodConvertToUnmanaged, + Argument(IdentifierName(context.GetManagedId())))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManaged.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManaged.cs new file mode 100644 index 00000000..8075315b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManaged.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StatelessUnmanagedToManaged(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => true; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.NativeType!.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Unmarshal => Unmarshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable Unmarshal(IdentifierStubContext context) + { + // managed = Marshaller.ConvertToManaged(unmanaged); + yield return Assign( + context.GetManagedId(), + context.PostfixManagedNullableSuppression( + InvocationExpression( + context.MarshallerType!.TypeName, + ShapeConstants.MethodConvertToManaged, + Argument(IdentifierName(context.GetNativeId()))))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManagedGuaranteed.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManagedGuaranteed.cs new file mode 100644 index 00000000..5718a4d4 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StatelessUnmanagedToManagedGuaranteed.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.ExpressionFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.StatementFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +internal class StatelessUnmanagedToManagedGuaranteed(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => true; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return context.NativeType!.TypeName; + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.GuaranteedUnmarshal => GuaranteedUnmarshal(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable GuaranteedUnmarshal(IdentifierStubContext context) + { + // managed = Marshaller.ConvertToManagedFinally(unmanaged); + yield return Assign( + context.GetManagedId(), + context.PostfixManagedNullableSuppression( + InvocationExpression( + context.MarshallerType!.TypeName, + ShapeConstants.MethodConvertToManagedFinally, + Argument(IdentifierName(context.GetNativeId()))))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StaticPinnableManagedValueMarshaller.cs b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StaticPinnableManagedValueMarshaller.cs new file mode 100644 index 00000000..afa5aac1 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/ShapeGenerators/StaticPinnableManagedValueMarshaller.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +public class StaticPinnableManagedValueMarshaller(IMarshalShapeGenerator innerGenerator) : IMarshalShapeGenerator +{ + public bool UsesNativeIdentifier => false; + + public TypeSyntax GetNativeType(IdentifierStubContext context) + { + return PointerType(PredefinedType(Token(SyntaxKind.VoidKeyword))); + } + + public IEnumerable Generate(MarshalPhase phase, IdentifierStubContext context) + { + return phase switch + { + MarshalPhase.Pin => GeneratePin(context), + _ => innerGenerator.Generate(phase, context) + }; + } + + private static IEnumerable GeneratePin(IdentifierStubContext context) + { + // fixed(void* __managed_native = managed.GetPinnableReference()) {} + yield return FixedStatement(VariableDeclaration( + PointerType( + PredefinedType( + Token(SyntaxKind.VoidKeyword)))) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier(context.GetNativeId())) + .WithInitializer( + EqualsValueClause( + PrefixUnaryExpression( + SyntaxKind.AddressOfExpression, + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + context.MarshallerType!.TypeName, + IdentifierName(ShapeConstants.MethodGetPinnableReference))) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + IdentifierName(context.GetManagedId())))))))))), Block()); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/StatefulCustomMarshalGeneratorFactoryStrategy.cs b/src/SampSharp.SourceGenerator/Marshalling/StatefulCustomMarshalGeneratorFactoryStrategy.cs new file mode 100644 index 00000000..8fc0dde2 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/StatefulCustomMarshalGeneratorFactoryStrategy.cs @@ -0,0 +1,60 @@ +using SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +namespace SampSharp.SourceGenerator.Marshalling; + +public class StatefulCustomMarshalGeneratorFactoryStrategy : ICustomMarshalGeneratorFactoryStrategy +{ + public static ICustomMarshalGeneratorFactoryStrategy Instance { get; } = new StatefulCustomMarshalGeneratorFactoryStrategy(); + + public IMarshalShapeGenerator Create(MarshallerShape shape) + { + var generator = EmptyMarshalShapeGenerator.Instance; + + if (shape.HasFlag(MarshallerShape.StatelessPinnableReference)) + { + return new StaticPinnableManagedValueMarshaller(generator); + } + + if (shape.HasFlag(MarshallerShape.ToUnmanaged)) + { + generator = new StatefulManagedToUnmanaged(generator); + } + + if (shape.HasFlag(MarshallerShape.ToManaged)) + { + generator = new StatefulUnmanagedToManaged(generator); + } + + if (shape.HasFlag(MarshallerShape.GuaranteedUnmarshal)) + { + generator = new StatefulGuaranteedUnmarshal(generator); + } + + if (shape.HasFlag(MarshallerShape.CallerAllocatedBuffer)) + { + generator = new CallerAllocatedBuffer(generator); + } + + if (shape.HasFlag(MarshallerShape.ToManaged)) + { + generator = new StatefulUnmanagedToManaged(generator); + } + + if (shape.HasFlag(MarshallerShape.Free)) + { + generator = new StatefulFree(generator); + } + + if (shape.HasFlag(MarshallerShape.OnInvoked)) + { + generator = new StatefulNotifyForSuccesfulInvokeMarshaller(generator); + } + + if (shape.HasFlag(MarshallerShape.StatefulPinnableReference)) + { + return new StatefulPinnableManagedValueMarshaller(generator); + } + + return generator; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/StatelessCustomMarshalGeneratorFactoryStrategy.cs b/src/SampSharp.SourceGenerator/Marshalling/StatelessCustomMarshalGeneratorFactoryStrategy.cs new file mode 100644 index 00000000..bb0e0a6a --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/StatelessCustomMarshalGeneratorFactoryStrategy.cs @@ -0,0 +1,44 @@ +using SampSharp.SourceGenerator.Marshalling.ShapeGenerators; + +namespace SampSharp.SourceGenerator.Marshalling; + +public class StatelessCustomMarshalGeneratorFactoryStrategy : ICustomMarshalGeneratorFactoryStrategy +{ + public static ICustomMarshalGeneratorFactoryStrategy Instance { get; } = new StatelessCustomMarshalGeneratorFactoryStrategy(); + + public IMarshalShapeGenerator Create(MarshallerShape shape) + { + var generator = EmptyMarshalShapeGenerator.Instance; + + if (shape.HasFlag(MarshallerShape.StatelessPinnableReference)) + { + return new StaticPinnableManagedValueMarshaller(generator); + } + + if (shape.HasFlag(MarshallerShape.GuaranteedUnmarshal)) + { + generator = new StatelessUnmanagedToManagedGuaranteed(generator); + } + else if (shape.HasFlag(MarshallerShape.ToUnmanaged)) + { + generator = new StatelessManagedToUnmanaged(generator); + } + + if (shape.HasFlag(MarshallerShape.ToManaged)) + { + generator = new StatelessUnmanagedToManaged(generator); + } + + if (shape.HasFlag(MarshallerShape.CallerAllocatedBuffer)) + { + generator = new CallerAllocatedBuffer(generator); + } + + if (shape.HasFlag(MarshallerShape.Free)) + { + generator = new StatelessFree(generator); + } + + return generator; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Marshalling/WellKnownMarshallerTypes.cs b/src/SampSharp.SourceGenerator/Marshalling/WellKnownMarshallerTypes.cs new file mode 100644 index 00000000..21d76fe0 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Marshalling/WellKnownMarshallerTypes.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.CodeAnalysis; + +namespace SampSharp.SourceGenerator.Marshalling; + +public record WellKnownMarshallerTypes(params (Func matcher, INamedTypeSymbol? marshaller)[] Marshallers) +{ + public static WellKnownMarshallerTypes Create(Compilation compilation) + { + var stringViewMarshaller = compilation.GetTypeByMetadataName(Constants.StringViewMarshallerFQN); + var booleanMarshaller = compilation.GetTypeByMetadataName(Constants.BooleanMarshallerFQN); + + var wellKnownMarshallerTypes = new WellKnownMarshallerTypes( + (x => x.SpecialType == SpecialType.System_String, stringViewMarshaller), + (x => x.SpecialType == SpecialType.System_Boolean, booleanMarshaller) + ); + return wellKnownMarshallerTypes; + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Models/ApiMethodStubGenerationContext.cs b/src/SampSharp.SourceGenerator/Models/ApiMethodStubGenerationContext.cs new file mode 100644 index 00000000..eb3114d5 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Models/ApiMethodStubGenerationContext.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Marshalling; + +namespace SampSharp.SourceGenerator.Models; + +public record ApiMethodStubGenerationContext( + MethodDeclarationSyntax Declaration, + IMethodSymbol Symbol, + IdentifierStubContext[] Parameters, + IdentifierStubContext ReturnValue, + string Library, + string NativeTypeName) : MarshallingStubGenerationContext(Symbol, Parameters, ReturnValue); \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Models/EventInterfaceStubGenerationContext.cs b/src/SampSharp.SourceGenerator/Models/EventInterfaceStubGenerationContext.cs new file mode 100644 index 00000000..5fd93a68 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Models/EventInterfaceStubGenerationContext.cs @@ -0,0 +1,31 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Models; + +public record EventInterfaceStubGenerationContext( + INamedTypeSymbol Symbol, + InterfaceDeclarationSyntax Syntax, + MarshallingStubGenerationContext[] Methods, + string Library, + string NativeTypeName) +{ + public TypeSyntax Type { get; } = GetSelfType(Symbol, Syntax); + + private static TypeSyntax GetSelfType(ISymbol symbol, InterfaceDeclarationSyntax syntax) + { + if (syntax.TypeParameterList == null) + { + return ParseTypeName(symbol.Name); + } + + return GenericName( + Identifier(symbol.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + syntax.TypeParameterList!.Parameters.Select(x => IdentifierName(x.Identifier))))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Models/MarshallingStubGenerationContext.cs b/src/SampSharp.SourceGenerator/Models/MarshallingStubGenerationContext.cs new file mode 100644 index 00000000..553fb90b --- /dev/null +++ b/src/SampSharp.SourceGenerator/Models/MarshallingStubGenerationContext.cs @@ -0,0 +1,14 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using SampSharp.SourceGenerator.Marshalling; + +namespace SampSharp.SourceGenerator.Models; + +public record MarshallingStubGenerationContext( + IMethodSymbol Symbol, + IdentifierStubContext[] Parameters, + IdentifierStubContext ReturnValue) +{ + public bool ReturnsByRef => Symbol.ReturnsByRef || Symbol.ReturnsByRefReadonly; + public bool RequiresMarshalling { get; } = ReturnValue.Shape != MarshallerShape.None || Parameters.Any(p => p.Shape != MarshallerShape.None); +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/Models/StructStubGenerationContext.cs b/src/SampSharp.SourceGenerator/Models/StructStubGenerationContext.cs new file mode 100644 index 00000000..ec2152a1 --- /dev/null +++ b/src/SampSharp.SourceGenerator/Models/StructStubGenerationContext.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.Models; + +public record StructStubGenerationContext( + ISymbol Symbol, + StructDeclarationSyntax Syntax, + ApiMethodStubGenerationContext[] Methods, + ImplementingType[] ImplementingTypes, + bool IsPartial, + string Library, + List PublicMembers) +{ + public TypeSyntax Type { get; } = GetSelfType(Symbol, Syntax); + + private static TypeSyntax GetSelfType(ISymbol symbol, StructDeclarationSyntax syntax) + { + if (syntax.TypeParameterList == null) + { + return ParseTypeName(symbol.Name); + } + + return GenericName( + Identifier(symbol.Name)) + .WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + syntax.TypeParameterList!.Parameters.Select(x => IdentifierName(x.Identifier))))); + } +} + +public readonly record struct ImplementingType(DefiniteType Type, DefiniteType[] CastPath); + +public readonly record struct DefiniteType(ITypeSymbol Symbol, TypeSyntax Syntax); \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/SampSharp.SourceGenerator.csproj b/src/SampSharp.SourceGenerator/SampSharp.SourceGenerator.csproj new file mode 100644 index 00000000..cb0c47be --- /dev/null +++ b/src/SampSharp.SourceGenerator/SampSharp.SourceGenerator.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + true + latest + enable + + + + + + + + + + + + diff --git a/src/SampSharp.SourceGenerator/SyntaxFactories/AttributeFactory.cs b/src/SampSharp.SourceGenerator/SyntaxFactories/AttributeFactory.cs new file mode 100644 index 00000000..4c5e2031 --- /dev/null +++ b/src/SampSharp.SourceGenerator/SyntaxFactories/AttributeFactory.cs @@ -0,0 +1,90 @@ +using System.CodeDom.Compiler; +using System.Reflection; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.SyntaxFactories; + +/// +/// Creates attributes syntax nodes. +/// +public static class AttributeFactory +{ + private static readonly string GeneratedCodeFQN = $"global::{typeof(GeneratedCodeAttribute).FullName}"; + private const string SkipLocalsInitFQN = "global::System.Runtime.CompilerServices.SkipLocalsInitAttribute"; + private static readonly string DllImportFQN = $"global::{typeof(DllImportAttribute).FullName}"; + private static readonly string CallingConventionFQN = $"global::{typeof(CallingConvention).FullName}"; + private const string UnmanagedCallersOnlyFQN = "global::System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute"; + + public static AttributeListSyntax GeneratedCode() + { + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + + return AttributeList( + SingletonSeparatedList( + Attribute( + ParseName(GeneratedCodeFQN)) + .WithArgumentList( + AttributeArgumentList( + SeparatedList([ + AttributeArgument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(assemblyName.Name))), + AttributeArgument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(assemblyName.Version.ToString()))) + ]))))); + } + + public static AttributeListSyntax SkipLocalsInit() + { + return AttributeList( + SingletonSeparatedList( + Attribute( + ParseName(SkipLocalsInitFQN)))); + } + + public static AttributeListSyntax UnmanagedCallersOnly() + { + return AttributeList( + SingletonSeparatedList( + Attribute( + ParseName(UnmanagedCallersOnlyFQN)))); + } + + public static AttributeListSyntax DllImport(string library, string entryPoint, CallingConvention callingConvention = CallingConvention.Cdecl) + { + var conv = MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParseTypeName(CallingConventionFQN), + IdentifierName(callingConvention.ToString())); + + return AttributeList( + SingletonSeparatedList( + Attribute(ParseName(DllImportFQN), + AttributeArgumentList( + SeparatedList([ + AttributeArgument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(library)) + ), + AttributeArgument(conv) + .WithNameEquals(NameEquals(nameof(DllImportAttribute.CallingConvention))), + AttributeArgument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(entryPoint)) + ) + .WithNameEquals(NameEquals(nameof(DllImportAttribute.EntryPoint))), + AttributeArgument( + LiteralExpression(SyntaxKind.TrueLiteralExpression) + ) + .WithNameEquals(NameEquals(nameof(DllImportAttribute.ExactSpelling))) + ]) + ) + ) + ) + ); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/SyntaxFactories/HelperSyntaxFactory.cs b/src/SampSharp.SourceGenerator/SyntaxFactories/HelperSyntaxFactory.cs new file mode 100644 index 00000000..3801f0dc --- /dev/null +++ b/src/SampSharp.SourceGenerator/SyntaxFactories/HelperSyntaxFactory.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using SampSharp.SourceGenerator.Marshalling; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static SampSharp.SourceGenerator.SyntaxFactories.TypeSyntaxFactory; + +namespace SampSharp.SourceGenerator.SyntaxFactories; + +/// +/// Creates SampSharp specific syntax nodes. +/// +public static class HelperSyntaxFactory +{ + public static LocalFunctionStatementSyntax GenerateExternFunction( + string library, + string externName, + TypeSyntax externReturnType, + IEnumerable parameters, + params ParameterSyntax[] parametersPrefix) + { + var externParameters = ToParameterListSyntax(parametersPrefix, parameters, true); + + return LocalFunctionStatement(externReturnType, "__PInvoke") + .WithModifiers(TokenList( + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.UnsafeKeyword), + Token(SyntaxKind.ExternKeyword) + )) + .WithParameterList(externParameters) + .WithAttributeLists( + SingletonList( + AttributeFactory.DllImport(library, externName))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)) + .WithLeadingTrivia(Comment(MarshallingCodeGenDocumentation.COMMENT_P_INVOKE)); + } + + public static ParameterListSyntax ToParameterListSyntax(ImmutableArray parameters) + { + return ToParameterListSyntax([], parameters.Select(x => new ParamForwardInfo(x.Name, TypeNameGlobal(x.Type), x.RefKind))); + } + + public static ParameterListSyntax ToParameterListSyntax(ParameterSyntax[] prefix, IEnumerable parameters, bool removeIn = false) + { + return ParameterList( + SeparatedList(prefix) + .AddRange( + parameters + .Select(parameter => Parameter(Identifier(parameter.Name)) + .WithType(parameter.Type) + .WithModifiers(GetRefTokens(parameter.RefKind, removeIn))))); + } + + public static ParamForwardInfo ToForwardInfo(IdentifierStubContext context, bool toExtern = false) + { + var paramType = context.Generator.GetNativeType(context); + var refKind = context.RefKind; + + if (toExtern && refKind is RefKind.In or RefKind.RefReadOnlyParameter) + { + paramType = PointerType(paramType); + refKind = RefKind.None; + } + + var name = context.Generator.UsesNativeIdentifier && + context.Direction == MarshalDirection.UnmanagedToManaged + ? context.GetNativeId() + : context.GetManagedId(); + + return new ParamForwardInfo(name, paramType, refKind); + } + + private static SyntaxTokenList GetRefTokens(RefKind refKind, bool removeIn) + { + return refKind switch + { + RefKind.Ref => TokenList(Token(SyntaxKind.RefKeyword)), + RefKind.RefReadOnlyParameter => TokenList(Token(SyntaxKind.RefKeyword), Token(SyntaxKind.ReadOnlyKeyword)), + RefKind.Out => TokenList(Token(SyntaxKind.OutKeyword)), + RefKind.In => removeIn ? default : TokenList(Token(SyntaxKind.InKeyword)), + _ => default + }; + } + + public static ArgumentSyntax WithPInvokeParameterRefToken(ArgumentSyntax argument, RefKind refKind) + { + switch (refKind) + { + case RefKind.Ref: + return argument.WithRefKindKeyword(Token(SyntaxKind.RefKeyword)); + case RefKind.Out: + return argument.WithRefKindKeyword(Token(SyntaxKind.OutKeyword)); + default: + return argument; + } + } + + public record struct ParamForwardInfo(string Name, TypeSyntax Type, RefKind RefKind); +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/SyntaxFactories/StatementFactory.cs b/src/SampSharp.SourceGenerator/SyntaxFactories/StatementFactory.cs new file mode 100644 index 00000000..61ef44cf --- /dev/null +++ b/src/SampSharp.SourceGenerator/SyntaxFactories/StatementFactory.cs @@ -0,0 +1,109 @@ +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.SyntaxFactories; + +/// +/// Creates common statement syntax nodes. +/// +public static class StatementFactory +{ + public static LocalDeclarationStatementSyntax DeclareLocal(TypeSyntax type, string identifier) + { + return LocalDeclarationStatement( + VariableDeclaration(type, + SingletonSeparatedList( + VariableDeclarator(Identifier(identifier)) + .WithInitializer( + EqualsValueClause( + LiteralExpression(SyntaxKind.DefaultLiteralExpression, Token(SyntaxKind.DefaultKeyword))))))); + } + + + public static LocalDeclarationStatementSyntax DeclareLocal(TypeSyntax type, string identifier, ExpressionSyntax value) + { + return LocalDeclarationStatement( + VariableDeclaration(type) + .WithVariables( + SingletonSeparatedList( + VariableDeclarator( + Identifier(identifier)) + .WithInitializer( + EqualsValueClause( + value))))); + } + + public static ExpressionStatementSyntax Invoke(TypeSyntax target, string method) + { + return ExpressionStatement( + ExpressionFactory.InvocationExpression( + target, + method)); + } + + public static ExpressionStatementSyntax Invoke(string target, string method) + { + return ExpressionStatement( + ExpressionFactory.InvocationExpression( + target, + method)); + } + + public static ExpressionStatementSyntax Invoke(TypeSyntax target, string method, params ArgumentSyntax[] arguments) + { + return ExpressionStatement( + ExpressionFactory.InvocationExpression( + target, + method, + arguments)); + } + public static ExpressionStatementSyntax Invoke(string target, string method, params ArgumentSyntax[] arguments) + { + return ExpressionStatement( + ExpressionFactory.InvocationExpression( + target, + method, + arguments)); + } + + public static ExpressionStatementSyntax Assign(string destination, ExpressionSyntax value) + { + return ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(destination), + value)); + } +} + +public static class ExpressionFactory +{ + public static InvocationExpressionSyntax InvocationExpression(string target, string method) + { + return InvocationExpression(IdentifierName(target), method); + } + + public static InvocationExpressionSyntax InvocationExpression(TypeSyntax target, string method) + { + return SyntaxFactory.InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + target, + IdentifierName(method))); + } + + public static InvocationExpressionSyntax InvocationExpression(string target, string method, params ArgumentSyntax[] arguments) + { + return InvocationExpression(IdentifierName(target), method, arguments); + } + + public static InvocationExpressionSyntax InvocationExpression(TypeSyntax target, string method, params ArgumentSyntax[] arguments) + { + return SyntaxFactory.InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + target, + IdentifierName(method))).WithArgumentList(ArgumentList(SeparatedList(arguments))); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/SyntaxFactories/TriviaFactory.cs b/src/SampSharp.SourceGenerator/SyntaxFactories/TriviaFactory.cs new file mode 100644 index 00000000..620d4d8a --- /dev/null +++ b/src/SampSharp.SourceGenerator/SyntaxFactories/TriviaFactory.cs @@ -0,0 +1,249 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.SyntaxFactories; + +/// +/// Creates trivia syntax. +/// +public static class TriviaFactory +{ + public static SyntaxTrivia AutoGeneratedComment() + { + return Comment("// "); + } + + public static SyntaxTriviaList InheritDoc() + { + return TriviaList( + Trivia( + DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + List([ + XmlText("/// "), + XmlEmptyElement("inheritdoc"), + XmlText() + .WithTextTokens( + TokenList( + XmlTextNewLine("\n", false))) + + ])))); + } + + private static TypeSyntax StripGenericParameters(TypeSyntax type) + { + if (type is QualifiedNameSyntax qual) + { + return qual.WithRight((SimpleNameSyntax)StripGenericParameters(qual.Right)); + } + + if (type is GenericNameSyntax { TypeArgumentList.Arguments.Count: > 0 } gen) + { + if (gen.TypeArgumentList.Arguments.Count == 1) + { + return gen.WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + ParseTypeName("T")))); + } + + return gen.WithTypeArgumentList( + TypeArgumentList( + SeparatedList( + Enumerable.Range(1, gen.TypeArgumentList.Arguments.Count) + .Select(i => ParseTypeName($"T{i}"))))); + } + + return type; + } + + public static XmlNodeSyntax SeeElement(TypeSyntax crefType) + { + return XmlEmptyElement("see") + .WithAttributes( + SingletonList( + XmlCrefAttribute( + NameMemberCref( + StripGenericParameters(crefType))))); + } + + public static XmlNodeSyntax CElement(string c) + { + return XmlElement( + XmlElementStartTag( + XmlName("c")), + SingletonList( + XmlText(c)), + XmlElementEndTag( + XmlName("c")) + ); + } + + public static SyntaxTriviaList DocsStructConstructor(TypeSyntax structType, params ParameterDoc[] parameters) + { + var summary = List([ + XmlText("Initializes a new instance of the "), + SeeElement(structType), + XmlText(" struct."), + ]); + + return Docs(summary, parameters); + } + + public static SyntaxTriviaList DocsOpEqual() + { + return Docs( + List([ + XmlText("Determines whether the specified values are equal."), + ]), [ + new ParameterDoc("lhs", "The value on the left side of the operator."), + new ParameterDoc("rhs", "The value on the right side of the operator.") + ], List([ + CElement("true"), + XmlText(" if the values are equal; otherwise, "), + CElement("false"), + XmlText("."), + ])); + } + + public static SyntaxTriviaList DocsOpNotEqual() + { + return Docs( + List([ + XmlText("Determines whether the specified values are not equal."), + ]), [ + new ParameterDoc("lhs", "The value on the left side of the operator."), + new ParameterDoc("rhs", "The value on the right side of the operator.") + ], List([ + CElement("true"), + XmlText(" if the values are not equal; otherwise, "), + CElement("false"), + XmlText("."), + ])); + } + + public static SyntaxTriviaList DocsOpCast(TypeSyntax from, TypeSyntax to) + { + return Docs( + List([ + XmlText("Casts the "), + SeeElement(from), + XmlText(" to a "), + SeeElement(to), + XmlText("."), + ]), [ + new ParameterDoc("value", List([ + XmlText("The "), + SeeElement(from), + XmlText(" to cast.") + ])), + ], List([ + XmlText("The converted "), + SeeElement(to), + XmlText(".")])); + } + + public static SyntaxTriviaList Docs(string summary, ParameterDoc[] parameters, string? returns = null) + { + var r = returns == null + ? (SyntaxList?)null + : SingletonList(XmlText(summary)); + + return Docs(SingletonList(XmlText(summary)), parameters, r); + } + + public static SyntaxTriviaList Docs(SyntaxList summary, ParameterDoc[] parameters, SyntaxList? returns = null) + { + var nodes = new List + { + XmlText("/// "), + XmlElement( + XmlElementStartTag( + XmlName("summary")), + summary, + XmlElementEndTag( + XmlName("summary")) + ), + XmlText() + .WithTextTokens( + TokenList( + XmlTextNewLine("\n", false) + ) + ) + }; + + foreach (var parameter in parameters) + { + nodes.AddRange([ + XmlText("/// "), + XmlElement( + XmlElementStartTag( + XmlName("param")) + .AddAttributes( + XmlTextAttribute( + "name", + parameter.Name)), + parameter.Content, + XmlElementEndTag(XmlName("param")) + ), + XmlText().WithTextTokens( + TokenList( + XmlTextNewLine("\n", false) + ) + ) + ]); + } + + if (returns.HasValue) + { + nodes.AddRange([ + XmlText("/// "), + XmlElement( + XmlElementStartTag(XmlName("returns")), + returns.Value, + XmlElementEndTag(XmlName("returns")) + ), + XmlText().WithTextTokens( + TokenList( + XmlTextNewLine("\n", false) + ) + ) + ]); + } + + return TriviaList( + Trivia( + DocumentationCommentTrivia( + SyntaxKind.SingleLineDocumentationCommentTrivia, + List(nodes) + ) + ) + ); + } + + public readonly record struct ParameterDoc(string Name, SyntaxList Content) + { + public ParameterDoc(string name, string text) : this(name, SingletonList(XmlText(text))) + { + } + } + + public static SyntaxTrivia NullableEnable() + { + return Trivia( + NullableDirectiveTrivia( + Token(SyntaxKind.EnableKeyword), + true)); + } + public static SyntaxTrivia NullableDisable() + { + return Trivia( + NullableDirectiveTrivia( + Token(SyntaxKind.DisableKeyword), + false)); + } +} \ No newline at end of file diff --git a/src/SampSharp.SourceGenerator/SyntaxFactories/TypeSyntaxFactory.cs b/src/SampSharp.SourceGenerator/SyntaxFactories/TypeSyntaxFactory.cs new file mode 100644 index 00000000..06271fdf --- /dev/null +++ b/src/SampSharp.SourceGenerator/SyntaxFactories/TypeSyntaxFactory.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace SampSharp.SourceGenerator.SyntaxFactories; + +/// +/// Creates type syntax nodes. +/// +public static class TypeSyntaxFactory +{ + /// + /// nint + /// + public static TypeSyntax IntPtrType { get; } = ParseTypeName("nint"); + + /// + /// int + /// + public static TypeSyntax IntType { get; } = PredefinedType(Token(SyntaxKind.IntKeyword)); + + /// + /// object + /// + public static TypeSyntax ObjectType { get; } = PredefinedType(Token(SyntaxKind.ObjectKeyword)); + + /// + /// global::System.Span<byte> + /// + public static TypeSyntax SpanOfBytes { get; } = QualifiedName( + AliasQualifiedName( + IdentifierName( + Token(SyntaxKind.GlobalKeyword)), + IdentifierName("System")), + GenericName( + Identifier("Span")) + .WithTypeArgumentList( + TypeArgumentList(SingletonSeparatedList( + PredefinedType( + Token(SyntaxKind.ByteKeyword)))))); + + public static IdentifierNameSyntax IdentifierNameGlobal(string typeFQN) + { + return IdentifierName(ToGlobalTypeString(typeFQN)); + } + + public static SyntaxToken IdentifierGlobal(string typeFQN) + { + return Identifier(ToGlobalTypeString(typeFQN)); + } + + public static TypeSyntax TypeNameGlobal(string typeFQN) + { + return ParseTypeName(ToGlobalTypeString(typeFQN)); + } + + public static TypeSyntax TypeNameGlobal(ITypeSymbol symbol) + { + if (symbol.TypeKind == TypeKind.TypeParameter) + { + return ParseTypeName(symbol.Name); + } + + if (symbol is INamedTypeSymbol { IsTupleType: true } namedTypeSymbol) + { + var elements = new List(); + foreach (var element in namedTypeSymbol.TupleElements) + { + var el = TupleElement(TypeNameGlobal(element.Type)); + if (element.IsExplicitlyNamedTupleElement) + { + el = el.WithIdentifier(Identifier(element.Name)); + } + + elements.Add(el); + } + + return TupleType(SeparatedList(elements)); + } + + return ParseTypeName( + symbol.SpecialType == SpecialType.None + ? ToGlobalTypeString(symbol) + : symbol.ToDisplayString()); + } + + public static TypeSyntax TypeNameGlobal(IMethodSymbol returnTypeOfMethod, bool includeNullable = false) + { + var result = TypeNameGlobal(returnTypeOfMethod.ReturnType); + + if (includeNullable && returnTypeOfMethod.ReturnNullableAnnotation == NullableAnnotation.Annotated) + { + result = NullableType(result); + } + + if (returnTypeOfMethod.ReturnsByRef || returnTypeOfMethod.ReturnsByRefReadonly) + { + result = returnTypeOfMethod.ReturnsByRefReadonly + ? RefType(result).WithReadOnlyKeyword(Token(SyntaxKind.ReadOnlyKeyword)) + : RefType(result); + } + + return result; + } + + public static TypeSyntax GenericType(string typeFQN, TypeSyntax typeArgument) + { + return GenericName( + IdentifierGlobal(typeFQN)) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList(typeArgument))); + } + + public static string ToGlobalTypeString(string typeFQN) + { + return $"global::{typeFQN}"; + } + + public static string ToGlobalTypeString(ITypeSymbol symbol) + { + return symbol.SpecialType == SpecialType.None && symbol.TypeKind != TypeKind.Pointer + ? ToGlobalTypeString(symbol.ToDisplayString()) + : symbol.ToDisplayString(); + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Core/Nickname.cs b/src/TestMode.OpenMp.Core/Nickname.cs new file mode 100644 index 00000000..ee1386b1 --- /dev/null +++ b/src/TestMode.OpenMp.Core/Nickname.cs @@ -0,0 +1,19 @@ +using SampSharp.OpenMp.Core; + +namespace TestMode.OpenMp.Core; + +[Extension(0x57a6f80937089f8b)] +public class Nickname : Extension +{ + public Nickname(string name) + { + Name = name; + } + + public string Name { get; } + + public override string ToString() + { + return $"{{name: {Name}}}"; + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Core/Startup.cs b/src/TestMode.OpenMp.Core/Startup.cs new file mode 100644 index 00000000..651d1d3f --- /dev/null +++ b/src/TestMode.OpenMp.Core/Startup.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Numerics; +using SampSharp.OpenMp.Core; +using SampSharp.OpenMp.Core.Api; +using SampSharp.OpenMp.Core.RobinHood; +using SampSharp.OpenMp.Core.Std; +using SampSharp.OpenMp.Core.Std.Chrono; + +namespace TestMode.OpenMp.Core; + +public class Startup : IStartup, + ICoreEventHandler, + IConsoleEventHandler, + IPlayerConnectEventHandler, + IPoolEventHandler +{ + public void Initialize(IStartupContext context) + { + context.UseOpenMpLogger(); + + Console.WriteLine("OnInit from managed c# code!"); + + Console.WriteLine($"Component version: {context.Info.Version}"); + Console.WriteLine($"size {context.Info.Size.Value} api {context.Info.ApiVersion}"); + Console.WriteLine($"Network bit stream version: {context.Core.GetNetworkBitStreamVersion()}"); + + Console.WriteLine($"core version: {context.Core.GetVersion()}"); + + var cfg = context.Core.GetConfig(); + + context.Core.GetPlayers().GetPlayerConnectDispatcher().AddEventHandler(this); + + // test config + var name = cfg.GetString("name"); + var announce = cfg.GetBool("announce"); + var lanMode = cfg.GetBool("network.use_lan_mode"); + var inputFilter = cfg.GetBool("chat_input_filter"); + Console.WriteLine($"name: {name} announce: {announce} use_lan_mode: {lanMode} chat_input_filter: {inputFilter}"); + + // test bans + + cfg.AddBan(new BanEntry("1.2.3.5", DateTimeOffset.UtcNow, "name", "reason")); + cfg.AddBan(new BanEntry("1.2.3.6", DateTimeOffset.UtcNow, "name", "reason2")); + cfg.WriteBans(); + + Console.WriteLine("written ban"); + + for (nint i = 0; i < cfg.GetBansCount().Value; i++) + { + var b = cfg.GetBan(new Size(i))!; + Console.WriteLine($"ban: {b.Name} {b.Address} {b.Reason} {b.Time}"); + } + + var alias = cfg.GetNameFromAlias("rcon"); + Console.WriteLine($"rcon alias: {alias}"); + + var vehiclesComponent = context.ComponentList.QueryComponent(); + + // test handlers + var dispatcher = context.Core.GetEventDispatcher(); + Console.WriteLine($"COUNT before:::::::::::::::::::::::::::: {dispatcher.Count()}"); + dispatcher.AddEventHandler(this); + Console.WriteLine($"COUNT after:::::::::::::::::::::::::::: {dispatcher.Count()}"); + + context.ComponentList.QueryComponent().GetEventDispatcher().AddEventHandler(this); + + var v = vehiclesComponent.Create(false, 401, new Vector3(5, 0, 10)); + v.GetColour(out var vcol); + Console.WriteLine($"vehicle color: <1: {vcol.First}, 2: {vcol.Second}>"); + + Console.WriteLine("add extension"); + v.AddExtension(new Nickname("Brum")); + + Console.WriteLine("get extension"); + var nick = v.GetExtension(); + Console.WriteLine((nick.ToString())); + + Console.WriteLine("remove extension"); + v.RemoveExtension(nick); + + Console.WriteLine("get extension"); + nick = v.TryGetExtension(); + Console.WriteLine((nick?.ToString() ?? "null")); + + + var playerPoolEventDispatcher = context.Core.GetPlayers().GetPoolEventDispatcher(); + Console.WriteLine("count before " + playerPoolEventDispatcher.Count()); + playerPoolEventDispatcher.AddEventHandler(this); + Console.WriteLine("count after " + playerPoolEventDispatcher.Count()); + + var tds = context.ComponentList.QueryComponent(); + var txd = tds.Create(new Vector2(0, 0), 98); + var txd2 = tds.Create(new Vector2(0, 0), 99); + var txt = txd.GetText(); + Console.WriteLine($"textdraw text: '{txt ?? "<>"}'"); + Console.WriteLine($"default plate: '{v.GetPlate() ?? "<>"}'"); + + Console.WriteLine($"id of textdraw: {txd.GetID()}, {txd2.GetID()}"); + + + Console.WriteLine(""); + context.Core.PrintLine("Hello, World!"); + context.Core.LogLine(LogLevel.Error, "Hello, World!"); + Console.WriteLine(""); + // try + // { + // throw new Exception("awful"); + // } + // catch(Exception e) + // { + // SampSharpExceptionHandler.HandleException("test", e); + // } + + var pool = vehiclesComponent.AsPool(); + + var vehCount = pool.Count(); + + Console.WriteLine($"Vehicle count: {vehCount.ToInt32()}"); + + Console.WriteLine("Vehicle iterator begin"); + foreach (var vehicle in pool) + { + Console.WriteLine($"id: {vehicle.GetID()} model: {vehicle.GetModel()} @ {vehicle.GetPosition()}"); + } + Console.WriteLine("Vehicle iterator end"); + + var tdPool = tds.AsPool(); + + foreach (var td in tdPool) + { + Console.WriteLine($"TD: {td.GetID()}, prev mdl: {td.GetPreviewModel()}"); + } + } + + public void OnTick(Microseconds elapsed, TimePoint now) + { + } + + public bool OnConsoleText(string command, string parameters, ref ConsoleCommandSenderData sender) + { + if (command == "banana") + { + Console.WriteLine($"BANANA!!! {parameters}"); + return true; + } + + Console.WriteLine($"cmd: {command}; params: {parameters}"); + + + return false; + } + + public void OnRconLoginAttempt(IPlayer player, string password, bool success) + { + Console.WriteLine($"login attempt by player {player.GetName()}({player.Handle:X}) w/pw {password}; {success}"); + } + + public void OnConsoleCommandListRequest(FlatHashSetStringView commands) + { + commands.Emplace("banana"); + } + + public void OnIncomingConnection(IPlayer player, string ipAddress, ushort port) + { + } + + public void OnPlayerConnect(IPlayer player) + { + Console.WriteLine($"Player connected: {player.GetNetworkData().Value.networkID.address.ToAddress()}"); + } + + public void OnPlayerDisconnect(IPlayer player, PeerDisconnectReason reason) + { + } + + public void OnPlayerClientInit(IPlayer player) + { + } + + public void OnPoolEntryCreated(IPlayer entry) + { + } + + public void OnPoolEntryDestroyed(IPlayer entry) + { + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Core/TestMode.OpenMp.Core.csproj b/src/TestMode.OpenMp.Core/TestMode.OpenMp.Core.csproj new file mode 100644 index 00000000..996bf23c --- /dev/null +++ b/src/TestMode.OpenMp.Core/TestMode.OpenMp.Core.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + true + + + + + + + + + + \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/AdminComponent.cs b/src/TestMode.OpenMp.Entities/AdminComponent.cs new file mode 100644 index 00000000..998530f0 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/AdminComponent.cs @@ -0,0 +1,8 @@ +using SampSharp.Entities; + +namespace TestMode.OpenMp.Entities; + +public class AdminComponent : Component +{ + +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/BasicsSystem.cs b/src/TestMode.OpenMp.Entities/BasicsSystem.cs new file mode 100644 index 00000000..f90d1674 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/BasicsSystem.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using SampSharp.Entities; +using SampSharp.Entities.SAMP; + +namespace TestMode.OpenMp.Entities; + +public class BasicsSystem : ISystem +{ + [Event] + public void OnGameModeInit(IServerService svr) + { + svr.AddPlayerClass(1, new Vector3(0, 0, 10), 0); + } + + // [Event] + // public bool OnPlayerRequestClass(Player player, int classId) + // { + // return true; + // } + // + // [Event] + // public bool OnPlayerRequestSpawn(Player player) + // { + // return true; + // } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/EntityDebugSystem.cs b/src/TestMode.OpenMp.Entities/EntityDebugSystem.cs new file mode 100644 index 00000000..027883da --- /dev/null +++ b/src/TestMode.OpenMp.Entities/EntityDebugSystem.cs @@ -0,0 +1,54 @@ +using SampSharp.Entities; +using SampSharp.Entities.SAMP; + +namespace TestMode.OpenMp.Entities; + +public class EntityDebugSystem : ISystem +{ + [Event] + public void OnConsoleCommandListRequest(ConsoleCommandCollection commands) + { + commands.Add("dump"); + } + + [Event] + public bool OnConsoleText(string command, string args, ConsoleCommandSender sender, IEntityManager entityManager) + { + if (command == "dump") + { + foreach (var entity in entityManager.GetRootEntities()) + { + DumpEntities(entityManager, entity, 0); + } + return true; + } + + return false; + + } + + private static void DumpEntities(IEntityManager entityManager, EntityId entity, int depth) + { + var ws = string.Concat(Enumerable.Repeat("| ", depth)); + + if (depth > 0) + { + var ws2 = string.Concat(Enumerable.Repeat("| ", depth - 1)); + Console.WriteLine($"{ws2}+-E: {entity}"); + } + else + { + Console.WriteLine($"E: {entity}"); + } + + foreach (var component in entityManager.GetComponents(entity)) + { + Console.WriteLine($"{ws}+C: {component.GetType().Name} ({component})"); + } + + foreach (var child in entityManager.GetChildren(entity)) + { + DumpEntities(entityManager, child, depth + 1); + } + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/MyCustomComponent.cs b/src/TestMode.OpenMp.Entities/MyCustomComponent.cs new file mode 100644 index 00000000..fca05cdb --- /dev/null +++ b/src/TestMode.OpenMp.Entities/MyCustomComponent.cs @@ -0,0 +1,11 @@ +using SampSharp.Entities; + +namespace TestMode.OpenMp.Entities; + +public class MyCustomComponent : Component +{ + public override string ToString() + { + return "( )"; + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/MyFirstSystem.cs b/src/TestMode.OpenMp.Entities/MyFirstSystem.cs new file mode 100644 index 00000000..3fa21636 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/MyFirstSystem.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using Microsoft.Extensions.Logging; +using SampSharp.Entities; +using SampSharp.Entities.SAMP; +using SampSharp.Entities.SAMP.Commands; +using SampSharp.OpenMp.Core; +using SampSharp.OpenMp.Core.Api; +using PlayerRecordingType = SampSharp.Entities.SAMP.PlayerRecordingType; + +namespace TestMode.OpenMp.Entities; + +public class MyFirstSystem : ISystem +{ + private int _ticks; + [Timer(1000)] + public void OnTimer() + { + if (_ticks++ == 3) + { + //throw new Exception("Test exception"); + } + } + + [Event] + public void OnGameModeInit(IWorldService world, IEntityManager entityManager, ILogger logger) + { + logger.LogInformation("whoop!"); + + var vehicle = world.CreateVehicle(VehicleModelType.Landstalker, new Vector3(0, 6, 15), 45, 4, 4); + vehicle.ChangeColor(5, 12); + vehicle.Bonnet = true; + vehicle.SetNumberPlate("SampSharp"); + + vehicle.AddComponent(); + + entityManager.AddComponent(EntityId.NewEntityId(), vehicle); + + var actor = world.CreateActor(1, new Vector3(15, 0, 5), 0); + + var spawn = ((IActor)actor).GetSpawnData(); + Console.WriteLine(spawn); + + Task.Run(async () => + { + Console.WriteLine($"[task] running on main thread? (1) {TaskHelper.IsMainThread()}"); + await Task.Delay(10); + Console.WriteLine($"[task] running on main thread? (2) {TaskHelper.IsMainThread()}"); + await TaskHelper.SwitchToMainThread(); + Console.WriteLine($"[task] running on main thread? (3) {TaskHelper.IsMainThread()}"); + }); + + _ = AsyncVoidTest(); + } + + private static async Task AsyncVoidTest() + { + Console.WriteLine($"[async void] running on main thread? (1) {TaskHelper.IsMainThread()}"); + Console.WriteLine("sync context: " + SynchronizationContext.Current); + await Task.Delay(10); + Console.WriteLine($"[async void] running on main thread? (2) {TaskHelper.IsMainThread()}"); + Console.WriteLine("sync context: " + SynchronizationContext.Current); + await TaskHelper.SwitchToMainThread(); + Console.WriteLine($"[async void] running on main thread? (3) {TaskHelper.IsMainThread()}"); + Console.WriteLine("sync context: " + SynchronizationContext.Current); + } + + [PlayerCommand("dialog-input")] + public void DialogInputCommand(Player player, IDialogService dialogService) + { + var diag = new InputDialog("Input", "Enter your name", "OK", "Cancel"); + + dialogService.Show(player, diag, r => player.SendClientMessage($"response: {r.Response}, {r.InputText ?? "<>"}")); + } + + [PlayerCommand("dialog-message")] + public void DialogMessageCommand(Player player, IDialogService dialogService) + { + var diag = new MessageDialog("Message", "This is a message dialog", "OK"); + + dialogService.Show(player, diag, r => player.SendClientMessage($"response: {r.Response}")); + } + + [PlayerCommand("dialog-list")] + public void DialogListCommand(Player player, IDialogService dialogService) + { + var diag = new ListDialog("List", "OK") + { + "A", "B", "C" + }; + + dialogService.Show(player, diag, r => player.SendClientMessage($"response: {r.Response} {r.ItemIndex} {r.Item?.Text ?? "<>"}")); + } + + [PlayerCommand] + public void NetCommand(Player player) + { + var n = player.GetNetworkStats(); + player.SendClientMessage(n.MessagesSent.ToString()); + } + + [PlayerCommand] + public void AkCommand(Player player) + { + player.GiveWeapon(Weapon.AK47, 200); + } + + + [PlayerCommand] + public void RefTestCommand(Player player) + { + var weaponState = player.WeaponState; + var anim = player.AnimationIndex; + var cfv = player.CameraFrontVector; + var cm = player.CameraMode; + player.GetKeys(out var keys, out var ud, out var lr); + player.PlaySound(5408); // 5408 - "No more bets please!" + player.GetAnimationName(out var lib, out var name); + + player.SendClientMessage($"Weapon state: {weaponState}, anim: {anim}, cfv: {cfv}, cm: {cm}, keys: {keys}, ud: {ud}, lr: {lr}, lib: {lib}, name: {name}"); + } + + [Event] + public void OnVehicleSpawn(Vehicle vehicle) + { + Console.WriteLine($"Vehicle {vehicle.Id} spawned!"); + } + + [Event] + public void OnVehicleStreamIn(Vehicle vehicle, Player player) + { + Console.WriteLine($"Vehicle {vehicle.Id} streams in for player {player}"); + } + + [Event] + public void OnVehicleStreamOut(Vehicle vehicle, Player player) + { + Console.WriteLine($"Vehicle {vehicle.Id} streams out for player {player}"); + } + + [Event] + public void OnRconLoginAttempt(Player player, string password, bool success) + { + if (success) + { + player.AddComponent(); + } + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/RotationTesting.cs b/src/TestMode.OpenMp.Entities/RotationTesting.cs new file mode 100644 index 00000000..1301d3d7 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/RotationTesting.cs @@ -0,0 +1,174 @@ +using System.Numerics; +using SampSharp.Entities; +using SampSharp.Entities.SAMP; + +namespace TestMode.OpenMp.Entities; + +public class RotationTestingSystem(IWorldService worldService, IEntityManager entityManager, IVehicleInfoService vehicleInfoService, ITimerService timerService) : ISystem +{ + [Event] + public bool OnConsoleText(string command, string args, ConsoleCommandSender sender) + { + if (command == "test") + { + var eulerIn = new Vector3(10, 20, 30); + var radIn = Vector3.DegreesToRadians(eulerIn); + var quat = MathHelper.CreateQuaternionFromYawPitchRoll(radIn); + var radOut = MathHelper.CreateYawPitchRollFromQuaternion(quat); + var eulerOut = Vector3.RadiansToDegrees(radOut); + Console.WriteLine($"{eulerIn} -> {eulerOut}"); + return true; + } + + return false; + } + + [Event] + public bool OnPlayerCommandText(Player player, string cmdText) + { + if (cmdText.StartsWith("/spawn") && cmdText.Length > 7) + { + var mdl = int.Parse(cmdText[7..]); + var vehicle = worldService.CreateVehicle((VehicleModelType)mdl, player.Position + GtaVector.Up * 5, 0, -1, -1); + player.PutInVehicle(vehicle, 0); + return true; + } + + if (cmdText.StartsWith("/coords")) + { + var pos = player.Position; + Console.WriteLine($"Position: {pos}"); + player.SendClientMessage($"Position: {pos}"); + + Mark(pos, "*", Color.White); + Mark(pos + Vector3.UnitX, "+X", Color.Red); + Mark(pos + Vector3.UnitY, "+Y", Color.Green); + Mark(pos + Vector3.UnitZ, "+Z", Color.Blue); + return true; + } + + if (cmdText == "/test") + { + var eulerIn = new Vector3(10, 20, 30); + var radIn = Vector3.DegreesToRadians(eulerIn); + var quat = MathHelper.CreateQuaternionFromYawPitchRoll(radIn); + var radOut = MathHelper.CreateYawPitchRollFromQuaternion(quat); + var eulerOut = Vector3.RadiansToDegrees(radOut); + Console.WriteLine($"{eulerIn} -> {eulerOut}"); + player.SendClientMessage($"{eulerIn} -> {eulerOut}"); + return true; + } + + if (cmdText.StartsWith("/arrow")) + { + var pts = cmdText.Length == 6 + ? + [ + 100, 200, 300 + ] + : cmdText[6..].Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(x => float.TryParse(x, out var y) ? y : 0).ToArray(); + + if (pts.Length < 3) { return false;} + var vec = new Vector3(pts[0], pts[1], pts[2]); + + var index = 0; + + ArrowTest("create(vec)", _ => { }); + // ArrowTest("Rotation = MathHelper.CreateQuaternionFromYawPitchRoll(vec)", obj => + // { + // var rads = Vector3.DegreesToRadians(vec); + // var quat = MathHelper.CreateQuaternionFromYawPitchRoll(rads); + // obj.Rotation = quat; + // }); + // ArrowTest("RotationEuler = vec", obj => + // { + // obj.RotationEuler = vec; + // }); + ArrowTest("RotationEuler = RotationEuler", obj => + { + player.SendClientMessage($"euser@create={vec}"); + player.SendClientMessage($"RotationEuler = {obj.RotationEuler}"); + + obj.RotationEuler = obj.RotationEuler; + }); + + return true; + + void ArrowTest(string txt, Action mod) + { + var offset = index++ * 1.0f; + + var pos = player.Position + GtaVector.Up + GtaVector.Forward * offset; + var obj = worldService.CreateObject(19132, pos, vec); + + mod(obj); + + timerService.Delay(_ => entityManager.Destroy(obj), TimeSpan.FromSeconds(60)); + + Mark(pos + GtaVector.Up, txt, Color.White, 60); + } + } + if (cmdText == "/circle") + { + var center = player.Position + GtaVector.Up; + + Mark(center, "[c]", Color.Red); + for(var angle = 0; angle < 360; angle += 45) + { + var pos = center + Vector3.Transform(GtaVector.Up * 3, Quaternion.CreateFromAxisAngle(GtaVector.Up, float.DegreesToRadians(angle))); + + Mark(pos, $"[{angle}]", Color.Blue); + } + + return true; + } + + if (cmdText == "/angle") + { + var v= player.Vehicle; + + if (v == null) + { + return false; + } + + var zAngle = v.Angle; + + var mat = Matrix4x4.CreateFromQuaternion(v.Rotation); + var zAngle2 = float.RadiansToDegrees(MathHelper.GetZAngleFromRotationMatrix(mat)); + + player.SendClientMessage($"Vehicle Z-angle(open.mp): {zAngle}, ZAngle through RotQuat(s#): {zAngle2}"); + + return true; + } + return false; + } + + + [Timer(100)] + public void UpdateMark() + { + foreach (var vehicle in entityManager.GetComponents()) + { + var label = vehicle.GetComponentInChildren() + ?? worldService.CreateTextLabel("[x]", Color.White, Vector3.Zero, 20, parent: vehicle); + + // calculate offset to the rear center bumper of the vehicle + var model = vehicle.Model; + var offset = vehicleInfoService.GetModelInfo(model, VehicleModelInfoType.PetrolCap); + + var rotMatrix = Matrix4x4.CreateFromQuaternion(vehicle.Rotation); + var trMatrix = Matrix4x4.CreateTranslation(offset) * rotMatrix * Matrix4x4.CreateTranslation(vehicle.Position); + + var point = trMatrix.Translation; + + label.Position = point; + } + } + + private void Mark(Vector3 point, string txt, Color color, int sec = 10) + { + var label = worldService.CreateTextLabel(txt, color, point, 100, 0, false); + timerService.Delay(_ => entityManager.Destroy(label), TimeSpan.FromSeconds(sec)); + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/Startup.cs b/src/TestMode.OpenMp.Entities/Startup.cs new file mode 100644 index 00000000..aa422588 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/Startup.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SampSharp.Entities; +using SampSharp.Entities.SAMP.Commands; +using SampSharp.OpenMp.Core; + +namespace TestMode.OpenMp.Entities; + +public class Startup : IEcsStartup +{ + public void Initialize(IStartupContext context) + { + context.UseEntities() + .UsePlayerCommands() + .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Information)); + } + + public void ConfigureServices(IServiceCollection services) + { + } + + public void Configure(IEcsBuilder builder) + { + } +} \ No newline at end of file diff --git a/src/TestMode.OpenMp.Entities/TestMode.OpenMp.Entities.csproj b/src/TestMode.OpenMp.Entities/TestMode.OpenMp.Entities.csproj new file mode 100644 index 00000000..444e2029 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/TestMode.OpenMp.Entities.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/src/TestMode.OpenMp.Entities/TestTicker.cs b/src/TestMode.OpenMp.Entities/TestTicker.cs new file mode 100644 index 00000000..4e8c95d1 --- /dev/null +++ b/src/TestMode.OpenMp.Entities/TestTicker.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; +using SampSharp.Entities; + +namespace TestMode.OpenMp.Entities; + +public class TestTicker : ITickingSystem +{ + [Event] + public void OnInitialized(ILogger logger) + { + logger.LogInformation("On initialized"); + } + + public void Tick() + { + + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/ActorTests.cs b/src/TestMode.UnitTests/ActorTests.cs new file mode 100644 index 00000000..cf57fd16 --- /dev/null +++ b/src/TestMode.UnitTests/ActorTests.cs @@ -0,0 +1,91 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class ActorTests : TestBase +{ + private readonly Actor _actor; + + public ActorTests() + { + _actor = Services.GetRequiredService().CreateActor(46, new Vector3(4, 5, 6), 45); + } + + protected override void Cleanup() + { + _actor.DestroyEntity(); + } + + [Fact] + public void CreateActor_should_set_properties() + { + _actor.Skin.ShouldBe(46); + _actor.Position.ShouldBe(new Vector3(4, 5, 6)); + _actor.Angle.ShouldBe(45); + } + + [Fact] + public void Angle_should_roundtrip() + { + _actor.Angle = 88; + _actor.Angle.ShouldBe(88); + } + + [Fact] + public void Position_should_roundtrip() + { + _actor.Position = new Vector3(1, 2, 3); + _actor.Position.ShouldBe(new Vector3(1, 2, 3)); + } + + [Fact] + public void Health_should_roundtrip() + { + _actor.Health = 94; + _actor.Health.ShouldBe(94); + } + + [Fact] + public void IsInvulnerable_should_roundtrip_true() + { + _actor.IsInvulnerable = true; + _actor.IsInvulnerable.ShouldBeTrue(); + } + + [Fact] + public void IsInvulnerable_should_roundtrip_false() + { + _actor.IsInvulnerable = false; + _actor.IsInvulnerable.ShouldBeFalse(); + } + + [Fact] + public void Skin_should_roundtrip() + { + _actor.Skin = 99; + _actor.Skin.ShouldBe(99); + } + + [Fact] + public void ApplyAnim_should_succeed() + { + _actor.ApplyAnimation("BASEBALL", "Bat_Hit_1", 1, false, false, false, false, TimeSpan.FromMilliseconds(830)); + } + + [Fact] + public void IsStreamedIn_should_succeed() + { + _actor.IsStreamedIn(Player); + } + + [Fact] + public void ClearAnimations_should_succeed() + { + _actor.ApplyAnimation("BASEBALL", "Bat_Hit_1", 1, false, false, false, false, TimeSpan.FromMilliseconds(830)); + _actor.ClearAnimations(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/GangZoneTests.cs b/src/TestMode.UnitTests/GangZoneTests.cs new file mode 100644 index 00000000..637f4807 --- /dev/null +++ b/src/TestMode.UnitTests/GangZoneTests.cs @@ -0,0 +1,100 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core.Api; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class GangZoneTests : TestBase +{ + private readonly GangZone _gangZone; + + public GangZoneTests() + { + _gangZone = Services.GetRequiredService().CreateGangZone(new Vector2(10, 11), new Vector2(20, 21)); + _gangZone.Color = new Colour(255, 0, 0, 100); + } + + protected override void Cleanup() + { + _gangZone?.Destroy(); + } + + [Fact] + public void CreateGangZone_should_set_properties() + { + _gangZone.Min.ShouldBe(new Vector2(10, 11)); + _gangZone.Max.ShouldBe(new Vector2(20, 21)); + } + + [Fact] + public void Min_should_be_correct() + { + _gangZone.Min.ShouldBe(new Vector2(10, 11)); + } + + [Fact] + public void Max_should_be_correct() + { + _gangZone.Max.ShouldBe(new Vector2(20, 21)); + } + + [Fact] + public void Color_should_rountrip() + { + _gangZone.Color = new Color(1, 2, 3, 4); + _gangZone.Color.ShouldBe(new Color(1, 2, 3, 4)); + } + + [Fact] + public void Show_should_work() + { + _gangZone.Show(); + } + [Fact] + public void Hide_should_work() + { + _gangZone.Hide(); + } + + [Fact] + public void Show_should_work_for_player() + { + _gangZone.Show(Player); + } + + [Fact] + public void Hide_should_work_for_player() + { + _gangZone.Show(Player); + _gangZone.Hide(Player); + } + + [Fact] + public void Flash_should_work() + { + _gangZone.Flash(Color.White); + } + + [Fact] + public void Flash_should_work_for_player() + { + _gangZone.Flash(Player, Color.White); + } + + [Fact] + public void StopFlash_should_work() + { + _gangZone.Flash(Color.White); + _gangZone.StopFlash(); + } + + [Fact] + public void StopFlash_should_work_for_player() + { + _gangZone.Flash(Player, Color.White); + _gangZone.StopFlash(Player); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/GlobalObjectTests.cs b/src/TestMode.UnitTests/GlobalObjectTests.cs new file mode 100644 index 00000000..ffe7afb0 --- /dev/null +++ b/src/TestMode.UnitTests/GlobalObjectTests.cs @@ -0,0 +1,145 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core.Api; +using Shouldly; +using Xunit; +using ObjectMaterialSize = SampSharp.Entities.SAMP.ObjectMaterialSize; +using ObjectMaterialTextAlign = SampSharp.Entities.SAMP.ObjectMaterialTextAlign; + +namespace TestMode.UnitTests; + +public class GlobalObjectTests : TestBase +{ + private readonly IWorldService _worldService; + private readonly GlobalObject _object; + + public GlobalObjectTests() + { + _worldService = Services.GetRequiredService(); + _object = _worldService.CreateObject(100, new Vector3(10, 20, 30), new Vector3(20, 10, 0), 30); + } + + protected override void Cleanup() + { + _object?.Destroy(); + } + + [Fact] + public void CreateObject_should_set_properties() + { + _object.ModelId.ShouldBe(100); + _object.Position.ShouldBe(new Vector3(10, 20, 30)); + _object.RotationEuler.ShouldBe(new Vector3(20, 10, 0), 0.8f); + _object.DrawDistance.ShouldBe(30); + } + + [Fact] + public void Position_should_roundtrip() + { + _object.Position = new Vector3(20, 30, 40); + _object.Position.ShouldBe(new Vector3(20, 30, 40)); + } + + [Fact] + public void IsMoving_should_return_correct_value() + { + _object.Move(new Vector3(100, 200, 300), 10, Vector3.Zero); + _object.IsMoving.ShouldBeTrue(); + _object.Stop(); + _object.IsMoving.ShouldBeFalse(); + } + + [Fact] + public void RotationEuler_should_roundtrip() + { + _object.RotationEuler = new Vector3(20, 30, 40); + _object.RotationEuler.ShouldBe(new Vector3(20, 30, 40), 0.8f); + } + + [Fact] + public void Move_and_Stop_should_succeed() + { + _object.Move(new Vector3(100, 200, 300), 10, Vector3.Zero); + _object.Stop(); + } + + [Fact] + public void Move_time_should_be_correct() + { + _object.Position = new Vector3(100, 0, 0); + var result = _object.Move(new Vector3(200, 0, 0), 10, Vector3.Zero); + _object.Stop(); + + result.ShouldBe(10000); + } + + [Fact] + public void DisableCameraCollisions_should_succeed() + { + _object.DisableCameraCollisions(); + } + + [Fact] + public void SetMaterial_should_succeed() + { + _object.SetMaterial(0, 0, "none", "none", Color.White); + } + + [Fact] + public void SetMaterialText_should_succeed() + { + _object.SetMaterialText(0, "test", ObjectMaterialSize.X128X128, "Arial", 12, true, Color.White, Color.White, ObjectMaterialTextAlign.Center); + } + + [Fact] + public void Core_GetMaterialData_should_succeed() + { + _object.SetMaterial(0, 123, "none", "none", Color.White); + + IObject obj = _object; + + obj.GetMaterialData(0, out var data).ShouldBeTrue(); + data.ShouldNotBeNull(); + data.Model.ShouldBe(123); + data.Txd.ShouldBe("none"); + data.Texture.ShouldBe("none"); + data.MaterialColour.ShouldBe((Colour)Color.White); + } + + [Fact] + public void AttachToPlayer_should_succeed() + { + _object.AttachTo(Player, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + + [Fact] + public void AttachToVehicle_should_succeed() + { + var vehicle = _worldService.CreateVehicle(VehicleModelType.Landstalker, new Vector3(0, 0, 0), 0, 0, 0); + + try + { + _object.AttachTo(vehicle, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + finally + { + vehicle.Destroy(); + } + } + + [Fact] + public void AttachToObject_should_succeed() + { + var obj = _worldService.CreateObject(100, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + + try + { + _object.AttachTo(obj, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + finally + { + obj.Destroy(); + } + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/AssemblyAttributes.cs b/src/TestMode.UnitTests/Infrastructure/AssemblyAttributes.cs new file mode 100644 index 00000000..34ce2f0d --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/AssemblyAttributes.cs @@ -0,0 +1,4 @@ +using TestMode.UnitTests; +using Xunit; + +[assembly:TestFramework(typeof(CustomTestFramework))] diff --git a/src/TestMode.UnitTests/Infrastructure/CustomTestExecutor.cs b/src/TestMode.UnitTests/Infrastructure/CustomTestExecutor.cs new file mode 100644 index 00000000..65641fa1 --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/CustomTestExecutor.cs @@ -0,0 +1,18 @@ +using SampSharp.OpenMp.Core; +using Xunit.Sdk; +using Xunit.v3; + +namespace TestMode.UnitTests; + +public class CustomTestExecutor : XunitTestFrameworkExecutor +{ + public CustomTestExecutor(IXunitTestAssembly testAssembly) : base(testAssembly) + { + } + + public override async ValueTask RunTestCases(IReadOnlyCollection testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions, CancellationToken cancellationToken) + { + await TaskHelper.SwitchToMainThread(); + await base.RunTestCases(testCases, executionMessageSink, executionOptions, cancellationToken); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/CustomTestFramework.cs b/src/TestMode.UnitTests/Infrastructure/CustomTestFramework.cs new file mode 100644 index 00000000..9ff9d0db --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/CustomTestFramework.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using Xunit.Internal; +using Xunit.v3; + +namespace TestMode.UnitTests; + +public class CustomTestFramework : XunitTestFramework +{ + protected override ITestFrameworkExecutor CreateExecutor(Assembly assembly) => + new CustomTestExecutor(new XunitTestAssembly(Guard.ArgumentNotNull(assembly), null, assembly.GetName().Version)); +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/MetaTest.cs b/src/TestMode.UnitTests/Infrastructure/MetaTest.cs new file mode 100644 index 00000000..4fc3bf2c --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/MetaTest.cs @@ -0,0 +1,16 @@ +using SampSharp.OpenMp.Core; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class MetaTest +{ + [Fact] + public void Test_should_run_on_main_thread() + { + var result = TaskHelper.IsMainThread(); + + result.ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/ShouldlyExtensions.cs b/src/TestMode.UnitTests/Infrastructure/ShouldlyExtensions.cs new file mode 100644 index 00000000..07a68641 --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/ShouldlyExtensions.cs @@ -0,0 +1,14 @@ +using System.Numerics; +using Shouldly; + +namespace TestMode.UnitTests; + +public static class ShouldlyExtensions +{ + public static void ShouldBe(this Vector3 actual, Vector3 expected, float tolerance = 0.0002f) + { + actual.X.ShouldBe(expected.X, tolerance: tolerance, customMessage: $"should be (X) {expected} but was {actual}"); + actual.Y.ShouldBe(expected.Y, tolerance: tolerance, customMessage: $"should be (Y) {expected} but was {actual}"); + actual.Z.ShouldBe(expected.Z, tolerance: tolerance, customMessage: $"should be (Z) {expected} but was {actual}"); + } +} diff --git a/src/TestMode.UnitTests/Infrastructure/Startup.cs b/src/TestMode.UnitTests/Infrastructure/Startup.cs new file mode 100644 index 00000000..4b862fb0 --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/Startup.cs @@ -0,0 +1,14 @@ +using SampSharp.Entities; +using SampSharp.OpenMp.Core; +using Shouldly; + +namespace TestMode.UnitTests; + +public class Startup : IStartup +{ + public void Initialize(IStartupContext context) + { + ShouldlyConfiguration.DefaultFloatingPointTolerance = 0.0005f; + context.UseEntities(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/TestBase.cs b/src/TestMode.UnitTests/Infrastructure/TestBase.cs new file mode 100644 index 00000000..4e208654 --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/TestBase.cs @@ -0,0 +1,19 @@ +using SampSharp.Entities.SAMP; + +namespace TestMode.UnitTests; + +public class TestBase : IDisposable +{ + public Player Player => XunitSystem.Player; + public IServiceProvider Services => XunitSystem.ServiceProvider; + + protected virtual void Cleanup() + { + } + + public void Dispose() + { + Cleanup(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/Infrastructure/XunitSystem.cs b/src/TestMode.UnitTests/Infrastructure/XunitSystem.cs new file mode 100644 index 00000000..35a9a537 --- /dev/null +++ b/src/TestMode.UnitTests/Infrastructure/XunitSystem.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Reflection; +using SampSharp.Entities; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit.Runner.Common; +using Xunit.Runner.InProc.SystemConsole; +using Xunit.Sdk; +using Xunit.v3; + +namespace TestMode.UnitTests; + +public class XunitSystem : ISystem +{ + public static Player Player { get; private set; } = null!; + public static IServiceProvider ServiceProvider { get; private set; } = null!; + private bool _started; + + public XunitSystem(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + [Event] + public void OnGameModeInit(IServerService serverService) + { + serverService.AddPlayerClass(1, new Vector3(0, 0, 10), 0); + + serverService.ConnectNpc("tester", "npcidle"); + } + + [Event] + public void OnPlayerConnect(Player player, ITimerService timerService) + { + if (!_started && player.IsNpc) + { + _started = true; + Player = player; + + timerService.Delay(sp => + { + _ = RunXUnit(); + }, TimeSpan.FromSeconds(0.5f)); + } + } + + public async Task RunXUnit() + { + var runner = new ConsoleRunner(["-parallel", "none"], Assembly.GetExecutingAssembly()); + var exitCode = await runner.EntryPoint(); + + Environment.Exit(exitCode); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/MenuTest.cs b/src/TestMode.UnitTests/MenuTest.cs new file mode 100644 index 00000000..dcd0c7d4 --- /dev/null +++ b/src/TestMode.UnitTests/MenuTest.cs @@ -0,0 +1,92 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core.Api; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class MenuTest : TestBase +{ + private readonly Menu _menu; + + public MenuTest() + { + _menu = Services.GetRequiredService().CreateMenu("title", Vector2.One, 400, 200); + } + + protected override void Cleanup() + { + _menu.DestroyEntity(); + } + + [Fact] + public void CreateMenu_should_set_properties() + { + _menu.Title.ShouldBe("title"); + _menu.Position.ShouldBe(Vector2.One); + _menu.Columns.ShouldBe(2); + _menu.Col0Width.ShouldBe(400); + _menu.Col1Width.ShouldBe(200); + } + + [Fact] + public void Col0Header_should_roundtrip() + { + _menu.Col0Header = "col0"; + _menu.Col0Header.ShouldBe("col0"); + } + + [Fact] + public void Col1Header_should_roundtrip() + { + _menu.Col1Header = "col1"; + _menu.Col1Header.ShouldBe("col1"); + } + + [Fact] + public void AddItem_should_succeed() + { + _menu.AddItem("left", "right"); + } + + [Fact] + public void AddItem_should_throw_on_invalid_col_count() + { + var ex = Should.Throw(() => _menu.AddItem("left")); + + ex.ParamName.ShouldBe("col1Text"); + } + + [Fact] + public void Show_should_succeed() + { + _menu.Show(Player); + } + + [Fact] + public void Hide_should_succeed() + { + _menu.Hide(Player); + } + + [Fact] + public void Disable_should_succeed() + { + _menu.Disable(); + + ((IMenu)_menu).IsEnabled().ShouldBeFalse(); + } + + [Fact] + public void DisableRow_should_succeed() + { + _menu.AddItem("a", "b"); + _menu.AddItem("c", "d"); + _menu.DisableRow(0); + + ((IMenu)_menu).IsRowEnabled(0).ShouldBeFalse(); + ((IMenu)_menu).IsRowEnabled(1).ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/PickupTests.cs b/src/TestMode.UnitTests/PickupTests.cs new file mode 100644 index 00000000..53200334 --- /dev/null +++ b/src/TestMode.UnitTests/PickupTests.cs @@ -0,0 +1,31 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class PickupTests : TestBase +{ + private readonly Pickup _pickup; + + public PickupTests() + { + _pickup = Services.GetRequiredService().CreatePickup(1234, PickupType.ScriptedActionsOnlyEveryFewSeconds, new Vector3(10, 20, 30), 10); + } + + protected override void Cleanup() + { + _pickup.DestroyEntity(); + } + + [Fact] + public void CreatePickup_should_set_properties() + { + _pickup.Model.ShouldBe(1234); + _pickup.SpawnType.ShouldBe(PickupType.ScriptedActionsOnlyEveryFewSeconds); + _pickup.Position.ShouldBe(new Vector3(10, 20, 30)); + _pickup.VirtualWorld.ShouldBe(10); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/PlayerObjectTests.cs b/src/TestMode.UnitTests/PlayerObjectTests.cs new file mode 100644 index 00000000..12613fb2 --- /dev/null +++ b/src/TestMode.UnitTests/PlayerObjectTests.cs @@ -0,0 +1,128 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class PlayerObjectTests : TestBase +{ + private readonly IWorldService _worldService; + private readonly PlayerObject _object; + + public PlayerObjectTests() + { + _worldService = Services.GetRequiredService(); + _object = _worldService.CreatePlayerObject(Player, 100, new Vector3(10, 20, 30), new Vector3(20, 10, 0), 30); + } + + protected override void Cleanup() + { + _object?.Destroy(); + } + + + [Fact] + public void CreateObject_should_set_properties() + { + _object.ModelId.ShouldBe(100); + _object.Position.ShouldBe(new Vector3(10, 20, 30)); + _object.RotationEuler.ShouldBe(new Vector3(20, 10, 0)); + _object.DrawDistance.ShouldBe(30); + } + + [Fact] + public void Position_should_roundtrip() + { + _object.Position = new Vector3(20, 30, 40); + _object.Position.ShouldBe(new Vector3(20, 30, 40)); + } + + [Fact] + public void Rotation_should_roundtrip() + { + _object.RotationEuler = new Vector3(20, 30, 40); + _object.RotationEuler.ShouldBe(new Vector3(20, 30, 40), tolerance: 0.8f); + } + + [Fact] + public void Move_and_Stop_should_succeed() + { + _object.Move(new Vector3(100, 200, 300), 10, Vector3.Zero); + _object.Stop(); + } + + [Fact] + public void Move_time_should_be_correct() + { + _object.Position = new Vector3(100, 0, 0); + var result = _object.Move(new Vector3(200, 0, 0), 10, Vector3.Zero); + _object.Stop(); + + result.ShouldBe(10000); + } + + [Fact] + public void DisableCameraCollisions_should_succeed() + { + _object.DisableCameraCollisions(); + } + + [Fact] + public void SetMaterial_should_succeed() + { + _object.SetMaterial(0, 0, "none", "none", Color.White); + } + + [Fact] + public void SetMaterialText_should_succeed() + { + _object.SetMaterialText(0, "test", ObjectMaterialSize.X128X128, "Arial", 12, true, Color.White, Color.White, ObjectMaterialTextAlign.Center); + } + + [Fact] + public void AttachToPlayer_should_succeed() + { + _object.AttachTo(Player, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + + [Fact] + public void AttachToVehicle_should_succeed() + { + var vehicle = _worldService.CreateVehicle(VehicleModelType.Landstalker, new Vector3(0, 0, 0), 0, 0, 0); + + try + { + _object.AttachTo(vehicle, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + finally + { + vehicle.Destroy(); + } + } + + [Fact] + public void AttachToObject_should_succeed() + { + var obj = _worldService.CreatePlayerObject(Player, 100, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + + try + { + _object.AttachTo(obj, new Vector3(0, 0, 0), new Vector3(0, 0, 0)); + } + finally + { + obj.Destroy(); + } + } + + [Fact] + public void IsMoving_should_return_correct_value() + { + _object.Move(new Vector3(100, 200, 300), 10, Vector3.Zero); + _object.IsMoving.ShouldBeTrue(); + _object.Stop(); + _object.IsMoving.ShouldBeFalse(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/PlayerTests.cs b/src/TestMode.UnitTests/PlayerTests.cs new file mode 100644 index 00000000..22e42b83 --- /dev/null +++ b/src/TestMode.UnitTests/PlayerTests.cs @@ -0,0 +1,722 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class PlayerTests : TestBase +{ + [Fact] + public void Position_get_should_succeed() + { + _ = Player.Position; + } + + [Fact] + public void Position_set_should_succeed() + { + Player.Position = new Vector3(10, 20, 30); + } + + [Fact] + public void Name_should_roundtrip() + { + Player.SetName("TestName"); + Player.Name.ShouldBe("TestName"); + } + + [Fact] + public void Interior_set_should_succeed() + { + // cannot roundtrip - setter sends a packet to the client + Player.Interior = 1; + } + + [Fact] + public void Interior_get_should_succeed() + { + _ = Player.Interior; + } + + [Fact] + public void Health_get_should_succeed() + { + _ = Player.Health; + } + + [Fact] + public void Health_set_should_succeed() + { + Player.Health = 100.0f; + } + + [Fact] + public void Armour_get_should_succeed() + { + _ = Player.Armour; + } + + [Fact] + public void Armour_set_should_succeed() + { + Player.Armour = 50.0f; + } + + [Fact] + public void Team_should_roundtrip() + { + Player.Team = 2; + Player.Team.ShouldBe(2); + } + + [Fact] + public void Score_should_roundtrip() + { + Player.Score = 10; + Player.Score.ShouldBe(10); + } + + [Fact] + public void DrunkLevel_should_roundtrip() + { + Player.DrunkLevel = 5; + Player.DrunkLevel.ShouldBe(5); + } + + [Fact] + public void Color_should_roundtrip() + { + var color = new Color(255, 0, 0); + Player.Color = color; + Player.Color.ShouldBe(color); + } + + [Fact] + public void Skin_should_roundtrip() + { + Player.Skin = 3; + Player.Skin.ShouldBe(3); + } + + [Fact] + public void Money_should_roundtrip() + { + Player.Money = 1000; + Player.Money.ShouldBe(1000); + } + + [Fact] + public void WantedLevel_should_roundtrip() + { + Player.WantedLevel = 3; + Player.WantedLevel.ShouldBe(3); + } + + [Fact] + public void FightStyle_should_roundtrip() + { + Player.FightStyle = FightStyle.Boxing; + Player.FightStyle.ShouldBe(FightStyle.Boxing); + } + + [Fact] + public void Velocity_set_should_succeed() + { + Player.Velocity = new Vector3(1, 2, 3); + } + + [Fact] + public void Velocity_get_should_succeed() + { + _ = Player.Velocity; + } + + [Fact] + public void SpecialAction_get_should_succeed() + { + _ = Player.SpecialAction; + } + + [Fact] + public void SpecialAction_set_should_succeed() + { + Player.SpecialAction = SpecialAction.Duck; + } + + [Fact] + public void CameraPosition_should_roundtrip() + { + var position = new Vector3(1, 2, 3); + Player.CameraPosition = position; + Player.CameraPosition.ShouldBe(position); + } + + [Fact] + public void WeaponAmmo_should_succeed() + { + _ = Player.WeaponAmmo; + } + + [Fact] + public void WeaponState_should_succeed() + { + _ = Player.WeaponState; + } + + [Fact] + public void Weapon_should_succeed() + { + _ = Player.Weapon; + } + + [Fact] + public void TargetPlayer_should_succeed() + { + _ = Player.TargetPlayer; + } + + [Fact] + public void State_should_succeed() + { + _ = Player.State; + } + + [Fact] + public void IpAddress_should_succeed() + { + _ = Player.IpAddress; + } + + [Fact] + public void EndPoint_should_succeed() + { + _ = Player.EndPoint; + } + + [Fact] + public void Ping_should_succeed() + { + _ = Player.Ping; + } + + [Fact] + public void CameraFrontVector_should_succeed() + { + _ = Player.CameraFrontVector; + } + + [Fact] + public void CameraMode_should_succeed() + { + _ = Player.CameraMode; + } + + [Fact] + public void TargetActor_should_succeed() + { + _ = Player.TargetActor; + } + + [Fact] + public void CameraTargetGlobalObject_should_succeed() + { + _ = Player.CameraTargetGlobalObject; + } + + [Fact] + public void CameraTargetVehicle_should_succeed() + { + _ = Player.CameraTargetVehicle; + } + + [Fact] + public void CameraTargetPlayer_should_succeed() + { + _ = Player.CameraTargetPlayer; + } + + [Fact] + public void CameraTargetActor_should_succeed() + { + _ = Player.CameraTargetActor; + } + + [Fact] + public void IsNpc_should_succeed() + { + _ = Player.IsNpc; + } + + [Fact] + public void Version_should_succeed() + { + _ = Player.Version; + } + + [Fact] + public void Gpci_should_succeed() + { + _ = Player.Gpci; + } + + [Fact] + public void MessagesReceived_should_succeed() + { + _ = Player.MessagesReceived; + } + + [Fact] + public void MessagesReceivedPerSecond_should_succeed() + { + _ = Player.MessagesReceivedPerSecond; + } + + [Fact] + public void MessagesSent_should_succeed() + { + _ = Player.MessagesSent; + } + + [Fact] + public void BytesReceived_should_succeed() + { + _ = Player.BytesReceived; + } + + [Fact] + public void BytesSent_should_succeed() + { + _ = Player.BytesSent; + } + + [Fact] + public void AspectCameraRatio_should_succeed() + { + _ = Player.AspectCameraRatio; + } + + [Fact] + public void CameraZoom_should_succeed() + { + _ = Player.CameraZoom; + } + + [Fact] + public void GetNetworkStats_should_succeed() + { + _ = Player.GetNetworkStats(); + } + + [Fact] + public void Spawn_should_succeed() + { + Player.Spawn(); + } + + [Fact] + public void PutCameraBehindPlayer_should_succeed() + { + Player.PutCameraBehindPlayer(); + } + + [Fact] + public void SetPositionFindZ_should_succeed() + { + Player.SetPositionFindZ(new Vector3(1, 2, 3)); + } + + [Fact] + public void IsPlayerStreamedIn_should_succeed() + { + var result = Player.IsPlayerStreamedIn(Player); + + result.ShouldBeTrue(); + } + + [Fact] + public void SetAmmo_should_succeed() + { + Player.GiveWeapon(Weapon.Colt45, 10); + Player.SetAmmo(Weapon.Colt45, 50); + } + + [Fact] + public void GiveWeapon_should_succeed() + { + Player.GiveWeapon(Weapon.Colt45, 100); + } + + [Fact] + public void ResetWeapons_should_succeed() + { + Player.ResetWeapons(); + } + + [Fact] + public void SetArmedWeapon_should_succeed() + { + Player.SetArmedWeapon(Weapon.Colt45); + } + + [Fact] + public void GetWeaponData_should_succeed() + { + Player.GetWeaponData(0, out _, out _); + } + + [Fact] + public void GiveMoney_should_succeed() + { + Player.GiveMoney(1000); + } + + [Fact] + public void ResetMoney_should_succeed() + { + Player.ResetMoney(); + } + + [Fact] + public void GetKeys_should_succeed() + { + Player.GetKeys(out _, out _, out _); + } + + [Fact] + public void SetTime_should_succeed() + { + Player.SetTime(12, 30); + } + + [Fact] + public void GetTime_should_succeed() + { + Player.GetTime(out _, out _); + } + + [Fact] + public void ToggleClock_should_succeed() + { + Player.ToggleClock(true); + } + + [Fact] + public void SetWeather_should_succeed() + { + Player.SetWeather(1); + } + + [Fact] + public void ForceClassSelection_should_succeed() + { + Player.ForceClassSelection(); + } + + [Fact] + public void PlayCrimeReport_should_succeed() + { + Player.PlayCrimeReport(Player, 16); + } + + [Fact] + public void PlayAudioStream_with_position_should_succeed() + { + Player.PlayAudioStream("http://example.com/stream", new Vector3(1, 2, 3), 100.0f); + } + + [Fact] + public void PlayAudioStream_should_succeed() + { + Player.PlayAudioStream("http://example.com/stream"); + } + + [Fact] + public void DisableRemoteVehicleCollisions_should_succeed() + { + Player.DisableRemoteVehicleCollisions(true); + } + + [Fact] + public void EnablePlayerCameraTarget_should_succeed() + { + Player.EnablePlayerCameraTarget(true); + } + + [Fact] + public void StopAudioStream_should_succeed() + { + Player.StopAudioStream(); + } + + [Fact] + public void SetShopName_should_succeed() + { + Player.SetShopName(ShopName.Ammunation1); + } + + [Fact] + public void SetSkillLevel_should_succeed() + { + Player.SetSkillLevel(WeaponSkill.Pistol, 999); + } + + [Fact] + public void PutInVehicle_with_seatId_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0); + + try + { + Player.PutInVehicle(vehicle, 0); + } + finally + { + vehicle.DestroyEntity(); + } + } + + [Fact] + public void PutInVehicle_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0); + + try + { + Player.PutInVehicle(vehicle); + } + finally + { + vehicle.DestroyEntity(); + } + } + + [Fact] + public void RemoveFromVehicle_should_succeed() + { + Player.RemoveFromVehicle(); + } + + [Fact] + public void ToggleControllable_should_succeed() + { + Player.ToggleControllable(true); + } + + [Fact] + public void PlaySound_with_point_should_succeed() + { + Player.PlaySound(1, new Vector3(1, 2, 3)); + } + + [Fact] + public void PlaySound_should_succeed() + { + Player.PlaySound(1); + } + + [Fact] + public void ApplyAnimation_with_forceSync_should_succeed() + { + Player.ApplyAnimation("AIRPORT", "THRW_BARL_THRW", 4.1f, true, false, false, false, TimeSpan.Zero, true); + } + + [Fact] + public void ApplyAnimation_should_succeed() + { + Player.ApplyAnimation("AIRPORT", "THRW_BARL_THRW", 4.1f, true, false, false, false, TimeSpan.Zero); + } + + [Fact] + public void ClearAnimations_with_forceSync_should_succeed() + { + Player.ClearAnimations(true); + } + + [Fact] + public void ClearAnimations_should_succeed() + { + Player.ClearAnimations(); + } + + [Fact] + public void GetAnimationName_should_succeed() + { + Player.GetAnimationName(out _, out _); + } + + [Fact] + public void SetPlayerMarker_should_succeed() + { + Player.SetPlayerMarker(Player, new Color(255, 0, 0)); + } + + [Fact] + public void ShowNameTagForPlayer_should_succeed() + { + Player.ShowNameTagForPlayer(Player, true); + } + + [Fact] + public void SetCameraLookAt_with_cut_should_succeed() + { + Player.SetCameraLookAt(new Vector3(1, 2, 3), CameraCut.Cut); + } + + [Fact] + public void SetCameraLookAt_should_succeed() + { + Player.SetCameraLookAt(new Vector3(1, 2, 3)); + } + + [Fact] + public void InterpolateCameraPosition_should_succeed() + { + Player.InterpolateCameraPosition(new Vector3(1, 2, 3), new Vector3(4, 5, 6), TimeSpan.FromSeconds(1), CameraCut.Cut); + } + + [Fact] + public void InterpolateCameraLookAt_should_succeed() + { + Player.InterpolateCameraLookAt(new Vector3(1, 2, 3), new Vector3(4, 5, 6), TimeSpan.FromSeconds(1), CameraCut.Cut); + } + + [Fact] + public void EnableStuntBonus_should_succeed() + { + Player.EnableStuntBonus(true); + } + + [Fact] + public void ToggleSpectating_should_succeed() + { + Player.ToggleSpectating(true); + } + + [Fact] + public void SpectatePlayer_with_mode_should_succeed() + { + Player.SpectatePlayer(Player, SpectateMode.Normal); + } + + [Fact] + public void SpectatePlayer_should_succeed() + { + Player.SpectatePlayer(Player); + } + + [Fact] + public void SpectateVehicle_with_mode_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0); + + try + { + Player.SpectateVehicle(vehicle, SpectateMode.Normal); + } + finally + { + vehicle.DestroyEntity(); + } + } + + [Fact] + public void SpectateVehicle_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.BMX, new Vector3(1, 2, 3), 0, 0, 0); + + try + { + Player.SpectateVehicle(vehicle); + } + finally + { + vehicle.DestroyEntity(); + } + } + + [Fact] + public void SendClientMessage_with_color_should_succeed() + { + Player.SendClientMessage(new Color(255, 0, 0), "Test message"); + } + + [Fact] + public void Kick_should_succeed() + { + Player.Kick(); + } + + [Fact] + public void Ban_with_reason_should_succeed() + { + Player.Ban("Test reason"); + } + + [Fact] + public void SendPlayerMessageToPlayer_should_succeed() + { + Player.SendPlayerMessageToPlayer(Player, "Test message"); + } + + [Fact] + public void GameText_should_succeed() + { + Player.GameText("Test text", TimeSpan.FromSeconds(5), 1); + } + + [Fact] + public void CreateExplosion_should_succeed() + { + Player.CreateExplosion(new Vector3(1, 2, 3), ExplosionType.LargeInvisible, 10.0f); + } + + [Fact] + public void SendDeathMessage_should_succeed() + { + Player.SendDeathMessage(Player, Player, Weapon.Colt45); + } + + [Fact] + public void AttachCameraToObject_GlobalObject_should_succeed() + { + var obj = Services.GetRequiredService().CreateObject(400, Vector3.Zero, Vector3.Zero); + + try + { + Player.AttachCameraToObject(obj); + } + finally + { + obj.DestroyEntity(); + } + } + + [Fact] + public void AttachCameraToObject_PlayerObject_should_succeed() + { + var obj = Services.GetRequiredService().CreateObject(400, Vector3.Zero, Vector3.Zero); + + try + { + Player.AttachCameraToObject(obj); + } + finally + { + obj.DestroyEntity(); + } + } + + [Fact] + public void RemoveDefaultObjects_should_succeed() + { + Player.RemoveDefaultObjects(1, new Vector3(1, 2, 3), 10.0f); + } + + [Fact] + public void RemoveMapIcon_should_succeed() + { + Player.RemoveMapIcon(1); + } +} diff --git a/src/TestMode.UnitTests/PlayerTextDrawTests.cs b/src/TestMode.UnitTests/PlayerTextDrawTests.cs new file mode 100644 index 00000000..290df499 --- /dev/null +++ b/src/TestMode.UnitTests/PlayerTextDrawTests.cs @@ -0,0 +1,160 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class PlayerTextDrawTests : TestBase +{ + private readonly PlayerTextDraw _textDraw; + + public PlayerTextDrawTests() + { + _textDraw = Services.GetRequiredService().CreatePlayerTextDraw(Player, Vector2.One, "text"); + } + + protected override void Cleanup() + { + _textDraw.DestroyEntity(); + } + + [Fact] + public void CreatePlayerTextDraw_should_set_properties() + { + _textDraw.Position.ShouldBe(Vector2.One); + _textDraw.Text.ShouldBe("text"); + } + + [Fact] + public void Text_should_roundtrip() + { + _textDraw.Text = "new text"; + _textDraw.Text.ShouldBe("new text"); + } + + [Fact] + public void Show_should_succeed() + { + _textDraw.Show(); + } + + [Fact] + public void Hide_should_succeed() + { + _textDraw.Hide(); + } + + [Fact] + public void PreviewModel_should_roundtrip() + { + _textDraw.PreviewModel = 123; + _textDraw.PreviewModel.ShouldBe(123); + } + + [Fact] + public void Position_should_roundtrip() + { + _ = _textDraw.Position; + } + + [Fact] + public void SetPreviewRotation_should_succeed() + { + _textDraw.SetPreviewRotation(Vector3.One); + } + + [Fact] + public void SetPreviewVehicleColor_should_succeed() + { + _textDraw.SetPreviewVehicleColor(1, 2); + } + + [Fact] + public void LetterSize_should_roundtrip() + { + _textDraw.LetterSize = new Vector2(2, 3); + _textDraw.LetterSize.ShouldBe(new Vector2(2, 3)); + } + + [Fact] + public void TextSize_should_roundtrip() + { + _textDraw.TextSize = new Vector2(4, 5); + _textDraw.TextSize.ShouldBe(new Vector2(4, 5)); + } + + [Fact] + public void Alignment_should_roundtrip() + { + _textDraw.Alignment = TextDrawAlignment.Center; + _textDraw.Alignment.ShouldBe(TextDrawAlignment.Center); + } + + [Fact] + public void ForeColor_should_roundtrip() + { + var color = new Color(255, 0, 0); + _textDraw.ForeColor = color; + _textDraw.ForeColor.ShouldBe(color); + } + + [Fact] + public void UseBox_should_roundtrip() + { + _textDraw.UseBox = true; + _textDraw.UseBox.ShouldBeTrue(); + } + + [Fact] + public void BoxColor_should_roundtrip() + { + var color = new Color(0, 255, 0); + _textDraw.BoxColor = color; + _textDraw.BoxColor.ShouldBe(color); + } + + [Fact] + public void Shadow_should_roundtrip() + { + _textDraw.Shadow = 2; + _textDraw.Shadow.ShouldBe(2); + } + + [Fact] + public void Outline_should_roundtrip() + { + _textDraw.Outline = 1; + _textDraw.Outline.ShouldBe(1); + } + + [Fact] + public void BackColor_should_roundtrip() + { + var color = new Color(0, 0, 255); + _textDraw.BackColor = color; + _textDraw.BackColor.ShouldBe(color); + } + + [Fact] + public void Font_should_roundtrip() + { + _textDraw.Font = TextDrawFont.DrawSprite; + _textDraw.Font.ShouldBe(TextDrawFont.DrawSprite); + } + + [Fact] + public void Proportional_should_roundtrip() + { + _textDraw.Proportional = true; + _textDraw.Proportional.ShouldBeTrue(); + } + + [Fact] + public void Selectable_should_roundtrip() + { + _textDraw.Selectable = true; + _textDraw.Selectable.ShouldBeTrue(); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/PlayerTextLabelTests.cs b/src/TestMode.UnitTests/PlayerTextLabelTests.cs new file mode 100644 index 00000000..b33e27b5 --- /dev/null +++ b/src/TestMode.UnitTests/PlayerTextLabelTests.cs @@ -0,0 +1,60 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class PlayerTextLabelTests : TestBase +{ + private readonly PlayerTextLabel _textLabel; + + public PlayerTextLabelTests() + { + _textLabel = Services.GetRequiredService().CreatePlayerTextLabel(Player, "text", Color.Red, new Vector3(10, 20, 30), 40); + } + + protected override void Cleanup() + { + _textLabel.DestroyEntity(); + } + + [Fact] + public void CreatePlayerTextLabel_should_set_properties() + { + _textLabel.Text.ShouldBe("text"); + _textLabel.Color.ShouldBe(Color.Red); + _textLabel.DrawDistance.ShouldBe(40); + _textLabel.TestLos.ShouldBeTrue(); + } + + [Fact] + public void AttachedEntity_should_be_null() + { + _textLabel.AttachedEntity.ShouldBeNull(); + } + + [Fact] + public void Attach_to_player_should_succeed() + { + _textLabel.Attach(Player); + _textLabel.AttachedEntity.ShouldBe(Player); + } + + [Fact] + public void Attach_to_vehicle_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0); + + try + { + _textLabel.Attach(vehicle); + _textLabel.AttachedEntity.ShouldBe(vehicle); + } + finally + { + vehicle.DestroyEntity(); + } + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/ServerServiceTests.cs b/src/TestMode.UnitTests/ServerServiceTests.cs new file mode 100644 index 00000000..dfad70da --- /dev/null +++ b/src/TestMode.UnitTests/ServerServiceTests.cs @@ -0,0 +1,151 @@ +using System.Numerics; +using System.Runtime.InteropServices; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core.Api; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class ServerServiceTests : TestBase +{ + private IServerService Sut => Services.GetRequiredService(); + + [Fact] + public void BlockIpAddress_should_succeed() + { + Sut.BlockIpAddress("127.0.0.1"); + } + + [Fact] + public void UnblockIpAddress_should_succeed() + { + Sut.UnBlockIpAddress("127.0.0.1"); + } + + [Fact] + public void AddPlayerClass_with_team_should_succeed() + { + var classId = Sut.AddPlayerClass(1, 2, new Vector3(0, 0, 0), 0.0f, Weapon.Colt45, 100); + classId.ShouldBeGreaterThanOrEqualTo(0); + } + + [Fact] + public void AddPlayerClass_without_team_should_succeed() + { + var classId = Sut.AddPlayerClass(2, new Vector3(0, 0, 0), 0.0f, Weapon.Colt45, 100); + classId.ShouldBeGreaterThanOrEqualTo(0); + } + + [Fact] + public void ConnectNpc_should_succeed() + { + Sut.ConnectNpc("TestNpc", "npc_script"); + } + + [Fact] + public void DisableInteriorEnterExits_should_succeed() + { + Sut.DisableInteriorEnterExits(); + } + + [Fact] + public void EnableStuntBonus_should_succeed() + { + Sut.EnableStuntBonus(true); + } + + [Fact] + public void EnableVehicleFriendlyFire_should_succeed() + { + Sut.EnableVehicleFriendlyFire(); + } + + [Fact] + public void GameModeExit_should_succeed() + { + Sut.GameModeExit(); + } + + [Fact] + public void GetConsoleVarAsBool_should_return_correct_value() + { + var result = Sut.GetConsoleVarAsBool("some_var"); + result.ShouldBeFalse(); + } + + [Fact] + public void GetConsoleVarAsInt_should_return_correct_value() + { + var result = Sut.GetConsoleVarAsInt("some_var"); + result.ShouldBe(0); + } + + [Fact] + public void GetConsoleVarAsString_should_return_correct_value() + { + var result = Sut.GetConsoleVarAsString("some_var"); + result.ShouldBeNull(); + } + + [Fact] + public void LimitGlobalChatRadius_should_succeed() + { + Sut.LimitGlobalChatRadius(100.0f); + } + + [Fact] + public void LimitPlayerMarkerRadius_should_succeed() + { + Sut.LimitPlayerMarkerRadius(100.0f); + } + + [Fact] + public void ManualVehicleEngineAndLights_should_succeed() + { + Sut.ManualVehicleEngineAndLights(); + } + + [Fact] + public void SendRconCommand_should_succeed() + { + Sut.SendRconCommand("echo Test"); + } + + [Fact] + public void SetGameModeText_should_succeed() + { + Sut.SetGameModeText("TestMode"); + } + + [Fact] + public void SetNameTagDrawDistance_should_succeed() + { + Sut.SetNameTagDrawDistance(100.0f); + } + + [Fact] + public void SetWorldTime_should_succeed() + { + Sut.SetWorldTime(12); + } + + [Fact] + public void ShowNameTags_should_succeed() + { + Sut.ShowNameTags(true); + } + + [Fact] + public void ShowPlayerMarkers_should_succeed() + { + Sut.ShowPlayerMarkers(PlayerMarkersMode.Global); + } + + [Fact] + public void UsePlayerPedAnims_should_succeed() + { + Sut.UsePlayerPedAnims(); + } +} diff --git a/src/TestMode.UnitTests/TestMode.UnitTests.csproj b/src/TestMode.UnitTests/TestMode.UnitTests.csproj new file mode 100644 index 00000000..90b65d33 --- /dev/null +++ b/src/TestMode.UnitTests/TestMode.UnitTests.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + false + + + + + + + + + + + + + + + diff --git a/src/TestMode.UnitTests/TestMode.UnitTests.csproj.DotSettings b/src/TestMode.UnitTests/TestMode.UnitTests.csproj.DotSettings new file mode 100644 index 00000000..38f22092 --- /dev/null +++ b/src/TestMode.UnitTests/TestMode.UnitTests.csproj.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file diff --git a/src/TestMode.UnitTests/TextDrawTests.cs b/src/TestMode.UnitTests/TextDrawTests.cs new file mode 100644 index 00000000..512e0452 --- /dev/null +++ b/src/TestMode.UnitTests/TextDrawTests.cs @@ -0,0 +1,178 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class TextDrawTests : TestBase +{ + private readonly TextDraw _textDraw; + + public TextDrawTests() + { + _textDraw = Services.GetRequiredService().CreateTextDraw(Vector2.One, "text"); + } + + protected override void Cleanup() + { + _textDraw.DestroyEntity(); + } + + [Fact] + public void CreatePlayerTextDraw_should_set_properties() + { + _textDraw.Position.ShouldBe(Vector2.One); + _textDraw.Text.ShouldBe("text"); + } + + [Fact] + public void Text_should_roundtrip() + { + _textDraw.Text = "new text"; + _textDraw.Text.ShouldBe("new text"); + } + + [Fact] + public void Show_should_succeed() + { + _textDraw.Show(); + } + + [Fact] + public void Show_player_should_succeed() + { + _textDraw.Show(Player); + } + + [Fact] + public void Hide_should_succeed() + { + _textDraw.Hide(); + } + + [Fact] + public void Hide_player_should_succeed() + { + _textDraw.Hide(Player); + } + + [Fact] + public void PreviewModel_should_roundtrip() + { + _textDraw.PreviewModel = 123; + _textDraw.PreviewModel.ShouldBe(123); + } + + [Fact] + public void Position_should_roundtrip() + { + _ = _textDraw.Position; + } + + [Fact] + public void SetPreviewRotation_should_succeed() + { + _textDraw.SetPreviewRotation(Vector3.One); + } + + [Fact] + public void SetPreviewVehicleColor_should_succeed() + { + _textDraw.SetPreviewVehicleColor(1, 2); + } + + [Fact] + public void LetterSize_should_roundtrip() + { + _textDraw.LetterSize = new Vector2(2, 3); + _textDraw.LetterSize.ShouldBe(new Vector2(2, 3)); + } + + [Fact] + public void TextSize_should_roundtrip() + { + _textDraw.TextSize = new Vector2(4, 5); + _textDraw.TextSize.ShouldBe(new Vector2(4, 5)); + } + + [Fact] + public void Alignment_should_roundtrip() + { + _textDraw.Alignment = TextDrawAlignment.Center; + _textDraw.Alignment.ShouldBe(TextDrawAlignment.Center); + } + + [Fact] + public void ForeColor_should_roundtrip() + { + var color = new Color(255, 0, 0); + _textDraw.ForeColor = color; + _textDraw.ForeColor.ShouldBe(color); + } + + [Fact] + public void UseBox_should_roundtrip() + { + _textDraw.UseBox = true; + _textDraw.UseBox.ShouldBeTrue(); + } + + [Fact] + public void BoxColor_should_roundtrip() + { + var color = new Color(0, 255, 0); + _textDraw.BoxColor = color; + _textDraw.BoxColor.ShouldBe(color); + } + + [Fact] + public void Shadow_should_roundtrip() + { + _textDraw.Shadow = 2; + _textDraw.Shadow.ShouldBe(2); + } + + [Fact] + public void Outline_should_roundtrip() + { + _textDraw.Outline = 1; + _textDraw.Outline.ShouldBe(1); + } + + [Fact] + public void BackColor_should_roundtrip() + { + var color = new Color(0, 0, 255); + _textDraw.BackColor = color; + _textDraw.BackColor.ShouldBe(color); + } + + [Fact] + public void Font_should_roundtrip() + { + _textDraw.Font = TextDrawFont.DrawSprite; + _textDraw.Font.ShouldBe(TextDrawFont.DrawSprite); + } + + [Fact] + public void Proportional_should_roundtrip() + { + _textDraw.Proportional = true; + _textDraw.Proportional.ShouldBeTrue(); + } + + [Fact] + public void Selectable_should_roundtrip() + { + _textDraw.Selectable = true; + _textDraw.Selectable.ShouldBeTrue(); + } + + [Fact] + public void SetPreviewRotation_with_zoom_should_succeed() + { + _textDraw.SetPreviewRotation(Vector3.One, 2.0f); + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/TextLabelTests.cs b/src/TestMode.UnitTests/TextLabelTests.cs new file mode 100644 index 00000000..fbb95ae9 --- /dev/null +++ b/src/TestMode.UnitTests/TextLabelTests.cs @@ -0,0 +1,74 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class TextLabelTests : TestBase +{ + private readonly TextLabel _textLabel; + + public TextLabelTests() + { + _textLabel = Services.GetRequiredService().CreateTextLabel("text", Color.Red, new Vector3(10, 20, 30), 40); + } + + protected override void Cleanup() + { + _textLabel.DestroyEntity(); + } + + [Fact] + public void CreatePlayerTextLabel_should_set_properties() + { + _textLabel.Text.ShouldBe("text"); + _textLabel.Color.ShouldBe(Color.Red); + _textLabel.DrawDistance.ShouldBe(40); + _textLabel.TestLos.ShouldBeTrue(); + } + + [Fact] + public void Text_should_roundtrip() + { + _textLabel.Text = "new text"; + _textLabel.Text.ShouldBe("new text"); + } + + [Fact] + public void Color_should_roundtrip() + { + _textLabel.Color = Color.Blue; + _textLabel.Color.ShouldBe(Color.Blue); + } + + [Fact] + public void AttachedEntity_should_be_null() + { + _textLabel.AttachedEntity.ShouldBeNull(); + } + + [Fact] + public void Attach_to_player_should_succeed() + { + _textLabel.Attach(Player); + _textLabel.AttachedEntity.ShouldBe(Player); + } + + [Fact] + public void Attach_to_vehicle_should_succeed() + { + var vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Alpha, Vector3.Zero, 0, 0, 0); + + try + { + _textLabel.Attach(vehicle); + _textLabel.AttachedEntity.ShouldBe(vehicle); + } + finally + { + vehicle.DestroyEntity(); + } + } +} \ No newline at end of file diff --git a/src/TestMode.UnitTests/VehicleTests.cs b/src/TestMode.UnitTests/VehicleTests.cs new file mode 100644 index 00000000..2fba5b4f --- /dev/null +++ b/src/TestMode.UnitTests/VehicleTests.cs @@ -0,0 +1,423 @@ +using System.Numerics; +using Microsoft.Extensions.DependencyInjection; +using SampSharp.Entities.SAMP; +using SampSharp.OpenMp.Core.Api; +using Shouldly; +using Xunit; + +namespace TestMode.UnitTests; + +public class VehicleTests : TestBase +{ + private readonly Vehicle _vehicle; + + public VehicleTests() + { + _vehicle = Services.GetRequiredService().CreateVehicle(VehicleModelType.Landstalker, new Vector3(10, 0, 5), 30, 5, 8); + } + + protected override void Cleanup() + { + _vehicle?.Destroy(); + } + + [Fact] + public void CreateVehicle_should_set_properties() + { + _vehicle.Model.ShouldBe(VehicleModelType.Landstalker); + _vehicle.Position.ShouldBe(new Vector3(10, 0, 5)); + _vehicle.Angle.ShouldBe(30); + _vehicle.Color1.ShouldBe(5); + _vehicle.Color2.ShouldBe(8); + } + + [Fact] + public void Position_should_roundtrip() + { + _vehicle.Position = new Vector3(1, 2, 3); + _vehicle.Position.ShouldBe(new Vector3(1, 2, 3)); + } + + [Fact] + public void Alarm_should_roundtrip_true() + { + _vehicle.Alarm = true; + _vehicle.Alarm.ShouldBeTrue(); + } + + [Fact] + public void Alarm_should_roundtrip_false() + { + _vehicle.Alarm = false; + _vehicle.Alarm.ShouldBeFalse(); + } + + [Fact] + public void Bonnet_should_roundtrip_true() + { + _vehicle.Bonnet = true; + _vehicle.Bonnet.ShouldBeTrue(); + } + [Fact] + public void Bonnet_should_roundtrip_false() + { + _vehicle.Bonnet = false; + _vehicle.Bonnet.ShouldBeFalse(); + } + [Fact] + public void Boot_should_roundtrip_true() + { + _vehicle.Boot = true; + _vehicle.Boot.ShouldBeTrue(); + } + [Fact] + public void Boot_should_roundtrip_false() + { + _vehicle.Boot = false; + _vehicle.Boot.ShouldBeFalse(); + } + [Fact] + public void Doors_should_roundtrip_true() + { + _vehicle.Doors = true; + _vehicle.Doors.ShouldBeTrue(); + } + [Fact] + public void Doors_should_roundtrip_false() + { + _vehicle.Doors = false; + _vehicle.Doors.ShouldBeFalse(); + } + [Fact] + public void Engine_should_roundtrip_true() + { + _vehicle.Engine = true; + _vehicle.Engine.ShouldBeTrue(); + } + [Fact] + public void Engine_should_roundtrip_false() + { + _vehicle.Engine = false; + _vehicle.Engine.ShouldBeFalse(); + } + [Fact] + public void Objective_should_roundtrip_true() + { + _vehicle.Objective = true; + _vehicle.Objective.ShouldBeTrue(); + } + [Fact] + public void Objective_should_roundtrip_false() + { + _vehicle.Objective = false; + _vehicle.Objective.ShouldBeFalse(); + } + [Fact] + public void Lights_should_roundtrip_true() + { + _vehicle.Lights = true; + _vehicle.Lights.ShouldBeTrue(); + } + [Fact] + public void Lights_should_roundtrip_false() + { + _vehicle.Lights = false; + _vehicle.Lights.ShouldBeFalse(); + } + [Fact] + public void IsBackLeftDoorOpen_should_roundtrip_true() + { + _vehicle.IsBackLeftDoorOpen = true; + _vehicle.IsBackLeftDoorOpen.ShouldBeTrue(); + } + [Fact] + public void IsBackLeftDoorOpen_should_roundtrip_false() + { + _vehicle.IsBackLeftDoorOpen = false; + _vehicle.IsBackLeftDoorOpen.ShouldBeFalse(); + } + [Fact] + public void IsBackLeftWindowClosed_should_roundtrip_true() + { + _vehicle.IsBackLeftWindowClosed = true; + _vehicle.IsBackLeftWindowClosed.ShouldBeTrue(); + } + [Fact] + public void IsBackLeftWindowClosed_should_roundtrip_false() + { + _vehicle.IsBackLeftWindowClosed = false; + _vehicle.IsBackLeftWindowClosed.ShouldBeFalse(); + } + [Fact] + public void IsBackRightDoorOpen_should_roundtrip_true() + { + _vehicle.IsBackRightDoorOpen = true; + _vehicle.IsBackRightDoorOpen.ShouldBeTrue(); + } + [Fact] + public void IsBackRightDoorOpen_should_roundtrip_false() + { + _vehicle.IsBackRightDoorOpen = false; + _vehicle.IsBackRightDoorOpen.ShouldBeFalse(); + } + [Fact] + public void IsBackRightWindowClosed_should_roundtrip_true() + { + _vehicle.IsBackRightWindowClosed = true; + _vehicle.IsBackRightWindowClosed.ShouldBeTrue(); + } + [Fact] + public void IsBackRightWindowClosed_should_roundtrip_false() + { + _vehicle.IsBackRightWindowClosed = false; + _vehicle.IsBackRightWindowClosed.ShouldBeFalse(); + } + [Fact] + public void IsDriverDoorOpen_should_roundtrip_true() + { + _vehicle.IsDriverDoorOpen = true; + _vehicle.IsDriverDoorOpen.ShouldBeTrue(); + } + [Fact] + public void IsDriverDoorOpen_should_roundtrip_false() + { + _vehicle.IsDriverDoorOpen = false; + _vehicle.IsDriverDoorOpen.ShouldBeFalse(); + } + [Fact] + public void IsDriverWindowClosed_should_roundtrip_true() + { + _vehicle.IsDriverWindowClosed = true; + _vehicle.IsDriverWindowClosed.ShouldBeTrue(); + } + [Fact] + public void IsDriverWindowClosed_should_roundtrip_false() + { + _vehicle.IsDriverWindowClosed = false; + _vehicle.IsDriverWindowClosed.ShouldBeFalse(); + } + [Fact] + public void IsPassengerDoorOpen_should_roundtrip_true() + { + _vehicle.IsPassengerDoorOpen = true; + _vehicle.IsPassengerDoorOpen.ShouldBeTrue(); + } + [Fact] + public void IsPassengerDoorOpen_should_roundtrip_false() + { + _vehicle.IsPassengerDoorOpen = false; + _vehicle.IsPassengerDoorOpen.ShouldBeFalse(); + } + [Fact] + public void IsPassengerWindowClosed_should_roundtrip_true() + { + _vehicle.IsPassengerWindowClosed = true; + _vehicle.IsPassengerWindowClosed.ShouldBeTrue(); + } + [Fact] + public void IsPassengerWindowClosed_should_roundtrip_false() + { + _vehicle.IsPassengerWindowClosed = false; + _vehicle.IsPassengerWindowClosed.ShouldBeFalse(); + } + + [Fact] + public void ChangeColor_should_succeed() + { + _vehicle.ChangeColor(5, 12); + } + + [Fact] + public void SetNumberPlate_should_succeed() + { + _vehicle.SetNumberPlate("SampSharp"); + } + + [Fact] + public void AddComponent_should_roundtirp() + { + _vehicle.AddComponent(1025); + _vehicle.GetComponentInSlot(CarModType.Wheels).ShouldBe(1025); + } + [Fact] + public void ChangePaintjob_should_succeed() + { + _vehicle.ChangePaintjob(54); + } + + [Fact] + public void GetDistanceFromPoint_should_succeed() + { + _vehicle.Position = new Vector3(10, 10, 0); + (_vehicle.GetDistanceFromPoint(new Vector3(20, 20, 0))).ShouldBe(MathF.Sqrt(10 * 10 * 2)); + } + + [Fact] + public void IsStreamedIn_should_succeed() + { + _vehicle.IsStreamedIn(Player); + } + + [Fact] + public void LinkToInterior_should_succeed() + { + _vehicle.LinkToInterior(4); + ((IVehicle)_vehicle).GetInterior().ShouldBe(4);// TODO add to api + _vehicle.LinkToInterior(0); + } + + [Fact] + public void VirtualWorld_should_roundtrip() + { + _vehicle.VirtualWorld = 23; + _vehicle.VirtualWorld.ShouldBe(23); + _vehicle.VirtualWorld = 0; + } + + [Fact] + public void Health_should_roundtrip() + { + _vehicle.Health = 876; + _vehicle.Health.ShouldBe(876); + } + + [Fact] + public void Repair_should_succeed() + { + _vehicle.Health = 500; + _vehicle.UpdateDamageStatus(13, 14, 15, 16); + _vehicle.Repair(); + + _vehicle.GetDamageStatus(out var panels, out var doors, out var lights, out var tires); + panels.ShouldBe(0); + doors.ShouldBe(0); + lights.ShouldBe(0); + tires.ShouldBe(0); + + _vehicle.Health.ShouldBe(1000); + } + + [Fact] + public void UpdateDamageStatus_should_roundtrip() + { + _vehicle.UpdateDamageStatus(13, 14, 15, 16); + _vehicle.GetDamageStatus(out var panels, out var doors, out var lights, out var tires); + + panels.ShouldBe(13); + doors.ShouldBe(14); + lights.ShouldBe(15); + tires.ShouldBe(16); + } + + [Fact] + public void Respawn_should_succeed() + { + _vehicle.Respawn(); + } + + [Fact] + public void SetAngularVelocity_should_succeed() + { + // doesn't roundtrip; the setter send a packet to the driver. + _vehicle.SetAngularVelocity(new Vector3(5, 6, 7)); + + } + + [Fact] + public void Angle_setter_should_roundtrip() + { + _vehicle.Angle = 45.0f; + _vehicle.Angle.ShouldBe(45); + } + + [Fact] + public void Model_should_be_correct() + { + _vehicle.Model.ShouldBe(VehicleModelType.Landstalker); + } + + [Fact] + public void HasTrailer_should_be_false_initially() + { + _vehicle.HasTrailer.ShouldBeFalse(); + } + + [Fact] + public void Velocity_setter_should_succeed() + { + // doesn't roundtrip; the setter send a packet to the driver. + Player.PutInVehicle(_vehicle); + + _vehicle.Velocity = new Vector3(1, 1, 1); + + Player.RemoveFromVehicle(true); + } + + [Fact] + public void Velocity_getter_should_succeed() + { + // doesn't roundtrip; the setter send a packet to the driver. + Player.PutInVehicle(_vehicle); + + var result = _vehicle.Velocity; + + result.ShouldBe(Vector3.Zero); + + Player.RemoveFromVehicle(true); + } + + [Fact] + public void IsSirenOn_should_be_false() + { + // state is only set by driver's packets - setter only sets steaming data and does not affect getter + // setter not available in sampsharp + _vehicle.IsSirenOn.ShouldBeFalse(); + } + + [Fact] + public void Rotation_should_roundtrip() + { + _vehicle.RotationEuler = new Vector3(0, 0, 90); + _vehicle.RotationEuler.ShouldBe(new Vector3(0, 0, 90)); + } + [Fact] + public void RemoveComponent_should_succeed() + { + + _vehicle.AddComponent(1025); + _vehicle.RemoveComponent(1025); + _vehicle.GetComponentInSlot(CarModType.Wheels).ShouldBe(0); + } + + [Fact] + public void Trailer_should_roundtrip() + { + var trailer = Services.GetRequiredService().CreateVehicle(VehicleModelType.ArticleTrailer, new Vector3(0, 0, 0), 0, 0, 0); + + try + { + _vehicle.Trailer = trailer; + _vehicle.Trailer.ShouldBe(trailer); + + _vehicle.Trailer = null; + _vehicle.Trailer.ShouldBeNull(); + } + finally + { + trailer.Destroy(); + } + } + + [Fact] + public void SetParametersForPlayer_should_succeed() + { + var parameters = new VehicleParameters( + VehicleParameterValue.On, VehicleParameterValue.Off, VehicleParameterValue.On, VehicleParameterValue.Off, + VehicleParameterValue.On, VehicleParameterValue.Off, VehicleParameterValue.On, VehicleParameterValue.Off, + VehicleParameterValue.On, VehicleParameterValue.Off, VehicleParameterValue.On, VehicleParameterValue.Off, + VehicleParameterValue.On, VehicleParameterValue.Off, VehicleParameterValue.On, VehicleParameterValue.Off); + + _vehicle.SetParametersForPlayer(Player, parameters); + } + +} \ No newline at end of file diff --git a/src/legacy/.editorconfig b/src/legacy/.editorconfig new file mode 100644 index 00000000..a1323c50 --- /dev/null +++ b/src/legacy/.editorconfig @@ -0,0 +1,287 @@ +root = true + +[*.{h,c,hpp,cpp,def}] +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = false + +# Project files +[*.{csproj,targets,props}] +indent_size = 2 +indent_style = space + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space + +# New line preferences +end_of_line = crlf +insert_final_newline = false +max_line_length = 160 + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false:warning +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_property = false:warning + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:warning +dotnet_style_predefined_type_for_member_access = true:warning + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent + +# Expression-level preferences +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_null_propagation = true:warning +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion + +# Field preferences +dotnet_style_readonly_field = true:warning + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:suggestion + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = true:warning +csharp_style_var_for_built_in_types = true:warning +csharp_style_var_when_type_is_apparent = true:warning + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = when_on_single_line:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:warning +csharp_style_expression_bodied_methods = false:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:silent +csharp_style_expression_bodied_properties = when_on_single_line:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion + +# Null-checking preferences +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_prefer_static_local_function = true:suggestion +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async + +# Code-block preferences +csharp_prefer_braces = when_multiline:silent +csharp_prefer_simple_using_statement = true:suggestion + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_or_internal_field_should_be__camelcase.severity = warning +dotnet_naming_rule.private_or_internal_field_should_be__camelcase.symbols = private_or_internal_field +dotnet_naming_rule.private_or_internal_field_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.severity = warning +dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.symbols = private_or_internal_static_field +dotnet_naming_rule.private_or_internal_static_field_should_be__camelcase.style = _camelcase + +dotnet_naming_rule.types_should_be_pascal_case.severity = warning +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private +dotnet_naming_symbols.private_or_internal_field.required_modifiers = + +dotnet_naming_symbols.private_or_internal_static_field.applicable_kinds = field +dotnet_naming_symbols.private_or_internal_static_field.applicable_accessibilities = internal, private +dotnet_naming_symbols.private_or_internal_static_field.required_modifiers = static + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal +dotnet_naming_symbols.non_field_members.required_modifiers = + +dotnet_naming_symbols.constant_fields.applicable_kinds = namespace, property, field, event, parameter, class, struct, interface, enum, delegate, method, local_function +dotnet_naming_symbols.constant_fields.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.constant_fields.required_modifiers = const + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style._camelcase.required_prefix = _ +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.capitalization = camel_case + +dotnet_naming_style.upper_case.required_prefix = +dotnet_naming_style.upper_case.required_suffix = +dotnet_naming_style.upper_case.word_separator = _ +dotnet_naming_style.upper_case.capitalization = all_upper + +# S1125: Boolean literals should not be redundant +dotnet_diagnostic.S1125.severity = none + +# S1135: Track uses of "TODO" tags +# Justification: Keep track of uncompleted work +dotnet_diagnostic.S1135.severity = warning + +# CS0618: Method is obsolete +# Justification: We're deprecating a lot of our own methods right now +dotnet_diagnostic.CS0618.severity = none + +# S3881: Fix this implementation of 'IDisposable' to conform to the dispose pattern. +# Justification: Let implementation decide whether to use dispose pattern. +dotnet_diagnostic.S3881.severity = none + +# S3453: Classes should not have only "private" constructors +# Justification: We use types with factory methods +dotnet_diagnostic.S3453.severity = none + +# CA1720: Identifier contains type name +# Justfication: Interop and type handing makes this diagnostic annoying +dotnet_diagnostic.CA1720.severity = none + +# S1172: Unused method parameters should be removed +# Justification: Diagnostic is faulty +dotnet_diagnostic.S1172.severity = none + +# IDE0046: Convert to conditional expression +dotnet_diagnostic.IDE0046.severity = silent + +[**TestMode**/**.cs] + +# CA1822: Mark members as static +# Justification: ECS system events +dotnet_diagnostic.CA1822.severity = none + +# S3218: Inner class members should not shadow outer class "static" or type members +# Justification: command systems "default" groups commands +dotnet_diagnostic.S3218.severity = none + +[**Tests/**.cs] + +# Test projects + +# CA1707: Identifiers should not contain underscores +dotnet_code_quality.CA1707.api_surface = private, internal, friend diff --git a/src/sampsharp-component/CMakeLists.txt b/src/sampsharp-component/CMakeLists.txt new file mode 100644 index 00000000..69dfff4d --- /dev/null +++ b/src/sampsharp-component/CMakeLists.txt @@ -0,0 +1,75 @@ +cmake_minimum_required(VERSION 3.19) +project(sampsharp LANGUAGES CXX VERSION 1.0.0) + +set(CMAKE_CXX_STANDARD 17) + +add_subdirectory("../../external/sdk" "${CMAKE_BINARY_DIR}/external/sdk") +include_directories(SYSTEM "../../external/dotnet") +include_directories(SYSTEM "../../external/sdk/include") +link_directories("../../external/dotnet") + +# can be specified as arugment cmake -DCOMPONENTS_DIR="path/to/x64/server/components" +if(DEFINED COMPONENTS_DIR AND NOT COMPONENTS_DIR STREQUAL "") + set(OUTDIR "${COMPONENTS_DIR}") +else() + set(OUTDIR "${CMAKE_BINARY_DIR}/artifacts") +endif() + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${OUTDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${OUTDIR}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${OUTDIR}") + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${OUTDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${OUTDIR}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${OUTDIR}") + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${OUTDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${OUTDIR}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${OUTDIR}") + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${OUTDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${OUTDIR}") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${OUTDIR}") + +if(WIN32) + set(LIBNETHOST "${CMAKE_CURRENT_SOURCE_DIR}/../../external/dotnet/libnethost.lib") +else() + set(LIBNETHOST "${CMAKE_CURRENT_SOURCE_DIR}/../../external/dotnet/libnethost.a") +endif() + +if(MSVC) + set(CXX_STANDARD "/std:c++latest") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${CXX_STANDARD} /Zc:preprocessor") +endif() + +file(GLOB_RECURSE component_source_list CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/*.hpp") + +add_library(SampSharp SHARED ${component_source_list}) + +set_target_properties(SampSharp PROPERTIES OUTPUT_NAME "SampSharp" PREFIX "") +set_property(TARGET SampSharp PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +target_compile_definitions(SampSharp PUBLIC + WIN32_LEAN_AND_MEAN + VC_EXTRALEAN + NOGDI + NETHOST_USE_AS_STATIC +) + +if(WIN32) +target_link_libraries(SampSharp PRIVATE + OMP-SDK + dbghelp + psapi + $<$:nethost> + $<$:${LIBNETHOST}> + $<$:${LIBNETHOST}> + $<$:${LIBNETHOST}> +) +else() + if(CMAKE_BUILD_TYPE STREQUAL "Debug") + target_link_libraries(SampSharp PRIVATE OMP-SDK nethost) + else() + target_link_libraries(SampSharp PRIVATE OMP-SDK ${LIBNETHOST}) + endif() +endif() \ No newline at end of file diff --git a/src/sampsharp-component/build.cmd b/src/sampsharp-component/build.cmd new file mode 100644 index 00000000..1f37226e --- /dev/null +++ b/src/sampsharp-component/build.cmd @@ -0,0 +1,30 @@ +@echo off +REM Build script for SampSharp component (x64) on Windows + +pushd "%~dp0" + +REM Navigate to root (2 levels up from src/sampsharp-component) +cd /d "..\.." +set "ROOTDIR=%CD%" +set "SRCDIR=%ROOTDIR%\src\sampsharp-component" +set "BUILDDIR=%ROOTDIR%\build\cmake\component" + +popd + +echo Building open.mp component... +echo Root: %ROOTDIR% +echo Build: %BUILDDIR% +echo. + +if not exist "%BUILDDIR%" mkdir "%BUILDDIR%" + +REM Configure with x64 architecture +cmake -S "%SRCDIR%" -B "%BUILDDIR%" -T ClangCL -DCMAKE_POLICY_VERSION_MINIMUM=3.5 +if errorlevel 1 exit /b 1 + +REM Build +cmake --build "%BUILDDIR%" --config RelWithDebInfo +if errorlevel 1 exit /b 1 + +echo. +echo Open.mp component build complete. Output: %BUILDDIR%\artifacts diff --git a/src/sampsharp-component/build.sh b/src/sampsharp-component/build.sh new file mode 100755 index 00000000..076c82aa --- /dev/null +++ b/src/sampsharp-component/build.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Build script for SampSharp component (x64) on Linux + +set -e + +SRCDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOTDIR="$(cd "$SRCDIR/../.." && pwd)" +BUILDDIR="$ROOTDIR/build/cmake/component" + +echo "Building open.mp component..." +echo "Root: $ROOTDIR" +echo "Build: $BUILDDIR" +echo + +mkdir -p "$BUILDDIR" + +# Configure +cmake -S "$SRCDIR" -B "$BUILDDIR" + +# Build +cmake --build "$BUILDDIR" --config RelWithDebInfo + +echo +echo "Open.mp component build complete. Output: $BUILDDIR/artifacts" diff --git a/src/sampsharp-component/compat.cpp b/src/sampsharp-component/compat.cpp new file mode 100644 index 00000000..17b64591 --- /dev/null +++ b/src/sampsharp-component/compat.cpp @@ -0,0 +1,30 @@ +#include "compat.hpp" + +#ifdef WINDOWS + +#include + +std::wstring widen_impl(std::string const &in) +{ + std::wstring out{}; + + if (!in.empty()) + { + const int inSize = static_cast(in.size()); + + const int len = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + in.c_str(), inSize, nullptr, 0); + if ( len == 0 ) + { + throw std::runtime_error("Invalid character sequence."); + } + + out.resize(len); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + in.c_str(), inSize, out.data(), static_cast(out.size())); + } + + return out; +} + +#endif diff --git a/src/sampsharp-component/compat.hpp b/src/sampsharp-component/compat.hpp new file mode 100644 index 00000000..3635db6e --- /dev/null +++ b/src/sampsharp-component/compat.hpp @@ -0,0 +1,25 @@ + +#pragma once + +#include +#include + +#ifdef WIN32 +# define WINDOWS true +#endif + +#ifdef WINDOWS +# include +# define STR(s) L ## s +# define widen(x) widen_impl(x) +# define PATH_SEP '\\' + +std::wstring widen_impl(std::string const &in); + +#else +# define STR(s) s +# define widen(x) x +# define PATH_SEP '/' +#endif + +using string_t = std::basic_string; diff --git a/src/sampsharp-component/crash-handler.cpp b/src/sampsharp-component/crash-handler.cpp new file mode 100644 index 00000000..408c7dc3 --- /dev/null +++ b/src/sampsharp-component/crash-handler.cpp @@ -0,0 +1,338 @@ +#include "crash-handler.hpp" + +#ifdef _WIN32 + +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(lib, "dbghelp.lib") +#pragma comment(lib, "psapi.lib") + +namespace sampsharp::crash +{ + namespace + { + ICore* g_core = nullptr; + std::atomic g_handling{false}; + std::atomic g_sym_initialized{false}; + + const char* codeName(const DWORD code) + { + switch (code) + { + case EXCEPTION_ACCESS_VIOLATION: return "ACCESS_VIOLATION"; + case EXCEPTION_STACK_OVERFLOW: return "STACK_OVERFLOW"; + case EXCEPTION_ILLEGAL_INSTRUCTION: return "ILLEGAL_INSTRUCTION"; + case EXCEPTION_PRIV_INSTRUCTION: return "PRIV_INSTRUCTION"; + case EXCEPTION_INT_DIVIDE_BY_ZERO: return "INT_DIVIDE_BY_ZERO"; + case EXCEPTION_INT_OVERFLOW: return "INT_OVERFLOW"; + case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "FLT_DIVIDE_BY_ZERO"; + case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "ARRAY_BOUNDS_EXCEEDED"; + case EXCEPTION_DATATYPE_MISALIGNMENT: return "DATATYPE_MISALIGNMENT"; + case EXCEPTION_IN_PAGE_ERROR: return "IN_PAGE_ERROR"; + case 0xC0000374: return "HEAP_CORRUPTION"; + case 0xC0000409: return "FAST_FAIL/STACK_BUFFER_OVERRUN"; + case 0xE06D7363: return "C++ EXCEPTION"; + case 0xE0434352: return "CLR EXCEPTION"; + default: return "UNKNOWN"; + } + } + + void vlog(const char* fmt, va_list ap) + { + char buf[4096]; + vsnprintf(buf, sizeof(buf), fmt, ap); + buf[sizeof(buf) - 1] = 0; + if (g_core) + g_core->logLn(LogLevel::Error, "%s", buf); + else + { + fputs(buf, stderr); + fputc('\n', stderr); + } + } + + void log(const char* fmt, ...) + { + va_list ap; + va_start(ap, fmt); + vlog(fmt, ap); + va_end(ap); + } + + void logModuleFor(void* addr) + { + HMODULE mod = nullptr; + if (!GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addr), &mod) || !mod) + { + log(" %p ", addr); + return; + } + char modPath[MAX_PATH] = {}; + GetModuleFileNameA(mod, modPath, MAX_PATH); + const char* modName = strrchr(modPath, '\\'); + modName = modName ? modName + 1 : modPath; + const uintptr_t offset = reinterpret_cast(addr) - reinterpret_cast(mod); + + char symStorage[sizeof(SYMBOL_INFO) + MAX_SYM_NAME] = {}; + auto* sym = reinterpret_cast(symStorage); + sym->SizeOfStruct = sizeof(SYMBOL_INFO); + sym->MaxNameLen = MAX_SYM_NAME; + DWORD64 disp = 0; + IMAGEHLP_LINE64 line{}; + line.SizeOfStruct = sizeof(IMAGEHLP_LINE64); + DWORD lineDisp = 0; + + const DWORD64 pc = reinterpret_cast(addr); + const BOOL haveSym = SymFromAddr(GetCurrentProcess(), pc, &disp, sym); + const BOOL haveLine = SymGetLineFromAddr64(GetCurrentProcess(), pc, &lineDisp, &line); + + if (haveSym && haveLine) + log(" %p %s!%s+0x%llx (%s:%lu)", addr, modName, sym->Name, + static_cast(disp), line.FileName, line.LineNumber); + else if (haveSym) + log(" %p %s!%s+0x%llx", addr, modName, sym->Name, + static_cast(disp)); + else + log(" %p %s+0x%llx", addr, modName, + static_cast(offset)); + } + + // Safely read 8 bytes from a possibly-bad address. Returns false on fault. + // Extracted into its own function because MSVC forbids __try in functions + // with C++ objects that have destructors (e.g. std::lock_guard below). + bool safeRead64(const DWORD64 addr, DWORD64* out) + { + __try + { + *out = *reinterpret_cast(addr); + return true; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + return false; + } + } + + void logStackTrace(CONTEXT* ctx) + { + static std::mutex walkMutex; + std::lock_guard lock(walkMutex); + + const HANDLE process = GetCurrentProcess(); + if (!g_sym_initialized.exchange(true)) + { + SymSetOptions(SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_FAIL_CRITICAL_ERRORS); + SymInitialize(process, nullptr, TRUE); + } + else + { + SymRefreshModuleList(process); + } + + CONTEXT walkCtx = *ctx; + DWORD machine; + STACKFRAME64 frame{}; +#ifdef _M_X64 + machine = IMAGE_FILE_MACHINE_AMD64; + + // If PC is bogus (null-call via bad function pointer or vtable), + // StackWalk64 can't find unwind info and bails on the first frame. + // Recover by lifting the return address off the top of the stack + // so we at least see the caller. [Rsp] holds the return address + // pushed by the call that jumped to the invalid target. + bool recovered_pc = false; + if (walkCtx.Rip == 0 || walkCtx.Rip < 0x10000) + { + log("[crash] PC is invalid (0x%llx) - recovering caller from [Rsp]", + static_cast(walkCtx.Rip)); + DWORD64 retAddr = 0; + if (safeRead64(walkCtx.Rsp, &retAddr)) + { + walkCtx.Rip = retAddr; + walkCtx.Rsp += sizeof(DWORD64); + recovered_pc = true; + log("[crash] recovered caller PC: 0x%llx", + static_cast(retAddr)); + } + else + { + log("[crash] could not read return address from stack"); + } + } + + frame.AddrPC.Offset = walkCtx.Rip; + frame.AddrFrame.Offset = walkCtx.Rbp; + frame.AddrStack.Offset = walkCtx.Rsp; +#else + machine = IMAGE_FILE_MACHINE_I386; + bool recovered_pc = false; + frame.AddrPC.Offset = walkCtx.Eip; + frame.AddrFrame.Offset = walkCtx.Ebp; + frame.AddrStack.Offset = walkCtx.Esp; +#endif + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrFrame.Mode = AddrModeFlat; + frame.AddrStack.Mode = AddrModeFlat; + + log("[crash] stack trace%s:", recovered_pc ? " (starting from recovered caller)" : ""); + int walked = 0; + for (int i = 0; i < 64; ++i) + { + if (!StackWalk64(machine, process, GetCurrentThread(), &frame, &walkCtx, + nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr)) + break; + if (frame.AddrPC.Offset == 0) + break; + logModuleFor(reinterpret_cast(frame.AddrPC.Offset)); + ++walked; + } + + // If StackWalk64 gave us nothing (bad PC, no unwind info for the + // top frame), fall back to scanning the stack for values that look + // like return addresses into known modules. Noisy but often the + // only signal we get for null-call / corrupted-vtable crashes. + if (walked == 0) + { + log("[crash] stack walk yielded 0 frames - scanning raw stack for return addresses:"); + const DWORD64 rsp = ctx->Rsp; + int found = 0; + for (int i = 0; i < 512 && found < 32; ++i) + { + DWORD64 val = 0; + if (!safeRead64(rsp + i * sizeof(DWORD64), &val)) + break; + if (val < 0x10000) continue; + HMODULE mod = nullptr; + if (GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(val), &mod) && mod) + { + logModuleFor(reinterpret_cast(val)); + ++found; + } + } + if (found == 0) + log("[crash] no return addresses found on stack"); + } + } + + void writeMinidump(EXCEPTION_POINTERS* ep) + { + char path[MAX_PATH]; + SYSTEMTIME st; + GetLocalTime(&st); + snprintf(path, sizeof(path), + "sampsharp_crash_%04u%02u%02u_%02u%02u%02u_%lu.dmp", + st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, + GetCurrentProcessId()); + + const HANDLE file = CreateFileA(path, GENERIC_WRITE, 0, nullptr, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + if (file == INVALID_HANDLE_VALUE) + { + log("[crash] failed to create minidump file '%s' (err=%lu)", path, GetLastError()); + return; + } + + MINIDUMP_EXCEPTION_INFORMATION mei{}; + mei.ThreadId = GetCurrentThreadId(); + mei.ExceptionPointers = ep; + mei.ClientPointers = FALSE; + + const auto type = static_cast( + MiniDumpWithFullMemory | + MiniDumpWithHandleData | + MiniDumpWithThreadInfo | + MiniDumpWithUnloadedModules | + MiniDumpWithFullMemoryInfo); + + const BOOL ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), + file, type, ep ? &mei : nullptr, nullptr, nullptr); + CloseHandle(file); + + if (ok) + log("[crash] minidump written: %s", path); + else + log("[crash] MiniDumpWriteDump failed (err=%lu)", GetLastError()); + } + + LONG WINAPI unhandledFilter(EXCEPTION_POINTERS* ep) + { + bool expected = false; + if (!g_handling.compare_exchange_strong(expected, true)) + return EXCEPTION_CONTINUE_SEARCH; + + const DWORD code = ep->ExceptionRecord->ExceptionCode; + void* addr = ep->ExceptionRecord->ExceptionAddress; + + log("================ SampSharp CRASH ================"); + log("[crash] code=0x%08lX (%s) addr=%p thread=%lu pid=%lu", + code, codeName(code), addr, + GetCurrentThreadId(), GetCurrentProcessId()); + + if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) + { + const ULONG_PTR kind = ep->ExceptionRecord->ExceptionInformation[0]; + const auto av_addr = reinterpret_cast(ep->ExceptionRecord->ExceptionInformation[1]); + const char* verb = kind == 0 ? "read from" : kind == 1 ? "write to" : "execute (DEP) at"; + log("[crash] access violation: %s %p", verb, av_addr); + } + + log("[crash] faulting instruction:"); + logModuleFor(addr); + + logStackTrace(ep->ContextRecord); + writeMinidump(ep); + + if (code == 0xC0000374) + { + log("[crash] HEAP_CORRUPTION is a delayed symptom - the real bug wrote"); + log("[crash] out-of-bounds (or freed twice) earlier on another code path."); + log("[crash] To pinpoint it, enable PageHeap once, reproduce, then disable:"); + log("[crash] gflags /p /enable omp-server.exe /full"); + log("[crash] gflags /p /disable omp-server.exe"); + log("[crash] With PageHeap the crash happens exactly at the bad write."); + } + log("================================================="); + + // Let the OS take it from here (WER / default termination). + return EXCEPTION_CONTINUE_SEARCH; + } + + LPTOP_LEVEL_EXCEPTION_FILTER g_previous_filter = nullptr; + } + + void install(ICore* core) + { + g_core = core; + + // Some CRT/host code calls SetUnhandledExceptionFilter(nullptr) to + // disable user filters. Re-arm ours and keep the previous one for + // chaining in case another component installs its own later. + g_previous_filter = SetUnhandledExceptionFilter(unhandledFilter); + + // Don't let WER silently swallow fatal errors without giving us a chance. + const UINT old = SetErrorMode(0); + SetErrorMode(old | SEM_NOGPFAULTERRORBOX | SEM_FAILCRITICALERRORS); + + if (core) + core->logLn(LogLevel::Message, "[SampSharp] crash handler installed"); + } +} + +#else // !_WIN32 + +namespace sampsharp::crash +{ + void install(ICore*) {} +} + +#endif diff --git a/src/sampsharp-component/crash-handler.hpp b/src/sampsharp-component/crash-handler.hpp new file mode 100644 index 00000000..35906073 --- /dev/null +++ b/src/sampsharp-component/crash-handler.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace sampsharp::crash +{ + // Installs a process-wide crash handler. Safe to call once during onLoad. + // On Windows: top-level SEH filter + vectored continue handler that logs + // exception code/address, module, stack trace, and writes a minidump next + // to the executable. No-op on other platforms. + void install(ICore* core); +} diff --git a/src/sampsharp-component/main.cpp b/src/sampsharp-component/main.cpp new file mode 100644 index 00000000..9a541f1b --- /dev/null +++ b/src/sampsharp-component/main.cpp @@ -0,0 +1,8 @@ +#include + +#include "sampsharp-component.hpp" + +COMPONENT_ENTRY_POINT() +{ + return SampSharpComponent::getInstance(); +} diff --git a/src/sampsharp-component/managed-host.cpp b/src/sampsharp-component/managed-host.cpp new file mode 100644 index 00000000..60790feb --- /dev/null +++ b/src/sampsharp-component/managed-host.cpp @@ -0,0 +1,219 @@ +#include "managed-host.hpp" + +#include +#include + +#if !defined WINDOWS +# include +# include +# define MAX_PATH PATH_MAX +#endif + +#define ERROR_MISSING_EXPORT 0x99999999 +#define ERROR_UNKNOWN 0x99999998 + +bool ManagedHost::initialize(const char ** error_ptr) +{ + if(_isReady) + { + return true; + } + + // Load host resolver and load functions from hosting library + int rc = load_hostfxr(nullptr); + if (rc) + { + *error_ptr = get_error(rc); + return false; + } + + _isReady = true; + return true; +} + +bool ManagedHost::isReady() const +{ + return _isReady; +} + +bool ManagedHost::loadFor(const StringView root_path, const StringView assembly_name, const char ** error_ptr) +{ + auto root_path_ = root_path.to_string(); + const auto assembly_name_ = assembly_name.to_string(); + + const char sep = root_path_.back(); + if(sep != '\\' && sep != '/') { + root_path_ = root_path_ + PATH_SEP; + } + + const string_t config_path = widen(root_path_) + widen(assembly_name_) + STR(".runtimeconfig.json"); + int rc = load_runtime(config_path.c_str(), &load_assembly_and_get_function_pointer_fptr); + + if (rc) + { + *error_ptr = get_error(rc); + return false; + } + + if (load_assembly_and_get_function_pointer_fptr == nullptr) + { + *error_ptr = get_error(ERROR_UNKNOWN); + return false; + } + + assem_path_ = widen((root_path_ + assembly_name_ + ".dll")); + + return true; +} + +bool ManagedHost::getEntryPoint(const StringView entry_type_name, const StringView name, void ** delegate_ptr, const char ** error_ptr) const +{ + if(load_assembly_and_get_function_pointer_fptr == nullptr) + { + return false; + } + + const string_t entry_type_name_w = widen(entry_type_name.to_string()); + const string_t name_w = widen(name.to_string()); + + const int rc = load_assembly_and_get_function_pointer_fptr( + assem_path_.c_str(), + entry_type_name_w.c_str(), + name_w.c_str(), + UNMANAGEDCALLERSONLY_METHOD, + nullptr, + delegate_ptr); + + if (rc) + { + *error_ptr = get_error(rc); + return false; + } + + return true; +} + +void *ManagedHost::load_library(const char_t * path) +{ +#ifdef WINDOWS + HMODULE h = ::LoadLibraryW(path); + assert(h != nullptr); + return (void *)h; +#else + void * h = dlopen(path, RTLD_LAZY | RTLD_LOCAL); + assert(h != nullptr); + return h; +#endif +} + +void * ManagedHost::get_export(void * h, const char * name) +{ +#ifdef WINDOWS + auto f = (void*)::GetProcAddress(static_cast(h), name); // NOLINT(clang-diagnostic-microsoft-cast) + assert(f != nullptr); + return f; +#else + void * f = dlsym(h, name); + assert(f != nullptr); + return f; +#endif +} + +int ManagedHost::load_hostfxr(const char_t * assembly_path) +{ + // Pre-allocate a large buffer for the path to hostfxr + char_t buffer[MAX_PATH]; + size_t buffer_size = sizeof(buffer) / sizeof(char_t); + + const get_hostfxr_parameters params { sizeof(get_hostfxr_parameters), assembly_path, nullptr }; + const int rc = get_hostfxr_path(buffer, &buffer_size, ¶ms); + if (rc != 0) + { + return rc; + } + + // Load hostfxr and get desired exports + void *lib = load_library(buffer); + init_for_config_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config"); + get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate"); + close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close"); + + if (!init_for_config_fptr || !get_delegate_fptr || !close_fptr) + { + return ERROR_MISSING_EXPORT; + } + + return 0; +} + +int ManagedHost::load_runtime(const char_t * config_path, load_assembly_and_get_function_pointer_fn * fptr) const +{ + // Load .NET Core + void * ptr = nullptr; + hostfxr_handle cxt = nullptr; + int rc = init_for_config_fptr(config_path, nullptr, &cxt); + if (rc != 0) + { + close_fptr(cxt); + return rc; + } + + if (cxt == nullptr) + { + return ERROR_UNKNOWN; + } + + // Get the load assembly function pointer + rc = get_delegate_fptr( + cxt, + hdt_load_assembly_and_get_function_pointer, + &ptr); + + close_fptr(cxt); + *fptr = (load_assembly_and_get_function_pointer_fn)ptr; + return rc; +} + +const char * ManagedHost::get_error(int code) const +{ + switch ((unsigned int)code) + { + case 0x00000000: return "Operation was successful"; + case 0x00000001: return "Initialization was successful, but another host context is already initialized"; + case 0x00000002: return "Initialization was successful, but another host context is already initialized and the requested context specified runtime properties which are not the same"; + case 0x80008081: return "One or more arguments are invalid"; + case 0x80008082: return "Failed to load a hosting component"; + case 0x80008083: return "One of the hosting components is missing"; + case 0x80008084: return "One of the hosting components is missing a required entry point"; + case 0x80008085: return "Failed to get the path of the current hosting component and determine the .NET installation location"; + case 0x80008087: return "The `coreclr` library could not be found"; + case 0x80008088: return "Failed to load the `coreclr` library or finding one of the required entry points"; + case 0x80008089: return "Call to `coreclr_initialize` failed"; + case 0x8000808a: return "Call to `coreclr_execute_assembly` failed"; + case 0x8000808b: return "Initialization of the `hostpolicy` dependency resolver failed"; + case 0x8000808c: return "Resolution of dependencies in `hostpolicy` failed"; + case 0x8000808e: return "Initialization of the `hostpolicy` library failed"; + case 0x80008092: return "Arguments to `hostpolicy` are invalid"; + case 0x80008093: return "The `.runtimeconfig.json` file is invalid"; + case 0x80008094: return "[internal usage only]"; + case 0x80008095: return "`apphost` failed to determine which application to run"; + case 0x80008096: return "Failed to find a compatible framework version"; + case 0x80008097: return "Host command failed"; + case 0x80008098: return "Buffer provided to a host API is too small to fit the requested value"; + case 0x8000809a: return "Application path imprinted in `apphost` doesn't exist"; + case 0x8000809b: return "Failed to find the requested SDK"; + case 0x8000809c: return "Application has multiple references to the same framework which are not compatible"; + case 0x8000809d: return "[internal usage only]"; + case 0x8000809f: return "Error extracting single-file bundle"; + case 0x800080a0: return "Error reading or writing files during single-file bundle extraction"; + case 0x800080a1: return "The application's `.runtimeconfig.json` contains a runtime property which is produced by the hosting layer"; + case 0x800080a2: return "Feature which requires certain version of the hosting layer was used on a version which doesn't support it"; + case 0x800080a3: return "Current state is incompatible with the requested operation"; + case 0x800080a4: return "Property requested by `hostfxr_get_runtime_property_value` doesn't exist"; + case 0x800080a5: return "Host configuration is incompatible with existing host context"; + case 0x800080a6: return "Hosting API does not support the requested scenario"; + case 0x800080a7: return "Support for a requested feature is disabled"; + case ERROR_MISSING_EXPORT: return "Missing export fuction in host library"; + default: return "Unkown error"; + } +} \ No newline at end of file diff --git a/src/sampsharp-component/managed-host.hpp b/src/sampsharp-component/managed-host.hpp new file mode 100644 index 00000000..16b7e867 --- /dev/null +++ b/src/sampsharp-component/managed-host.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include "types.hpp" +#include "compat.hpp" + +class ManagedHost final +{ +private: + bool _isReady = false; + + string_t assem_path_; + hostfxr_initialize_for_runtime_config_fn init_for_config_fptr = nullptr; + hostfxr_get_runtime_delegate_fn get_delegate_fptr = nullptr; + hostfxr_close_fn close_fptr = nullptr; + load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer_fptr = nullptr; + + static void * load_library(const char_t *); + static void * get_export(void *, const char *); + + int load_hostfxr(const char_t * assembly_path); + + int load_runtime(const char_t * config_path, load_assembly_and_get_function_pointer_fn * fptr) const; + const char * get_error(int code) const; + +public: + bool isReady() const; + bool initialize(const char ** error_ptr); + bool loadFor(const StringView root_path, const StringView assembly_name, const char ** error_ptr); + bool getEntryPoint(const StringView entry_type_name, const StringView name, void ** delegate_ptr, const char ** error_ptr) const; +}; \ No newline at end of file diff --git a/src/sampsharp-component/proxies/api.cpp b/src/sampsharp-component/proxies/api.cpp new file mode 100644 index 00000000..d4ee1612 --- /dev/null +++ b/src/sampsharp-component/proxies/api.cpp @@ -0,0 +1,1003 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "../proxy-api.hpp" + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wreturn-type-c-linkage" // has C-linkage specified, but returns user-defined type '' which is incompatible with C [-Wreturn-type-c-linkage] + +#endif + +#ifdef _MSC_VER +# pragma warning(push) +# pragma warning(disable: 4190) // has C-linkage specified, but returns UDT '' which is incompatible with C + +#endif + +// Type aliases to prevent them from breaking proxy macros +using IntPair = Pair; +using BoolStringPair = Pair; +using HoursMinutesPair = Pair; +using SizePair = Pair; +using NewConnectionPlayerPair = Pair; +using CarriagesArray = StaticArray; +using VehicleModelArray = StaticArray; +using SkillsArray = StaticArray; + +// include/Server/Components/Actors +PROXY(IActor, void, setSkin, int); +PROXY(IActor, int, getSkin); +PROXY(IActor, void, applyAnimation, AnimationData&); +PROXY(IActor, const AnimationData&, getAnimation); +PROXY(IActor, void, clearAnimations); +PROXY(IActor, void, setHealth, float); +PROXY(IActor, float, getHealth); +PROXY(IActor, void, setInvulnerable, bool); +PROXY(IActor, bool, isInvulnerable); +PROXY(IActor, bool, isStreamedInForPlayer, IPlayer&); +PROXY(IActor, void, streamInForPlayer, IPlayer&); +PROXY(IActor, void, streamOutForPlayer, IPlayer&); +PROXY(IActor, const ActorSpawnData&, getSpawnData); +PROXY_CAST(IActor, IEntity); + +PROXY(IActorsComponent, IActor*, create, int, Vector3, float); + +PROXY_EVENT_DISPATCHER(IActorsComponent, ActorEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(ActorEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerGiveDamageActor, IPlayer&, IActor&, float, unsigned, BodyPart) + PROXY_EVENT_HANDLER_EVENT(void, onActorStreamOut, IActor&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onActorStreamIn, IActor&, IPlayer&) +PROXY_EVENT_HANDLER_END(ActorEventHandler, onPlayerGiveDamageActor, onActorStreamOut, onActorStreamIn) + +// include/Server/Components/Checkpoints +PROXY(ICheckpointDataBase, Vector3, getPosition); +PROXY(ICheckpointDataBase, void, setPosition, Vector3&); +PROXY(ICheckpointDataBase, float, getRadius); +PROXY(ICheckpointDataBase, void, setRadius, float); +PROXY(ICheckpointDataBase, bool, isPlayerInside); +PROXY(ICheckpointDataBase, void, setPlayerInside, bool); +PROXY(ICheckpointDataBase, void, enable); +PROXY(ICheckpointDataBase, void, disable); +PROXY(ICheckpointDataBase, bool, isEnabled); + +PROXY(IRaceCheckpointData, RaceCheckpointType, getType); +PROXY(IRaceCheckpointData, void, setType, RaceCheckpointType); +PROXY(IRaceCheckpointData, Vector3, getNextPosition); +PROXY(IRaceCheckpointData, void, setNextPosition, Vector3&); + +PROXY(IPlayerCheckpointData, IRaceCheckpointData&, getRaceCheckpoint); +PROXY(IPlayerCheckpointData, ICheckpointData&, getCheckpoint); + +PROXY_EVENT_DISPATCHER(ICheckpointsComponent, PlayerCheckpointEventHandler, getEventDispatcher); + +PROXY_EVENT_HANDLER_BEGIN(PlayerCheckpointEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerEnterCheckpoint, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerLeaveCheckpoint, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerEnterRaceCheckpoint, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerLeaveRaceCheckpoint, IPlayer&) +PROXY_EVENT_HANDLER_END(PlayerCheckpointEventHandler, onPlayerEnterCheckpoint, onPlayerLeaveCheckpoint, + onPlayerEnterRaceCheckpoint, onPlayerLeaveRaceCheckpoint) + +// include/Server/Components/Classes +PROXY(IClass, const PlayerClass&, getClass); +PROXY(IClass, void, setClass, PlayerClass&); +PROXY_CAST(IClass, IIDProvider); + +PROXY(IPlayerClassData, const PlayerClass&, getClass); +PROXY(IPlayerClassData, void, setSpawnInfo, PlayerClass&); +PROXY(IPlayerClassData, void, spawnPlayer); + +PROXY(IClassesComponent, IClass*, create, int, int, Vector3, float, WeaponSlots&); + +PROXY_EVENT_DISPATCHER(IClassesComponent, ClassEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(ClassEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerRequestClass, IPlayer&, unsigned int) +PROXY_EVENT_HANDLER_END(ClassEventHandler, onPlayerRequestClass) + + +// include/Server/Components/Console +PROXY(IConsoleComponent, void, send, StringView, ConsoleCommandSenderData&); +PROXY(IConsoleComponent, void, sendMessage, ConsoleCommandSenderData&, StringView); + +PROXY_EVENT_DISPATCHER(IConsoleComponent, ConsoleEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(ConsoleEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onConsoleText, StringView, StringView, const ConsoleCommandSenderData&) + PROXY_EVENT_HANDLER_EVENT(void, onRconLoginAttempt, IPlayer&, StringView, bool) + PROXY_EVENT_HANDLER_EVENT(void, onConsoleCommandListRequest, FlatHashSet&) +PROXY_EVENT_HANDLER_END(ConsoleEventHandler, onConsoleText, onRconLoginAttempt, onConsoleCommandListRequest) + +PROXY(IPlayerConsoleData, bool, hasConsoleAccess); +PROXY(IPlayerConsoleData, void, setConsoleAccessibility, bool); + +// include/Server/Components/CustomModels +PROXY(IPlayerCustomModelsData, uint32_t, getCustomSkin); +PROXY(IPlayerCustomModelsData, void, setCustomSkin, uint32_t); +PROXY(IPlayerCustomModelsData, bool, sendDownloadUrl, StringView); + +PROXY(ICustomModelsComponent, bool, addCustomModel, ModelType, int32_t, int32_t, StringView, StringView, int32_t, + uint8_t, uint8_t); +PROXY(ICustomModelsComponent, bool, getBaseModel, uint32_t&, uint32_t&); +PROXY(ICustomModelsComponent, StringView, getModelNameFromChecksum, uint32_t); +PROXY(ICustomModelsComponent, bool, isValidCustomModel, int32_t); +PROXY(ICustomModelsComponent, bool, getCustomModelPath, int32_t, StringView&, StringView&); + +PROXY_EVENT_DISPATCHER(ICustomModelsComponent, PlayerModelsEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerModelsEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerFinishedDownloading, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerRequestDownload, IPlayer&, ModelDownloadType, uint32_t) +PROXY_EVENT_HANDLER_END(PlayerModelsEventHandler, onPlayerFinishedDownloading, onPlayerRequestDownload) + +// include/Server/Components/Databases +// @skip + +// include/Server/Components/Dialogs +PROXY(IPlayerDialogData, void, hide, IPlayer&); +PROXY(IPlayerDialogData, void, show, IPlayer&, int, DialogStyle, StringView, StringView, StringView, StringView); +PROXY(IPlayerDialogData, void, get, int&, DialogStyle&, StringView&, StringView&, StringView&, StringView&); +PROXY(IPlayerDialogData, int, getActiveID) + +PROXY_EVENT_DISPATCHER(IDialogsComponent, PlayerDialogEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerDialogEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onDialogResponse, IPlayer&, int, DialogResponse, int, StringView) +PROXY_EVENT_HANDLER_END(PlayerDialogEventHandler, onDialogResponse) + +// include/Server/Components/Fixes +PROXY(IPlayerFixesData, bool, sendGameText, StringView, Milliseconds, int); +PROXY(IPlayerFixesData, bool, hideGameText, int); +PROXY(IPlayerFixesData, bool, hasGameText, int); +PROXY(IPlayerFixesData, bool, getGameText, int, StringView&, Milliseconds&, Milliseconds&); +PROXY(IPlayerFixesData, void, applyAnimation, IPlayer*, IActor*, AnimationData*); + +PROXY(IFixesComponent, bool, sendGameTextToAll, StringView, Milliseconds, int); +PROXY(IFixesComponent, bool, hideGameTextForAll, int); +PROXY(IFixesComponent, void, clearAnimation, IPlayer*, IActor*); + +// include/Server/Components/GangZones +PROXY(IBaseGangZone, bool, isShownForPlayer, IPlayer&); +PROXY(IBaseGangZone, bool, isFlashingForPlayer, IPlayer&); +PROXY(IBaseGangZone, void, showForPlayer, IPlayer&, Colour&); +PROXY(IBaseGangZone, void, hideForPlayer, IPlayer&); +PROXY(IBaseGangZone, void, flashForPlayer, IPlayer&, Colour&); +PROXY(IBaseGangZone, void, stopFlashForPlayer, IPlayer&); +PROXY(IBaseGangZone, GangZonePos, getPosition); +PROXY(IBaseGangZone, void, setPosition, GangZonePos&); +PROXY(IBaseGangZone, bool, isPlayerInside, IPlayer&); +PROXY(IBaseGangZone, const FlatHashSet&, getShownFor); +PROXY(IBaseGangZone, Colour, getFlashingColourForPlayer, IPlayer&); +PROXY(IBaseGangZone, Colour, getColourForPlayer, IPlayer&); +PROXY(IBaseGangZone, void, setLegacyPlayer, IPlayer*); +PROXY(IBaseGangZone, IPlayer*, getLegacyPlayer); +PROXY_CAST(IBaseGangZone, IIDProvider); + +PROXY(IGangZonesComponent, IGangZone*, create, GangZonePos); +PROXY(IGangZonesComponent, const FlatHashSet&, getCheckingGangZones); +PROXY(IGangZonesComponent, void, useGangZoneCheck, IGangZone&, bool); +PROXY(IGangZonesComponent, int, toLegacyID, int); +PROXY(IGangZonesComponent, int, fromLegacyID, int); +PROXY(IGangZonesComponent, void, releaseLegacyID, int); +PROXY(IGangZonesComponent, int, reserveLegacyID); +PROXY(IGangZonesComponent, void, setLegacyID, int, int); + +PROXY_EVENT_DISPATCHER(IGangZonesComponent, GangZoneEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(GangZoneEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerEnterGangZone, IPlayer&, IGangZone&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerLeaveGangZone, IPlayer&, IGangZone&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClickGangZone, IPlayer&, IGangZone&) +PROXY_EVENT_HANDLER_END(GangZoneEventHandler, onPlayerEnterGangZone, onPlayerLeaveGangZone, onPlayerClickGangZone) + +PROXY(IPlayerGangZoneData, int, toLegacyID, int); +PROXY(IPlayerGangZoneData, int, fromLegacyID, int); +PROXY(IPlayerGangZoneData, void, releaseLegacyID, int); +PROXY(IPlayerGangZoneData, int, reserveLegacyID); +PROXY(IPlayerGangZoneData, void, setLegacyID, int, int); +PROXY(IPlayerGangZoneData, int, toClientID, int); +PROXY(IPlayerGangZoneData, int, fromClientID, int); +PROXY(IPlayerGangZoneData, void, releaseClientID, int); +PROXY(IPlayerGangZoneData, int, reserveClientID); +PROXY(IPlayerGangZoneData, void, setClientID, int, int); + +// include/Server/Components/LegacyConfig +PROXY(ILegacyConfigComponent, StringView, getConfig, StringView); +PROXY(ILegacyConfigComponent, StringView, getLegacy, StringView); + +// include/Server/Components/Menus +PROXY(IMenu, void, setColumnHeader, StringView, MenuColumn); +PROXY(IMenu, int, addCell, StringView, MenuColumn); +PROXY(IMenu, void, disableRow, MenuRow); +PROXY(IMenu, bool, isRowEnabled, MenuRow); +PROXY(IMenu, void, disable); +PROXY(IMenu, bool, isEnabled); +PROXY(IMenu, const Vector2&, getPosition); +PROXY(IMenu, int, getRowCount, MenuColumn); +PROXY(IMenu, int, getColumnCount); +PROXY_PTR(IMenu, Vector2, getColumnWidths); +PROXY(IMenu, StringView, getColumnHeader, MenuColumn); +PROXY(IMenu, StringView, getCell, MenuColumn, MenuRow); +PROXY(IMenu, void, initForPlayer, IPlayer&); +PROXY(IMenu, void, showForPlayer, IPlayer&); +PROXY(IMenu, void, hideForPlayer, IPlayer&); +PROXY_CAST(IMenu, IIDProvider); + +PROXY(IPlayerMenuData, uint8_t, getMenuID); +PROXY(IPlayerMenuData, void, setMenuID, uint8_t); + +PROXY(IMenusComponent, IMenu*, create, StringView, Vector2, uint8_t, float, float); + +PROXY_EVENT_DISPATCHER(IMenusComponent, MenuEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(MenuEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerSelectedMenuRow, IPlayer&, MenuRow) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerExitedMenu, IPlayer&) +PROXY_EVENT_HANDLER_END(MenuEventHandler, onPlayerSelectedMenuRow, onPlayerExitedMenu) + +// include/Server/Components/NPCs +PROXY(INPC, IPlayer*, getPlayer); +PROXY(INPC, Vector3, getPosition); +PROXY(INPC, void, setPosition, const Vector3&, bool); +PROXY(INPC, GTAQuat, getRotation); +PROXY(INPC, void, setRotation, const GTAQuat&, bool); +PROXY(INPC, int, getVirtualWorld); +PROXY(INPC, void, setVirtualWorld, int); +PROXY(INPC, unsigned, getInterior); +PROXY(INPC, void, setInterior, unsigned); +PROXY(INPC, Vector3, getVelocity); +PROXY(INPC, void, setVelocity, Vector3, bool); +PROXY(INPC, void, spawn); +PROXY(INPC, void, respawn); +PROXY(INPC, bool, isDead); +PROXY(INPC, void, setSkin, int); +PROXY(INPC, void, setWeapon, uint8_t); +PROXY(INPC, uint8_t, getWeapon); +PROXY(INPC, void, setAmmo, int); +PROXY(INPC, int, getAmmo); +PROXY(INPC, float, getHealth); +PROXY(INPC, void, setHealth, float); +PROXY(INPC, float, getArmour); +PROXY(INPC, void, setArmour, float); +PROXY(INPC, bool, isInvulnerable); +PROXY(INPC, void, setInvulnerable, bool); +PROXY(INPC, bool, isMoving); +PROXY(INPC, bool, move, Vector3, NPCMoveType, float, float); +PROXY(INPC, void, stopMove); +PROXY(INPC, void, clearAnimations); +PROXY(INPC, void, applyAnimation, const AnimationData&); +PROXY(INPC, bool, isStreamedInForPlayer, const IPlayer&); +PROXY_CAST(INPC, IIDProvider); +PROXY(INPCComponent, INPC*, create, StringView); +PROXY(INPCComponent, void, destroy, INPC&); +PROXY(INPCComponent, int, createPath); +PROXY(INPCComponent, bool, destroyPath, int); +PROXY(INPCComponent, bool, addPointToPath, int, const Vector3&, float); +PROXY(INPCComponent, bool, isValidPath, int); +PROXY(INPCComponent, int, loadRecord, StringView); +PROXY(INPCComponent, bool, unloadRecord, int); +PROXY_CAST(INPCComponent, INetworkComponent); + +PROXY_EVENT_DISPATCHER(INPCComponent, NPCEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(NPCEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onNPCFinishMove, INPC&) + PROXY_EVENT_HANDLER_EVENT(void, onNPCCreate, INPC&) + PROXY_EVENT_HANDLER_EVENT(void, onNPCDestroy, INPC&) + PROXY_EVENT_HANDLER_EVENT(void, onNPCSpawn, INPC&) + PROXY_EVENT_HANDLER_EVENT(void, onNPCRespawn, INPC&) + PROXY_EVENT_HANDLER_EVENT(void, onNPCDeath, INPC&, IPlayer*, int) +PROXY_EVENT_HANDLER_END(NPCEventHandler, onNPCFinishMove, onNPCCreate, onNPCDestroy, onNPCSpawn, onNPCRespawn, + onNPCDeath) + +// include/Server/Components/Objects +PROXY(IBaseObject, void, setDrawDistance, float); +PROXY(IBaseObject, float, getDrawDistance); +PROXY(IBaseObject, void, setModel, int); +PROXY(IBaseObject, int, getModel); +PROXY(IBaseObject, void, setCameraCollision, bool); +PROXY(IBaseObject, bool, getCameraCollision); +PROXY(IBaseObject, void, move, ObjectMoveData&); +PROXY(IBaseObject, bool, isMoving); +PROXY(IBaseObject, void, stop); +PROXY(IBaseObject, const ObjectMoveData&, getMovingData); +PROXY(IBaseObject, void, attachToVehicle, IVehicle&, Vector3, Vector3); +PROXY(IBaseObject, void, resetAttachment); +PROXY(IBaseObject, const ObjectAttachmentData&, getAttachmentData); +PROXY(IBaseObject, bool, getMaterialData, uint32_t, const ObjectMaterialData*&); +PROXY(IBaseObject, void, setMaterial, uint32_t, int, StringView, StringView, Colour); +PROXY(IBaseObject, void, setMaterialText, uint32_t, StringView, ObjectMaterialSize, StringView, int, bool, Colour, + Colour, ObjectMaterialTextAlign); +PROXY_CAST(IBaseObject, IEntity); + +PROXY(IObject, void, attachToPlayer, IPlayer&, Vector3, Vector3); +PROXY(IObject, void, attachToObject, IObject&, Vector3, Vector3, bool); + +PROXY(IPlayerObject, void, attachToObject, IPlayerObject&, Vector3, Vector3); +PROXY(IPlayerObject, void, attachToPlayer, IPlayer&, Vector3, Vector3); + +PROXY(IObjectsComponent, void, setDefaultCameraCollision, bool); +PROXY(IObjectsComponent, bool, getDefaultCameraCollision); +PROXY(IObjectsComponent, IObject*, create, int, Vector3, Vector3, float); +PROXY_EVENT_DISPATCHER(IObjectsComponent, ObjectEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(ObjectEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onMoved, IObject&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerObjectMoved, IPlayer&, IPlayerObject&) + PROXY_EVENT_HANDLER_EVENT(void, onObjectSelected, IPlayer&, IObject&, int, Vector3) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerObjectSelected, IPlayer&, IPlayerObject&, int, Vector3) + PROXY_EVENT_HANDLER_EVENT(void, onObjectEdited, IPlayer&, IObject&, ObjectEditResponse, Vector3, Vector3) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerObjectEdited, IPlayer&, IPlayerObject&, ObjectEditResponse, Vector3, + Vector3) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerAttachedObjectEdited, IPlayer&, int, bool, const ObjectAttachmentSlotData&) +PROXY_EVENT_HANDLER_END(ObjectEventHandler, onMoved, onPlayerObjectMoved, onObjectSelected, onPlayerObjectSelected, + onObjectEdited, onPlayerObjectEdited, onPlayerAttachedObjectEdited) + +PROXY(IPlayerObjectData, IPlayerObject*, create, int, Vector3, Vector3, float); +PROXY(IPlayerObjectData, void, setAttachedObject, int, ObjectAttachmentSlotData&); +PROXY(IPlayerObjectData, void, removeAttachedObject, int); +PROXY(IPlayerObjectData, bool, hasAttachedObject, int); +PROXY(IPlayerObjectData, const ObjectAttachmentSlotData&, getAttachedObject, int); +PROXY(IPlayerObjectData, void, beginSelecting); +PROXY(IPlayerObjectData, bool, selectingObject); +PROXY(IPlayerObjectData, void, endEditing); +PROXY(IPlayerObjectData, void, beginEditing, IObject&); +PROXY_OVERLOAD(IPlayerObjectData, void, beginEditing, _player, IPlayerObject&); +PROXY(IPlayerObjectData, bool, editingObject); +PROXY(IPlayerObjectData, void, editAttachedObject, int); +PROXY_CAST_NAMED(IPlayerObjectData, IPlayerObjectData, IPool, IPool); + +// include/Server/Components/Pawn +// @skip + +// include/Server/Components/Pickups + +PROXY(IBasePickup, void, setType, PickupType, bool); +PROXY(IBasePickup, PickupType, getType); +PROXY(IBasePickup, void, setPositionNoUpdate, Vector3); +PROXY(IBasePickup, void, setModel, int, bool); +PROXY(IBasePickup, int, getModel); +PROXY(IBasePickup, bool, isStreamedInForPlayer, const IPlayer&); +PROXY(IBasePickup, void, streamInForPlayer, IPlayer&); +PROXY(IBasePickup, void, streamOutForPlayer, IPlayer&); +PROXY(IBasePickup, void, setPickupHiddenForPlayer, IPlayer&, bool); +PROXY(IBasePickup, bool, isPickupHiddenForPlayer, IPlayer&); +PROXY(IBasePickup, void, setLegacyPlayer, IPlayer*); +PROXY(IBasePickup, IPlayer*, getLegacyPlayer); +PROXY_CAST(IBasePickup, IEntity); + +PROXY(IPickupsComponent, IPickup*, create, int, PickupType, Vector3, uint32_t, bool); +PROXY(IPickupsComponent, int, toLegacyID, int); +PROXY(IPickupsComponent, int, fromLegacyID, int); +PROXY(IPickupsComponent, void, releaseLegacyID, int); +PROXY(IPickupsComponent, int, reserveLegacyID); +PROXY(IPickupsComponent, void, setLegacyID, int, int); + +PROXY_EVENT_DISPATCHER(IPickupsComponent, PickupEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PickupEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerPickUpPickup, IPlayer&, IPickup&) +PROXY_EVENT_HANDLER_END(PickupEventHandler, onPlayerPickUpPickup) + +PROXY(IPlayerPickupData, int, toLegacyID, int); +PROXY(IPlayerPickupData, int, fromLegacyID, int); +PROXY(IPlayerPickupData, void, releaseLegacyID, int); +PROXY(IPlayerPickupData, int, reserveLegacyID); +PROXY(IPlayerPickupData, void, setLegacyID, int, int); +PROXY(IPlayerPickupData, int, toClientID, int); +PROXY(IPlayerPickupData, int, fromClientID, int); +PROXY(IPlayerPickupData, void, releaseClientID, int); +PROXY(IPlayerPickupData, int, reserveClientID); +PROXY(IPlayerPickupData, void, setClientID, int, int); + +// include/Server/Components/Recordings +PROXY(IPlayerRecordingData, void, start, PlayerRecordingType, StringView); +PROXY(IPlayerRecordingData, void, stop); + +// include/Server/Components/TextDraws +PROXY_PTR(ITextDrawBase, Vector2, getPosition); +PROXY(ITextDrawBase, ITextDrawBase&, setPosition, Vector2); +PROXY(ITextDrawBase, void, setText, StringView); +PROXY(ITextDrawBase, StringView, getText); +PROXY(ITextDrawBase, ITextDrawBase&, setLetterSize, Vector2); +PROXY_PTR(ITextDrawBase, Vector2, getLetterSize); +PROXY(ITextDrawBase, ITextDrawBase&, setTextSize, Vector2); +PROXY_PTR(ITextDrawBase, Vector2, getTextSize); +PROXY(ITextDrawBase, ITextDrawBase&, setAlignment, TextDrawAlignmentTypes); +PROXY(ITextDrawBase, TextDrawAlignmentTypes, getAlignment); +PROXY(ITextDrawBase, ITextDrawBase&, setColour, Colour); +PROXY_PTR(ITextDrawBase, Colour, getLetterColour); +PROXY(ITextDrawBase, ITextDrawBase&, useBox, bool); +PROXY(ITextDrawBase, bool, hasBox); +PROXY(ITextDrawBase, ITextDrawBase&, setBoxColour, Colour); +PROXY_PTR(ITextDrawBase, Colour, getBoxColour); +PROXY(ITextDrawBase, ITextDrawBase&, setShadow, int); +PROXY(ITextDrawBase, int, getShadow); +PROXY(ITextDrawBase, ITextDrawBase&, setOutline, int); +PROXY(ITextDrawBase, int, getOutline); +PROXY(ITextDrawBase, ITextDrawBase&, setBackgroundColour, Colour); +PROXY_PTR(ITextDrawBase, Colour, getBackgroundColour); +PROXY(ITextDrawBase, ITextDrawBase&, setStyle, TextDrawStyle); +PROXY(ITextDrawBase, TextDrawStyle, getStyle); +PROXY(ITextDrawBase, ITextDrawBase&, setProportional, bool); +PROXY(ITextDrawBase, bool, isProportional); +PROXY(ITextDrawBase, ITextDrawBase&, setSelectable, bool); +PROXY(ITextDrawBase, bool, isSelectable); +PROXY(ITextDrawBase, ITextDrawBase&, setPreviewModel, int); +PROXY(ITextDrawBase, int, getPreviewModel); +PROXY(ITextDrawBase, ITextDrawBase&, setPreviewRotation, Vector3); +PROXY(ITextDrawBase, Vector3, getPreviewRotation); +PROXY(ITextDrawBase, ITextDrawBase&, setPreviewVehicleColour, int, int); +PROXY_PTR(ITextDrawBase, IntPair, getPreviewVehicleColour); +PROXY(ITextDrawBase, ITextDrawBase&, setPreviewZoom, float); +PROXY(ITextDrawBase, float, getPreviewZoom); +PROXY(ITextDrawBase, void, restream); +PROXY_CAST(ITextDrawBase, IIDProvider); + +PROXY(ITextDraw, void, showForPlayer, IPlayer&); +PROXY(ITextDraw, void, hideForPlayer, IPlayer&); +PROXY(ITextDraw, bool, isShownForPlayer, const IPlayer&); +PROXY(ITextDraw, void, setTextForPlayer, IPlayer&, StringView); + +PROXY(IPlayerTextDraw, void, show); +PROXY(IPlayerTextDraw, void, hide); +PROXY(IPlayerTextDraw, bool, isShown); + +PROXY(ITextDrawsComponent, ITextDraw*, create, Vector2, StringView); +PROXY_OVERLOAD(ITextDrawsComponent, ITextDraw*, create, _model, Vector2, int); +PROXY_EVENT_DISPATCHER(ITextDrawsComponent, TextDrawEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(TextDrawEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClickTextDraw, IPlayer&, ITextDraw&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClickPlayerTextDraw, IPlayer&, IPlayerTextDraw&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerCancelTextDrawSelection, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerCancelPlayerTextDrawSelection, IPlayer&) +PROXY_EVENT_HANDLER_END(TextDrawEventHandler, onPlayerClickTextDraw, onPlayerClickPlayerTextDraw, + onPlayerCancelTextDrawSelection, onPlayerCancelPlayerTextDrawSelection) + +PROXY(IPlayerTextDrawData, void, beginSelection, Colour); +PROXY(IPlayerTextDrawData, bool, isSelecting); +PROXY(IPlayerTextDrawData, void, endSelection); +PROXY(IPlayerTextDrawData, IPlayerTextDraw*, create, Vector2, StringView); +PROXY_OVERLOAD(IPlayerTextDrawData, IPlayerTextDraw*, create, _model, Vector2, int); +PROXY_CAST_NAMED(IPlayerTextDrawData, IPlayerTextDrawData, IPool, IPool); + +// include/Server/Components/TextLabels +PROXY(ITextLabelBase, void, setText, StringView); +PROXY(ITextLabelBase, StringView, getText); +PROXY(ITextLabelBase, void, setColour, Colour); +PROXY_PTR(ITextLabelBase, Colour, getColour); +PROXY(ITextLabelBase, void, setDrawDistance, float); +PROXY(ITextLabelBase, float, getDrawDistance); +PROXY(ITextLabelBase, void, attachToPlayer, IPlayer&, Vector3); +PROXY(ITextLabelBase, void, attachToVehicle, IVehicle&, Vector3); +PROXY(ITextLabelBase, const TextLabelAttachmentData&, getAttachmentData); +PROXY(ITextLabelBase, void, detachFromPlayer, Vector3); +PROXY(ITextLabelBase, void, detachFromVehicle, Vector3); +PROXY(ITextLabelBase, void, setTestLOS, bool); +PROXY(ITextLabelBase, bool, getTestLOS); +PROXY(ITextLabelBase, void, setColourAndText, Colour, StringView); +PROXY_CAST(ITextLabelBase, IEntity); + +PROXY(ITextLabel, bool, isStreamedInForPlayer, IPlayer&); +PROXY(ITextLabel, void, streamInForPlayer, IPlayer&); +PROXY(ITextLabel, void, streamOutForPlayer, IPlayer&); + +PROXY(ITextLabelsComponent, ITextLabel *, create, StringView, Colour, Vector3, float, int, bool); +PROXY_OVERLOAD(ITextLabelsComponent, ITextLabel *, create, _player, StringView, Colour, Vector3, float, int, bool, + IPlayer&); +PROXY_OVERLOAD(ITextLabelsComponent, ITextLabel *, create, _vehicle, StringView, Colour, Vector3, float, int, bool, + IVehicle&); + +PROXY(IPlayerTextLabelData, IPlayerTextLabel *, create, StringView, Colour, Vector3, float, bool); +PROXY_OVERLOAD(IPlayerTextLabelData, IPlayerTextLabel *, create, _player, StringView, Colour, Vector3, float, bool, + IPlayer&); +PROXY_OVERLOAD(IPlayerTextLabelData, IPlayerTextLabel *, create, _vehicle, StringView, Colour, Vector3, float, bool, + IVehicle&); +PROXY_CAST_NAMED(IPlayerTextLabelData, IPlayerTextLabelData, IPool, IPool); + +// include/Server/Components/Timers +// @skip + +// include/Server/Components/Unicode +// @skip + +// include/Server/Components/Variables +// @skip + +// include/Server/Components/Vehicles + +PROXY(IVehicle, void, setSpawnData, VehicleSpawnData&); +PROXY_PTR(IVehicle, VehicleSpawnData, getSpawnData); +PROXY(IVehicle, bool, isStreamedInForPlayer, IPlayer&); +PROXY(IVehicle, void, streamInForPlayer, IPlayer&); +PROXY(IVehicle, void, streamOutForPlayer, IPlayer&); +PROXY(IVehicle, void, setColour, int, int); +PROXY_PTR(IVehicle, IntPair, getColour); +PROXY(IVehicle, void, setHealth, float); +PROXY(IVehicle, float, getHealth); +PROXY(IVehicle, bool, updateFromDriverSync, VehicleDriverSyncPacket&, IPlayer&); +PROXY(IVehicle, bool, updateFromPassengerSync, VehiclePassengerSyncPacket&, IPlayer&); +PROXY(IVehicle, bool, updateFromUnoccupied, VehicleUnoccupiedSyncPacket&, IPlayer&); +PROXY(IVehicle, bool, updateFromTrailerSync, VehicleTrailerSyncPacket&, IPlayer&); +PROXY(IVehicle, const FlatPtrHashSet&, streamedForPlayers); +PROXY(IVehicle, IPlayer*, getDriver); +PROXY(IVehicle, const FlatHashSet&, getPassengers); +PROXY(IVehicle, void, setPlate, StringView); +PROXY(IVehicle, StringView, getPlate); +PROXY(IVehicle, void, setDamageStatus, int, int, uint8_t, uint8_t, IPlayer*); +PROXY(IVehicle, void, getDamageStatus, int&, int&, int&, int&); +PROXY(IVehicle, void, setPaintJob, int); +PROXY(IVehicle, int, getPaintJob); +PROXY(IVehicle, void, addComponent, int); +PROXY(IVehicle, int, getComponentInSlot, int); +PROXY(IVehicle, void, removeComponent, int); +PROXY(IVehicle, void, putPlayer, IPlayer&, int); +PROXY(IVehicle, void, setZAngle, float); +PROXY(IVehicle, float, getZAngle); +PROXY(IVehicle, void, setParams, VehicleParams&); +PROXY(IVehicle, void, setParamsForPlayer, IPlayer&, VehicleParams&); +PROXY_PTR(IVehicle, VehicleParams, getParams); +PROXY(IVehicle, bool, isDead); +PROXY(IVehicle, void, respawn); +PROXY(IVehicle, Seconds, getRespawnDelay); +PROXY(IVehicle, void, setRespawnDelay, Seconds); +PROXY(IVehicle, bool, isRespawning); +PROXY(IVehicle, void, setInterior, int); +PROXY(IVehicle, int, getInterior); +PROXY(IVehicle, void, attachTrailer, IVehicle&); +PROXY(IVehicle, void, detachTrailer); +PROXY(IVehicle, bool, isTrailer); +PROXY(IVehicle, IVehicle*, getTrailer); +PROXY(IVehicle, IVehicle*, getCab); +PROXY(IVehicle, void, repair); +PROXY(IVehicle, void, addCarriage, IVehicle*, int); +PROXY(IVehicle, void, updateCarriage, Vector3, Vector3); +PROXY(IVehicle, const CarriagesArray&, getCarriages); +PROXY(IVehicle, void, setVelocity, Vector3); +PROXY(IVehicle, Vector3, getVelocity); +PROXY(IVehicle, void, setAngularVelocity, Vector3); +PROXY(IVehicle, Vector3, getAngularVelocity); +PROXY(IVehicle, int, getModel); +PROXY(IVehicle, uint8_t, getLandingGearState); +PROXY(IVehicle, bool, hasBeenOccupied); +PROXY(IVehicle, const TimePoint&, getLastOccupiedTime); +PROXY(IVehicle, const TimePoint&, getLastSpawnTime); +PROXY(IVehicle, bool, isOccupied); +PROXY(IVehicle, void, setSiren, bool); +PROXY(IVehicle, uint8_t, getSirenState); +PROXY(IVehicle, uint32_t, getHydraThrustAngle); +PROXY(IVehicle, float, getTrainSpeed); +PROXY(IVehicle, int, getLastDriverPoolID); +PROXY_CAST(IVehicle, IEntity); + +PROXY(IVehiclesComponent, VehicleModelArray&, models); +PROXY(IVehiclesComponent, IVehicle*, create, bool, int, Vector3, float, int, int, Seconds, bool); +PROXY_EVENT_DISPATCHER(IVehiclesComponent, VehicleEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(VehicleEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onVehicleStreamIn, IVehicle&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onVehicleStreamOut, IVehicle&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onVehicleDeath, IVehicle&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerEnterVehicle, IPlayer&, IVehicle&, bool) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerExitVehicle, IPlayer&, IVehicle&) + PROXY_EVENT_HANDLER_EVENT(void, onVehicleDamageStatusUpdate, IVehicle&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(bool, onVehiclePaintJob, IPlayer&, IVehicle&, int) + PROXY_EVENT_HANDLER_EVENT(bool, onVehicleMod, IPlayer&, IVehicle&, int) + PROXY_EVENT_HANDLER_EVENT(bool, onVehicleRespray, IPlayer&, IVehicle&, int, int) + PROXY_EVENT_HANDLER_EVENT(void, onEnterExitModShop, IPlayer&, bool, int) + PROXY_EVENT_HANDLER_EVENT(void, onVehicleSpawn, IVehicle&) + PROXY_EVENT_HANDLER_EVENT(bool, onUnoccupiedVehicleUpdate, IVehicle&, IPlayer&, UnoccupiedVehicleUpdate const) + PROXY_EVENT_HANDLER_EVENT(bool, onTrailerUpdate, IPlayer&, IVehicle&) + PROXY_EVENT_HANDLER_EVENT(bool, onVehicleSirenStateChange, IPlayer&, IVehicle&, uint8_t) +PROXY_EVENT_HANDLER_END(VehicleEventHandler, onVehicleStreamIn, onVehicleStreamOut, onVehicleDeath, + onPlayerEnterVehicle, onPlayerExitVehicle, onVehicleDamageStatusUpdate, onVehiclePaintJob, + onVehicleMod, onVehicleRespray, onEnterExitModShop, onVehicleSpawn, + onUnoccupiedVehicleUpdate, onTrailerUpdate, onVehicleSirenStateChange) + +PROXY(IPlayerVehicleData, IVehicle*, getVehicle); +PROXY(IPlayerVehicleData, void, resetVehicle); +PROXY(IPlayerVehicleData, int, getSeat); +PROXY(IPlayerVehicleData, bool, isInModShop); +PROXY(IPlayerVehicleData, bool, isInDriveByMode); +PROXY(IPlayerVehicleData, bool, isCuffed); + +// include/component +PROXY(IExtensible, IExtension*, getExtension, UID); +PROXY(IExtensible, bool, addExtension, IExtension*, bool); +PROXY(IExtensible, bool, removeExtension, IExtension*); +PROXY_OVERLOAD(IExtensible, bool, removeExtension, _uid, UID); + +PROXY(IComponent, int, supportedVersion); +PROXY(IComponent, StringView, componentName); +PROXY_CAST(IComponent, IUIDProvider); + +PROXY(IComponentList, IComponent*, queryComponent, UID); + +// include/core +PROXY(IConfig, StringView, getString, StringView); +PROXY(IConfig, int*, getInt, StringView); +PROXY(IConfig, float*, getFloat, StringView); +PROXY(IConfig, size_t, getStrings, StringView, Span); +PROXY(IConfig, size_t, getStringsCount, StringView); +PROXY(IConfig, ConfigOptionType, getType, StringView); +PROXY(IConfig, size_t, getBansCount); +PROXY(IConfig, const BanEntry&, getBan, size_t); +PROXY(IConfig, void, addBan, BanEntry&); +PROXY_OVERLOAD(IConfig, void, removeBan, _index, size_t); +PROXY(IConfig, void, removeBan, BanEntry&); +PROXY(IConfig, void, writeBans); +PROXY(IConfig, void, reloadBans); +PROXY(IConfig, void, clearBans); +PROXY(IConfig, bool, isBanned, BanEntry&); +PROXY_PTR(IConfig, BoolStringPair, getNameFromAlias, StringView); +PROXY(IConfig, void, enumOptions, OptionEnumeratorCallback&); +PROXY(IConfig, bool *, getBool, StringView); + +PROXY(ICore, SemanticVersion, getVersion); +PROXY(ICore, int, getNetworkBitStreamVersion); +PROXY(ICore, IPlayerPool&, getPlayers); +PROXY(ICore, IConfig&, getConfig); +PROXY(ICore, const FlatPtrHashSet&, getNetworks); +PROXY(ICore, unsigned, getTickCount); +PROXY(ICore, void, setGravity, float); +PROXY(ICore, float, getGravity); +PROXY(ICore, void, setWeather, int); +PROXY(ICore, void, setWorldTime, Hours); +PROXY(ICore, void, useStuntBonuses, bool); +PROXY(ICore, void, setData, SettableCoreDataType, StringView); +PROXY(ICore, void, setThreadSleep, Microseconds); +PROXY(ICore, void, useDynTicks, const bool); +PROXY(ICore, void, resetAll); +PROXY(ICore, void, reloadAll); +PROXY(ICore, StringView, getWeaponName, PlayerWeapon); +PROXY(ICore, void, connectBot, StringView, StringView); +PROXY(ICore, unsigned, tickRate); +PROXY(ICore, StringView, getVersionHash); +PROXY_CAST(ICore, ILogger); + +PROXY_EVENT_DISPATCHER(ICore, CoreEventHandler, getEventDispatcher); +PROXY_EVENT_HANDLER_BEGIN(CoreEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onTick, Microseconds, TimePoint) +PROXY_EVENT_HANDLER_END(CoreEventHandler, onTick) + +// include/entity +PROXY(IIDProvider, int, getID); + +PROXY(IEntity, Vector3, getPosition); +PROXY(IEntity, void, setPosition, Vector3); +PROXY(IEntity, GTAQuat, getRotation); +PROXY(IEntity, void, setRotation, GTAQuat); +PROXY(IEntity, int, getVirtualWorld); +PROXY(IEntity, void, setVirtualWorld, int); + +// include/events +PROXY_NAMED(IEventDispatcher, IEventDispatcher, bool, addEventHandler, void **, event_order_t); +PROXY_NAMED(IEventDispatcher, IEventDispatcher, bool, hasEventHandler, void **, event_order_t); +PROXY_NAMED(IEventDispatcher, IEventDispatcher, bool, removeEventHandler, void **); +PROXY_NAMED(IEventDispatcher, IEventDispatcher, size_t, count); + +PROXY_NAMED(IIndexedEventDispatcher, IIndexedEventDispatcher, bool, addEventHandler, void **, size_t, + event_order_t); +PROXY_NAMED(IIndexedEventDispatcher, IIndexedEventDispatcher, bool, hasEventHandler, void **, size_t, + event_order_t); +PROXY_NAMED(IIndexedEventDispatcher, IIndexedEventDispatcher, bool, removeEventHandler, void **, size_t); +PROXY_NAMED(IIndexedEventDispatcher, IIndexedEventDispatcher, size_t, count, size_t); +PROXY_NAMED_OVERLOAD(IIndexedEventDispatcher, IIndexedEventDispatcher, size_t, count, _index, size_t); + +// include/network +PROXY_EVENT_HANDLER_BEGIN(NetworkEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPeerConnect, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPeerDisconnect, IPlayer&, PeerDisconnectReason) +PROXY_EVENT_HANDLER_END(NetworkEventHandler, onPeerConnect, onPeerDisconnect) + +PROXY_EVENT_HANDLER_BEGIN(NetworkInEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onReceivePacket, IPlayer&, int, NetworkBitStream&) + PROXY_EVENT_HANDLER_EVENT(bool, onReceiveRPC, IPlayer&, int, NetworkBitStream&) +PROXY_EVENT_HANDLER_END(NetworkInEventHandler, onReceivePacket, onReceiveRPC) + +PROXY_EVENT_HANDLER_BEGIN(SingleNetworkInEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onReceive, IPlayer&, NetworkBitStream&) +PROXY_EVENT_HANDLER_END(SingleNetworkInEventHandler, onReceive) + +PROXY_EVENT_HANDLER_BEGIN(NetworkOutEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onSendPacket, IPlayer*, int, NetworkBitStream&) + PROXY_EVENT_HANDLER_EVENT(bool, onSendRPC, IPlayer*, int, NetworkBitStream&) +PROXY_EVENT_HANDLER_END(NetworkOutEventHandler, onSendPacket, onSendRPC) + +PROXY_EVENT_HANDLER_BEGIN(SingleNetworkOutEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onSend, IPlayer*, NetworkBitStream&) +PROXY_EVENT_HANDLER_END(SingleNetworkOutEventHandler, onSend) + +PROXY(INetwork, ENetworkType, getNetworkType); +PROXY(INetwork, IEventDispatcher&, getEventDispatcher); +PROXY(INetwork, IEventDispatcher&, getInEventDispatcher); +PROXY_INDEXED_EVENT_DISPATCHER(INetwork, SingleNetworkInEventHandler, getPerRPCInEventDispatcher); +PROXY_INDEXED_EVENT_DISPATCHER(INetwork, SingleNetworkInEventHandler, getPerPacketInEventDispatcher); +PROXY_EVENT_DISPATCHER(INetwork, NetworkOutEventHandler, getOutEventDispatcher); +PROXY_INDEXED_EVENT_DISPATCHER(INetwork, SingleNetworkOutEventHandler, getPerRPCOutEventDispatcher); +PROXY_INDEXED_EVENT_DISPATCHER(INetwork, SingleNetworkOutEventHandler, getPerPacketOutEventDispatcher); +PROXY(INetwork, bool, sendPacket, IPlayer&, Span, int, bool); +PROXY(INetwork, bool, broadcastPacket, Span, int, const IPlayer*, bool); +PROXY(INetwork, bool, sendRPC, IPlayer&, int, Span, int, bool); +PROXY(INetwork, bool, broadcastRPC, int, Span, int, IPlayer*, bool); +PROXY(INetwork, NetworkStats, getStatistics, IPlayer*); +PROXY(INetwork, unsigned, getPing, IPlayer&); +PROXY(INetwork, void, disconnect, IPlayer&); +PROXY(INetwork, void, ban, BanEntry&, Milliseconds); +PROXY(INetwork, void, unban, BanEntry&); +PROXY(INetwork, void, update); + +PROXY(INetworkComponent, INetwork*, getNetwork); + +PROXY(INetworkQueryExtension, bool, addRule, StringView, StringView) +PROXY(INetworkQueryExtension, bool, removeRule, StringView) +PROXY(INetworkQueryExtension, bool, isValidRule, StringView) + +// include/player +PROXY(IPlayer, void, kick); +PROXY(IPlayer, void, ban, StringView); +PROXY(IPlayer, bool, isBot); +PROXY(IPlayer, const PeerNetworkData&, getNetworkData); +PROXY(IPlayer, unsigned, getPing); +PROXY(IPlayer, bool, sendPacket, Span, int, bool); +PROXY(IPlayer, bool, sendRPC, int, Span, int, bool); +PROXY(IPlayer, void, broadcastRPCToStreamed, int, Span, int, bool); +PROXY(IPlayer, void, broadcastPacketToStreamed, Span, int, bool); +PROXY(IPlayer, void, broadcastSyncPacket, Span, int); +PROXY(IPlayer, void, spawn); +PROXY(IPlayer, ClientVersion, getClientVersion); +PROXY(IPlayer, StringView, getClientVersionName); +PROXY(IPlayer, void, setPositionFindZ, Vector3); +PROXY(IPlayer, void, setCameraPosition, Vector3); +PROXY(IPlayer, Vector3, getCameraPosition); +PROXY(IPlayer, void, setCameraLookAt, Vector3, int); +PROXY(IPlayer, Vector3, getCameraLookAt); +PROXY(IPlayer, void, setCameraBehind); +PROXY(IPlayer, void, interpolateCameraPosition, Vector3, Vector3, int, PlayerCameraCutType); +PROXY(IPlayer, void, interpolateCameraLookAt, Vector3, Vector3, int, PlayerCameraCutType); +PROXY(IPlayer, void, attachCameraToObject, IObject&); +PROXY_OVERLOAD(IPlayer, void, attachCameraToObject, _player, IPlayerObject&); +PROXY(IPlayer, EPlayerNameStatus, setName, StringView); +PROXY(IPlayer, StringView, getName); +PROXY(IPlayer, StringView, getSerial); +PROXY(IPlayer, void, giveWeapon, WeaponSlotData); +PROXY(IPlayer, void, removeWeapon, uint8_t); +PROXY(IPlayer, void, setWeaponAmmo, WeaponSlotData); +PROXY(IPlayer, WeaponSlots, getWeapons); +PROXY_PTR(IPlayer, WeaponSlotData, getWeaponSlot, int); +PROXY(IPlayer, void, resetWeapons); +PROXY(IPlayer, void, setArmedWeapon, uint32_t); +PROXY(IPlayer, uint32_t, getArmedWeapon); +PROXY(IPlayer, uint32_t, getArmedWeaponAmmo); +PROXY(IPlayer, void, setShopName, StringView); +PROXY(IPlayer, StringView, getShopName); +PROXY(IPlayer, void, setDrunkLevel, int); +PROXY(IPlayer, int, getDrunkLevel); +PROXY(IPlayer, void, setColour, Colour); +PROXY(IPlayer, const Colour&, getColour); +PROXY(IPlayer, void, setOtherColour, IPlayer&, Colour); +PROXY(IPlayer, bool, getOtherColour, IPlayer&, Colour&); +PROXY(IPlayer, void, setControllable, bool); +PROXY(IPlayer, bool, getControllable); +PROXY(IPlayer, void, setSpectating, bool); +PROXY(IPlayer, void, setWantedLevel, unsigned); +PROXY(IPlayer, unsigned, getWantedLevel); +PROXY(IPlayer, void, playSound, uint32_t, Vector3); +PROXY(IPlayer, uint32_t, lastPlayedSound); +PROXY(IPlayer, void, playAudio, StringView, bool, Vector3, float); +PROXY(IPlayer, bool, playerCrimeReport, IPlayer&, int); +PROXY(IPlayer, void, stopAudio); +PROXY(IPlayer, StringView, lastPlayedAudio); +PROXY(IPlayer, void, createExplosion, Vector3, int, float); +PROXY(IPlayer, void, sendDeathMessage, IPlayer&, IPlayer*, int); +PROXY(IPlayer, void, sendEmptyDeathMessage); +PROXY(IPlayer, void, removeDefaultObjects, unsigned, Vector3, float); +PROXY(IPlayer, void, forceClassSelection); +PROXY(IPlayer, void, setMoney, int); +PROXY(IPlayer, void, giveMoney, int); +PROXY(IPlayer, void, resetMoney); +PROXY(IPlayer, int, getMoney); +PROXY(IPlayer, void, setMapIcon, int, Vector3, int, Colour, MapIconStyle); +PROXY(IPlayer, void, unsetMapIcon, int); +PROXY(IPlayer, void, useStuntBonuses, bool); +PROXY(IPlayer, void, toggleOtherNameTag, IPlayer&, bool); +PROXY(IPlayer, void, setTime, Hours, Minutes); +PROXY_PTR(IPlayer, HoursMinutesPair, getTime); +PROXY(IPlayer, void, useClock, bool); +PROXY(IPlayer, bool, hasClock); +PROXY(IPlayer, void, useWidescreen, bool); +PROXY(IPlayer, bool, hasWidescreen); +PROXY(IPlayer, void, setTransform, GTAQuat); +PROXY(IPlayer, void, setHealth, float); +PROXY(IPlayer, float, getHealth); +PROXY(IPlayer, void, setScore, int); +PROXY(IPlayer, int, getScore); +PROXY(IPlayer, void, setArmour, float); +PROXY(IPlayer, float, getArmour); +PROXY(IPlayer, void, setGravity, float); +PROXY(IPlayer, float, getGravity); +PROXY(IPlayer, void, setWorldTime, Hours); +PROXY(IPlayer, void, applyAnimation, const AnimationData&, PlayerAnimationSyncType); +PROXY(IPlayer, void, clearAnimations, PlayerAnimationSyncType); +PROXY(IPlayer, PlayerAnimationData, getAnimationData); +PROXY(IPlayer, PlayerSurfingData, getSurfingData); +PROXY(IPlayer, void, streamInForPlayer, IPlayer&); +PROXY(IPlayer, bool, isStreamedInForPlayer, const IPlayer&); +PROXY(IPlayer, void, streamOutForPlayer, IPlayer&); +PROXY(IPlayer, const FlatPtrHashSet&, streamedForPlayers); +PROXY(IPlayer, PlayerState, getState); +PROXY(IPlayer, void, setTeam, int); +PROXY(IPlayer, int, getTeam); +PROXY(IPlayer, void, setSkin, int, bool); +PROXY(IPlayer, int, getSkin); +PROXY(IPlayer, void, setChatBubble, StringView, const Colour&, float, Milliseconds); +PROXY(IPlayer, void, sendClientMessage, const Colour&, StringView); +PROXY(IPlayer, void, sendChatMessage, IPlayer&, StringView); +PROXY(IPlayer, void, sendCommand, StringView); +PROXY(IPlayer, void, sendGameText, StringView, Milliseconds, int); +PROXY(IPlayer, void, hideGameText, int); +PROXY(IPlayer, bool, hasGameText, int); +PROXY(IPlayer, bool, getGameText, int, StringView&, Milliseconds&, Milliseconds&); +PROXY(IPlayer, void, setWeather, int); +PROXY(IPlayer, int, getWeather); +PROXY(IPlayer, void, setWorldBounds, Vector4); +PROXY(IPlayer, Vector4, getWorldBounds); +PROXY(IPlayer, void, setFightingStyle, PlayerFightingStyle); +PROXY(IPlayer, PlayerFightingStyle, getFightingStyle); +PROXY(IPlayer, void, setSkillLevel, PlayerWeaponSkill, int); +PROXY(IPlayer, void, setAction, PlayerSpecialAction); +PROXY(IPlayer, PlayerSpecialAction, getAction); +PROXY(IPlayer, void, setVelocity, Vector3); +PROXY(IPlayer, Vector3, getVelocity); +PROXY(IPlayer, void, setInterior, unsigned); +PROXY(IPlayer, unsigned, getInterior); +PROXY(IPlayer, const PlayerKeyData&, getKeyData); +PROXY(IPlayer, const SkillsArray&, getSkillLevels); +PROXY(IPlayer, const PlayerAimData&, getAimData); +PROXY(IPlayer, const PlayerBulletData&, getBulletData); +PROXY(IPlayer, void, useCameraTargeting, bool); +PROXY(IPlayer, bool, hasCameraTargeting); +PROXY(IPlayer, void, removeFromVehicle, bool); +PROXY(IPlayer, IPlayer*, getCameraTargetPlayer); +PROXY(IPlayer, IVehicle*, getCameraTargetVehicle); +PROXY(IPlayer, IObject*, getCameraTargetObject); +PROXY(IPlayer, IActor*, getCameraTargetActor); +PROXY(IPlayer, IPlayer*, getTargetPlayer); +PROXY(IPlayer, IActor*, getTargetActor); +PROXY(IPlayer, void, setRemoteVehicleCollisions, bool); +PROXY(IPlayer, void, spectatePlayer, IPlayer&, PlayerSpectateMode); +PROXY(IPlayer, void, spectateVehicle, IVehicle&, PlayerSpectateMode); +PROXY(IPlayer, const PlayerSpectateData&, getSpectateData); +PROXY(IPlayer, void, sendClientCheck, int, int, int, int); +PROXY(IPlayer, void, toggleGhostMode, bool); +PROXY(IPlayer, bool, isGhostModeEnabled); +PROXY(IPlayer, int, getDefaultObjectsRemoved); +PROXY(IPlayer, bool, getKickStatus); +PROXY(IPlayer, void, clearTasks, PlayerAnimationSyncType); +PROXY(IPlayer, void, allowWeapons, bool); +PROXY(IPlayer, bool, areWeaponsAllowed); +PROXY(IPlayer, void, allowTeleport, bool); +PROXY(IPlayer, bool, isTeleportAllowed); +PROXY(IPlayer, bool, isUsingOfficialClient); +PROXY_CAST(IPlayer, IEntity); + +PROXY(IPlayerPool, const FlatPtrHashSet&, entries); +PROXY(IPlayerPool, const FlatPtrHashSet&, players); +PROXY(IPlayerPool, const FlatPtrHashSet&, bots); +PROXY(IPlayerPool, bool, isNameTaken, StringView, const IPlayer*); +PROXY(IPlayerPool, void, sendClientMessageToAll, const Colour&, StringView); +PROXY(IPlayerPool, void, sendChatMessageToAll, IPlayer&, StringView); +PROXY(IPlayerPool, void, sendGameTextToAll, StringView, Milliseconds, int); +PROXY(IPlayerPool, void, hideGameTextForAll, int); +PROXY(IPlayerPool, void, sendDeathMessageToAll, IPlayer*, IPlayer&, int); +PROXY(IPlayerPool, void, sendEmptyDeathMessageToAll); +PROXY(IPlayerPool, void, createExplosionForAll, Vector3, int, float); +PROXY_PTR(IPlayerPool, NewConnectionPlayerPair, requestPlayer, const PeerNetworkData&, const PeerRequestParams&); +PROXY(IPlayerPool, void, broadcastPacket, Span, int, const IPlayer*, bool); +PROXY(IPlayerPool, void, broadcastRPC, int, Span, int, const IPlayer*, bool); +PROXY(IPlayerPool, bool, isNameValid, StringView); +PROXY(IPlayerPool, void, allowNickNameCharacter, char, bool); +PROXY(IPlayerPool, bool, isNickNameCharacterAllowed, char); +PROXY(IPlayerPool, Colour, getDefaultColour, int); +PROXY_CAST_NAMED(IPlayerPool, IPlayerPool, IReadOnlyPool, IReadOnlyPool); + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerSpawnEventHandler, getPlayerSpawnDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerSpawnEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerRequestSpawn, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerSpawn, IPlayer&) +PROXY_EVENT_HANDLER_END(PlayerSpawnEventHandler, onPlayerRequestSpawn, onPlayerSpawn) + + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerConnectEventHandler, getPlayerConnectDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerConnectEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onIncomingConnection, IPlayer&, StringView, unsigned short) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerConnect, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerDisconnect, IPlayer&, PeerDisconnectReason) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClientInit, IPlayer&) +PROXY_EVENT_HANDLER_END(PlayerConnectEventHandler, onIncomingConnection, onPlayerConnect, onPlayerDisconnect, + onPlayerClientInit) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerStreamEventHandler, getPlayerStreamDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerStreamEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerStreamIn, IPlayer&, IPlayer&) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerStreamOut, IPlayer&, IPlayer&) +PROXY_EVENT_HANDLER_END(PlayerStreamEventHandler, onPlayerStreamIn, onPlayerStreamOut) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerTextEventHandler, getPlayerTextDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerTextEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerText, IPlayer&, StringView) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerCommandText, IPlayer&, StringView) +PROXY_EVENT_HANDLER_END(PlayerTextEventHandler, onPlayerText, onPlayerCommandText) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerShotEventHandler, getPlayerShotDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerShotEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerShotMissed, IPlayer&, const PlayerBulletData&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerShotPlayer, IPlayer&, IPlayer&, const PlayerBulletData&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerShotVehicle, IPlayer&, IVehicle&, const PlayerBulletData&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerShotObject, IPlayer&, IObject&, const PlayerBulletData&) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerShotPlayerObject, IPlayer&, IPlayerObject&, const PlayerBulletData&) +PROXY_EVENT_HANDLER_END(PlayerShotEventHandler, onPlayerShotMissed, onPlayerShotPlayer, onPlayerShotVehicle, + onPlayerShotObject, onPlayerShotPlayerObject) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerChangeEventHandler, getPlayerChangeDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerChangeEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerScoreChange, IPlayer&, int) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerNameChange, IPlayer&, StringView) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerInteriorChange, IPlayer&, unsigned, unsigned) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerStateChange, IPlayer&, PlayerState, PlayerState) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerKeyStateChange, IPlayer&, uint32_t, uint32_t) +PROXY_EVENT_HANDLER_END(PlayerChangeEventHandler, onPlayerScoreChange, onPlayerNameChange, onPlayerInteriorChange, + onPlayerStateChange, onPlayerKeyStateChange) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerDamageEventHandler, getPlayerDamageDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerDamageEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerDeath, IPlayer&, IPlayer*, int) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerTakeDamage, IPlayer&, IPlayer*, float, unsigned, BodyPart) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerGiveDamage, IPlayer&, IPlayer&, float, unsigned, BodyPart) +PROXY_EVENT_HANDLER_END(PlayerDamageEventHandler, onPlayerDeath, onPlayerTakeDamage, onPlayerGiveDamage) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerClickEventHandler, getPlayerClickDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerClickEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClickMap, IPlayer&, Vector3) + PROXY_EVENT_HANDLER_EVENT(void, onPlayerClickPlayer, IPlayer&, IPlayer&, PlayerClickSource) +PROXY_EVENT_HANDLER_END(PlayerClickEventHandler, onPlayerClickMap, onPlayerClickPlayer) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerCheckEventHandler, getPlayerCheckDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerCheckEventHandler) + PROXY_EVENT_HANDLER_EVENT(void, onClientCheckResponse, IPlayer&, int, int, int) +PROXY_EVENT_HANDLER_END(PlayerCheckEventHandler, onClientCheckResponse) + +PROXY_EVENT_DISPATCHER(IPlayerPool, PlayerUpdateEventHandler, getPlayerUpdateDispatcher); +PROXY_EVENT_HANDLER_BEGIN(PlayerUpdateEventHandler) + PROXY_EVENT_HANDLER_EVENT(bool, onPlayerUpdate, IPlayer&, TimePoint) +PROXY_EVENT_HANDLER_END(PlayerUpdateEventHandler, onPlayerUpdate) + +PROXY_EVENT_DISPATCHER_NAMED(IPlayerPool, PoolEventHandler, PoolEventHandler, getPoolEventDispatcher); + +// include/pool +PROXY_CAST_NAMED(IPoolComponent, IPoolComponent, IPool, IPool); +PROXY_NAMED(IReadOnlyPool, IReadOnlyPool, void*, get, int); +PROXY_NAMED_PTR(IReadOnlyPool, IReadOnlyPool, SizePair, bounds); + +PROXY_NAMED(IPool, IPool, void, release, int); +PROXY_NAMED(IPool, IPool, void, lock, int); +PROXY_NAMED(IPool, IPool, bool, unlock, int); +PROXY_NAMED(IPool, IPool, IEventDispatcher>&, getPoolEventDispatcher); +PROXY_NAMED(IPool, IPool, size_t, count); + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#ifdef _MSC_VER +# pragma warning(pop) +#endif diff --git a/src/sampsharp-component/proxies/extension.cpp b/src/sampsharp-component/proxies/extension.cpp new file mode 100644 index 00000000..e535f216 --- /dev/null +++ b/src/sampsharp-component/proxies/extension.cpp @@ -0,0 +1,68 @@ + +#include + +#include "../proxy-api.hpp" + +typedef void(API_CALLTYPE * free_fn)(); + +class ManagedExtensionImpl final : IExtension +{ +private: + UID extensionID_; + free_fn free_fptr_; + void * handle_; + +public: + ManagedExtensionImpl(UID extensionID, free_fn free_fptr, void * handle) : + extensionID_(extensionID), + free_fptr_(free_fptr), + handle_(handle) { } + + UID getExtensionID() override { return extensionID_; } + + void freeExtension() override + { + free_fptr_(); + delete this; + } + + void * getHandle() {return handle_; } + + void reset() override { } +}; + +extern "C" SDK_EXPORT ManagedExtensionImpl * __CDECL ManagedExtensionImpl_create(UID a, free_fn b, void * c) +{ + return new ManagedExtensionImpl(a, b, c); +} + +extern "C" SDK_EXPORT void __CDECL ManagedExtensionImpl_delete(ManagedExtensionImpl * handler) +{ + delete handler; +} + +extern "C" SDK_EXPORT void * __CDECL ManagedExtensionImpl_getHandle(ManagedExtensionImpl * handler) +{ + return handler->getHandle(); +} + +struct workaround { + void * vptr; + FlatHashMap> miscExtensions; +}; + +extern "C" SDK_EXPORT IExtension * __CDECL IExtensible_getExtension_workaround(IExtensible * subject, UID extensionID) +{ + // workaround for the fact that the SDK doesn't expose the miscExtensions field + // ref: https://github.com/openmultiplayer/open.mp-sdk/issues/44 + workaround * w = (workaround *) subject; + + auto it = w->miscExtensions.find(extensionID); + if (it != w->miscExtensions.end()) + { + return it->second.first; + } + + return subject->getExtension(extensionID); +} + diff --git a/src/sampsharp-component/proxies/logger.cpp b/src/sampsharp-component/proxies/logger.cpp new file mode 100644 index 00000000..47123609 --- /dev/null +++ b/src/sampsharp-component/proxies/logger.cpp @@ -0,0 +1,25 @@ +#include + +extern "C" SDK_EXPORT void __CDECL ILogger_printLn(ILogger * subject, const char * msg) +{ + if (!subject) { return; } + return subject->printLn("%s", msg); +} + +extern "C" SDK_EXPORT void __CDECL ILogger_logLn(ILogger * subject, LogLevel level, const char * msg) +{ + if (!subject) { return; } + return subject->logLn(level, "%s", msg); +} + +extern "C" SDK_EXPORT void __CDECL ILogger_printLnU8(ILogger * subject, const char * msg) +{ + if (!subject) { return; } + return subject->printLnU8("%s", msg); +} + +extern "C" SDK_EXPORT void __CDECL ILogger_logLnU8(ILogger * subject, LogLevel level, const char * msg) +{ + if (!subject) { return; } + return subject->logLnU8(level, "%s", msg); +} diff --git a/src/sampsharp-component/proxies/pool-event-handler.cpp b/src/sampsharp-component/proxies/pool-event-handler.cpp new file mode 100644 index 00000000..53db3c73 --- /dev/null +++ b/src/sampsharp-component/proxies/pool-event-handler.cpp @@ -0,0 +1,36 @@ + +#include + +#include "../proxy-api.hpp" + +// PoolEventHandler +class PoolEventHandlerImpl final : PoolEventHandler +{ + typedef void(API_CALLTYPE * handle_fn)(void *&); + + handle_fn onPoolEntryCreated_; + handle_fn onPoolEntryDestroyed_; +public: + PoolEventHandlerImpl(void ** onPoolEntryCreated, void ** onPoolEntryDestroyed) : + onPoolEntryCreated_(reinterpret_cast(onPoolEntryCreated)), + onPoolEntryDestroyed_(reinterpret_cast(onPoolEntryDestroyed)) { } + + void onPoolEntryCreated(void *& entry) override + { + onPoolEntryCreated_(entry); + } + void onPoolEntryDestroyed(void *& entry) override + { + onPoolEntryDestroyed_(entry); + } +}; + +extern "C" SDK_EXPORT PoolEventHandlerImpl * __CDECL PoolEventHandlerImpl_create(void ** a, void ** b) +{ + return new PoolEventHandlerImpl(a, b); +} + +extern "C" SDK_EXPORT void __CDECL PoolEventHandlerImpl_delete(const PoolEventHandlerImpl * handler) +{ + delete handler; +} diff --git a/src/sampsharp-component/proxies/pool.cpp b/src/sampsharp-component/proxies/pool.cpp new file mode 100644 index 00000000..1361d389 --- /dev/null +++ b/src/sampsharp-component/proxies/pool.cpp @@ -0,0 +1,16 @@ +#include + +template +struct IPoolHack : public IPool { + const FlatPtrHashSet& exposeEntries() { + return this->entries(); // Accessing protected method + } +}; + +extern "C" SDK_EXPORT const void* __CDECL IPool_entries(IPool* pool) +{ + IPoolHack* hack = static_cast*>(pool); + const FlatPtrHashSet& entriesRef = hack->exposeEntries(); + + return static_cast(&entriesRef); +} \ No newline at end of file diff --git a/src/sampsharp-component/proxies/robin-hood.cpp b/src/sampsharp-component/proxies/robin-hood.cpp new file mode 100644 index 00000000..e941dc54 --- /dev/null +++ b/src/sampsharp-component/proxies/robin-hood.cpp @@ -0,0 +1,54 @@ +#include + +struct FlatHashSetPtr { void* a; void* b; }; + +extern "C" SDK_EXPORT FlatHashSetPtr __CDECL FlatPtrHashSet_begin(FlatPtrHashSet& set) +{ + auto it = set.begin(); + return *(FlatHashSetPtr*)⁢ +} + +extern "C" SDK_EXPORT FlatHashSetPtr __CDECL FlatPtrHashSet_end(FlatPtrHashSet& set) +{ + auto it = set.end(); + return *(FlatHashSetPtr*)⁢ +} + +extern "C" SDK_EXPORT FlatHashSetPtr __CDECL FlatPtrHashSet_inc(FlatPtrHashSet::iterator value) +{ + value++; + return *(FlatHashSetPtr*)&value; +} + +extern "C" SDK_EXPORT size_t __CDECL FlatPtrHashSet_size(FlatPtrHashSet& set) +{ + return set.size(); +} + +extern "C" SDK_EXPORT FlatHashSetPtr __CDECL FlatHashSetStringView_begin(FlatHashSet& set) +{ + auto it = set.begin(); + return *(FlatHashSetPtr*)⁢ +} + +extern "C" SDK_EXPORT struct FlatHashSetPtr __CDECL FlatHashSetStringView_end(FlatHashSet& set) +{ + auto it = set.end(); + return *(FlatHashSetPtr*)⁢ +} + +extern "C" SDK_EXPORT FlatHashSetPtr __CDECL FlatHashSetStringView_inc(FlatHashSet::iterator value) +{ + value++; + return *(FlatHashSetPtr*)&value; +} + +extern "C" SDK_EXPORT size_t __CDECL FlatHashSetStringView_size(FlatHashSet& set) +{ + return set.size(); +} + +extern "C" SDK_EXPORT void __CDECL FlatHashSetStringView_emplace(FlatHashSet& set, StringView value) +{ + set.emplace(value); +} \ No newline at end of file diff --git a/src/sampsharp-component/proxies/vehicles.cpp b/src/sampsharp-component/proxies/vehicles.cpp new file mode 100644 index 00000000..996f24b6 --- /dev/null +++ b/src/sampsharp-component/proxies/vehicles.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +extern "C" SDK_EXPORT bool __CDECL vehicles_isValidComponentForVehicleModel(int vehicleModel, int componentId) +{ + return Impl::isValidComponentForVehicleModel(vehicleModel, componentId); +} +extern "C" SDK_EXPORT bool __CDECL vehicles_getVehicleComponentSlot(int component) +{ + return Impl::getVehicleComponentSlot(component); +} +extern "C" SDK_EXPORT bool vehicles_getVehicleModelInfo(int model, VehicleModelInfoType type, Vector3& out) +{ + return Impl::getVehicleModelInfo(model, type, out); +} +extern "C" SDK_EXPORT void vehicles_getRandomVehicleColour(int modelid, int& colour1, int& colour2, int& colour3, int& colour4) +{ + return Impl::getRandomVehicleColour(modelid, colour1, colour2, colour3, colour4); +} +extern "C" SDK_EXPORT uint32_t vehicles_carColourIndexToColour(int index, uint32_t alpha = 0xFF) +{ + return Impl::carColourIndexToColour(index, alpha); +} + +extern "C" SDK_EXPORT uint8_t vehicles_getVehiclePassengerSeats(int model) +{ + return Impl::getVehiclePassengerSeats(model); +} \ No newline at end of file diff --git a/src/sampsharp-component/proxy-api.hpp b/src/sampsharp-component/proxy-api.hpp new file mode 100644 index 00000000..d41433e9 --- /dev/null +++ b/src/sampsharp-component/proxy-api.hpp @@ -0,0 +1,224 @@ +#pragma once + +#include +#include +#include + +#if defined(_WIN32) + #define API_CALLTYPE __stdcall +#else + #define API_CALLTYPE +#endif + +// +// Null-subject guard. Used by PROXY() macros to avoid dereferencing a null +// pointer when gamemode code calls a method on a destroyed/disconnected +// open.mp entity (IPlayer, IVehicle, etc.). Every proxy export goes through +// this helper when `subject == nullptr`. +// +// * void return -> return; +// * reference return -> terminate (cannot manufacture a reference). +// * anything default-constructible -> return T{} (e.g. 0, nullptr, {}). +// +// NOTE: This catches only literal null handles. Stale/freed pointers that +// the managed side cached before a disconnect are still UB; tracking live +// handles in a registry is a separate follow-up. +// +namespace sampsharp +{ + template + [[noreturn]] inline T proxy_default_unsupported(const char* name) + { + std::fprintf(stderr, + "sampsharp: PROXY call with null subject returning an unsupported type for '%s'. Aborting.\n", + name); + std::terminate(); + } + + template + inline T proxy_default(const char* name) + { + if constexpr (std::is_void_v) + { + (void)name; + return; + } + else if constexpr (std::is_reference_v) + { + proxy_default_unsupported(name); + } + else if constexpr (std::is_default_constructible_v) + { + (void)name; + return T{}; + } + else + { + proxy_default_unsupported(name); + } + } +} // namespace sampsharp + +// +// internal macros +// + +// expand variadic args as a numbered parameter list. e.g. _EXPAND_PARAM(a, b, X, Y) -> aXb _2, aYb _1 +#define _EXPAND_PARAM(prefix,postfix,...) _EXPAND_PARAM_N(__VA_ARGS__,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0)(prefix,postfix,##__VA_ARGS__) +#define _EXPAND_PARAM_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, n, ...) _EXPAND_PARAM ## n +#define _EXPAND_PARAM1(prefix, postfix, type, ...) prefix##type##postfix _1 +#define _EXPAND_PARAM2(prefix, postfix, type, ...) prefix##type##postfix _2, _EXPAND_PARAM1(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM3(prefix, postfix, type, ...) prefix##type##postfix _3, _EXPAND_PARAM2(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM4(prefix, postfix, type, ...) prefix##type##postfix _4, _EXPAND_PARAM3(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM5(prefix, postfix, type, ...) prefix##type##postfix _5, _EXPAND_PARAM4(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM6(prefix, postfix, type, ...) prefix##type##postfix _6, _EXPAND_PARAM5(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM7(prefix, postfix, type, ...) prefix##type##postfix _7, _EXPAND_PARAM6(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM8(prefix, postfix, type, ...) prefix##type##postfix _8, _EXPAND_PARAM7(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM9(prefix, postfix, type, ...) prefix##type##postfix _9, _EXPAND_PARAM8(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM10(prefix, postfix, type, ...) prefix##type##postfix _10, _EXPAND_PARAM10(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM11(prefix, postfix, type, ...) prefix##type##postfix _11, _EXPAND_PARAM11(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM12(prefix, postfix, type, ...) prefix##type##postfix _12, _EXPAND_PARAM12(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM13(prefix, postfix, type, ...) prefix##type##postfix _13, _EXPAND_PARAM13(prefix, postfix, ##__VA_ARGS__) +#define _EXPAND_PARAM14(prefix, postfix, type, ...) prefix##type##postfix _14, _EXPAND_PARAM14(prefix, postfix, ##__VA_ARGS__) + +// expand variadic args as a numbered argument list. e.g. _EXPAND_ARG(,X, Y) -> _2, _1 +#define _EXPAND_ARG(prefix, ...) _EXPAND_ARG_N(__VA_ARGS__,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0)(prefix,##__VA_ARGS__) +#define _EXPAND_ARG_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, n, ...) _EXPAND_ARG ## n +#define _EXPAND_ARG1(prefix,type, ...) prefix _1 +#define _EXPAND_ARG2(prefix,type, ...) prefix _2, _EXPAND_ARG1(prefix,##__VA_ARGS__) +#define _EXPAND_ARG3(prefix,type, ...) prefix _3, _EXPAND_ARG2(prefix,##__VA_ARGS__) +#define _EXPAND_ARG4(prefix,type, ...) prefix _4, _EXPAND_ARG3(prefix,##__VA_ARGS__) +#define _EXPAND_ARG5(prefix,type, ...) prefix _5, _EXPAND_ARG4(prefix,##__VA_ARGS__) +#define _EXPAND_ARG6(prefix,type, ...) prefix _6, _EXPAND_ARG5(prefix,##__VA_ARGS__) +#define _EXPAND_ARG7(prefix,type, ...) prefix _7, _EXPAND_ARG6(prefix,##__VA_ARGS__) +#define _EXPAND_ARG8(prefix,type, ...) prefix _8, _EXPAND_ARG7(prefix,##__VA_ARGS__) +#define _EXPAND_ARG9(prefix,type, ...) prefix _9, _EXPAND_ARG8(prefix,##__VA_ARGS__) +#define _EXPAND_ARG10(prefix,type, ...) prefix _10, _EXPAND_ARG9(prefix,##__VA_ARGS__) +#define _EXPAND_ARG11(prefix,type, ...) prefix _11, _EXPAND_ARG10(prefix,##__VA_ARGS__) +#define _EXPAND_ARG12(prefix,type, ...) prefix _12, _EXPAND_ARG11(prefix,##__VA_ARGS__) +#define _EXPAND_ARG13(prefix,type, ...) prefix _13, _EXPAND_ARG12(prefix,##__VA_ARGS__) +#define _EXPAND_ARG14(prefix,type, ...) prefix _14, _EXPAND_ARG13(prefix,##__VA_ARGS__) + +/// expand variadic args as a numbered initializer list. e.g. _EXPAND_INIT(X, Y) -> X_(_2), Y_(_1) +#define _EXPAND_INIT(...) _EXPAND_INIT_N(__VA_ARGS__,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0)(__VA_ARGS__) +#define _EXPAND_INIT_N(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, n, ...) _EXPAND_INIT ## n +#define _EXPAND_INIT1(type, ...) type##_(_1) +#define _EXPAND_INIT2(type, ...) type##_(_2), _EXPAND_INIT1(__VA_ARGS__) +#define _EXPAND_INIT3(type, ...) type##_(_3), _EXPAND_INIT2(__VA_ARGS__) +#define _EXPAND_INIT4(type, ...) type##_(_4), _EXPAND_INIT3(__VA_ARGS__) +#define _EXPAND_INIT5(type, ...) type##_(_5), _EXPAND_INIT4(__VA_ARGS__) +#define _EXPAND_INIT6(type, ...) type##_(_6), _EXPAND_INIT5(__VA_ARGS__) +#define _EXPAND_INIT7(type, ...) type##_(_7), _EXPAND_INIT6(__VA_ARGS__) +#define _EXPAND_INIT8(type, ...) type##_(_8), _EXPAND_INIT7(__VA_ARGS__) +#define _EXPAND_INIT9(type, ...) type##_(_9), _EXPAND_INIT8(__VA_ARGS__) +#define _EXPAND_INIT10(type, ...) type##_(_10), _EXPAND_INIT9(__VA_ARGS__) +#define _EXPAND_INIT11(type, ...) type##_(_11), _EXPAND_INIT10(__VA_ARGS__) +#define _EXPAND_INIT12(type, ...) type##_(_12), _EXPAND_INIT11(__VA_ARGS__) +#define _EXPAND_INIT13(type, ...) type##_(_13), _EXPAND_INIT12(__VA_ARGS__) +#define _EXPAND_INIT14(type, ...) type##_(_14), _EXPAND_INIT13(__VA_ARGS__) + +#define __PROXY_IMPL(type_subject, type_return, method, proxy_name, ...) \ + extern "C" SDK_EXPORT type_return __CDECL \ + proxy_name(type_subject * subject __VA_OPT__(, _EXPAND_PARAM(,,##__VA_ARGS__))) \ + { \ + if (!subject) { return ::sampsharp::proxy_default(#proxy_name); } \ + return subject -> method ( \ + __VA_OPT__(_EXPAND_ARG(,##__VA_ARGS__)) \ + ); \ + } + +#define __PROXY_IMPL_RESULT_PTR(type_subject, type_return, method, proxy_name, ...) \ + extern "C" SDK_EXPORT void __CDECL \ + proxy_name(type_subject * subject __VA_OPT__(, _EXPAND_PARAM(,,__VA_ARGS__)), type_return * result) \ + { \ + if (!subject) { if (result) { *result = type_return{}; } return; } \ + *result = subject -> method ( \ + __VA_OPT__(_EXPAND_ARG(,##__VA_ARGS__)) \ + ); \ + } + +// +// macros for definition of exported proxy functions +// + + +#define __PROXY_CAST_NAMED(type_from, type_from_name, type_to, type_to_name) \ + extern "C" SDK_EXPORT type_to * __CDECL \ + cast_##type_from_name##_to_##type_to_name(type_from * from) \ + { \ + return static_cast(from); \ + } + +/// proxy function for casting from one type to another and vice versa e.g. PROXY_CAST_NAMED(IFoo, foo, IBar, bar) -> IBar * cast_foo_to_bar(IFoo * from) { return static_cast(from); } +#define PROXY_CAST_NAMED(type_from, type_from_name, type_to, type_to_name) \ + __PROXY_CAST_NAMED(type_from, type_from_name, type_to, type_to_name); \ + __PROXY_CAST_NAMED(type_to, type_to_name, type_from, type_from_name) + +/// proxy function for casting from one type to another e.g. PROXY_CAST(IFoo, IBar) -> IBar * cast_IFoo_to_IBar(IFoo * from) { return static_cast(from); } +#define PROXY_CAST(type_from, type_to) PROXY_CAST_NAMED(type_from, type_from, type_to, type_to) + +/// proxy function macro. e.g. PROXY(subj, int, foo, bool) -> int subj_foo(subj * x, bool _1) { return x->foo(_1); } +#define PROXY(type_subject, type_return, method, ...) __PROXY_IMPL(type_subject, type_return, method, type_subject##_##method, __VA_ARGS__) + +/// proxy function macro. e.g. PROXY_RESULT_PTR(subj, int, foo, bool) -> void subj_foo(subj * x, bool _1, int * result) { *result = x->foo(_1); } +#define PROXY_PTR(type_subject, type_return, method, ...) __PROXY_IMPL_RESULT_PTR(type_subject, type_return, method, type_subject##_##method, __VA_ARGS__) + +/// proxy function macro for an overload. output is similar to PROXY macro, except the function name is post-fixed by overload argument +#define PROXY_OVERLOAD(type_subject, type_return, method, overload, ...) __PROXY_IMPL(type_subject, type_return, method, type_subject##_##method##overload, __VA_ARGS__) + +/// proxy function macro for an overload. output is similar to PROXY_PTR macro, except the function name is post-fixed by overload argument +#define PROXY_OVERLOAD_PTR(type_subject, type_return, method, overload, ...) __PROXY_IMPL_RESULT_PTR(type_subject, type_return, method, type_subject##_##method##overload, __VA_ARGS__) + +/// proxy function macro. e.g. PROXY(subj, int, foo, bool) -> int subj_foo(subj * x, bool _1) { return x->foo(_1); } +#define PROXY_NAMED(type_subject, name_subject, type_return, method, ...) __PROXY_IMPL(type_subject, type_return, method, name_subject##_##method, __VA_ARGS__) + +/// proxy function macro. e.g. PROXY_RESULT_PTR(subj, int, foo, bool) -> void subj_foo(subj * x, bool _1, int * result) { *result = x->foo(_1); } +#define PROXY_NAMED_PTR(type_subject, name_subject, type_return, method, ...) __PROXY_IMPL_RESULT_PTR(type_subject, type_return, method, name_subject##_##method, __VA_ARGS__) + +/// proxy function macro for an overload. output is similar to PROXY macro, except the function name is post-fixed by overload argument +#define PROXY_NAMED_OVERLOAD(type_subject, name_subject, type_return, method, overload, ...) __PROXY_IMPL(type_subject, type_return, method, name_subject##_##method##overload, __VA_ARGS__) + +/// proxy function macro for an overload. output is similar to PROXY_PTR macro, except the function name is post-fixed by overload argument +#define PROXY_NAMED_OVERLOAD_PTR(type_subject, name_subject, type_return, method, overload, ...) __PROXY_IMPL_RESULT_PTR(type_subject, type_return, method, name_subject##_##method##overload, __VA_ARGS__) + +/// proxy for event dispatcher functions and function to get the event dispatcher +#define PROXY_EVENT_DISPATCHER(type_subject, type_handler, method) \ + PROXY(type_subject, IEventDispatcher&, method); + +/// proxy for event dispatcher functions and function to get the event dispatcher wit specific handler name +#define PROXY_EVENT_DISPATCHER_NAMED(type_subject, handler_type, handler_name, method) \ + PROXY(type_subject, IEventDispatcher&, method); + +/// proxy for event dispatcher functions and function to get the event dispatcher +#define PROXY_INDEXED_EVENT_DISPATCHER(type_subject, type_handler, method) \ + PROXY(type_subject, IIndexedEventDispatcher&, method); + +/// start of event handler proxy class +#define PROXY_EVENT_HANDLER_BEGIN(handler_type) \ + class handler_type##Impl final : handler_type { + +/// end of event handler proxy class + functions for creating/destroying proxy +#define PROXY_EVENT_HANDLER_END(handler_type, ...) \ + public: \ + handler_type##Impl(_EXPAND_ARG(void**, __VA_ARGS__)) : \ + _EXPAND_INIT(__VA_ARGS__) { } \ + }; \ + extern "C" SDK_EXPORT handler_type##Impl* __CDECL handler_type##Impl_create(_EXPAND_ARG(void**, __VA_ARGS__)) \ + { \ + return new handler_type##Impl(_EXPAND_ARG(,##__VA_ARGS__)); \ + } \ + extern "C" SDK_EXPORT void __CDECL handler_type##Impl_delete(handler_type##Impl* handler) \ + { \ + delete handler; \ + } + +/// event handler function in event handler proxy class +#define PROXY_EVENT_HANDLER_EVENT(type_return, name, ...) \ + private: \ + typedef type_return(API_CALLTYPE * name##_fn)(_EXPAND_PARAM(, , __VA_ARGS__)); \ + void** name##_ = nullptr; \ + public: \ + type_return name(_EXPAND_PARAM(, , __VA_ARGS__)) override \ + { \ + return ((name##_fn)name##_)(_EXPAND_ARG(,##__VA_ARGS__)); \ + } diff --git a/src/sampsharp-component/publish.cmd b/src/sampsharp-component/publish.cmd new file mode 100644 index 00000000..7b0a6f90 --- /dev/null +++ b/src/sampsharp-component/publish.cmd @@ -0,0 +1,33 @@ +@echo off +REM Publish script for SampSharp component - copies release DLL to artifacts + +setlocal enabledelayedexpansion + +pushd "%~dp0" + +cd /d "..\.." +set "ROOTDIR=%CD%" +set "BUILDDIR=%ROOTDIR%\build\cmake\component" +set "ARTIFACTDIR=%ROOTDIR%\build\artifacts\sampsharp-component" + +popd + +echo Publishing SampSharp component... +echo Source: %BUILDDIR%\artifacts +echo Destination: %ARTIFACTDIR% +echo. + +if not exist "%BUILDDIR%\artifacts\SampSharp.dll" ( + echo Error: SampSharp.dll not found at %BUILDDIR%\artifacts + echo Please run build first: build.cmd + exit /b 1 +) + +if not exist "%ARTIFACTDIR%" mkdir "%ARTIFACTDIR%" + +echo Copying files... +copy "%BUILDDIR%\artifacts\SampSharp.dll" "%ARTIFACTDIR%" /y +copy "%BUILDDIR%\artifacts\SampSharp.pdb" "%ARTIFACTDIR%" /y 2>nul + +echo. +echo Artifacts published to: %ARTIFACTDIR% diff --git a/src/sampsharp-component/publish.sh b/src/sampsharp-component/publish.sh new file mode 100755 index 00000000..74fe4bf6 --- /dev/null +++ b/src/sampsharp-component/publish.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Publish script for SampSharp component - copies release library to artifacts + +set -e + +SRCDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOTDIR="$(cd "$SRCDIR/../.." && pwd)" +BUILDDIR="$ROOTDIR/build/cmake/component" +ARTIFACTDIR="$ROOTDIR/build/artifacts/sampsharp-component" + +echo "Publishing SampSharp component..." +echo "Source: $BUILDDIR/artifacts" +echo "Destination: $ARTIFACTDIR" +echo + +if [ ! -f "$BUILDDIR/artifacts/SampSharp.so" ] && [ ! -f "$BUILDDIR/artifacts/SampSharp.dll" ]; then + echo "Error: SampSharp.so or SampSharp.dll not found at $BUILDDIR/artifacts" + echo "Please run build first: build.sh" + exit 1 +fi + +mkdir -p "$ARTIFACTDIR" + +echo "Copying files..." +if [ -f "$BUILDDIR/artifacts/SampSharp.so" ]; then + cp "$BUILDDIR/artifacts/SampSharp.so" "$ARTIFACTDIR/" +fi +if [ -f "$BUILDDIR/artifacts/SampSharp.dll" ]; then + cp "$BUILDDIR/artifacts/SampSharp.dll" "$ARTIFACTDIR/" || true + cp "$BUILDDIR/artifacts/SampSharp.pdb" "$ARTIFACTDIR/" 2>/dev/null || true +fi + +echo +echo "Artifacts published to: $ARTIFACTDIR" diff --git a/src/sampsharp-component/sampsharp-component.cpp b/src/sampsharp-component/sampsharp-component.cpp new file mode 100644 index 00000000..f1f84945 --- /dev/null +++ b/src/sampsharp-component/sampsharp-component.cpp @@ -0,0 +1,125 @@ +#include "sampsharp-component.hpp" +#include "crash-handler.hpp" +#include "version.hpp" + +#define CFG_DIRECTORY "sampsharp.directory" +#define CFG_ASSEMBLY "sampsharp.assembly" +#define CFG_ENTRY_POINT_TYPE "sampsharp.entry_point_type" +#define CFG_ENTRY_POINT_METHOD "sampsharp.entry_point_method" +#define CFG_CLEANUP_METHOD "sampsharp.cleanup_method" +#define CFG_DISABLE_CRASH_HANDLER "sampsharp.disable_crash_handler" + +StringView SampSharpComponent::componentName() const +{ + return "SampSharp"; +} + +SemanticVersion SampSharpComponent::componentVersion() const +{ + return { VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_BUILD }; +} + +void SampSharpComponent::onLoad(ICore* c) +{ + core_ = c; + + bool disableCrashHandler = *c->getConfig().getBool(CFG_DISABLE_CRASH_HANDLER); + if(!disableCrashHandler) { + sampsharp::crash::install(c); + } +} + +void SampSharpComponent::provideConfiguration(ILogger& logger, IEarlyConfig& config, const bool defaults) +{ + #define initConfigString(key, value) \ + if(defaults || config.getType(key) == ConfigOptionType_None) { \ + config.setString(key, value); \ + } + + initConfigString(CFG_DIRECTORY, "gamemode"); + initConfigString(CFG_ASSEMBLY, "GameMode"); + initConfigString(CFG_ENTRY_POINT_TYPE, "SampSharp.Entrypoint"); + initConfigString(CFG_ENTRY_POINT_METHOD, "Initialize"); + initConfigString(CFG_CLEANUP_METHOD, "Cleanup"); + + if(defaults || config.getType(CFG_DISABLE_CRASH_HANDLER) == ConfigOptionType_None) { + config.setBool(CFG_DISABLE_CRASH_HANDLER, false); + } +} + +void SampSharpComponent::onInit(IComponentList* components) +{ + const IConfig& config = core_->getConfig(); + + const auto directory = config.getString(CFG_DIRECTORY); + const auto assembly = config.getString(CFG_ASSEMBLY); + const auto entry_point_type = config.getString(CFG_ENTRY_POINT_TYPE); + const auto entry_point_method = config.getString(CFG_ENTRY_POINT_METHOD); + const auto cleanup_method = config.getString(CFG_CLEANUP_METHOD); + + std::string entry_point = entry_point_type.to_string() + ", " + assembly.to_string(); + const auto full_entry_point = StringView(entry_point); + + const char * error = nullptr; + + if(!managed_host_.initialize(&error)) + { + core_->logLn(Error, "Failed to initialize the .NET host framework resolver. Has the .NET runtime been installed?"); + core_->logLn(Error, "Error message: %s", error); + return; + } + + if(!managed_host_.loadFor(directory, assembly, &error)) + { + core_->logLn(Error, "Failed to initialize the .NET runtime for '%s/%s'. Is the '*.runtimeconfig.json' file available? Is the .NET runtime available?", directory.to_string().c_str(), assembly.to_string().c_str()); + core_->logLn(Error, "Error message: %s", error); + return; + } + + on_init_fn on_init; + if(!managed_host_.getEntryPoint(full_entry_point, entry_point_method, reinterpret_cast(&on_init), &error)) + { + core_->logLn(Error, "The entrypoint '%s.%s, %s' could not be found.", entry_point_type.to_string().c_str(), entry_point_method.to_string().c_str(), assembly.to_string().c_str()); + core_->logLn(Error, "Error message: %s", error); + return; + } + + if(!managed_host_.getEntryPoint(full_entry_point, cleanup_method, reinterpret_cast(&on_cleanup_), &error)) + { + core_->logLn(Error, "The entrypoint '%s.%s, %s' could not be found.", entry_point_type.to_string().c_str(), cleanup_method.to_string().c_str(), assembly.to_string().c_str()); + core_->logLn(Error, "Error message: %s", error); + return; + } + + SampSharpInfo info { VERSION_API, componentVersion() }; + SampSharpInitParams init { core_, components, &info }; + + on_init(init); +} + +void SampSharpComponent::onReady() +{ +} + +void SampSharpComponent::free() +{ + if (on_cleanup_) + { + on_cleanup_(); + } + + delete this; +} + +void SampSharpComponent::reset() +{ +} + +SampSharpComponent* SampSharpComponent::getInstance() +{ + if (instance_ == nullptr) + { + instance_ = new SampSharpComponent(); + } + return instance_; +} diff --git a/src/sampsharp-component/sampsharp-component.hpp b/src/sampsharp-component/sampsharp-component.hpp new file mode 100644 index 00000000..c0dc7401 --- /dev/null +++ b/src/sampsharp-component/sampsharp-component.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include "managed-host.hpp" + +struct SampSharpInfo +{ + SampSharpInfo(int api_version, SemanticVersion version) : + size(sizeof(SampSharpInfo)), + api_version(api_version), + version(version) { } + + // sizeof(SampSharpInfo) for backwards compatibility + size_t size; + // version of SampSharp component <> hosted API. Version mismatch will cause launch failure. + int api_version; + // version of the SampSharp component + SemanticVersion version; +}; + + +struct SampSharpInitParams +{ + SampSharpInitParams(ICore * core, IComponentList * componentList, SampSharpInfo * info) : + size(sizeof(SampSharpInitParams)), + info(info), + core(core), + componentList(componentList) { } + + // sizeof(SampSharpInitParams) for backwards compatibility + size_t size; + SampSharpInfo * info; + ICore * core; + IComponentList * componentList; +}; + +typedef void (CORECLR_DELEGATE_CALLTYPE *on_init_fn)(SampSharpInitParams); +typedef void (CORECLR_DELEGATE_CALLTYPE *on_cleanup_fn)(); + +struct ISampSharpComponent : IComponent +{ + PROVIDE_UID(0x0B61929D1E94A319); +}; + +class SampSharpComponent final + : public ISampSharpComponent +{ +private: + ICore * core_ = nullptr; + ManagedHost managed_host_ {}; + inline static SampSharpComponent * instance_ = nullptr; + on_cleanup_fn on_cleanup_ = nullptr; + +public: + StringView componentName() const override; + + SemanticVersion componentVersion() const override; + + void onLoad(ICore * c) override; + + void provideConfiguration(ILogger & logger, IEarlyConfig & config, bool defaults) override; + + void onInit(IComponentList * components) override; + + void onReady() override; + + void free() override; + + void reset() override; + + static SampSharpComponent * getInstance(); +}; diff --git a/src/sampsharp-component/version.hpp b/src/sampsharp-component/version.hpp new file mode 100644 index 00000000..0812e09f --- /dev/null +++ b/src/sampsharp-component/version.hpp @@ -0,0 +1,7 @@ +#pragma once + +#define VERSION_API 1 +#define VERSION_MAJOR 0 +#define VERSION_MINOR 11 +#define VERSION_PATCH 0 +#define VERSION_BUILD 0 \ No newline at end of file