diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..33cb509e --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.project +.classpath +.idea/ +*.iml +target/ +logs/ +log/ +test-output/ +hexid.txt + +.vscode/ \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..eebc3c18 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..e364bfe9 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,4 @@ +wrapperVersion=3.3.4 +distributionType=script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..04cfa2c1 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,6 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding//src/test/java=UTF-8 +encoding//src/test/resources=UTF-8 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.apt.core.prefs b/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 00000000..8963047b --- /dev/null +++ b/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=target/generated-sources/annotations +org.eclipse.jdt.apt.genTestSrcDir=target/generated-test-sources/test-annotations diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..3a5c6da3 --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,525 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.notowning=org.eclipse.jdt.annotation.NotOwning +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.annotation.owning=org.eclipse.jdt.annotation.Owning +org.eclipse.jdt.core.compiler.annotation.resourceanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=25 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=25 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompatibleOwningContract=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.insufficientResourceAnalysis=warning +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=warning +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=warning +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=warning +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLambdaParameter=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=25 +org.eclipse.jdt.core.formatter.align_arrows_in_switch_on_columns=false +org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false +org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 +org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line=true +org.eclipse.jdt.core.formatter.align_type_members_on_columns=false +org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false +org.eclipse.jdt.core.formatter.align_with_spaces=false +org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=48 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=0 +org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=0 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 +org.eclipse.jdt.core.formatter.alignment_for_assignment=4 +org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_compact_if=0 +org.eclipse.jdt.core.formatter.alignment_for_compact_loops=16 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=0 +org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain=0 +org.eclipse.jdt.core.formatter.alignment_for_enum_constants=49 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=49 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon=0 +org.eclipse.jdt.core.formatter.alignment_for_logical_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_module_statements=16 +org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16 +org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_record_components=0 +org.eclipse.jdt.core.formatter.alignment_for_relational_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_resources_in_try=85 +org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=84 +org.eclipse.jdt.core.formatter.alignment_for_shift_operator=0 +org.eclipse.jdt.core.formatter.alignment_for_string_concatenation=16 +org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=48 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=0 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 +org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 +org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 +org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 +org.eclipse.jdt.core.formatter.blank_lines_after_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_after_package=1 +org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_field=0 +org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0 +org.eclipse.jdt.core.formatter.blank_lines_before_imports=1 +org.eclipse.jdt.core.formatter.blank_lines_before_member_type=1 +org.eclipse.jdt.core.formatter.blank_lines_before_method=1 +org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1 +org.eclipse.jdt.core.formatter.blank_lines_before_package=0 +org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1 +org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch=0 +org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=1 +org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_block_in_case_after_arrow=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_lambda_body=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_constructor=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_record_declaration=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line +org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line +org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped=false +org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions=false +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=true +org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=true +org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position=false +org.eclipse.jdt.core.formatter.comment.format_block_comments=true +org.eclipse.jdt.core.formatter.comment.format_header=false +org.eclipse.jdt.core.formatter.comment.format_html=true +org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true +org.eclipse.jdt.core.formatter.comment.format_line_comments=true +org.eclipse.jdt.core.formatter.comment.format_source_code=true +org.eclipse.jdt.core.formatter.comment.indent_parameter_description=true +org.eclipse.jdt.core.formatter.comment.indent_root_tags=true +org.eclipse.jdt.core.formatter.comment.indent_tag_description=false +org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags=do not insert +org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert +org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags=false +org.eclipse.jdt.core.formatter.comment.line_length=300 +org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries=true +org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true +org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false +org.eclipse.jdt.core.formatter.compact_else_if=true +org.eclipse.jdt.core.formatter.continuation_indentation=1 +org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=1 +org.eclipse.jdt.core.formatter.disabling_tag=@formatter\:off +org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on +org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false +org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header=true +org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true +org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_empty_lines=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true +org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true +org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true +org.eclipse.jdt.core.formatter.indentation.size=4 +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_label=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert +org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert +org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert +org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert +org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_after_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_not_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_after_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources=insert +org.eclipse.jdt.core.formatter.insert_space_after_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_additive_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case=insert +org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default=insert +org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow=insert +org.eclipse.jdt.core.formatter.insert_space_before_logical_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try=insert +org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert +org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert +org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert +org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_relational_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources=do not insert +org.eclipse.jdt.core.formatter.insert_space_before_shift_operator=insert +org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation=insert +org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert +org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert +org.eclipse.jdt.core.formatter.join_line_comments=false +org.eclipse.jdt.core.formatter.join_lines_in_comments=true +org.eclipse.jdt.core.formatter.join_wrapped_lines=false +org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_code_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=true +org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=false +org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_method_body_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line=false +org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line=false +org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false +org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line=one_line_never +org.eclipse.jdt.core.formatter.lineSplit=320 +org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false +org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false +org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body=0 +org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block=0 +org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=1 +org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement=common_lines +org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause=common_lines +org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false +org.eclipse.jdt.core.formatter.tabulation.char=tab +org.eclipse.jdt.core.formatter.tabulation.size=4 +org.eclipse.jdt.core.formatter.text_block_indentation=0 +org.eclipse.jdt.core.formatter.use_on_off_tags=true +org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false +org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false +org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true +org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true +org.eclipse.jdt.core.formatter.wrap_before_logical_operator=true +org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator=true +org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true +org.eclipse.jdt.core.formatter.wrap_before_relational_operator=true +org.eclipse.jdt.core.formatter.wrap_before_shift_operator=true +org.eclipse.jdt.core.formatter.wrap_before_string_concatenation=true +org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator=false +org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true +org.eclipse.jdt.core.javaFormatter=org.eclipse.jdt.core.defaultJavaFormatter diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..6a5a2714 --- /dev/null +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,147 @@ +cleanup.add_all=true +cleanup.add_default_serial_version_id=false +cleanup.add_generated_serial_version_id=true +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=true +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=true +cleanup.also_simplify_lambda=true +cleanup.always_use_blocks=true +cleanup.always_use_parentheses_in_expressions=true +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.array_with_curly=false +cleanup.arrays_fill=false +cleanup.bitwise_conditional_expression=false +cleanup.boolean_literal=true +cleanup.boolean_value_rather_than_comparison=true +cleanup.break_loop=false +cleanup.collection_cloning=false +cleanup.comparing_on_criteria=false +cleanup.comparison_statement=true +cleanup.controlflow_merge=false +cleanup.convert_functional_interfaces=true +cleanup.convert_to_enhanced_for_loop=true +cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true +cleanup.convert_to_switch_expressions=true +cleanup.correct_indentation=false +cleanup.do_while_rather_than_while=true +cleanup.double_negation=true +cleanup.else_if=false +cleanup.embedded_if=false +cleanup.evaluate_nullable=false +cleanup.extract_increment=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.hash=false +cleanup.if_condition=true +cleanup.insert_inferred_type_arguments=false +cleanup.instanceof=true +cleanup.instanceof_keyword=true +cleanup.invert_equals=false +cleanup.join=true +cleanup.lazy_logical_operator=true +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=true +cleanup.map_cloning=false +cleanup.merge_conditional_blocks=false +cleanup.multi_catch=false +cleanup.never_use_blocks=false +cleanup.never_use_parentheses_in_expressions=false +cleanup.no_string_creation=true +cleanup.no_super=true +cleanup.number_suffix=false +cleanup.objects_equals=false +cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false +cleanup.operand_factorization=false +cleanup.organize_imports=true +cleanup.overridden_assignment=false +cleanup.overridden_assignment_move_decl=true +cleanup.plain_replacement=true +cleanup.precompile_regex=true +cleanup.primitive_comparison=true +cleanup.primitive_parsing=true +cleanup.primitive_rather_than_wrapper=true +cleanup.primitive_serialization=true +cleanup.pull_out_if_from_if_else=true +cleanup.pull_up_assignment=false +cleanup.push_down_negation=false +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.reduce_indentation=false +cleanup.redundant_comparator=true +cleanup.redundant_falling_through_block_end=false +cleanup.remove_private_constructors=true +cleanup.remove_redundant_modifiers=true +cleanup.remove_redundant_semicolons=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=false +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=true +cleanup.remove_unnecessary_array_creation=false +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=true +cleanup.remove_unused_method_parameters=false +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=true +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.return_expression=false +cleanup.simplify_lambda_expression_and_method_ref=true +cleanup.single_used_field=false +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.standard_comparison=false +cleanup.static_inner_class=true +cleanup.strictly_equal_or_different=false +cleanup.stringbuffer_to_stringbuilder=true +cleanup.stringbuilder=true +cleanup.stringbuilder_for_local_vars=true +cleanup.stringconcat_stringbuffer_stringbuilder=false +cleanup.stringconcat_to_textblock=false +cleanup.substring=true +cleanup.switch=false +cleanup.system_property=false +cleanup.system_property_boolean=false +cleanup.system_property_file_encoding=false +cleanup.system_property_file_separator=false +cleanup.system_property_line_separator=false +cleanup.system_property_path_separator=false +cleanup.ternary_operator=false +cleanup.try_with_resource=false +cleanup.unlooped_while=false +cleanup.unreachable_block=false +cleanup.use_anonymous_class_creation=false +cleanup.use_autoboxing=false +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_directly_map_method=true +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=true +cleanup.use_string_is_blank=true +cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=true +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_unboxing=false +cleanup.use_var=false +cleanup.useless_continue=false +cleanup.useless_return=false +cleanup.valueof_rather_than_instantiation=true +cleanup_profile=_L2J Clean Up v0.5 +cleanup_settings_version=2 +eclipse.preferences.version=1 +formatter_profile=_L2J Formatter v0.9 +formatter_settings_version=23 +org.eclipse.jdt.ui.text.custom_code_templates= diff --git a/.settings/org.eclipse.m2e.core.prefs b/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..61b142f0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,636 @@ +# GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 [Free Software Foundation, Inc.](http://fsf.org/) + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +## Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most +of our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can do +these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify it: +responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must pass on to the recipients the same freedoms that you received. +You must make sure that they, too, receive or can get the source code. And you +must show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: + + 1. assert copyright on the software, and + 2. offer you this License giving you legal permission to copy, distribute + and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that +there is no warranty for this free software. For both users' and authors' sake, +the GPL requires that modified versions be marked as changed, so that their +problems will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. This +is fundamentally incompatible with the aim of protecting users' freedom to +change the software. The systematic pattern of such abuse occurs in the area of +products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on +general-purpose computers, but in those that do, we wish to avoid the special +danger that patents applied to a free program could make it effectively +proprietary. To prevent this, the GPL assures that patents cannot be used to +render the program non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + +## TERMS AND CONDITIONS + +### 0. Definitions. + +*This License* refers to version 3 of the GNU General Public License. + +*Copyright* also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +*The Program* refers to any copyrightable work licensed under this License. +Each licensee is addressed as *you*. *Licensees* and *recipients* may be +individuals or organizations. + +To *modify* a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a *modified version* of the earlier work or a work +*based on* the earlier work. + +A *covered work* means either the unmodified Program or a work based on the +Program. + +To *propagate* a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To *convey* a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer +network, with no transfer of a copy, is not conveying. + +An interactive user interface displays *Appropriate Legal Notices* to the +extent that it includes a convenient and prominently visible feature that + + 1. displays an appropriate copyright notice, and + 2. tells the user that there is no warranty for the work (except to the + extent that warranties are provided), that licensees may convey the work + under this License, and how to view a copy of this License. + +If the interface presents a list of user commands or options, such as a menu, a +prominent item in the list meets this criterion. + +### 1. Source Code. + +The *source code* for a work means the preferred form of the work for making +modifications to it. *Object code* means any non-source form of a work. + +A *Standard Interface* means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The *System Libraries* of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only +to enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A *Major Component*, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The *Corresponding Source* for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as +by intimate data communication or control flow between those subprograms and +other parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +### 2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes it +unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure +under any applicable law fulfilling obligations under article 11 of the WIPO +copyright treaty adopted on 20 December 1996, or similar laws prohibiting or +restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on +each copy an appropriate copyright notice; keep intact all notices stating that +this License and any non-permissive terms added in accord with section 7 apply +to the code; keep intact all notices of the absence of any warranty; and give +all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + + - a) The work must carry prominent notices stating that you modified it, and + giving a relevant date. + - b) The work must carry prominent notices stating that it is released under + this License and any conditions added under section 7. This requirement + modifies the requirement in section 4 to *keep intact all notices*. + - c) You must license the entire work, as a whole, under this License to + anyone who comes into possession of a copy. This License will therefore + apply, along with any applicable section 7 additional terms, to the whole + of the work, and all its parts, regardless of how they are packaged. This + License gives no permission to license the work in any other way, but it + does not invalidate such permission if you have separately received it. + - d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your work need + not make them do so. + +A compilation of a covered work with other separate and independent works, +which are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an *aggregate* if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + + - a) Convey the object code in, or embodied in, a physical product (including + a physical distribution medium), accompanied by the Corresponding Source + fixed on a durable physical medium customarily used for software + interchange. + - b) Convey the object code in, or embodied in, a physical product (including + a physical distribution medium), accompanied by a written offer, valid for + at least three years and valid for as long as you offer spare parts or + customer support for that product model, to give anyone who possesses the + object code either + 1. a copy of the Corresponding Source for all the software in the product + that is covered by this License, on a durable physical medium + customarily used for software interchange, for a price no more than your + reasonable cost of physically performing this conveying of source, or + 2. access to copy the Corresponding Source from a network server at no + charge. + - c) Convey individual copies of the object code with a copy of the written + offer to provide the Corresponding Source. This alternative is allowed only + occasionally and noncommercially, and only if you received the object code + with such an offer, in accord with subsection 6b. + - d) Convey the object code by offering access from a designated place + (gratis or for a charge), and offer equivalent access to the Corresponding + Source in the same way through the same place at no further charge. You + need not require recipients to copy the Corresponding Source along with the + object code. If the place to copy the object code is a network server, the + Corresponding Source may be on a different server operated by you or a + third party) that supports equivalent copying facilities, provided you + maintain clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the Corresponding + Source, you remain obligated to ensure that it is available for as long as + needed to satisfy these requirements. + - e) Convey the object code using peer-to-peer transmission, provided you + inform other peers where the object code and Corresponding Source of the + work are being offered to the general public at no charge under subsection + 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A *User Product* is either + + 1. a *consumer product*, which means any tangible personal property which is + normally used for personal, family, or household purposes, or + 2. anything designed or sold for incorporation into a dwelling. + +In determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a +particular user, *normally used* refers to a typical or common use of that +class of product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected to use, +the product. A product is a consumer product regardless of whether the product +has substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +*Installation Information* for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified +version of its Corresponding Source. The information must suffice to ensure +that the continued functioning of the modified object code is in no case +prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require +no special password or key for unpacking, reading or copying. + +### 7. Additional Terms. + +*Additional permissions* are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added +by you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + + - a) Disclaiming warranty or limiting liability differently from the terms of + sections 15 and 16 of this License; or + - b) Requiring preservation of specified reasonable legal notices or author + attributions in that material or in the Appropriate Legal Notices displayed + by works containing it; or + - c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in reasonable + ways as different from the original version; or + - d) Limiting the use for publicity purposes of names of licensors or authors + of the material; or + - e) Declining to grant rights under trademark law for use of some trade + names, trademarks, or service marks; or + - f) Requiring indemnification of licensors and authors of that material by + anyone who conveys the material (or modified versions of it) with + contractual assumptions of liability to the recipient, for any liability + that these contractual assumptions directly impose on those licensors and + authors. + +All other non-permissive additional terms are considered *further restrictions* +within the meaning of section 10. If the Program as you received it, or any +part of it, contains a notice stating that it is governed by this License along +with a term that is a further restriction, you may remove that term. If a +license document contains a further restriction but permits relicensing or +conveying under this License, you may add to a covered work material governed +by the terms of that license document, provided that the further restriction +does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +### 8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, +and will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated + + - a) provisionally, unless and until the copyright holder explicitly and + finally terminates your license, and + - b) permanently, if the copyright holder fails to notify you of the + violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses +of parties who have received copies or rights from you under this License. If +your rights have been terminated and not permanently reinstated, you do not +qualify to receive new licenses for the same material under section 10. + +### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy +of the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An *entity transaction* is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work +results from an entity transaction, each party to that transaction who receives +a copy of the work also receives whatever licenses to the work the party's +predecessor in interest had or could give under the previous paragraph, plus a +right to possession of the Corresponding Source of the work from the +predecessor in interest, if the predecessor has it or can get it with +reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +### 11. Patents. + +A *contributor* is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's *contributor version*. + +A contributor's *essential patent claims* are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, *control* includes the right to grant +patent sublicenses in a manner consistent with the requirements of this +License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a *patent license* is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent +infringement). To *grant* such a patent license to a party means to make such +an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available +network server or other readily accessible means, then you must either + + 1. cause the Corresponding Source to be so available, or + 2. arrange to deprive yourself of the benefit of the patent license for this + particular work, or + 3. arrange, in a manner consistent with the requirements of this License, to + extend the patent license to downstream recipients. + +*Knowingly relying* means you have actual knowledge that, but for the patent +license, your conveying the covered work in a country, or your recipient's use +of the covered work in a country, would infringe one or more identifiable +patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients +of the covered work and works based on it. + +A patent license is *discriminatory* if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license + + - a) in connection with copies of the covered work conveyed by you (or copies + made from those copies), or + - b) primarily for and in connection with specific products or compilations + that contain the covered work, unless you entered into that arrangement, or + that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to +you under applicable patent law. + +### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +### 13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to +link or combine any covered work with a work licensed under version 3 of the +GNU Affero General Public License into a single combined work, and to convey +the resulting work. The terms of this License will continue to apply to the +part which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License *or any later +version* applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Free Software Foundation. If the Program does not specify a version number +of the GNU General Public License, you may choose any version ever published by +the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the +GNU General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright +holder as a result of your choosing to follow a later version. + +### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE +LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + +## END OF TERMS AND CONDITIONS ### + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion +of warranty; and each file should have at least the *copyright* line and a +pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w` and `show c` should show the appropriate +parts of the General Public License. Of course, your program's commands might +be different; for a GUI interface, you would use an *about box*. + +You should also get your employer (if you work as a programmer) or school, if +any, to sign a *copyright disclaimer* for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +[http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider +it more useful to permit linking proprietary applications with the library. If +this is what you want to do, use the GNU Lesser General Public License instead +of this License. But first, please read +[http://www.gnu.org/philosophy/why-not-lgpl.html](http://www.gnu.org/philosophy/why-not-lgpl.html). \ No newline at end of file diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml new file mode 100644 index 00000000..8037cb4a --- /dev/null +++ b/bitbucket-pipelines.yml @@ -0,0 +1,22 @@ +image: eclipse-temurin:25-jdk-ubi10-minimal + +pipelines: + default: + - step: + caches: + - maven + script: + - /bin/sh mvnw clean install + artifacts: + - target/*.zip + - step: + deployment: Production + script: + - pipe: atlassian/ftp-deploy:0.7.1 + variables: + USER: $FTP_USER + PASSWORD: $FTP_PASSWORD + SERVER: $FTP_HOST + REMOTE_PATH: $FTP_REMOTE_PATH + LOCAL_PATH: target/ + SET_ARGS: "ftp:ssl-allow yes ftp:ssl-force yes ftp:ssl-protect-data yes ssl:verify-certificate false" \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 00000000..53c0e1cd --- /dev/null +++ b/mvnw @@ -0,0 +1,338 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ]; then + + if [ -f /usr/local/etc/mavenrc ]; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ]; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ]; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false +darwin=false +mingw=false +case "$(uname)" in +CYGWIN*) cygwin=true ;; +MINGW*) mingw=true ;; +Darwin*) + darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)" + export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home" + export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ]; then + if [ -r /etc/gentoo-release ]; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \ + && JAVA_HOME="$( + cd "$JAVA_HOME" || ( + echo "cannot cd into $JAVA_HOME." >&2 + exit 1 + ) + pwd + )" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin; then + javaHome="$(dirname "$javaExecutable")" + javaExecutable="$(cd "$javaHome" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "$javaExecutable")" + fi + javaHome="$(dirname "$javaExecutable")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ]; then + if [ -n "$JAVA_HOME" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$( + \unset -f command 2>/dev/null + \command -v java + )" + fi +fi + +if [ ! -x "$JAVACMD" ]; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ]; then + echo "Warning: JAVA_HOME environment variable is not set." >&2 +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ]; then + echo "Path not specified to find_maven_basedir" >&2 + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ]; do + if [ -d "$wdir"/.mvn ]; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$( + cd "$wdir/.." || exit 1 + pwd + ) + fi + # end of workaround + done + printf '%s' "$( + cd "$basedir" || exit 1 + pwd + )" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' <"$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1 +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + fi + while IFS="=" read -r key value; do + case "$key" in wrapperUrl) + wrapperUrl=$(trim "${value-}") + break + ;; + esac + done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget >/dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget ${QUIET:+"$QUIET"} "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget ${QUIET:+"$QUIET"} --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl >/dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl ${QUIET:+"$QUIET"} -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl ${QUIET:+"$QUIET"} --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in wrapperSha256Sum) + wrapperSha256Sum=$(trim "${value-}") + break + ;; + esac +done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c - >/dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] \ + && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] \ + && CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] \ + && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..c453424c --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,206 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. >&2 +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. >&2 +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. >&2 +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.4/maven-wrapper-3.3.4.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..a8fea876 --- /dev/null +++ b/pom.xml @@ -0,0 +1,203 @@ + + 4.0.0 + com.l2jserver + l2j-server-game + 2.6.3.0-SNAPSHOT + L2J Game Server + + 25 + 25 + UTF-8 + UTF-8 + + 2.0.17 + 2.25.3 + 1.3.0 + 2.13.2 + 1.0.12 + 5.3.0 + + 2.6.9.0 + 2.6.5.0 + + 6.0.1 + 5.21.0 + 3.27.6 + + 8 + 12.1.9 + 3.8.0 + 3.14.1 + 3.4.2 + 3.5.4 + + + + jitpack.io + https://jitpack.io + + + + l2jserver + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + -Xshare:off -javaagent:"${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar" + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + full + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + libs/ + com.l2jserver.gameserver.GameServer + + + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + package + + single + + + ${project.artifactId}-${project.version} + false + + src/main/assembly/zip.xml + + + + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} + + + org.mdkt.compiler + InMemoryJavaCompiler + ${InMemoryJavaCompiler.version} + + + com.google.code.gson + gson + ${gson.version} + + + org.aeonbits.owner + owner-java8 + ${owner.version} + + + net.dv8tion + JDA + ${jda.version} + + + com.google.protobuf + protobuf-java + + + + + + org.bitbucket.l2jserver + l2j-server-commons + ${l2j-server-commons.version} + + + org.bitbucket.l2jserver + l2j-server-geo-driver + ${l2j-server-geo-driver.version} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + + ci + + + + org.owasp + dependency-check-maven + ${dependency-check-maven.version} + + ${dependency-check-maven.level} + false + ${env.NVD_API_KEY} + + + + + check + + + + + + + + + \ No newline at end of file diff --git a/src/main/assembly/zip.xml b/src/main/assembly/zip.xml new file mode 100644 index 00000000..cda9313b --- /dev/null +++ b/src/main/assembly/zip.xml @@ -0,0 +1,43 @@ + + zip + false + + zip + + + + ${project.build.directory}/l2jserver.jar + . + + + src/main/resources/GameServer_loop.sh + . + + + src/main/resources/startGameServer.bat + . + + + src/main/resources/startGameServer.sh + . + + + + + src/main/resources/ + + config/* + + . + + + + + libs + + ${project.groupId}:${project.artifactId}:jar:* + + + + \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/Custom.java b/src/main/java/com/l2jserver/gameserver/Custom.java new file mode 100644 index 00000000..6c0d29cc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/Custom.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +/** + * Custom. + * @author Zoey76 + * @version 2.6.2.0 + */ +@Documented +@Retention(RUNTIME) +public @interface Custom { + +} diff --git a/src/main/java/com/l2jserver/gameserver/FortUpdater.java b/src/main/java/com/l2jserver/gameserver/FortUpdater.java new file mode 100644 index 00000000..45f9335c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/FortUpdater.java @@ -0,0 +1,95 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.fortress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; + +/** + * Class managing periodical events with castle. + * @author Vice + * @since 2008 + */ +public class FortUpdater implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(FortUpdater.class); + private final L2Clan _clan; + private final Fort _fort; + private int _runCount; + private final UpdaterType _updaterType; + + public enum UpdaterType { + MAX_OWN_TIME, // gives fort back to NPC clan + PERIODIC_UPDATE // raise blood oath/supply level + } + + public FortUpdater(Fort fort, L2Clan clan, int runCount, UpdaterType ut) { + _fort = fort; + _clan = clan; + _runCount = runCount; + _updaterType = ut; + } + + @Override + public void run() { + try { + switch (_updaterType) { + case PERIODIC_UPDATE -> { + _runCount++; + if ((_fort.getOwnerClan() == null) || (_fort.getOwnerClan() != _clan)) { + return; + } + + _fort.getOwnerClan().increaseBloodOathCount(); + + if (_fort.getFortState() == 2) { + if (_clan.getWarehouse().getAdena() >= fortress().getFeeForCastle()) { + _clan.getWarehouse().destroyItemByItemId("FS_fee_for_Castle", Inventory.ADENA_ID, fortress().getFeeForCastle(), null, null); + _fort.getContractedCastle().addToTreasuryNoTax(fortress().getFeeForCastle()); + _fort.raiseSupplyLvL(); + } else { + _fort.setFortState(1, 0); + } + } + _fort.saveFortVariables(); + } + case MAX_OWN_TIME -> { + if ((_fort.getOwnerClan() == null) || (_fort.getOwnerClan() != _clan)) { + return; + } + if (_fort.getOwnedTime() > (fortress().getMaxKeepTime() * 3600)) { + _fort.removeOwner(true); + _fort.setFortState(0, 0); + } + } + } + } catch (Exception e) { + LOG.error("There has been a problem updating forts!", e); + } + } + + public int getRunCount() { + return _runCount; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/GameServer.java b/src/main/java/com/l2jserver/gameserver/GameServer.java new file mode 100644 index 00000000..4775e870 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/GameServer.java @@ -0,0 +1,538 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.database; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.geodata; +import static com.l2jserver.gameserver.config.Configuration.hexId; +import static com.l2jserver.gameserver.config.Configuration.mmo; +import static com.l2jserver.gameserver.config.Configuration.server; +import static com.l2jserver.gameserver.config.Configuration.telnet; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.awt.Toolkit; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Calendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.UPnPService; +import com.l2jserver.commons.dao.ServerNameDAO; +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.IPv4Filter; +import com.l2jserver.commons.util.Util; +import com.l2jserver.gameserver.bbs.service.ForumsBBSManager; +import com.l2jserver.gameserver.cache.HtmCache; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.json.ExperienceData; +import com.l2jserver.gameserver.data.sql.impl.AnnouncementsTable; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.sql.impl.CharSummonTable; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.sql.impl.CrestTable; +import com.l2jserver.gameserver.data.sql.impl.NpcBufferTable; +import com.l2jserver.gameserver.data.sql.impl.OfflineTradersTable; +import com.l2jserver.gameserver.data.sql.impl.SummonSkillsTable; +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.data.xml.impl.ArmorSetsData; +import com.l2jserver.gameserver.data.xml.impl.BuyListData; +import com.l2jserver.gameserver.data.xml.impl.CategoryData; +import com.l2jserver.gameserver.data.xml.impl.ClassListData; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.data.xml.impl.EnchantItemData; +import com.l2jserver.gameserver.data.xml.impl.EnchantItemGroupsData; +import com.l2jserver.gameserver.data.xml.impl.EnchantItemHPBonusData; +import com.l2jserver.gameserver.data.xml.impl.EnchantItemOptionsData; +import com.l2jserver.gameserver.data.xml.impl.EnchantSkillGroupsData; +import com.l2jserver.gameserver.data.xml.impl.FishData; +import com.l2jserver.gameserver.data.xml.impl.FishingMonstersData; +import com.l2jserver.gameserver.data.xml.impl.FishingRodsData; +import com.l2jserver.gameserver.data.xml.impl.HennaData; +import com.l2jserver.gameserver.data.xml.impl.HitConditionBonusData; +import com.l2jserver.gameserver.data.xml.impl.InitialEquipmentData; +import com.l2jserver.gameserver.data.xml.impl.InitialShortcutData; +import com.l2jserver.gameserver.data.xml.impl.KarmaData; +import com.l2jserver.gameserver.data.xml.impl.MultisellData; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.data.xml.impl.OptionData; +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.data.xml.impl.PlayerCreationPointData; +import com.l2jserver.gameserver.data.xml.impl.PlayerTemplateData; +import com.l2jserver.gameserver.data.xml.impl.PlayerXpPercentLostData; +import com.l2jserver.gameserver.data.xml.impl.RecipeData; +import com.l2jserver.gameserver.data.xml.impl.SecondaryAuthData; +import com.l2jserver.gameserver.data.xml.impl.SiegeScheduleData; +import com.l2jserver.gameserver.data.xml.impl.SkillLearnData; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.data.xml.impl.StaticObjectData; +import com.l2jserver.gameserver.data.xml.impl.TransformData; +import com.l2jserver.gameserver.data.xml.impl.UIData; +import com.l2jserver.gameserver.datatables.AugmentationData; +import com.l2jserver.gameserver.datatables.BotReportTable; +import com.l2jserver.gameserver.datatables.EventDroplist; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.handler.EffectHandler; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.AirShipManager; +import com.l2jserver.gameserver.instancemanager.AntiFeedManager; +import com.l2jserver.gameserver.instancemanager.AuctionManager; +import com.l2jserver.gameserver.instancemanager.BoatManager; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.CastleManorManager; +import com.l2jserver.gameserver.instancemanager.ClanHallManager; +import com.l2jserver.gameserver.instancemanager.ClanHallSiegeManager; +import com.l2jserver.gameserver.instancemanager.CoupleManager; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.instancemanager.DayNightSpawnManager; +import com.l2jserver.gameserver.instancemanager.DimensionalRiftManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; +import com.l2jserver.gameserver.instancemanager.GlobalVariablesManager; +import com.l2jserver.gameserver.instancemanager.GraciaSeedsManager; +import com.l2jserver.gameserver.instancemanager.GrandBossManager; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.instancemanager.ItemAuctionManager; +import com.l2jserver.gameserver.instancemanager.ItemsOnGroundManager; +import com.l2jserver.gameserver.instancemanager.MailManager; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.instancemanager.MercTicketManager; +import com.l2jserver.gameserver.instancemanager.PetitionManager; +import com.l2jserver.gameserver.instancemanager.PunishmentManager; +import com.l2jserver.gameserver.instancemanager.QuestManager; +import com.l2jserver.gameserver.instancemanager.RaidBossPointsManager; +import com.l2jserver.gameserver.instancemanager.RaidBossSpawnManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.AutoSpawnHandler; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.PartyMatchRoomList; +import com.l2jserver.gameserver.model.PartyMatchWaitingList; +import com.l2jserver.gameserver.model.entity.Hero; +import com.l2jserver.gameserver.model.entity.TvTManager; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.olympiad.Olympiad; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.L2GamePacketHandler; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.pathfinding.PathFinding; +import com.l2jserver.gameserver.script.faenor.FaenorScriptEngine; +import com.l2jserver.gameserver.scripting.ScriptEngineManager; +import com.l2jserver.gameserver.status.Status; +import com.l2jserver.gameserver.taskmanager.KnownListUpdateTaskManager; +import com.l2jserver.gameserver.taskmanager.TaskManager; +import com.l2jserver.gameserver.util.DeadLockDetector; +import com.l2jserver.mmocore.SelectorConfig; +import com.l2jserver.mmocore.SelectorThread; + +public final class GameServer { + + private static final Logger LOG = LoggerFactory.getLogger(GameServer.class); + + private static final String DATAPACK = "-dp"; + + private static final String SCRIPT = "-s"; + + private static final String GEODATA = "-gd"; + + private final SelectorThread _selectorThread; + + private final L2GamePacketHandler _gamePacketHandler; + + private final DeadLockDetector _deadDetectThread; + + public static GameServer gameServer; + + public static final Calendar dateTimeServerStarted = Calendar.getInstance(); + + public GameServer() throws Exception { + + final var serverLoadStart = startTimedSection("Database"); + ConnectionFactory.builder() // + .withUrl(database().getURL()) // + .withUser(database().getUser()) // + .withPassword(database().getPassword()) // + .withMaxIdleTime(database().getMaxIdleTime()) // + .withMaxPoolSize(database().getMaxConnections()) // + .build(); + + DAOFactory.getInstance(); + + if (!IdFactory.getInstance().isInitialized()) { + LOG.error("Could not read object IDs from database. Please check your configuration."); + throw new Exception("Could not initialize the Id factory!"); + } + endTimedSection("Database", serverLoadStart); + + ThreadPoolManager.getInstance(); + EventDispatcher.getInstance(); + ScriptEngineManager.getInstance(); + + final var worldStart = startTimedSection("World"); + GameTimeController.init(); + InstanceManager.getInstance(); + L2World.getInstance(); + MapRegionManager.getInstance(); + AnnouncementsTable.getInstance(); + GlobalVariablesManager.getInstance(); + endTimedSection("World", worldStart); + + final var dataStart = startTimedSection("Data"); + CategoryData.getInstance(); + SecondaryAuthData.getInstance(); + endTimedSection("Data", dataStart); + + final var effectsStart = startTimedSection("Effects"); + EffectHandler.getInstance().executeScript(); + endTimedSection("Effects", effectsStart); + + final var esgStart = startTimedSection("Enchant Skill Groups"); + EnchantSkillGroupsData.getInstance(); + endTimedSection("Enchant Skill Groups", esgStart); + + final var skillTreesStart = startTimedSection("Skill Trees"); + SkillTreesData.getInstance(); + endTimedSection("Skill Trees", skillTreesStart); + + final var skillsStart = startTimedSection("Skills"); + SkillData.getInstance(); + SummonSkillsTable.getInstance(); + endTimedSection("Skills", skillsStart); + + final var itemsStart = startTimedSection("Items"); + ItemTable.getInstance(); + EnchantItemGroupsData.getInstance(); + EnchantItemData.getInstance(); + EnchantItemOptionsData.getInstance(); + OptionData.getInstance(); + EnchantItemHPBonusData.getInstance(); + MultisellData.getInstance(); + RecipeData.getInstance(); + ArmorSetsData.getInstance(); + FishData.getInstance(); + FishingMonstersData.getInstance(); + FishingRodsData.getInstance(); + HennaData.getInstance(); + endTimedSection("Items", itemsStart); + + final var charactersStart = startTimedSection("Characters"); + ClassListData.getInstance(); + InitialEquipmentData.getInstance(); + InitialShortcutData.getInstance(); + ExperienceData.getInstance(); + PlayerXpPercentLostData.getInstance(); + KarmaData.getInstance(); + HitConditionBonusData.getInstance(); + PlayerTemplateData.getInstance(); + PlayerCreationPointData.getInstance(); + CharNameTable.getInstance(); + AdminData.getInstance(); + RaidBossPointsManager.getInstance(); + PetDataTable.getInstance(); + CharSummonTable.getInstance().init(); + endTimedSection("Characters", charactersStart); + + final var bbsStart = startTimedSection("BBS"); + if (general().enableCommunityBoard()) { + ForumsBBSManager.getInstance().load(); + } + endTimedSection("BBS", bbsStart); + + final var clansStart = startTimedSection("Clans"); + ClanTable.getInstance(); + ClanHallSiegeManager.getInstance(); + ClanHallManager.getInstance(); + AuctionManager.getInstance(); + endTimedSection("Clans", clansStart); + + final var geodataStart = startTimedSection("Geodata"); + GeoData.getInstance(); + if (geodata().getPathFinding() > 0) { + PathFinding.getInstance(); + } + endTimedSection("Geodata", geodataStart); + + final var npcsStart = startTimedSection("NPCs"); + SkillLearnData.getInstance(); + NpcData.getInstance(); + WalkingManager.getInstance(); + StaticObjectData.getInstance(); + ZoneManager.getInstance(); + DoorData.getInstance(); + CastleManager.getInstance().loadInstances(); + NpcBufferTable.getInstance(); + GrandBossManager.getInstance().initZones(); + EventDroplist.getInstance(); + endTimedSection("NPCs", npcsStart); + + final var auctionStart = startTimedSection("Auction Manager"); + ItemAuctionManager.getInstance(); + endTimedSection("Auction Manager", auctionStart); + + final var olympiadStart = startTimedSection("Olympiad"); + Olympiad.getInstance(); + Hero.getInstance(); + endTimedSection("Olympiad", olympiadStart); + + final var sevenSignsStart = startTimedSection("Seven Signs"); + SevenSigns.getInstance(); + endTimedSection("Seven Signs", sevenSignsStart); + + // Call to load caches + final var cacheStart = startTimedSection("Cache"); + HtmCache.getInstance(); + CrestTable.getInstance(); + UIData.getInstance(); + PartyMatchWaitingList.getInstance(); + PartyMatchRoomList.getInstance(); + PetitionManager.getInstance(); + AugmentationData.getInstance(); + CursedWeaponsManager.getInstance(); + TransformData.getInstance(); + BotReportTable.getInstance(); + QuestManager.getInstance(); + BoatManager.getInstance(); + AirShipManager.getInstance(); + GraciaSeedsManager.getInstance(); + endTimedSection("Cache", cacheStart); + + final var handlersStart = startTimedSection("Handlers"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/handlers/MasterHandler.java"); + endTimedSection("Handlers", handlersStart); + + final var aiStart = startTimedSection("AI"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/ai/AILoader.java"); + endTimedSection("AI", aiStart); + + final var instancesStart = startTimedSection("Instances"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/instances/InstanceLoader.java"); + endTimedSection("Instances", instancesStart); + + final var graciaStart = startTimedSection("Gracia"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/gracia/GraciaLoader.java"); + endTimedSection("Gracia", graciaStart); + + final var hellboundStart = startTimedSection("Hellbound"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/hellbound/HellboundLoader.java"); + endTimedSection("Hellbound", hellboundStart); + + final var questsStart = startTimedSection("Quests"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/quests/QuestLoader.java"); + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/quests/TerritoryWarScripts/TerritoryWarSuperClass.java"); + endTimedSection("Quests", questsStart); + + final var scriptsStart = startTimedSection("Scripts"); + ScriptEngineManager.getInstance().executeScriptList(new File(server().getDatapackRoot(), "data/scripts.cfg")); + endTimedSection("Scripts", scriptsStart); + + final var buyListStart = startTimedSection("BuyLists"); + BuyListData.getInstance().load(); + endTimedSection("BuyLists", buyListStart); + + final var spawnsStart = startTimedSection("Spawns"); + SpawnTable.getInstance().load(); + DayNightSpawnManager.getInstance().trim().notifyChangeMode(); + FourSepulchersManager.getInstance().init(); + DimensionalRiftManager.getInstance(); + RaidBossSpawnManager.getInstance(); + endTimedSection("Spawns", spawnsStart); + + final var siegeStart = startTimedSection("Siege"); + SiegeManager.getInstance().getSieges(); + CastleManager.getInstance().activateInstances(); + FortManager.getInstance().loadInstances(); + FortManager.getInstance().activateInstances(); + FortSiegeManager.getInstance(); + SiegeScheduleData.getInstance(); + TerritoryWarManager.getInstance(); + CastleManorManager.getInstance(); + MercTicketManager.getInstance(); + endTimedSection("Siege", siegeStart); + + final var otherStart = startTimedSection("Other"); + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance(); + } + + if ((general().getAutoDestroyDroppedItemAfter() > 0) || (general().getAutoDestroyHerbTime() > 0)) { + ItemsAutoDestroy.getInstance(); + } + + MonsterRace.getInstance(); + SevenSigns.getInstance().spawnSevenSignsNPC(); + SevenSignsFestival.getInstance(); + AutoSpawnHandler.getInstance(); + FaenorScriptEngine.getInstance(); + + if (customs().allowWedding()) { + CoupleManager.getInstance(); + } + + TaskManager.getInstance(); + + AntiFeedManager.getInstance().registerEvent(AntiFeedManager.GAME_ID); + + if (general().allowMail()) { + MailManager.getInstance(); + } + + PunishmentManager.getInstance(); + + Runtime.getRuntime().addShutdownHook(Shutdown.getInstance()); + + LOG.info("Free Object Ids remaining {}.", IdFactory.getInstance().size()); + + TvTManager.getInstance(); + KnownListUpdateTaskManager.getInstance(); + + if ((customs().offlineTradeEnable() || customs().offlineCraftEnable()) && customs().restoreOffliners()) { + OfflineTradersTable.getInstance().restoreOfflineTraders(); + } + + // Multilingual Support + if (customs().multiLangEnable()) { + if (customs().multiLangSystemMessageEnable()) { + SystemMessageId.reloadLocalisations(); + } + if (customs().multiLangNpcStringEnable()) { + NpcStringId.reloadLocalisations(); + } + } + + endTimedSection("Other", otherStart); + + if (general().deadLockDetector()) { + _deadDetectThread = new DeadLockDetector(); + _deadDetectThread.setDaemon(true); + _deadDetectThread.start(); + } else { + _deadDetectThread = null; + } + System.gc(); + // maxMemory is the upper limit the jvm can use, totalMemory the size of + // the current allocation pool, freeMemory the unused memory in the allocation pool + long freeMem = ((Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory()) + Runtime.getRuntime().freeMemory()) / 1048576; + long totalMem = Runtime.getRuntime().maxMemory() / 1048576; + LOG.info("Started, free memory {} Mb of {} Mb", freeMem, totalMem); + Toolkit.getDefaultToolkit().beep(); + LoginServerThread.getInstance().start(); + + final SelectorConfig sc = new SelectorConfig(); + sc.MAX_READ_PER_PASS = mmo().getMaxReadPerPass(); + sc.MAX_SEND_PER_PASS = mmo().getMaxSendPerPass(); + sc.SLEEP_TIME = mmo().getSleepTime(); + sc.HELPER_BUFFER_COUNT = mmo().getHelperBufferCount(); + sc.TCP_NODELAY = mmo().isTcpNoDelay(); + + _gamePacketHandler = new L2GamePacketHandler(); + _selectorThread = new SelectorThread<>(sc, _gamePacketHandler, _gamePacketHandler, _gamePacketHandler, new IPv4Filter()); + + InetAddress bindAddress = null; + if (!server().getHost().equals("*")) { + try { + bindAddress = InetAddress.getByName(server().getHost()); + } catch (UnknownHostException ex) { + LOG.warn("Bind address is invalid, using all available IPs!", ex); + } + } + + try { + _selectorThread.openServerSocket(bindAddress, server().getPort()); + _selectorThread.start(); + LOG.info("Now listening on {}:{}", server().getHost(), server().getPort()); + } catch (IOException ex) { + LOG.error("Failed to open server socket!", ex); + System.exit(1); + } + + if (server().enableUPnP()) { + startTimedSection("UPnP"); + UPnPService.getInstance().load(server().getPort(), "L2J Game Server"); + } + + if (telnet().isEnabled()) { + new Status(telnet().getPort(), telnet().getPassword()).start(); + } else { + LOG.info("Telnet server is currently disabled."); + } + + LOG.info("Maximum numbers of connected players {}.", server().getMaxOnlineUsers()); + LOG.info("Server {} loaded in {} seconds.", ServerNameDAO.getServer(hexId().getServerID()), MILLISECONDS.toSeconds(System.currentTimeMillis() - serverLoadStart)); + } + + public static void main(String[] args) throws Exception { + final String datapackRoot = Util.parseArg(args, DATAPACK, true); + if (datapackRoot != null) { + server().setProperty("DatapackRoot", datapackRoot); + } + + final String scriptRoot = Util.parseArg(args, SCRIPT, true); + if (scriptRoot != null) { + server().setProperty("ScriptRoot", scriptRoot); + } + + final String geodata = Util.parseArg(args, GEODATA, true); + if (geodata != null) { + geodata().setProperty("GeoDataPath", geodata); + } + + gameServer = new GameServer(); + } + + public long getUsedMemoryMB() { + return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576; + } + + public SelectorThread getSelectorThread() { + return _selectorThread; + } + + public L2GamePacketHandler getL2GamePacketHandler() { + return _gamePacketHandler; + } + + public DeadLockDetector getDeadLockDetectorThread() { + return _deadDetectThread; + } + + public static long startTimedSection(String sectionName) { + final var sBuilder = new StringBuilder("=[ " + sectionName + " ]"); + while (sBuilder.length() < 61) { + sBuilder.insert(0, "-"); + } + LOG.info(sBuilder.toString()); + + return System.currentTimeMillis(); + } + + private static void endTimedSection(String sectionName, long startTime) { + LOG.info("[ {} loaded in {}ms ]", sectionName, System.currentTimeMillis() - startTime); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/GameTimeController.java b/src/main/java/com/l2jserver/gameserver/GameTimeController.java new file mode 100644 index 00000000..eb868e69 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/GameTimeController.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import java.util.Calendar; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.instancemanager.DayNightSpawnManager; +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Game Time controller class. + * @author Forsaiken + */ +public final class GameTimeController extends Thread { + private static final Logger LOG = LoggerFactory.getLogger(GameTimeController.class); + + public static final int TICKS_PER_SECOND = 10; // not able to change this without checking through code + public static final int MILLIS_IN_TICK = 1000 / TICKS_PER_SECOND; + public static final int IG_DAYS_PER_DAY = 6; + public static final int MILLIS_PER_IG_DAY = (3600000 * 24) / IG_DAYS_PER_DAY; + public static final int SECONDS_PER_IG_DAY = MILLIS_PER_IG_DAY / 1000; + public static final int MINUTES_PER_IG_DAY = SECONDS_PER_IG_DAY / 60; + public static final int TICKS_PER_IG_DAY = SECONDS_PER_IG_DAY * TICKS_PER_SECOND; + public static final int TICKS_PER_IG_HOUR = TICKS_PER_IG_DAY / 24; + public static final int TICKS_PER_IG_MINUTE = TICKS_PER_IG_HOUR / 60; + public static final int TICKS_SUN_STATE_CHANGE = TICKS_PER_IG_DAY / 4; + + private static GameTimeController _instance; + + private final Set _movingObjects = ConcurrentHashMap.newKeySet(); + private final long _referenceTime; + + private GameTimeController() { + super("GameTimeController"); + super.setDaemon(true); + super.setPriority(MAX_PRIORITY); + + final Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + _referenceTime = c.getTimeInMillis(); + + super.start(); + } + + public static void init() { + _instance = new GameTimeController(); + } + + public int getGameTime() { + return (getGameTicks() % TICKS_PER_IG_DAY) / TICKS_PER_IG_MINUTE; + } + + public int getGameHour() { + return getGameTime() / 60; + } + + public int getGameMinute() { + return getGameTime() % 60; + } + + public boolean isNight() { + return getGameHour() < 6; + } + + /** + * The true GameTime tick. Directly taken from current time. This represents the tick of the time. + * @return + */ + public int getGameTicks() { + return (int) ((System.currentTimeMillis() - _referenceTime) / MILLIS_IN_TICK); + } + + /** + * Add a L2Character to movingObjects of GameTimeController. + * @param cha The L2Character to add to movingObjects of GameTimeController + */ + public void registerMovingObject(final L2Character cha) { + if (cha == null) { + return; + } + + _movingObjects.add(cha); + } + + /** + * Move all L2Characters contained in movingObjects of GameTimeController.
+ * Concept :
+ * All L2Character in movement are identified in movingObjects of GameTimeController.
+ * Actions :
+ *
    + *
  • Update the position of each L2Character
  • + *
  • If movement is finished, the L2Character is removed from movingObjects
  • + *
  • Create a task to update the _knownObject and _knowPlayers of each L2Character that finished its movement and of their already known L2Object then notify AI with EVT_ARRIVED
  • + *
+ */ + private void moveObjects() { + _movingObjects.removeIf(L2Character::updatePosition); + } + + public void stopTimer() { + super.interrupt(); + LOG.info("Stopping {}", getClass().getSimpleName()); + } + + @Override + public void run() { + LOG.debug("{}: Started.", getClass().getSimpleName()); + + long nextTickTime, sleepTime; + boolean isNight = isNight(); + + if (isNight) { + ThreadPoolManager.getInstance().executeAi(() -> DayNightSpawnManager.getInstance().notifyChangeMode()); + } + + while (true) { + nextTickTime = System.currentTimeMillis() + MILLIS_IN_TICK; + + try { + moveObjects(); + } catch (final Throwable e) { + LOG.warn("Unable to move objects!", e); + } + + sleepTime = nextTickTime - System.currentTimeMillis(); + if (sleepTime > 0) { + try { + Thread.sleep(sleepTime); + } catch (final InterruptedException e) { + + } + } + + if (isNight() != isNight) { + isNight = !isNight; + + ThreadPoolManager.getInstance().executeAi(() -> DayNightSpawnManager.getInstance().notifyChangeMode()); + } + } + } + + public static GameTimeController getInstance() { + return _instance; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/GeoData.java b/src/main/java/com/l2jserver/gameserver/GeoData.java new file mode 100644 index 00000000..97edad23 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/GeoData.java @@ -0,0 +1,582 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.geodata; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.util.GeoUtils; +import com.l2jserver.gameserver.util.LinePointIterator; +import com.l2jserver.gameserver.util.LinePointIterator3D; +import com.l2jserver.geodriver.Cell; +import com.l2jserver.geodriver.GeoDriver; + +/** + * Geodata. + * @author -Nemesiss- + * @author HorridoJoho + */ +public class GeoData { + + private static final Logger LOG = LoggerFactory.getLogger(GeoData.class); + + private static final String FILE_NAME_FORMAT = "%d_%d.l2j"; + + private static final int ELEVATED_SEE_OVER_DISTANCE = 2; + + private static final int MAX_SEE_OVER_HEIGHT = 48; + + private static final int SPAWN_Z_DELTA_LIMIT = 100; + + private static final Map GEODATA_REGIONS = new HashMap<>(); + + private final GeoDriver _driver = new GeoDriver(); + + protected GeoData() { + loadGeodataRegions(); + + int loadedRegions = 0; + try { + for (int regionX = L2World.TILE_X_MIN; regionX <= L2World.TILE_X_MAX; regionX++) { + for (int regionY = L2World.TILE_Y_MIN; regionY <= L2World.TILE_Y_MAX; regionY++) { + final Path geoFilePath = geodata().getGeoDataPath().toPath().resolve(String.format(FILE_NAME_FORMAT, regionX, regionY)); + final Boolean loadFile = GEODATA_REGIONS.get(regionX + "_" + regionY); + if (loadFile != null) { + if (loadFile) { + LOG.info("Loading {}...", geoFilePath.getFileName()); + _driver.loadRegion(geoFilePath, regionX, regionY); + loadedRegions++; + } + } else if (geodata().tryLoadUnspecifiedRegions() && Files.exists(geoFilePath)) { + try { + LOG.info("Loading {}...", geoFilePath.getFileName()); + _driver.loadRegion(geoFilePath, regionX, regionY); + loadedRegions++; + } catch (Exception ex) { + LOG.warn("Failed to load {}!", geoFilePath.getFileName(), ex); + } + } + } + } + } catch (Exception ex) { + LOG.error("Failed to load geodata!", ex); + System.exit(1); + } + + LOG.info("Loaded {} regions.", loadedRegions); + } + + private static void loadGeodataRegions() { + for (int regionX = L2World.TILE_X_MIN; regionX <= L2World.TILE_X_MAX; regionX++) { + for (int regionY = L2World.TILE_Y_MIN; regionY <= L2World.TILE_Y_MAX; regionY++) { + String key = regionX + "_" + regionY; + if (geodata().getIncludedRegions().contains(regionX + "_" + regionY)) { + GEODATA_REGIONS.put(key, true); + } else if (geodata().getExcludedRegions().contains(regionX + "_" + regionY)) { + GEODATA_REGIONS.put(key, false); + } + } + } + } + + public boolean hasGeoPos(int geoX, int geoY) { + return _driver.hasGeoPos(geoX, geoY); + } + + public boolean checkNearestNswe(int geoX, int geoY, int worldZ, int nswe) { + return _driver.checkNearestNswe(geoX, geoY, worldZ, nswe); + } + + public boolean checkNearestNsweAntiCornerCut(int geoX, int geoY, int worldZ, int nswe) { + boolean can = true; + if ((nswe & Cell.NSWE_NORTH_EAST) == Cell.NSWE_NORTH_EAST) { + // can = canEnterNeighbors(prevX, prevY - 1, prevGeoZ, Direction.EAST) && canEnterNeighbors(prevX + 1, prevY, prevGeoZ, Direction.NORTH); + can = checkNearestNswe(geoX, geoY - 1, worldZ, Cell.NSWE_EAST) && checkNearestNswe(geoX + 1, geoY, worldZ, Cell.NSWE_NORTH); + } + + if (can && ((nswe & Cell.NSWE_NORTH_WEST) == Cell.NSWE_NORTH_WEST)) { + // can = canEnterNeighbors(prevX, prevY - 1, prevGeoZ, Direction.WEST) && canEnterNeighbors(prevX - 1, prevY, prevGeoZ, Direction.NORTH); + can = checkNearestNswe(geoX, geoY - 1, worldZ, Cell.NSWE_WEST) && checkNearestNswe(geoX - 1, geoY, worldZ, Cell.NSWE_NORTH); + } + + if (can && ((nswe & Cell.NSWE_SOUTH_EAST) == Cell.NSWE_SOUTH_EAST)) { + // can = canEnterNeighbors(prevX, prevY + 1, prevGeoZ, Direction.EAST) && canEnterNeighbors(prevX + 1, prevY, prevGeoZ, Direction.SOUTH); + can = checkNearestNswe(geoX, geoY + 1, worldZ, Cell.NSWE_EAST) && checkNearestNswe(geoX + 1, geoY, worldZ, Cell.NSWE_SOUTH); + } + + if (can && ((nswe & Cell.NSWE_SOUTH_WEST) == Cell.NSWE_SOUTH_WEST)) { + // can = canEnterNeighbors(prevX, prevY + 1, prevGeoZ, Direction.WEST) && canEnterNeighbors(prevX - 1, prevY, prevGeoZ, Direction.SOUTH); + can = checkNearestNswe(geoX, geoY + 1, worldZ, Cell.NSWE_WEST) && checkNearestNswe(geoX - 1, geoY, worldZ, Cell.NSWE_SOUTH); + } + + return can && checkNearestNswe(geoX, geoY, worldZ, nswe); + } + + public int getNearestZ(int geoX, int geoY, int worldZ) { + return _driver.getNearestZ(geoX, geoY, worldZ); + } + + public int getNextLowerZ(int geoX, int geoY, int worldZ) { + return _driver.getNextLowerZ(geoX, geoY, worldZ); + } + + public int getNextHigherZ(int geoX, int geoY, int worldZ) { + return _driver.getNextHigherZ(geoX, geoY, worldZ); + } + + public int getGeoX(int worldX) { + return _driver.getGeoX(worldX); + } + + public int getGeoY(int worldY) { + return _driver.getGeoY(worldY); + } + + public int getWorldX(int geoX) { + return _driver.getWorldX(geoX); + } + + public int getWorldY(int geoY) { + return _driver.getWorldY(geoY); + } + + // /////////////////// + // L2J METHODS + /** + * Gets the height. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @return the height + */ + public int getHeight(int x, int y, int z) { + return getNearestZ(getGeoX(x), getGeoY(y), z); + } + + /** + * Gets the spawn height. + * @param x the x coordinate + * @param y the y coordinate + * @param z the the z coordinate + * @return the spawn height + */ + public int getSpawnHeight(int x, int y, int z) { + final int geoX = getGeoX(x); + final int geoY = getGeoY(y); + + if (!hasGeoPos(geoX, geoY)) { + return z; + } + + int nextLowerZ = getNextLowerZ(geoX, geoY, z + 20); + return Math.abs(nextLowerZ - z) <= SPAWN_Z_DELTA_LIMIT ? nextLowerZ : z; + } + + /** + * Gets the spawn height. + * @param location the location + * @return the spawn height + */ + public int getSpawnHeight(Location location) { + return getSpawnHeight(location.getX(), location.getY(), location.getZ()); + } + + /** + * Can see target. Doors as target always return true. Checks doors between. + * @param cha the character + * @param target the target + * @return {@code true} if the character can see the target (LOS), {@code false} otherwise + */ + public boolean canSeeTarget(L2Object cha, L2Object target) { + if (target == null) { + return false; + } + if (target.isDoor()) { + return true; + } + return canSeeTarget(cha.getX(), cha.getY(), cha.getZ(), cha.getInstanceId(), target.getX(), target.getY(), target.getZ(), target.getInstanceId()); + } + + /** + * Can see target. Checks doors between. + * @param cha the character + * @param worldPosition the world position + * @return {@code true} if the character can see the target at the given world position, {@code false} otherwise + */ + public boolean canSeeTarget(L2Object cha, ILocational worldPosition) { + return canSeeTarget(cha.getX(), cha.getY(), cha.getZ(), cha.getInstanceId(), worldPosition.getX(), worldPosition.getY(), worldPosition.getZ()); + } + + /** + * Can see target. Checks doors between. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param instanceId + * @param tx the target's x coordinate + * @param ty the target's y coordinate + * @param tz the target's z coordinate + * @param tInstanceId the target's instanceId + * @return + */ + public boolean canSeeTarget(int x, int y, int z, int instanceId, int tx, int ty, int tz, int tInstanceId) { + if ((instanceId != tInstanceId)) { + return false; + } + return canSeeTarget(x, y, z, instanceId, tx, ty, tz); + } + + /** + * Can see target. Checks doors between. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param instanceId + * @param tx the target's x coordinate + * @param ty the target's y coordinate + * @param tz the target's z coordinate + * @return {@code true} if there is line of sight between the given coordinate sets, {@code false} otherwise + */ + public boolean canSeeTarget(int x, int y, int z, int instanceId, int tx, int ty, int tz) { + if (DoorData.getInstance().checkIfDoorsBetween(x, y, z, tx, ty, tz, instanceId, true)) { + return false; + } + return canSeeTarget(x, y, z, tx, ty, tz); + } + + private int getLosGeoZ(int prevX, int prevY, int prevGeoZ, int curX, int curY, int nswe) { + if ((((nswe & Cell.NSWE_NORTH) != 0) && ((nswe & Cell.NSWE_SOUTH) != 0)) || (((nswe & Cell.NSWE_WEST) != 0) && ((nswe & Cell.NSWE_EAST) != 0))) { + throw new RuntimeException("Multiple directions!"); + } + + if (checkNearestNsweAntiCornerCut(prevX, prevY, prevGeoZ, nswe)) { + return getNearestZ(curX, curY, prevGeoZ); + } + return getNextHigherZ(curX, curY, prevGeoZ); + } + + /** + * Can see target. Does not check doors between. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param tx the target's x coordinate + * @param ty the target's y coordinate + * @param tz the target's z coordinate + * @return {@code true} if there is line of sight between the given coordinate sets, {@code false} otherwise + */ + public boolean canSeeTarget(int x, int y, int z, int tx, int ty, int tz) { + int geoX = getGeoX(x); + int geoY = getGeoY(y); + int tGeoX = getGeoX(tx); + int tGeoY = getGeoY(ty); + + z = getNearestZ(geoX, geoY, z); + tz = getNearestZ(tGeoX, tGeoY, tz); + + // fast path + if ((geoX == tGeoX) && (geoY == tGeoY)) { + if (hasGeoPos(tGeoX, tGeoY)) { + return z == tz; + } + + return true; + } + + if (tz > z) { + int tmp = tx; + tx = x; + x = tmp; + + tmp = ty; + ty = y; + y = tmp; + + tmp = tz; + tz = z; + z = tmp; + + tmp = tGeoX; + tGeoX = geoX; + geoX = tmp; + + tmp = tGeoY; + tGeoY = geoY; + geoY = tmp; + } + + LinePointIterator3D pointIter = new LinePointIterator3D(geoX, geoY, z, tGeoX, tGeoY, tz); + // first point is guaranteed to be available, skip it, we can always see our own position + pointIter.next(); + int prevX = pointIter.x(); + int prevY = pointIter.y(); + int prevZ = pointIter.z(); + int prevGeoZ = prevZ; + int ptIndex = 0; + while (pointIter.next()) { + int curX = pointIter.x(); + int curY = pointIter.y(); + + if ((curX == prevX) && (curY == prevY)) { + continue; + } + + int beeCurZ = pointIter.z(); + int curGeoZ = prevGeoZ; + + // check if the position has geodata + if (hasGeoPos(curX, curY)) { + int nswe = GeoUtils.computeNswe(prevX, prevY, curX, curY); + curGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, curX, curY, nswe); + int maxHeight; + if (ptIndex < ELEVATED_SEE_OVER_DISTANCE) { + maxHeight = z + MAX_SEE_OVER_HEIGHT; + } else { + maxHeight = beeCurZ + MAX_SEE_OVER_HEIGHT; + } + + boolean canSeeThrough = false; + if (curGeoZ <= maxHeight) { + if ((nswe & Cell.NSWE_NORTH_EAST) == Cell.NSWE_NORTH_EAST) { + int northGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX, prevY - 1, Cell.NSWE_EAST); + int eastGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX + 1, prevY, Cell.NSWE_NORTH); + canSeeThrough = (northGeoZ <= maxHeight) && (eastGeoZ <= maxHeight) && (northGeoZ <= getNearestZ(prevX, prevY - 1, beeCurZ)) && (eastGeoZ <= getNearestZ(prevX + 1, prevY, beeCurZ)); + } else if ((nswe & Cell.NSWE_NORTH_WEST) == Cell.NSWE_NORTH_WEST) { + int northGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX, prevY - 1, Cell.NSWE_WEST); + int westGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX - 1, prevY, Cell.NSWE_NORTH); + canSeeThrough = (northGeoZ <= maxHeight) && (westGeoZ <= maxHeight) && (northGeoZ <= getNearestZ(prevX, prevY - 1, beeCurZ)) && (westGeoZ <= getNearestZ(prevX - 1, prevY, beeCurZ)); + } else if ((nswe & Cell.NSWE_SOUTH_EAST) == Cell.NSWE_SOUTH_EAST) { + int southGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX, prevY + 1, Cell.NSWE_EAST); + int eastGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX + 1, prevY, Cell.NSWE_SOUTH); + canSeeThrough = (southGeoZ <= maxHeight) && (eastGeoZ <= maxHeight) && (southGeoZ <= getNearestZ(prevX, prevY + 1, beeCurZ)) && (eastGeoZ <= getNearestZ(prevX + 1, prevY, beeCurZ)); + } else if ((nswe & Cell.NSWE_SOUTH_WEST) == Cell.NSWE_SOUTH_WEST) { + int southGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX, prevY + 1, Cell.NSWE_WEST); + int westGeoZ = getLosGeoZ(prevX, prevY, prevGeoZ, prevX - 1, prevY, Cell.NSWE_SOUTH); + canSeeThrough = (southGeoZ <= maxHeight) && (westGeoZ <= maxHeight) && (southGeoZ <= getNearestZ(prevX, prevY + 1, beeCurZ)) && (westGeoZ <= getNearestZ(prevX - 1, prevY, beeCurZ)); + } else { + canSeeThrough = true; + } + } + + if (!canSeeThrough) { + return false; + } + } + + prevX = curX; + prevY = curY; + prevGeoZ = curGeoZ; + ++ptIndex; + } + + return true; + } + + /** + * Verifies if the is a path between origin's location and destination, if not returns the closest location. + * @param origin the origin + * @param destination the destination + * @return the destination if there is a path or the closes location + */ + public Location moveCheck(ILocational origin, ILocational destination) { + return moveCheck(origin.getX(), origin.getY(), origin.getZ(), destination.getX(), destination.getY(), destination.getZ(), origin.getInstanceId()); + } + + /** + * Move check. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + * @param tx the target's x coordinate + * @param ty the target's y coordinate + * @param tz the target's z coordinate + * @param instanceId the instance id + * @return the last Location (x,y,z) where player can walk - just before wall + */ + public Location moveCheck(int x, int y, int z, int tx, int ty, int tz, int instanceId) { + int geoX = getGeoX(x); + int geoY = getGeoY(y); + z = getNearestZ(geoX, geoY, z); + int tGeoX = getGeoX(tx); + int tGeoY = getGeoY(ty); + tz = getNearestZ(tGeoX, tGeoY, tz); + + if (DoorData.getInstance().checkIfDoorsBetween(x, y, z, tx, ty, tz, instanceId, false)) { + return new Location(x, y, getHeight(x, y, z)); + } + + LinePointIterator pointIter = new LinePointIterator(geoX, geoY, tGeoX, tGeoY); + // first point is guaranteed to be available + pointIter.next(); + int prevX = pointIter.x(); + int prevY = pointIter.y(); + int prevZ = z; + + while (pointIter.next()) { + int curX = pointIter.x(); + int curY = pointIter.y(); + int curZ = getNearestZ(curX, curY, prevZ); + + if (hasGeoPos(prevX, prevY)) { + int nswe = GeoUtils.computeNswe(prevX, prevY, curX, curY); + if (!checkNearestNsweAntiCornerCut(prevX, prevY, prevZ, nswe)) { + // can't move, return previous location + return new Location(getWorldX(prevX), getWorldY(prevY), prevZ); + } + } + + prevX = curX; + prevY = curY; + prevZ = curZ; + } + + if (hasGeoPos(prevX, prevY) && (prevZ != tz)) { + // different floors, return start location + return new Location(x, y, z); + } + + return new Location(tx, ty, tz); + } + + /** + * Checks if its possible to move from one location to another. + * @param fromX the X coordinate to start checking from + * @param fromY the Y coordinate to start checking from + * @param fromZ the Z coordinate to start checking from + * @param toX the X coordinate to end checking at + * @param toY the Y coordinate to end checking at + * @param toZ the Z coordinate to end checking at + * @param instanceId the instance ID + * @return {@code true} if the character at start coordinates can move to end coordinates, {@code false} otherwise + */ + public boolean canMove(int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int instanceId) { + int geoX = getGeoX(fromX); + int geoY = getGeoY(fromY); + fromZ = getNearestZ(geoX, geoY, fromZ); + int tGeoX = getGeoX(toX); + int tGeoY = getGeoY(toY); + toZ = getNearestZ(tGeoX, tGeoY, toZ); + + if (DoorData.getInstance().checkIfDoorsBetween(fromX, fromY, fromZ, toX, toY, toZ, instanceId, false)) { + return false; + } + + LinePointIterator pointIter = new LinePointIterator(geoX, geoY, tGeoX, tGeoY); + // first point is guaranteed to be available + pointIter.next(); + int prevX = pointIter.x(); + int prevY = pointIter.y(); + int prevZ = fromZ; + + while (pointIter.next()) { + int curX = pointIter.x(); + int curY = pointIter.y(); + int curZ = getNearestZ(curX, curY, prevZ); + + if (hasGeoPos(prevX, prevY)) { + int nswe = GeoUtils.computeNswe(prevX, prevY, curX, curY); + if (!checkNearestNsweAntiCornerCut(prevX, prevY, prevZ, nswe)) { + return false; + } + } + + prevX = curX; + prevY = curY; + prevZ = curZ; + } + + // different floors + return !hasGeoPos(prevX, prevY) || (prevZ == toZ); + } + + public int traceTerrainZ(int x, int y, int z, int tx, int ty) { + int geoX = getGeoX(x); + int geoY = getGeoY(y); + z = getNearestZ(geoX, geoY, z); + int tGeoX = getGeoX(tx); + int tGeoY = getGeoY(ty); + + LinePointIterator pointIter = new LinePointIterator(geoX, geoY, tGeoX, tGeoY); + // first point is guaranteed to be available + pointIter.next(); + int prevZ = z; + + while (pointIter.next()) { + int curX = pointIter.x(); + int curY = pointIter.y(); + int curZ = getNearestZ(curX, curY, prevZ); + + prevZ = curZ; + } + + return prevZ; + } + + /** + * Checks if its possible to move from one location to another. + * @param from the {@code ILocational} to start checking from + * @param toX the X coordinate to end checking at + * @param toY the Y coordinate to end checking at + * @param toZ the Z coordinate to end checking at + * @return {@code true} if the character at start coordinates can move to end coordinates, {@code false} otherwise + */ + public boolean canMove(ILocational from, int toX, int toY, int toZ) { + return canMove(from.getX(), from.getY(), from.getZ(), toX, toY, toZ, from.getInstanceId()); + } + + /** + * Checks if its possible to move from one location to another. + * @param from the {@code ILocational} to start checking from + * @param to the {@code ILocational} to end checking at + * @return {@code true} if the character at start coordinates can move to end coordinates, {@code false} otherwise + */ + public boolean canMove(ILocational from, ILocational to) { + return canMove(from, to.getX(), to.getY(), to.getZ()); + } + + /** + * Checks the specified position for available geodata. + * @param x the X coordinate + * @param y the Y coordinate + * @return {@code true} if there is geodata for the given coordinates, {@code false} otherwise + */ + public boolean hasGeo(int x, int y) { + return hasGeoPos(getGeoX(x), getGeoY(y)); + } + + public static GeoData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final GeoData INSTANCE = new GeoData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/InstanceListManager.java b/src/main/java/com/l2jserver/gameserver/InstanceListManager.java new file mode 100644 index 00000000..4bda3405 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/InstanceListManager.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +/** + * Interface for managers of list of instances. + * @author fordfrog + */ +public interface InstanceListManager { + /** + * Loads instances with their data from persistent format.
+ * This method has no side effect as calling methods of another instance manager. + */ + void loadInstances(); + + /** + * For each loaded instance, updates references to related instances. + */ + void updateReferences(); + + /** + * Activates instances so their setup is performed. + */ + void activateInstances(); +} diff --git a/src/main/java/com/l2jserver/gameserver/ItemsAutoDestroy.java b/src/main/java/com/l2jserver/gameserver/ItemsAutoDestroy.java new file mode 100644 index 00000000..3280b043 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ItemsAutoDestroy.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.enums.ItemLocation; +import com.l2jserver.gameserver.instancemanager.ItemsOnGroundManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +public final class ItemsAutoDestroy { + private final Map _items = new ConcurrentHashMap<>(); + + protected ItemsAutoDestroy() { + ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(this::removeItems, 5000, 5000); + } + + public static ItemsAutoDestroy getInstance() { + return SingletonHolder._instance; + } + + public synchronized void addItem(L2ItemInstance item) { + item.setDropTime(System.currentTimeMillis()); + _items.put(item.getObjectId(), item); + } + + public synchronized void removeItems() { + final long curtime = System.currentTimeMillis(); + for (L2ItemInstance item : _items.values()) { + if (item == null) { + continue; + } + + if ((item.getDropTime() == 0) || (item.getItemLocation() != ItemLocation.VOID)) { + _items.remove(item.getObjectId()); + } else { + if (item.getItem().getAutoDestroyTime() > 0) { + if ((curtime - item.getDropTime()) > item.getItem().getAutoDestroyTime()) { + L2World.getInstance().removeVisibleObject(item, item.getWorldRegion()); + L2World.getInstance().removeObject(item); + _items.remove(item.getObjectId()); + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().removeObject(item); + } + } + } else if (item.getItem().hasExImmediateEffect()) { + if ((curtime - item.getDropTime()) > general().getAutoDestroyHerbTime()) { + L2World.getInstance().removeVisibleObject(item, item.getWorldRegion()); + L2World.getInstance().removeObject(item); + _items.remove(item.getObjectId()); + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().removeObject(item); + } + } + } else { + final long sleep = ((general().getAutoDestroyDroppedItemAfter() == 0) ? 3600000 : general().getAutoDestroyDroppedItemAfter() * 1000); + + if ((curtime - item.getDropTime()) > sleep) { + L2World.getInstance().removeVisibleObject(item, item.getWorldRegion()); + L2World.getInstance().removeObject(item); + _items.remove(item.getObjectId()); + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().removeObject(item); + } + } + } + } + } + } + + private static class SingletonHolder { + protected static final ItemsAutoDestroy _instance = new ItemsAutoDestroy(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/LoginServerThread.java b/src/main/java/com/l2jserver/gameserver/LoginServerThread.java new file mode 100644 index 00000000..9d57791c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/LoginServerThread.java @@ -0,0 +1,705 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.hexId; +import static com.l2jserver.gameserver.config.Configuration.ip; +import static com.l2jserver.gameserver.config.Configuration.server; +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.file.Files; +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.RSAPublicKeySpec; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.network.BaseSendablePacket; +import com.l2jserver.commons.security.crypt.NewCrypt; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.commons.util.Util; +import com.l2jserver.gameserver.config.Configuration; +import com.l2jserver.gameserver.config.HexIdConfiguration; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.L2GameClient.GameClientState; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.gameserverpackets.AuthRequest; +import com.l2jserver.gameserver.network.gameserverpackets.BlowFishKey; +import com.l2jserver.gameserver.network.gameserverpackets.ChangeAccessLevel; +import com.l2jserver.gameserver.network.gameserverpackets.ChangePassword; +import com.l2jserver.gameserver.network.gameserverpackets.PlayerAuthRequest; +import com.l2jserver.gameserver.network.gameserverpackets.PlayerInGame; +import com.l2jserver.gameserver.network.gameserverpackets.PlayerLogout; +import com.l2jserver.gameserver.network.gameserverpackets.PlayerTracert; +import com.l2jserver.gameserver.network.gameserverpackets.ReplyCharacters; +import com.l2jserver.gameserver.network.gameserverpackets.SendMail; +import com.l2jserver.gameserver.network.gameserverpackets.ServerStatus; +import com.l2jserver.gameserver.network.gameserverpackets.TempBan; +import com.l2jserver.gameserver.network.loginserverpackets.AuthResponse; +import com.l2jserver.gameserver.network.loginserverpackets.ChangePasswordResponse; +import com.l2jserver.gameserver.network.loginserverpackets.InitLS; +import com.l2jserver.gameserver.network.loginserverpackets.KickPlayer; +import com.l2jserver.gameserver.network.loginserverpackets.LoginServerFail; +import com.l2jserver.gameserver.network.loginserverpackets.PlayerAuthResponse; +import com.l2jserver.gameserver.network.loginserverpackets.RequestCharacters; +import com.l2jserver.gameserver.network.serverpackets.CharSelectionInfo; +import com.l2jserver.gameserver.network.serverpackets.LoginFail; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class LoginServerThread extends Thread { + + protected static final Logger LOG = LoggerFactory.getLogger(LoginServerThread.class); + + protected static final Logger LOG_ACCOUNTING = LoggerFactory.getLogger("accounting"); + + private static final int TEENAGER = 15; + + private static final int ADULT = 18; + + private final String _hostname; + private final int _port; + private final int _gamePort; + private Socket _loginSocket; + private OutputStream _out; + + /** + * The BlowFish engine used to encrypt packets
+ * It is first initialized with a unified key:
+ * "_;v.]05-31!|+-%xT!^[$\00"
+ *
+ * and then after handshake, with a new key sent by
+ * login server during the handshake. This new key is stored
+ * in blowfishKey + */ + private NewCrypt _blowfish; + private final byte[] _hexID; + private final boolean _acceptAlternate; + private final int _requestID; + private final boolean _reserveHost; + private int _maxPlayer; + private final List _waitingClients; + private final Map _accountsInGameServer = new ConcurrentHashMap<>(); + private int _status; + private String _serverName; + private final List _subnets; + private final List _hosts; + + protected LoginServerThread() { + super("LoginServerThread"); + _port = server().getLoginPort(); + _gamePort = server().getPort(); + _hostname = server().getLoginHost(); + if (hexId().getHexID() == null) { + _hexID = Util.generateHex(16); + _requestID = server().getRequestServerId(); + hexId().setProperty("ServerID", String.valueOf(_requestID)); + } else { + _hexID = hexId().getHexID().toByteArray(); + _requestID = hexId().getServerID(); + } + _acceptAlternate = server().acceptAlternateId(); + _reserveHost = server().reserveHostOnLogin(); + _subnets = ip().getSubnets(); + _hosts = ip().getHosts(); + _waitingClients = new CopyOnWriteArrayList<>(); + _maxPlayer = server().getMaxOnlineUsers(); + } + + @Override + public void run() { + while (!isInterrupted()) { + int lengthHi; + int lengthLo; + int length; + boolean checksumOk; + try { + // Connection + LOG.info("Connecting to login server on {}:{}", _hostname, _port); + _loginSocket = new Socket(_hostname, _port); + InputStream in = _loginSocket.getInputStream(); + _out = new BufferedOutputStream(_loginSocket.getOutputStream()); + + // init Blowfish + byte[] blowfishKey = Util.generateHex(40); + // Protect the new blowfish key what cannot begin with zero + if (blowfishKey[0] == 0) { + blowfishKey[0] = (byte) Rnd.get(32, 64); + } + _blowfish = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00"); + while (!isInterrupted()) { + lengthLo = in.read(); + lengthHi = in.read(); + length = (lengthHi * 256) + lengthLo; + + if (lengthHi < 0) { + LOG.info("LoginServerThread: Login terminated the connection."); + break; + } + + byte[] incoming = new byte[length - 2]; + + int receivedBytes = 0; + int newBytes = 0; + int left = length - 2; + while ((newBytes != -1) && (receivedBytes < (length - 2))) { + newBytes = in.read(incoming, receivedBytes, left); + receivedBytes = receivedBytes + newBytes; + left -= newBytes; + } + + if (receivedBytes != (length - 2)) { + LOG.warn("Incomplete Packet is sent to the server, closing connection.(LS)"); + break; + } + + // decrypt if we have a key + _blowfish.decrypt(incoming, 0, incoming.length); + checksumOk = NewCrypt.verifyChecksum(incoming); + + if (!checksumOk) { + LOG.warn("Incorrect packet checksum, ignoring packet (LS)"); + break; + } + + int packetType = incoming[0] & 0xff; + // send the blowfish key through the rsa encryption + // now, only accept packet with the new encryption + // login will close the connection here + switch (packetType) { + case 0x00 -> { + InitLS init = new InitLS(incoming); + RSAPublicKey publicKey; + try { + KeyFactory kfac = KeyFactory.getInstance("RSA"); + BigInteger modulus = new BigInteger(init.getRSAKey()); + RSAPublicKeySpec kspec1 = new RSAPublicKeySpec(modulus, RSAKeyGenParameterSpec.F4); + publicKey = (RSAPublicKey) kfac.generatePublic(kspec1); + } catch (GeneralSecurityException e) { + LOG.warn("Trouble while init the public key send by login"); + break; + } + sendPacket(new BlowFishKey(blowfishKey, publicKey)); + _blowfish = new NewCrypt(blowfishKey); + sendPacket(new AuthRequest(_requestID, _acceptAlternate, _hexID, _gamePort, _reserveHost, _maxPlayer, _subnets, _hosts)); + } + case 0x01 -> { + LoginServerFail lsf = new LoginServerFail(incoming); + LOG.info("Damn! Registration Failed: {}", lsf.getReasonString()); + } + case 0x02 -> { + AuthResponse aresp = new AuthResponse(incoming); + int serverID = aresp.getServerId(); + _serverName = aresp.getServerName(); + saveHexid(serverID, hexToString(_hexID)); + LOG.info("Registered on login as Server {}: {}", serverID, _serverName); + ServerStatus st = new ServerStatus(); + if (general().getServerListBrackets()) { + st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.ON); + } else { + st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.OFF); + } + st.addAttribute(ServerStatus.SERVER_TYPE, general().getServerListType()); + if (general().serverGMOnly()) { + st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY); + } else { + st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO); + } + if (general().getServerListAge() == TEENAGER) { + st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_15); + } else if (general().getServerListAge() == ADULT) { + st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_18); + } else { + st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_ALL); + } + sendPacket(st); + if (L2World.getInstance().getAllPlayersCount() > 0) { + final List playerList = new ArrayList<>(); + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + playerList.add(player.getAccountName()); + } + sendPacket(new PlayerInGame(playerList)); + } + } + case 0x03 -> { + PlayerAuthResponse par = new PlayerAuthResponse(incoming); + String account = par.getAccount(); + WaitingClient wcToRemove = null; + synchronized (_waitingClients) { + for (WaitingClient wc : _waitingClients) { + if (wc.account.equals(account)) { + wcToRemove = wc; + } + } + } + if (wcToRemove != null) { + if (par.isAuthed()) { + PlayerInGame pig = new PlayerInGame(par.getAccount()); + sendPacket(pig); + wcToRemove.gameClient.setState(GameClientState.AUTHED); + wcToRemove.gameClient.setSessionId(wcToRemove.session); + CharSelectionInfo cl = new CharSelectionInfo(wcToRemove.account, wcToRemove.gameClient.getSessionId().playOkID1); + wcToRemove.gameClient.getConnection().sendPacket(cl); + wcToRemove.gameClient.setCharSelection(cl.getCharInfo()); + } else { + LOG.warn("Session key is not correct. Closing connection for account {}.", wcToRemove.account); + // wcToRemove.gameClient.getConnection().sendPacket(new LoginFail(LoginFail.SYSTEM_ERROR_LOGIN_LATER)); + wcToRemove.gameClient.close(new LoginFail(LoginFail.SYSTEM_ERROR_LOGIN_LATER)); + _accountsInGameServer.remove(wcToRemove.account); + } + _waitingClients.remove(wcToRemove); + } + } + case 0x04 -> { + KickPlayer kp = new KickPlayer(incoming); + doKickPlayer(kp.getAccount()); + } + case 0x05 -> { + RequestCharacters rc = new RequestCharacters(incoming); + getCharsOnServer(rc.getAccount()); + } + case 0x06 -> new ChangePasswordResponse(incoming); + } + } + } catch (UnknownHostException e) { + LOG.warn("Unknown host!", e); + } catch (SocketException e) { + LOG.warn("LoginServer not avaible, trying to reconnect..."); + } catch (IOException e) { + LOG.warn("Disconnected from Login, Trying to reconnect!", e); + } finally { + try { + _loginSocket.close(); + if (isInterrupted()) { + return; + } + } catch (Exception e) { + } + } + + try { + Thread.sleep(5000); // 5 seconds tempo. + } catch (InterruptedException e) { + return; // never swallow an interrupt! + } + } + } + + /** + * Adds the waiting client and send request. + * @param acc the account + * @param client the game client + * @param key the session key + */ + public void addWaitingClientAndSendRequest(String acc, L2GameClient client, SessionKey key) { + WaitingClient wc = new WaitingClient(acc, client, key); + synchronized (_waitingClients) { + _waitingClients.add(wc); + } + PlayerAuthRequest par = new PlayerAuthRequest(acc, key); + try { + sendPacket(par); + } catch (IOException e) { + LOG.warn("Error while sending player auth request!"); + } + } + + /** + * Removes the waiting client. + * @param client the client + */ + public void removeWaitingClient(L2GameClient client) { + WaitingClient toRemove = null; + synchronized (_waitingClients) { + for (WaitingClient c : _waitingClients) { + if (c.gameClient == client) { + toRemove = c; + } + } + if (toRemove != null) { + _waitingClients.remove(toRemove); + } + } + } + + /** + * Send logout for the given account. + * @param account the account + */ + public void sendLogout(String account) { + if (account == null) { + return; + } + PlayerLogout pl = new PlayerLogout(account); + try { + sendPacket(pl); + } catch (IOException e) { + LOG.warn("Error while sending logout packet to login!"); + } finally { + _accountsInGameServer.remove(account); + } + } + + /** + * Adds the game server login. + * @param account the account + * @param client the client + * @return {@code true} if account was not already logged in, {@code false} otherwise + */ + public boolean addGameServerLogin(String account, L2GameClient client) { + return _accountsInGameServer.putIfAbsent(account, client) == null; + } + + /** + * Send access level. + * @param account the account + * @param level the access level + */ + public void sendAccessLevel(String account, int level) { + ChangeAccessLevel cal = new ChangeAccessLevel(account, level); + try { + sendPacket(cal); + } catch (IOException e) { + } + } + + /** + * Send client tracert. + * @param account the account + * @param address the address + */ + public void sendClientTracert(String account, String[] address) { + PlayerTracert ptc = new PlayerTracert(account, address[0], address[1], address[2], address[3], address[4]); + try { + sendPacket(ptc); + } catch (IOException e) { + } + } + + /** + * Send mail. + * @param account the account + * @param mailId the mail id + * @param args the args + */ + public void sendMail(String account, String mailId, String... args) { + SendMail sem = new SendMail(account, mailId, args); + try { + sendPacket(sem); + } catch (IOException e) { + } + } + + /** + * Send temp ban. + * @param account the account + * @param ip the ip + * @param time the time + */ + public void sendTempBan(String account, String ip, long time) { + TempBan tbn = new TempBan(account, ip, time); + try { + sendPacket(tbn); + } catch (IOException e) { + } + } + + /** + * Hex to string. + * @param hex the hex value + * @return the hex value as string + */ + private String hexToString(byte[] hex) { + return new BigInteger(hex).toString(16); + } + + /** + * Kick player for the given account. + * @param account the account + */ + public void doKickPlayer(String account) { + L2GameClient client = _accountsInGameServer.get(account); + if (client != null) { + LOG_ACCOUNTING.warn("Kicked by login: {}", client); + client.setAdditionalClosePacket(SystemMessage.getSystemMessage(SystemMessageId.ANOTHER_LOGIN_WITH_ACCOUNT)); + client.closeNow(); + } + } + + /** + * Gets the chars on server. + * @param account the account + */ + private void getCharsOnServer(String account) { + + int chars = 0; + List charToDel = new ArrayList<>(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT deletetime FROM characters WHERE account_name=?")) { + ps.setString(1, account); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + chars++; + long delTime = rs.getLong("deletetime"); + if (delTime != 0) { + charToDel.add(delTime); + } + } + } + } catch (SQLException e) { + LOG.warn("Exception: getCharsOnServer!", e); + } + + ReplyCharacters rec = new ReplyCharacters(account, chars, charToDel); + try { + sendPacket(rec); + } catch (IOException e) { + } + } + + /** + * Send packet. + * @param sl the sendable packet + * @throws IOException Signals that an I/O exception has occurred. + */ + private void sendPacket(BaseSendablePacket sl) throws IOException { + byte[] data = sl.getContent(); + NewCrypt.appendChecksum(data); + _blowfish.crypt(data, 0, data.length); + + int len = data.length + 2; + synchronized (_out) // avoids tow threads writing in the mean time + { + _out.write(len & 0xff); + _out.write((len >> 8) & 0xff); + _out.write(data); + _out.flush(); + } + } + + /** + * Sets the max player. + * @param maxPlayer The maxPlayer to set. + */ + public void setMaxPlayer(int maxPlayer) { + sendServerStatus(ServerStatus.MAX_PLAYERS, maxPlayer); + _maxPlayer = maxPlayer; + } + + /** + * Gets the max player. + * @return Returns the maxPlayer. + */ + public int getMaxPlayer() { + return _maxPlayer; + } + + /** + * Send server status. + * @param id the id + * @param value the value + */ + public void sendServerStatus(int id, int value) { + ServerStatus ss = new ServerStatus(); + ss.addAttribute(id, value); + try { + sendPacket(ss); + } catch (IOException e) { + } + } + + /** + * Send Server Type Config to LS. + */ + public void sendServerType() { + ServerStatus ss = new ServerStatus(); + ss.addAttribute(ServerStatus.SERVER_TYPE, general().getServerListType()); + try { + sendPacket(ss); + } catch (IOException e) { + } + } + + /** + * Send change password. + * @param accountName the account name + * @param charName the char name + * @param oldpass the old pass + * @param newpass the new pass + */ + public void sendChangePassword(String accountName, String charName, String oldpass, String newpass) { + ChangePassword cp = new ChangePassword(accountName, charName, oldpass, newpass); + try { + sendPacket(cp); + } catch (IOException e) { + } + } + + /** + * Gets the status string. + * @return the status string + */ + public String getStatusString() { + return ServerStatus.STATUS_STRING[_status]; + } + + /** + * Gets the server name. + * @return the server name. + */ + public String getServerName() { + return _serverName; + } + + /** + * Sets the server status. + * @param status the new server status + */ + public void setServerStatus(int status) { + switch (status) { + case ServerStatus.STATUS_AUTO -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO); + _status = status; + } + case ServerStatus.STATUS_DOWN -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_DOWN); + _status = status; + } + case ServerStatus.STATUS_FULL -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_FULL); + _status = status; + } + case ServerStatus.STATUS_GM_ONLY -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY); + _status = status; + } + case ServerStatus.STATUS_GOOD -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GOOD); + _status = status; + } + case ServerStatus.STATUS_NORMAL -> { + sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_NORMAL); + _status = status; + } + default -> throw new IllegalArgumentException("Status does not exists:" + status); + } + } + + public L2GameClient getClient(String name) { + return name != null ? _accountsInGameServer.get(name) : null; + } + + public static class SessionKey { + public int playOkID1; + public int playOkID2; + public int loginOkID1; + public int loginOkID2; + + /** + * Instantiates a new session key. + * @param loginOK1 the login o k1 + * @param loginOK2 the login o k2 + * @param playOK1 the play o k1 + * @param playOK2 the play o k2 + */ + public SessionKey(int loginOK1, int loginOK2, int playOK1, int playOK2) { + playOkID1 = playOK1; + playOkID2 = playOK2; + loginOkID1 = loginOK1; + loginOkID2 = loginOK2; + } + + @Override + public String toString() { + return "PlayOk: " + playOkID1 + " " + playOkID2 + " LoginOk:" + loginOkID1 + " " + loginOkID2; + } + } + + private static class WaitingClient { + public String account; + public L2GameClient gameClient; + public SessionKey session; + + /** + * Instantiates a new waiting client. + * @param acc the acc + * @param client the client + * @param key the key + */ + public WaitingClient(String acc, L2GameClient client, SessionKey key) { + account = acc; + gameClient = client; + session = key; + } + } + + /** + * Save hexadecimal ID of the server in the L2Properties file. + * @param serverId the ID of the server whose hexId to save + * @param hexId the hexadecimal ID to store + */ + public static void saveHexid(int serverId, String hexId) { + final var hexIdFilePath = Configuration.getCustomOrDefaultPath(HexIdConfiguration.FILENAME); + hexId().setProperty(HexIdConfiguration.SERVERID_KEY, String.valueOf(serverId)); + hexId().setProperty(HexIdConfiguration.HEXID_KEY, hexId); + + try { + Files.createDirectories(hexIdFilePath.getParent()); + try (var out = Files.newOutputStream(hexIdFilePath, CREATE, TRUNCATE_EXISTING)) { + hexId().store(out, "the hexID to auth into login"); + LOG.info("Saved {}.", hexIdFilePath); + } + } catch (Exception ex) { + LOG.warn("Failed to save {}.", hexIdFilePath, ex); + } + } + + /** + * Gets the single instance of LoginServerThread. + * @return single instance of LoginServerThread + */ + public static LoginServerThread getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final LoginServerThread _instance = new LoginServerThread(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/MonsterRace.java b/src/main/java/com/l2jserver/gameserver/MonsterRace.java new file mode 100644 index 00000000..344654a9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/MonsterRace.java @@ -0,0 +1,125 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.L2Npc; + +public class MonsterRace { + protected static final Logger LOG = LoggerFactory.getLogger(MonsterRace.class); + + private final L2Npc[] _monsters; + private int[][] _speeds; + private final int[] _first, _second; + + protected MonsterRace() { + _monsters = new L2Npc[8]; + _speeds = new int[8][20]; + _first = new int[2]; + _second = new int[2]; + } + + public static MonsterRace getInstance() { + return SingletonHolder._instance; + } + + public void newRace() { + int random; + + for (int i = 0; i < 8; i++) { + int id = 31003; + random = Rnd.get(24); + while (true) { + for (int j = i - 1; j >= 0; j--) { + if (_monsters[j].getTemplate().getId() == (id + random)) { + random = Rnd.get(24); + } + } + break; + } + try { + final var objectId = IdFactory.getInstance().getNextId(); + final var template = NpcData.getInstance().getTemplate(id + random); + final var constructor = Class.forName("com.l2jserver.gameserver.model.actor.instance." + template.getType() + "Instance").getConstructors()[0]; + _monsters[i] = (L2Npc) constructor.newInstance(objectId, template); + } catch (Exception e) { + LOG.warn("Unable to create monster!", e); + } + } + newSpeeds(); + } + + public void newSpeeds() { + _speeds = new int[8][20]; + int total; + _first[1] = 0; + _second[1] = 0; + for (int i = 0; i < 8; i++) { + total = 0; + for (int j = 0; j < 20; j++) { + if (j == 19) { + _speeds[i][j] = 100; + } else { + _speeds[i][j] = Rnd.get(60) + 65; + } + total += _speeds[i][j]; + } + if (total >= _first[1]) { + _second[0] = _first[0]; + _second[1] = _first[1]; + _first[0] = 8 - i; + _first[1] = total; + } else if (total >= _second[1]) { + _second[0] = 8 - i; + _second[1] = total; + } + } + } + + /** + * @return Returns the monsters. + */ + public L2Npc[] getMonsters() { + return _monsters; + } + + /** + * @return Returns the speeds. + */ + public int[][] getSpeeds() { + return _speeds; + } + + public int getFirstPlace() { + return _first[0]; + } + + public int getSecondPlace() { + return _second[0]; + } + + private static class SingletonHolder { + protected static final MonsterRace _instance = new MonsterRace(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/RecipeController.java b/src/main/java/com/l2jserver/gameserver/RecipeController.java new file mode 100644 index 00000000..d9384204 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/RecipeController.java @@ -0,0 +1,664 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.data.xml.impl.RecipeData; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.StatType; +import com.l2jserver.gameserver.model.L2ManufactureItem; +import com.l2jserver.gameserver.model.L2RecipeInstance; +import com.l2jserver.gameserver.model.L2RecipeList; +import com.l2jserver.gameserver.model.L2RecipeStatInstance; +import com.l2jserver.gameserver.model.TempItem; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.RecipeBookItemList; +import com.l2jserver.gameserver.network.serverpackets.RecipeItemMakeInfo; +import com.l2jserver.gameserver.network.serverpackets.RecipeShopItemInfo; +import com.l2jserver.gameserver.network.serverpackets.SetupGauge; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +public class RecipeController { + protected static final Map _activeMakers = new ConcurrentHashMap<>(); + + protected RecipeController() { + // Prevent external initialization. + } + + public void requestBookOpen(L2PcInstance player, boolean isDwarvenCraft) { + // Check if player is trying to alter recipe book while engaged in manufacturing. + if (!_activeMakers.containsKey(player.getObjectId())) { + RecipeBookItemList response = new RecipeBookItemList(isDwarvenCraft, player.getMaxMp()); + response.addRecipes(isDwarvenCraft ? player.getDwarvenRecipeBook() : player.getCommonRecipeBook()); + player.sendPacket(response); + return; + } + player.sendPacket(SystemMessageId.CANT_ALTER_RECIPEBOOK_WHILE_CRAFTING); + } + + public void requestMakeItemAbort(L2PcInstance player) { + _activeMakers.remove(player.getObjectId()); // TODO: anything else here? + } + + public void requestManufactureItem(L2PcInstance manufacturer, int recipeListId, L2PcInstance player) { + final L2RecipeList recipeList = RecipeData.getInstance().getValidRecipeList(player, recipeListId); + if (recipeList == null) { + return; + } + + List dwarfRecipes = Arrays.asList(manufacturer.getDwarvenRecipeBook()); + List commonRecipes = Arrays.asList(manufacturer.getCommonRecipeBook()); + + if (!dwarfRecipes.contains(recipeList) && !commonRecipes.contains(recipeList)) { + Util.handleIllegalPlayerAction(player, "Warning!! Character " + player.getName() + " of account " + player.getAccountName() + " sent a false recipe id."); + return; + } + + // Check if manufacturer is under manufacturing store or private store. + if (character().alternativeCrafting() && _activeMakers.containsKey(manufacturer.getObjectId())) { + player.sendPacket(SystemMessageId.CLOSE_STORE_WINDOW_AND_TRY_AGAIN); + return; + } + + final RecipeItemMaker maker = new RecipeItemMaker(manufacturer, recipeList, player); + if (maker._isValid) { + if (character().alternativeCrafting()) { + _activeMakers.put(manufacturer.getObjectId(), maker); + ThreadPoolManager.getInstance().scheduleGeneral(maker, 100); + } else { + maker.run(); + } + } + } + + public void requestMakeItem(L2PcInstance player, int recipeListId) { + // Check if player is trying to operate a private store or private workshop while engaged in combat. + if (player.isInCombat() || player.isInDuel()) { + player.sendPacket(SystemMessageId.CANT_OPERATE_PRIVATE_STORE_DURING_COMBAT); + return; + } + + final L2RecipeList recipeList = RecipeData.getInstance().getValidRecipeList(player, recipeListId); + if (recipeList == null) { + return; + } + + List dwarfRecipes = Arrays.asList(player.getDwarvenRecipeBook()); + List commonRecipes = Arrays.asList(player.getCommonRecipeBook()); + + if (!dwarfRecipes.contains(recipeList) && !commonRecipes.contains(recipeList)) { + Util.handleIllegalPlayerAction(player, "Warning!! Character " + player.getName() + " of account " + player.getAccountName() + " sent a false recipe id."); + return; + } + + // Check if player is busy (possible if alt game creation is enabled) + if (character().alternativeCrafting() && _activeMakers.containsKey(player.getObjectId())) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1); + sm.addItemName(recipeList.getItemId()); + sm.addString("You are busy creating."); + player.sendPacket(sm); + return; + } + + final RecipeItemMaker maker = new RecipeItemMaker(player, recipeList, player); + if (maker._isValid) { + if (character().alternativeCrafting()) { + _activeMakers.put(player.getObjectId(), maker); + ThreadPoolManager.getInstance().scheduleGeneral(maker, 100); + } else { + maker.run(); + } + } + } + + private static class RecipeItemMaker implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(RecipeItemMaker.class); + + protected boolean _isValid; + protected List _items = null; + protected final L2RecipeList _recipeList; + protected final L2PcInstance _player; // "crafter" + protected final L2PcInstance _target; // "customer" + protected final Skill _skill; + protected final int _skillId; + protected final int _skillLevel; + protected int _creationPasses = 1; + protected int _itemGrab; + protected int _exp = -1; + protected int _sp = -1; + protected long _price; + protected int _totalItems; + protected int _delay; + + public RecipeItemMaker(L2PcInstance pPlayer, L2RecipeList pRecipeList, L2PcInstance pTarget) { + _player = pPlayer; + _target = pTarget; + _recipeList = pRecipeList; + + _isValid = false; + _skillId = _recipeList.isDwarvenRecipe() ? CommonSkill.CREATE_DWARVEN.getId() : CommonSkill.CREATE_COMMON.getId(); + _skillLevel = _player.getSkillLevel(_skillId); + _skill = _player.getKnownSkill(_skillId); + + _player.isInCraftMode(true); + + if (_player.isAlikeDead()) { + _player.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + if (_target.isAlikeDead()) { + _target.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + if (_target.isProcessingTransaction()) { + _target.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + if (_player.isProcessingTransaction()) { + _player.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + // validate recipe list + if (_recipeList.getRecipes().length == 0) { + _player.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + // validate skill level + if (_recipeList.getLevel() > _skillLevel) { + _player.sendPacket(ActionFailed.STATIC_PACKET); + abort(); + return; + } + + // check that customer can afford to pay for creation services + if (_player != _target) { + final L2ManufactureItem item = _player.getManufactureItems().get(_recipeList.getId()); + if (item != null) { + _price = item.getCost(); + if (_target.getAdena() < _price) // check price + { + _target.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + abort(); + return; + } + } + } + + // make temporary items + _items = listItems(false); + if (_items == null) { + abort(); + return; + } + + for (TempItem i : _items) { + _totalItems += i.getQuantity(); + } + + // initial statUse checks + if (!calculateStatUse(false, false)) { + abort(); + return; + } + + // initial AltStatChange checks + if (character().alternativeCrafting()) { + calculateAltStatChange(); + } + + updateMakeInfo(true); + updateCurMp(); + updateCurLoad(); + + _player.isInCraftMode(false); + _isValid = true; + } + + @Override + public void run() { + if (!character().crafting()) { + _target.sendMessage("Item creation is currently disabled."); + abort(); + return; + } + + if (_player == null) { + LOG.warn("Cannot perform craft, player is null!"); + abort(); + return; + } + + if (_target == null) { + LOG.warn("Cannot perform craft, target is null!"); + abort(); + return; + } + + if (!_player.isOnline()) { + LOG.warn("Cannot perform craft, player is not online!"); + abort(); + return; + } + + if (!_target.isOnline()) { + LOG.warn("Cannot perform craft, target is not online!"); + abort(); + return; + } + + if (character().alternativeCrafting() && !_activeMakers.containsKey(_player.getObjectId())) { + if (_target != _player) { + _target.sendMessage("Manufacture aborted!"); + _player.sendMessage("Manufacture aborted!"); + } else { + _player.sendMessage("Item creation aborted!"); + } + + abort(); + return; + } + + if (character().alternativeCrafting() && !_items.isEmpty()) { + + if (!calculateStatUse(true, true)) { + return; // check stat use + } + updateCurMp(); // update craft window mp bar + + grabSomeItems(); // grab (equip) some more items with a nice msg to player + + // if still not empty, schedule another pass + if (!_items.isEmpty()) { + _delay = (int) (character().getCraftingSpeed() * _player.getMReuseRate(_skill) * GameTimeController.TICKS_PER_SECOND * GameTimeController.MILLIS_IN_TICK); + + // FIXME: please fix this packet to show crafting animation (somebody) + _player.broadcastPacket(new MagicSkillUse(_player, _skillId, _skillLevel, _delay, 0)); + + _player.sendPacket(new SetupGauge(0, _delay)); + ThreadPoolManager.getInstance().scheduleGeneral(this, 100 + _delay); + } else { + // for alt mode, sleep delay msec before finishing + _player.sendPacket(new SetupGauge(0, _delay)); + + try { + Thread.sleep(_delay); + } catch (InterruptedException e) { + } finally { + finishCrafting(); + } + } + } // for old craft mode just finish + else { + finishCrafting(); + } + } + + private void finishCrafting() { + if (!character().alternativeCrafting()) { + calculateStatUse(false, true); + } + + // first take adena for manufacture + if ((_target != _player) && (_price > 0)) // customer must pay for services + { + // attempt to pay for item + L2ItemInstance adenatransfer = _target.transferItem("PayManufacture", _target.getInventory().getAdenaInstance().getObjectId(), _price, _player.getInventory(), _player); + + if (adenatransfer == null) { + _target.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + abort(); + return; + } + } + + _items = listItems(true); // this line actually takes materials from inventory + if (_items == null) { + // handle possible cheaters here + // (they click craft then try to get rid of items in order to get free craft) + } else if (Rnd.get(100) < _recipeList.getSuccessRate()) { + rewardPlayer(); // and immediately puts created item in its place + updateMakeInfo(true); + } else { + if (_target != _player) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.CREATION_OF_S2_FOR_C1_AT_S3_ADENA_FAILED); + msg.addString(_target.getName()); + msg.addItemName(_recipeList.getItemId()); + msg.addLong(_price); + _player.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_FAILED_TO_CREATE_S2_FOR_S3_ADENA); + msg.addString(_player.getName()); + msg.addItemName(_recipeList.getItemId()); + msg.addLong(_price); + _target.sendPacket(msg); + } else { + _target.sendPacket(SystemMessageId.ITEM_MIXING_FAILED); + } + updateMakeInfo(false); + } + // update load and mana bar of craft window + updateCurMp(); + updateCurLoad(); + _activeMakers.remove(_player.getObjectId()); + _player.isInCraftMode(false); + _target.sendPacket(new ItemList(_target, false)); + } + + private void updateMakeInfo(boolean success) { + if (_target == _player) { + _target.sendPacket(new RecipeItemMakeInfo(_recipeList.getId(), _target, success)); + } else { + _target.sendPacket(new RecipeShopItemInfo(_player, _recipeList.getId())); + } + } + + private void updateCurLoad() { + StatusUpdate su = new StatusUpdate(_target); + su.addAttribute(StatusUpdate.CUR_LOAD, _target.getCurrentLoad()); + _target.sendPacket(su); + } + + private void updateCurMp() { + StatusUpdate su = new StatusUpdate(_target); + su.addAttribute(StatusUpdate.CUR_MP, (int) _target.getCurrentMp()); + _target.sendPacket(su); + } + + private void grabSomeItems() { + int grabItems = _itemGrab; + while ((grabItems > 0) && !_items.isEmpty()) { + TempItem item = _items.get(0); + + int count = item.getQuantity(); + if (count >= grabItems) { + count = grabItems; + } + + item.setQuantity(item.getQuantity() - count); + if (item.getQuantity() <= 0) { + _items.remove(0); + } else { + _items.set(0, item); + } + + grabItems -= count; + + if (_target == _player) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_S2_EQUIPPED); // you equipped ... + sm.addLong(count); + sm.addItemName(item.getItemId()); + _player.sendPacket(sm); + } else { + _target.sendMessage("Manufacturer " + _player.getName() + " used " + count + " " + item.getItemName()); + } + } + } + + // AltStatChange parameters make their effect here + private void calculateAltStatChange() { + _itemGrab = _skillLevel; + + for (L2RecipeStatInstance altStatChange : _recipeList.getAltStatChange()) { + if (altStatChange.getType() == StatType.XP) { + _exp = altStatChange.getValue(); + } else if (altStatChange.getType() == StatType.SP) { + _sp = altStatChange.getValue(); + } else if (altStatChange.getType() == StatType.GIM) { + _itemGrab *= altStatChange.getValue(); + } + } + // determine number of creation passes needed + _creationPasses = (_totalItems / _itemGrab) + ((_totalItems % _itemGrab) != 0 ? 1 : 0); + if (_creationPasses < 1) { + _creationPasses = 1; + } + } + + // StatUse + private boolean calculateStatUse(boolean isWait, boolean isReduce) { + boolean ret = true; + for (L2RecipeStatInstance statUse : _recipeList.getStatUse()) { + double modifiedValue = statUse.getValue() / _creationPasses; + if (statUse.getType() == StatType.HP) { + // we do not want to kill the player, so its CurrentHP must be greater than the reduce value + if (_player.getCurrentHp() <= modifiedValue) { + // rest (wait for HP) + if (character().alternativeCrafting() && isWait) { + _player.sendPacket(new SetupGauge(0, _delay)); + ThreadPoolManager.getInstance().scheduleGeneral(this, 100 + _delay); + } else { + _target.sendPacket(SystemMessageId.NOT_ENOUGH_HP); + abort(); + } + ret = false; + } else if (isReduce) { + _player.reduceCurrentHp(modifiedValue, _player, _skill); + } + } else if (statUse.getType() == StatType.MP) { + if (_player.getCurrentMp() < modifiedValue) { + // rest (wait for MP) + if (character().alternativeCrafting() && isWait) { + _player.sendPacket(new SetupGauge(0, _delay)); + ThreadPoolManager.getInstance().scheduleGeneral(this, 100 + _delay); + } else { + _target.sendPacket(SystemMessageId.NOT_ENOUGH_MP); + abort(); + } + ret = false; + } else if (isReduce) { + _player.reduceCurrentMp(modifiedValue); + } + } else { + // there is an unknown StatUse value + _target.sendMessage("Recipe error!!!, please tell this to your GM."); + ret = false; + abort(); + } + } + return ret; + } + + private List listItems(boolean remove) { + L2RecipeInstance[] recipes = _recipeList.getRecipes(); + Inventory inv = _target.getInventory(); + List materials = new ArrayList<>(); + SystemMessage sm; + + for (L2RecipeInstance recipe : recipes) { + if (recipe.getQuantity() > 0) { + L2ItemInstance item = inv.getItemByItemId(recipe.getItemId()); + long itemQuantityAmount = item == null ? 0 : item.getCount(); + + // check materials + if (itemQuantityAmount < recipe.getQuantity()) { + sm = SystemMessage.getSystemMessage(SystemMessageId.MISSING_S2_S1_TO_CREATE); + sm.addItemName(recipe.getItemId()); + sm.addLong(recipe.getQuantity() - itemQuantityAmount); + _target.sendPacket(sm); + + abort(); + return null; + } + + // make new temporary object, just for counting purposes + materials.add(new TempItem(item, recipe.getQuantity())); + } + } + + if (remove) { + for (TempItem tmp : materials) { + inv.destroyItemByItemId("Manufacture", tmp.getItemId(), tmp.getQuantity(), _target, _player); + + if (tmp.getQuantity() > 1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(tmp.getItemId()); + sm.addLong(tmp.getQuantity()); + _target.sendPacket(sm); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(tmp.getItemId()); + _target.sendPacket(sm); + } + } + } + return materials; + } + + private void abort() { + updateMakeInfo(false); + _player.isInCraftMode(false); + _activeMakers.remove(_player.getObjectId()); + } + + private void rewardPlayer() { + int rareProdId = _recipeList.getRareItemId(); + int itemId = _recipeList.getItemId(); + int itemCount = _recipeList.getCount(); + L2Item template = ItemTable.getInstance().getTemplate(itemId); + + // check that the current recipe has a rare production or not + if ((rareProdId != -1) && ((rareProdId == itemId) || character().craftMasterwork())) { + if (Rnd.get(100) < _recipeList.getRarity()) { + itemId = rareProdId; + itemCount = _recipeList.getRareCount(); + } + } + + _target.getInventory().addItem("Manufacture", itemId, itemCount, _target, _player); + + // inform customer of earned item + SystemMessage sm; + if (_target != _player) { + // inform manufacturer of earned profit + if (itemCount == 1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_CREATED_FOR_C1_FOR_S3_ADENA); + sm.addString(_target.getName()); + sm.addItemName(itemId); + sm.addLong(_price); + _player.sendPacket(sm); + + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_CREATED_S2_FOR_S3_ADENA); + sm.addString(_player.getName()); + sm.addItemName(itemId); + sm.addLong(_price); + _target.sendPacket(sm); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S3_S_CREATED_FOR_C1_FOR_S4_ADENA); + sm.addString(_target.getName()); + sm.addInt(itemCount); + sm.addItemName(itemId); + sm.addLong(_price); + _player.sendPacket(sm); + + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_CREATED_S2_S3_S_FOR_S4_ADENA); + sm.addString(_player.getName()); + sm.addInt(itemCount); + sm.addItemName(itemId); + sm.addLong(_price); + _target.sendPacket(sm); + } + } + + if (itemCount > 1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S); + sm.addItemName(itemId); + sm.addLong(itemCount); + _target.sendPacket(sm); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_ITEM_S1); + sm.addItemName(itemId); + _target.sendPacket(sm); + } + + if (character().alternativeCrafting()) { + int recipeLevel = _recipeList.getLevel(); + if (_exp < 0) { + _exp = template.getReferencePrice() * itemCount; + _exp /= recipeLevel; + } + if (_sp < 0) { + _sp = _exp / 10; + } + if (itemId == rareProdId) { + _exp *= character().getCraftingRareXpRate(); + _sp *= character().getCraftingRareSpRate(); + } + + if (_exp < 0) { + _exp = 0; + } + if (_sp < 0) { + _sp = 0; + } + + for (int i = _skillLevel; i > recipeLevel; i--) { + _exp /= 4; + _sp /= 4; + } + + // Added multiplication of Creation speed with XP/SP gain slower crafting -> more XP, + // faster crafting -> less XP you can use ALT_GAME_CREATION_XP_RATE/SP to modify XP/SP gained (default = 1) + _player.addExpAndSp((long) (_exp * character().getCraftingXpRate() * character().getCraftingSpeed()), // + (int) (_sp * character().getCraftingSpRate() * character().getCraftingSpeed())); + } + updateMakeInfo(true); // success + } + } + + public static RecipeController getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final RecipeController _instance = new RecipeController(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/SevenSigns.java b/src/main/java/com/l2jserver/gameserver/SevenSigns.java new file mode 100644 index 00000000..4ad4a72c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/SevenSigns.java @@ -0,0 +1,1392 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.config.Configuration.sevenSigns; +import static com.l2jserver.gameserver.network.SystemMessageId.COMPETITION_PERIOD_BEGUN; +import static com.l2jserver.gameserver.network.SystemMessageId.DAWN_OBTAINED_AVARICE; +import static com.l2jserver.gameserver.network.SystemMessageId.DAWN_OBTAINED_GNOSIS; +import static com.l2jserver.gameserver.network.SystemMessageId.DAWN_OBTAINED_STRIFE; +import static com.l2jserver.gameserver.network.SystemMessageId.DAWN_WON; +import static com.l2jserver.gameserver.network.SystemMessageId.DUSK_OBTAINED_AVARICE; +import static com.l2jserver.gameserver.network.SystemMessageId.DUSK_OBTAINED_GNOSIS; +import static com.l2jserver.gameserver.network.SystemMessageId.DUSK_OBTAINED_STRIFE; +import static com.l2jserver.gameserver.network.SystemMessageId.DUSK_WON; +import static com.l2jserver.gameserver.network.SystemMessageId.PREPARATIONS_PERIOD_BEGUN; +import static com.l2jserver.gameserver.network.SystemMessageId.QUEST_EVENT_PERIOD_BEGUN; +import static com.l2jserver.gameserver.network.SystemMessageId.QUEST_EVENT_PERIOD_ENDED; +import static com.l2jserver.gameserver.network.SystemMessageId.RESULTS_PERIOD_BEGUN; +import static com.l2jserver.gameserver.network.SystemMessageId.SEAL_OF_STRIFE_FORBIDS_SUMMONING; +import static com.l2jserver.gameserver.network.SystemMessageId.SEAL_VALIDATION_PERIOD_BEGUN; +import static com.l2jserver.gameserver.network.SystemMessageId.SEAL_VALIDATION_PERIOD_ENDED; +import static com.l2jserver.gameserver.network.SystemMessageId.VALIDATION_PERIOD_BEGUN; + +import java.sql.SQLException; +import java.util.Calendar; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.model.AutoSpawnHandler; +import com.l2jserver.gameserver.model.AutoSpawnHandler.AutoSpawnInstance; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SSQInfo; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * Seven Signs engine. + * @author Tempy + */ +public class SevenSigns { + + private static final Logger LOG = LoggerFactory.getLogger(SevenSigns.class); + + public static final String SEVEN_SIGNS_HTML_PATH = "data/html/seven_signs/"; + + public static final int CABAL_NULL = 0; + public static final int CABAL_DUSK = 1; + public static final int CABAL_DAWN = 2; + + public static final int SEAL_NULL = 0; + public static final int SEAL_AVARICE = 1; + public static final int SEAL_GNOSIS = 2; + public static final int SEAL_STRIFE = 3; + + public static final int PERIOD_COMP_RECRUITING = 0; + public static final int PERIOD_COMPETITION = 1; + public static final int PERIOD_COMP_RESULTS = 2; + public static final int PERIOD_SEAL_VALIDATION = 3; + + public static final int PERIOD_START_HOUR = 18; + public static final int PERIOD_START_MINS = 0; + public static final int PERIOD_START_DAY = Calendar.MONDAY; + + // The quest event and seal validation periods last for approximately one week + // with a 15 minutes "interval" period sandwiched between them. + public static final int PERIOD_MINOR_LENGTH = 900000; + public static final int PERIOD_MAJOR_LENGTH = 604800000 - PERIOD_MINOR_LENGTH; + + public static final int RECORD_SEVEN_SIGNS_ID = 5707; + public static final int RECORD_SEVEN_SIGNS_COST = 500; + + // NPC Related Constants \\ + public static final int ORATOR_NPC_ID = 31094; + public static final int PREACHER_NPC_ID = 31093; + public static final int MAMMON_MERCHANT_ID = 31113; + public static final int MAMMON_BLACKSMITH_ID = 31126; + public static final int MAMMON_MARKETEER_ID = 31092; + public static final int LILITH_NPC_ID = 25283; + public static final int ANAKIM_NPC_ID = 25286; + public static final int CREST_OF_DAWN_ID = 31170; + public static final int CREST_OF_DUSK_ID = 31171; + + // Seal Stone Related Constants + public static final int SEAL_STONE_BLUE_ID = 6360; + public static final int SEAL_STONE_GREEN_ID = 6361; + public static final int SEAL_STONE_RED_ID = 6362; + + public static final int[] SEAL_STONE_IDS = { + SEAL_STONE_BLUE_ID, + SEAL_STONE_GREEN_ID, + SEAL_STONE_RED_ID + }; + + public static final int SEAL_STONE_BLUE_VALUE = 3; + public static final int SEAL_STONE_GREEN_VALUE = 5; + public static final int SEAL_STONE_RED_VALUE = 10; + + public static final int BLUE_CONTRIB_POINTS = 3; + public static final int GREEN_CONTRIB_POINTS = 5; + public static final int RED_CONTRIB_POINTS = 10; + + private final Calendar _nextPeriodChange = Calendar.getInstance(); + + protected int _activePeriod; + protected int _currentCycle; + protected double _dawnStoneScore; + protected double _duskStoneScore; + protected int _dawnFestivalScore; + protected int _duskFestivalScore; + protected int _compWinner; + protected int _previousWinner; + protected Calendar _lastSave = Calendar.getInstance(); + + protected Map _signsPlayerData = new LinkedHashMap<>(); + private final Map _signsSealOwners = new LinkedHashMap<>(); + private final Map _signsDuskSealTotals = new LinkedHashMap<>(); + private final Map _signsDawnSealTotals = new LinkedHashMap<>(); + + private static final String LOAD_DATA = "SELECT charId, cabal, seal, position, paid_type, red_stones, green_stones, blue_stones, ancient_adena_amount, contribution_score FROM seven_signs"; + + private static final String LOAD_STATUS = "SELECT * FROM seven_signs_status WHERE id=0"; + + private static final String INSERT_PLAYER = "INSERT INTO seven_signs (charId, cabal, seal, position, paid_type) VALUES (?,?,?,?,?)"; + + private static final String UPDATE_PLAYER = "UPDATE seven_signs SET cabal=?, seal=?, position=?, paid_type=?, red_stones=?, green_stones=?, blue_stones=?, ancient_adena_amount=?, contribution_score=? WHERE charId=?"; + + private static final String UPDATE_STATUS = "UPDATE seven_signs_status SET current_cycle=?, active_period=?, previous_winner=?, " + "dawn_stone_score=?, dawn_festival_score=?, dusk_stone_score=?, dusk_festival_score=?, " + + "avarice_owner=?, gnosis_owner=?, strife_owner=?, avarice_dawn_score=?, gnosis_dawn_score=?, " + "strife_dawn_score=?, avarice_dusk_score=?, gnosis_dusk_score=?, strife_dusk_score=?, " + "festival_cycle=?, accumulated_bonus0=?, accumulated_bonus1=?, accumulated_bonus2=?," + + "accumulated_bonus3=?, accumulated_bonus4=?, date=? WHERE id=0"; + + protected SevenSigns() { + try { + restoreSevenSignsData(); + } catch (Exception ex) { + LOG.error("Failed to load configuration!", ex); + } + + LOG.info("Currently in the {} period.", getCurrentPeriodName()); + + initializeSeals(); + + if (isSealValidationPeriod()) { + if (getCabalHighestScore() == CABAL_NULL) { + LOG.info("The competition ended with a tie last week."); + } else { + LOG.info("The {} were victorious last week.", getCabalName(getCabalHighestScore())); + } + } else if (getCabalHighestScore() == CABAL_NULL) { + LOG.info("The competition, if the current trend continues, will end in a tie this week."); + } else { + LOG.info("The {} are in the lead this week.", getCabalName(getCabalHighestScore())); + } + + long milliToChange = 0; + if (isNextPeriodChangeInPast()) { + LOG.info("Next period change was in the past (server was offline), changing periods now!"); + } else { + setCalendarForNextPeriodChange(); + milliToChange = getMilliToPeriodChange(); + } + + // Schedule a time for the next period change. + SevenSignsPeriodChange sspc = new SevenSignsPeriodChange(); + ThreadPoolManager.getInstance().scheduleGeneral(sspc, milliToChange); + + // Thanks to http://rainbow.arch.scriptmania.com/scripts/timezone_countdown.html for help with this. + double numSecs = (milliToChange / 1000) % 60; + double countDown = ((milliToChange / 1000.0) - numSecs) / 60; + int numMins = (int) Math.floor(countDown % 60); + countDown = (countDown - numMins) / 60; + int numHours = (int) Math.floor(countDown % 24); + int numDays = (int) Math.floor((countDown - numHours) / 24); + + LOG.info("Next period begins in {} days, {} hours and {} mins.", numDays, numHours, numMins); + + } + + private boolean isNextPeriodChangeInPast() { + Calendar lastPeriodChange = Calendar.getInstance(); + switch (getCurrentPeriod()) { + case PERIOD_SEAL_VALIDATION, PERIOD_COMPETITION -> { + lastPeriodChange.set(Calendar.DAY_OF_WEEK, PERIOD_START_DAY); + lastPeriodChange.set(Calendar.HOUR_OF_DAY, PERIOD_START_HOUR); + lastPeriodChange.set(Calendar.MINUTE, PERIOD_START_MINS); + lastPeriodChange.set(Calendar.SECOND, 0); + // if we hit next week, just turn back 1 week + if (Calendar.getInstance().before(lastPeriodChange)) { + lastPeriodChange.add(Calendar.HOUR, -24 * 7); + } + } + // because of the short duration of this period, just check it from last save + case PERIOD_COMP_RECRUITING, PERIOD_COMP_RESULTS -> lastPeriodChange.setTimeInMillis(_lastSave.getTimeInMillis() + PERIOD_MINOR_LENGTH); + } + + // because of previous "date" column usage, check only if it already contains usable data for us + return (_lastSave.getTimeInMillis() > 7) && _lastSave.before(lastPeriodChange); + } + + /** + * Registers all random spawns and auto-chats for Seven Signs NPCs, along with spawns for the Preachers of Doom and Orators of Revelations at the beginning of the Seal Validation period. + */ + public void spawnSevenSignsNPC() { + final AutoSpawnInstance merchantSpawn = AutoSpawnHandler.getInstance().getAutoSpawnInstance(MAMMON_MERCHANT_ID, false); + final AutoSpawnInstance blacksmithSpawn = AutoSpawnHandler.getInstance().getAutoSpawnInstance(MAMMON_BLACKSMITH_ID, false); + final AutoSpawnInstance lilithSpawn = AutoSpawnHandler.getInstance().getAutoSpawnInstance(LILITH_NPC_ID, false); + final AutoSpawnInstance anakimSpawn = AutoSpawnHandler.getInstance().getAutoSpawnInstance(ANAKIM_NPC_ID, false); + final List crestOfDawnSpawns = AutoSpawnHandler.getInstance().getAutoSpawnInstances(CREST_OF_DAWN_ID); + final List crestOfDuskSpawns = AutoSpawnHandler.getInstance().getAutoSpawnInstances(CREST_OF_DUSK_ID); + final List oratorSpawns = AutoSpawnHandler.getInstance().getAutoSpawnInstances(ORATOR_NPC_ID); + final List preacherSpawns = AutoSpawnHandler.getInstance().getAutoSpawnInstances(PREACHER_NPC_ID); + final List marketeerSpawns = AutoSpawnHandler.getInstance().getAutoSpawnInstances(MAMMON_MARKETEER_ID); + + if (isSealValidationPeriod() || isCompResultsPeriod()) { + for (AutoSpawnInstance spawnInst : marketeerSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, true); + } + + if ((getSealOwner(SEAL_GNOSIS) == getCabalHighestScore()) && (getSealOwner(SEAL_GNOSIS) != CABAL_NULL)) { + if (!npc().announceMammonSpawn()) { + blacksmithSpawn.setBroadcast(false); + } + + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(blacksmithSpawn.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(blacksmithSpawn, true); + } + + for (AutoSpawnInstance spawnInst : oratorSpawns) { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(spawnInst.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, true); + } + } + + for (AutoSpawnInstance spawnInst : preacherSpawns) { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(spawnInst.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, true); + } + } + } else { + AutoSpawnHandler.getInstance().setSpawnActive(blacksmithSpawn, false); + + for (AutoSpawnInstance spawnInst : oratorSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, false); + } + + for (AutoSpawnInstance spawnInst : preacherSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, false); + } + } + + if ((getSealOwner(SEAL_AVARICE) == getCabalHighestScore()) && (getSealOwner(SEAL_AVARICE) != CABAL_NULL)) { + if (!npc().announceMammonSpawn()) { + merchantSpawn.setBroadcast(false); + } + + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(merchantSpawn.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(merchantSpawn, true); + } + + switch (getCabalHighestScore()) { + case CABAL_DAWN -> { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(lilithSpawn.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(lilithSpawn, true); + } + AutoSpawnHandler.getInstance().setSpawnActive(anakimSpawn, false); + for (AutoSpawnInstance dawnCrest : crestOfDawnSpawns) { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(dawnCrest.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(dawnCrest, true); + } + } + for (AutoSpawnInstance duskCrest : crestOfDuskSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(duskCrest, false); + } + } + case CABAL_DUSK -> { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(anakimSpawn.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(anakimSpawn, true); + } + AutoSpawnHandler.getInstance().setSpawnActive(lilithSpawn, false); + for (AutoSpawnInstance duskCrest : crestOfDuskSpawns) { + if (!AutoSpawnHandler.getInstance().getAutoSpawnInstance(duskCrest.getObjectId(), true).isSpawnActive()) { + AutoSpawnHandler.getInstance().setSpawnActive(duskCrest, true); + } + } + for (AutoSpawnInstance dawnCrest : crestOfDawnSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(dawnCrest, false); + } + } + } + } else { + AutoSpawnHandler.getInstance().setSpawnActive(merchantSpawn, false); + AutoSpawnHandler.getInstance().setSpawnActive(lilithSpawn, false); + AutoSpawnHandler.getInstance().setSpawnActive(anakimSpawn, false); + for (AutoSpawnInstance dawnCrest : crestOfDawnSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(dawnCrest, false); + } + for (AutoSpawnInstance duskCrest : crestOfDuskSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(duskCrest, false); + } + } + } else { + AutoSpawnHandler.getInstance().setSpawnActive(merchantSpawn, false); + AutoSpawnHandler.getInstance().setSpawnActive(blacksmithSpawn, false); + AutoSpawnHandler.getInstance().setSpawnActive(lilithSpawn, false); + AutoSpawnHandler.getInstance().setSpawnActive(anakimSpawn, false); + for (AutoSpawnInstance dawnCrest : crestOfDawnSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(dawnCrest, false); + } + for (AutoSpawnInstance duskCrest : crestOfDuskSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(duskCrest, false); + } + for (AutoSpawnInstance spawnInst : oratorSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, false); + } + + for (AutoSpawnInstance spawnInst : preacherSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, false); + } + + for (AutoSpawnInstance spawnInst : marketeerSpawns) { + AutoSpawnHandler.getInstance().setSpawnActive(spawnInst, false); + } + } + } + + public static String getCabalShortName(int cabal) { + return switch (cabal) { + case CABAL_DAWN -> "dawn"; + case CABAL_DUSK -> "dusk"; + default -> "No Cabal"; + }; + + } + + public static String getCabalName(int cabal) { + return switch (cabal) { + case CABAL_DAWN -> "Lords of Dawn"; + case CABAL_DUSK -> "Revolutionaries of Dusk"; + default -> "No Cabal"; + }; + } + + public static String getSealName(int seal, boolean shortName) { + String sealName = (!shortName) ? "Seal of " : ""; + + switch (seal) { + case SEAL_AVARICE -> sealName += "Avarice"; + case SEAL_GNOSIS -> sealName += "Gnosis"; + case SEAL_STRIFE -> sealName += "Strife"; + } + return sealName; + } + + public final int getCurrentCycle() { + return _currentCycle; + } + + public final int getCurrentPeriod() { + return _activePeriod; + } + + private int getDaysToPeriodChange() { + int numDays = _nextPeriodChange.get(Calendar.DAY_OF_WEEK) - PERIOD_START_DAY; + if (numDays < 0) { + return -numDays; + } + return 7 - numDays; + } + + public final long getMilliToPeriodChange() { + return (_nextPeriodChange.getTimeInMillis() - System.currentTimeMillis()); + } + + protected void setCalendarForNextPeriodChange() { + // Calculate the number of days until the next period + // A period starts at 18:00 pm (local time), like on official servers. + // Otherwise... + switch (getCurrentPeriod()) { + case PERIOD_SEAL_VALIDATION, PERIOD_COMPETITION -> { + int daysToChange = getDaysToPeriodChange(); + if (daysToChange == 7) { + if (_nextPeriodChange.get(Calendar.HOUR_OF_DAY) < PERIOD_START_HOUR) { + daysToChange = 0; + } else if ((_nextPeriodChange.get(Calendar.HOUR_OF_DAY) == PERIOD_START_HOUR) && (_nextPeriodChange.get(Calendar.MINUTE) < PERIOD_START_MINS)) { + daysToChange = 0; + } + } + if (daysToChange > 0) { + _nextPeriodChange.add(Calendar.DATE, daysToChange); + } + _nextPeriodChange.set(Calendar.HOUR_OF_DAY, PERIOD_START_HOUR); + _nextPeriodChange.set(Calendar.MINUTE, PERIOD_START_MINS); + } + case PERIOD_COMP_RECRUITING, PERIOD_COMP_RESULTS -> _nextPeriodChange.add(Calendar.MILLISECOND, PERIOD_MINOR_LENGTH); + } + LOG.info("Next period change set to {}.", _nextPeriodChange.getTime()); + } + + public final String getCurrentPeriodName() { + return switch (_activePeriod) { + case PERIOD_COMP_RECRUITING -> "Quest Event Initialization"; + case PERIOD_COMPETITION -> "Competition (Quest Event)"; + case PERIOD_COMP_RESULTS -> "Quest Event Results"; + case PERIOD_SEAL_VALIDATION -> "Seal Validation"; + default -> null; + }; + } + + /** + * @return {@code true} if it's competition period, {@code false} otherwise + */ + public final boolean isCompetitionPeriod() { + return (_activePeriod == PERIOD_COMPETITION); + } + + public final boolean isSealValidationPeriod() { + return (_activePeriod == PERIOD_SEAL_VALIDATION); + } + + public final boolean isCompResultsPeriod() { + return (_activePeriod == PERIOD_COMP_RESULTS); + } + + public final boolean isInitializationPeriod() { + return (_activePeriod == PERIOD_COMP_RECRUITING); + } + + public boolean isJoinableToDawn(L2PcInstance player) { + return (player.getClan() != null) && (player.getClan().getCastleId() > 0); + } + + /** + * returns true if the given date is in Seal Validation or in Quest Event Results period + * @param date + * @return + */ + public boolean isDateInSealValidPeriod(Calendar date) { + long nextPeriodChange = getMilliToPeriodChange(); + long nextQuestStart = 0; + long nextValidStart = 0; + long tillDate = date.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + while (((2 * PERIOD_MAJOR_LENGTH) + (2 * PERIOD_MINOR_LENGTH)) < tillDate) { + tillDate -= ((2 * PERIOD_MAJOR_LENGTH) + (2 * PERIOD_MINOR_LENGTH)); + } + while (tillDate < 0) { + tillDate += ((2 * PERIOD_MAJOR_LENGTH) + (2 * PERIOD_MINOR_LENGTH)); + } + + switch (getCurrentPeriod()) { + case PERIOD_COMP_RECRUITING -> { + nextValidStart = nextPeriodChange + PERIOD_MAJOR_LENGTH; + nextQuestStart = nextValidStart + PERIOD_MAJOR_LENGTH + PERIOD_MINOR_LENGTH; + } + case PERIOD_COMPETITION -> { + nextValidStart = nextPeriodChange; + nextQuestStart = nextPeriodChange + PERIOD_MAJOR_LENGTH + PERIOD_MINOR_LENGTH; + } + case PERIOD_COMP_RESULTS -> { + nextQuestStart = nextPeriodChange + PERIOD_MAJOR_LENGTH; + nextValidStart = nextQuestStart + PERIOD_MAJOR_LENGTH + PERIOD_MINOR_LENGTH; + } + case PERIOD_SEAL_VALIDATION -> { + nextQuestStart = nextPeriodChange; + nextValidStart = nextPeriodChange + PERIOD_MAJOR_LENGTH + PERIOD_MINOR_LENGTH; + } + } + + return ((nextQuestStart >= tillDate) || (tillDate >= nextValidStart)) && ((nextValidStart >= nextQuestStart) || ((tillDate >= nextValidStart) && (nextQuestStart >= tillDate))); + } + + public final int getCurrentScore(int cabal) { + double totalStoneScore = _dawnStoneScore + _duskStoneScore; + return switch (cabal) { + case CABAL_NULL -> 0; + case CABAL_DAWN -> Math.round((float) (_dawnStoneScore / ((float) totalStoneScore == 0 ? 1 : totalStoneScore)) * 500) + _dawnFestivalScore; + case CABAL_DUSK -> Math.round((float) (_duskStoneScore / ((float) totalStoneScore == 0 ? 1 : totalStoneScore)) * 500) + _duskFestivalScore; + default -> 0; + }; + } + + public final double getCurrentStoneScore(int cabal) { + return switch (cabal) { + case CABAL_NULL -> 0; + case CABAL_DAWN -> _dawnStoneScore; + case CABAL_DUSK -> _duskStoneScore; + default -> 0; + }; + } + + public final int getCurrentFestivalScore(int cabal) { + return switch (cabal) { + case CABAL_NULL -> 0; + case CABAL_DAWN -> _dawnFestivalScore; + case CABAL_DUSK -> _duskFestivalScore; + default -> 0; + }; + } + + public final int getCabalHighestScore() { + if (getCurrentScore(CABAL_DUSK) == getCurrentScore(CABAL_DAWN)) { + return CABAL_NULL; + } else if (getCurrentScore(CABAL_DUSK) > getCurrentScore(CABAL_DAWN)) { + return CABAL_DUSK; + } else { + return CABAL_DAWN; + } + } + + public final int getSealOwner(int seal) { + return _signsSealOwners.get(seal); + } + + public final int getSealProportion(int seal, int cabal) { + if (cabal == CABAL_NULL) { + return 0; + } else if (cabal == CABAL_DUSK) { + return _signsDuskSealTotals.get(seal); + } else { + return _signsDawnSealTotals.get(seal); + } + } + + public final int getTotalMembers(int cabal) { + int cabalMembers = 0; + String cabalName = getCabalShortName(cabal); + + for (StatsSet sevenDat : _signsPlayerData.values()) { + if (sevenDat.getString("cabal").equals(cabalName)) { + cabalMembers++; + } + } + + return cabalMembers; + } + + public int getPlayerStoneContrib(int objectId) { + final StatsSet currPlayer = _signsPlayerData.get(objectId); + if (currPlayer == null) { + return 0; + } + + int stoneCount = 0; + stoneCount += currPlayer.getInt("red_stones"); + stoneCount += currPlayer.getInt("green_stones"); + stoneCount += currPlayer.getInt("blue_stones"); + + return stoneCount; + } + + public int getDepositedSSQItemCount(L2PcInstance player, int stoneId) { + final StatsSet currPlayer = _signsPlayerData.get(player.getObjectId()); + if (currPlayer == null) { + return 0; + } + + return switch (stoneId) { + case SEAL_STONE_BLUE_ID -> currPlayer.getInt("blue_stones"); + case SEAL_STONE_GREEN_ID -> currPlayer.getInt("green_stones"); + case SEAL_STONE_RED_ID -> currPlayer.getInt("red_stones"); + default -> 0; + }; + } + + public int deleteDepositedSSQItemAndGiveRewards(L2PcInstance player, int stoneId, int amount) { + StatsSet currPlayer = _signsPlayerData.get(player.getObjectId()); + + int rewardAmount = amount * switch (stoneId) { + case SEAL_STONE_BLUE_ID -> SEAL_STONE_BLUE_VALUE; + case SEAL_STONE_GREEN_ID -> SEAL_STONE_GREEN_VALUE; + case SEAL_STONE_RED_ID -> SEAL_STONE_RED_VALUE; + default -> 0; + }; + + player.addAncientAdena("SevenSigns", rewardAmount, player, true); + + currPlayer.set("red_stones", 0); + currPlayer.set("green_stones", 0); + currPlayer.set("blue_stones", 0); + + _signsPlayerData.put(player.getObjectId(), currPlayer); + if (!sevenSigns().sevenSignsLazyUpdate()) { + saveSevenSignsData(player.getObjectId()); + saveSevenSignsStatus(); + } + + return rewardAmount; + } + + public int getPlayerContribScore(int objectId) { + final StatsSet currPlayer = _signsPlayerData.get(objectId); + if (currPlayer == null) { + return 0; + } + + return currPlayer.getInt("contribution_score"); + } + + public int getPlayerAdenaCollect(int objectId) { + final StatsSet currPlayer = _signsPlayerData.get(objectId); + if (currPlayer == null) { + return 0; + } + + return currPlayer.getInt("ancient_adena_amount"); + } + + public int getPlayerSeal(int objectId) { + final StatsSet currPlayer = _signsPlayerData.get(objectId); + if (currPlayer == null) { + return SEAL_NULL; + } + + return currPlayer.getInt("seal"); + } + + public int getPlayerCabal(int objectId) { + final StatsSet currPlayer = _signsPlayerData.get(objectId); + if (currPlayer == null) { + return CABAL_NULL; + } + + String playerCabal = currPlayer.getString("cabal"); + if (playerCabal.equalsIgnoreCase("dawn")) { + return CABAL_DAWN; + } else if (playerCabal.equalsIgnoreCase("dusk")) { + return CABAL_DUSK; + } else { + return CABAL_NULL; + } + } + + public int getSSQPrevWinner() { + return _previousWinner; + } + + /** + * Restores all Seven Signs data and settings, usually called at server startup. + */ + protected void restoreSevenSignsData() { + try (var con = ConnectionFactory.getInstance().getConnection()) { + try (var s = con.createStatement(); + var rs = s.executeQuery(LOAD_DATA)) { + while (rs.next()) { + var charObjId = rs.getInt("charId"); + var sevenDat = new StatsSet(); + sevenDat.set("charId", charObjId); + sevenDat.set("cabal", rs.getString("cabal")); + sevenDat.set("seal", rs.getInt("seal")); + sevenDat.set("position", rs.getInt("position")); + sevenDat.set("paid_type", rs.getInt("paid_type")); + sevenDat.set("red_stones", rs.getInt("red_stones")); + sevenDat.set("green_stones", rs.getInt("green_stones")); + sevenDat.set("blue_stones", rs.getInt("blue_stones")); + sevenDat.set("ancient_adena_amount", rs.getDouble("ancient_adena_amount")); + sevenDat.set("contribution_score", rs.getDouble("contribution_score")); + _signsPlayerData.put(charObjId, sevenDat); + } + } + + try (var s = con.createStatement(); + var rs = s.executeQuery(LOAD_STATUS)) { + + while (rs.next()) { + _currentCycle = rs.getInt("current_cycle"); + _activePeriod = rs.getInt("active_period"); + _previousWinner = rs.getInt("previous_winner"); + + _dawnStoneScore = rs.getDouble("dawn_stone_score"); + _dawnFestivalScore = rs.getInt("dawn_festival_score"); + _duskStoneScore = rs.getDouble("dusk_stone_score"); + _duskFestivalScore = rs.getInt("dusk_festival_score"); + + _signsSealOwners.put(SEAL_AVARICE, rs.getInt("avarice_owner")); + _signsSealOwners.put(SEAL_GNOSIS, rs.getInt("gnosis_owner")); + _signsSealOwners.put(SEAL_STRIFE, rs.getInt("strife_owner")); + + _signsDawnSealTotals.put(SEAL_AVARICE, rs.getInt("avarice_dawn_score")); + _signsDawnSealTotals.put(SEAL_GNOSIS, rs.getInt("gnosis_dawn_score")); + _signsDawnSealTotals.put(SEAL_STRIFE, rs.getInt("strife_dawn_score")); + _signsDuskSealTotals.put(SEAL_AVARICE, rs.getInt("avarice_dusk_score")); + _signsDuskSealTotals.put(SEAL_GNOSIS, rs.getInt("gnosis_dusk_score")); + _signsDuskSealTotals.put(SEAL_STRIFE, rs.getInt("strife_dusk_score")); + + _lastSave.setTimeInMillis(rs.getLong("date")); + } + } + } catch (SQLException ex) { + LOG.warn("Unable to load Seven Signs data from database!", ex); + } + } + + /** + * Saves all Seven Signs player data.
+ * Should be called on period change and shutdown only. + */ + public void saveSevenSignsData() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_PLAYER)) { + for (StatsSet sevenDat : _signsPlayerData.values()) { + ps.setString(1, sevenDat.getString("cabal")); + ps.setInt(2, sevenDat.getInt("seal")); + ps.setInt(3, sevenDat.getInt("position")); + ps.setInt(4, sevenDat.getInt("paid_type")); + + ps.setInt(5, sevenDat.getInt("red_stones")); + ps.setInt(6, sevenDat.getInt("green_stones")); + ps.setInt(7, sevenDat.getInt("blue_stones")); + ps.setDouble(8, sevenDat.getDouble("ancient_adena_amount")); + ps.setDouble(9, sevenDat.getDouble("contribution_score")); + ps.setInt(10, sevenDat.getInt("charId")); + ps.addBatch(); + } + ps.executeBatch(); + } catch (SQLException ex) { + LOG.warn("Unable to save data to database!", ex); + } + } + + public final void saveSevenSignsData(int objectId) { + StatsSet sevenDat = _signsPlayerData.get(objectId); + if (sevenDat == null) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_PLAYER)) { + ps.setString(1, sevenDat.getString("cabal")); + ps.setInt(2, sevenDat.getInt("seal")); + ps.setInt(3, sevenDat.getInt("position")); + ps.setInt(4, sevenDat.getInt("paid_type")); + + ps.setInt(5, sevenDat.getInt("red_stones")); + ps.setInt(6, sevenDat.getInt("green_stones")); + ps.setInt(7, sevenDat.getInt("blue_stones")); + ps.setDouble(8, sevenDat.getDouble("ancient_adena_amount")); + ps.setDouble(9, sevenDat.getDouble("contribution_score")); + ps.setInt(10, sevenDat.getInt("charId")); + ps.execute(); + } catch (SQLException ex) { + LOG.error("Unable to save data to database!", ex); + } + } + + public final void saveSevenSignsStatus() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_STATUS)) { + ps.setInt(1, _currentCycle); + ps.setInt(2, _activePeriod); + ps.setInt(3, _previousWinner); + ps.setDouble(4, _dawnStoneScore); + ps.setInt(5, _dawnFestivalScore); + ps.setDouble(6, _duskStoneScore); + ps.setInt(7, _duskFestivalScore); + ps.setInt(8, _signsSealOwners.get(SEAL_AVARICE)); + ps.setInt(9, _signsSealOwners.get(SEAL_GNOSIS)); + ps.setInt(10, _signsSealOwners.get(SEAL_STRIFE)); + ps.setInt(11, _signsDawnSealTotals.get(SEAL_AVARICE)); + ps.setInt(12, _signsDawnSealTotals.get(SEAL_GNOSIS)); + ps.setInt(13, _signsDawnSealTotals.get(SEAL_STRIFE)); + ps.setInt(14, _signsDuskSealTotals.get(SEAL_AVARICE)); + ps.setInt(15, _signsDuskSealTotals.get(SEAL_GNOSIS)); + ps.setInt(16, _signsDuskSealTotals.get(SEAL_STRIFE)); + ps.setInt(17, SevenSignsFestival.getInstance().getCurrentFestivalCycle()); + + for (int i = 0; i < SevenSignsFestival.FESTIVAL_COUNT; i++) { + ps.setInt(18 + i, SevenSignsFestival.getInstance().getAccumulatedBonus(i)); + } + _lastSave = Calendar.getInstance(); + ps.setLong(18 + SevenSignsFestival.FESTIVAL_COUNT, _lastSave.getTimeInMillis()); + ps.execute(); + } catch (SQLException ex) { + LOG.warn("Unable to save data to database!", ex); + } + } + + /** + * Used to reset the cabal details of all players, and update the database.
+ * Primarily used when beginning a new cycle, and should otherwise never be called. + */ + protected void resetPlayerData() { + // Reset each player's contribution data as well as seal and cabal. + for (StatsSet sevenDat : _signsPlayerData.values()) { + // Reset the player's cabal and seal information + sevenDat.set("cabal", ""); + sevenDat.set("seal", SEAL_NULL); + sevenDat.set("position", 0); + sevenDat.set("paid_type", 0); + sevenDat.set("contribution_score", 0); + } + } + + /** + * Used to specify cabal-related details for the specified player.
+ * This method checks to see if the player has registered before and will update the database if necessary. + * @param player + * @param chosenCabal + * @param chosenSeal + * @param position + * @param score + * @param paidType + * @return the cabal ID the player has joined. + */ + public int addSSQMember(L2PcInstance player, int chosenCabal, int chosenSeal, int position, int score, int paidType) { + StatsSet currPlayerData = _signsPlayerData.get(player.getObjectId()); + + if (currPlayerData != null) { + // If the seal validation period has passed, + // cabal information was removed and so "re-register" player + currPlayerData.set("cabal", getCabalShortName(chosenCabal)); + currPlayerData.set("seal", chosenSeal); + currPlayerData.set("position", position); + currPlayerData.set("paid_type", paidType); + currPlayerData.set("contribution_score", score); + + _signsPlayerData.put(player.getObjectId(), currPlayerData); + } else { + currPlayerData = new StatsSet(); + currPlayerData.set("charId", player.getObjectId()); + currPlayerData.set("cabal", getCabalShortName(chosenCabal)); + currPlayerData.set("seal", chosenSeal); + currPlayerData.set("position", position); + currPlayerData.set("paid_type", paidType); + currPlayerData.set("red_stones", 0); + currPlayerData.set("green_stones", 0); + currPlayerData.set("blue_stones", 0); + currPlayerData.set("ancient_adena_amount", 0); + currPlayerData.set("contribution_score", 0); + + _signsPlayerData.put(player.getObjectId(), currPlayerData); + + // Update data in database, as we have a new player signing up. + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_PLAYER)) { + ps.setInt(1, player.getObjectId()); + ps.setString(2, getCabalShortName(chosenCabal)); + ps.setInt(3, chosenSeal); + ps.setInt(4, position); + ps.setInt(5, paidType); + ps.execute(); + } catch (SQLException ex) { + LOG.warn("Failed to save data!", ex); + } + } + + // Increasing Seal total score for the player chosen Seal. + if ("dawn".equals(currPlayerData.getString("cabal"))) { + _signsDawnSealTotals.put(chosenSeal, _signsDawnSealTotals.get(chosenSeal) + 1); + } else { + _signsDuskSealTotals.put(chosenSeal, _signsDuskSealTotals.get(chosenSeal) + 1); + } + + if (!sevenSigns().sevenSignsLazyUpdate()) { + saveSevenSignsStatus(); + } + + return chosenCabal; + } + + /** + * Returns the amount of ancient adena the specified player can claim, if any.
+ * If removeReward = True, all the ancient adena owed to them is removed, then DB is updated. + * @param objectId + * @param removeReward + * @return + */ + public int getAncientAdenaReward(int objectId, boolean removeReward) { + StatsSet currPlayer = _signsPlayerData.get(objectId); + int rewardAmount = currPlayer.getInt("ancient_adena_amount"); + + currPlayer.set("red_stones", 0); + currPlayer.set("green_stones", 0); + currPlayer.set("blue_stones", 0); + currPlayer.set("ancient_adena_amount", 0); + + if (removeReward) { + _signsPlayerData.put(objectId, currPlayer); + if (!sevenSigns().sevenSignsLazyUpdate()) { + saveSevenSignsData(objectId); + saveSevenSignsStatus(); + } + } + + return rewardAmount; + } + + /** + * Used to add the specified player's seal stone contribution points to the current total for their cabal.
+ * Returns the point score the contribution was worth.
+ * Each stone count must be broken down and specified by the stone's color. + * @param blueCount + * @param greenCount + * @param redCount + * @return + */ + public long depositSSQItem(L2PcInstance player, long blueCount, long greenCount, long redCount) { + StatsSet currPlayer = _signsPlayerData.get(player.getObjectId()); + + long contribScore = calcContributionScore(blueCount, greenCount, redCount); + long totalAncientAdena = currPlayer.getLong("ancient_adena_amount") + calcAncientAdenaReward(blueCount, greenCount, redCount); + long totalContribScore = currPlayer.getLong("contribution_score") + contribScore; + + if (totalContribScore > sevenSigns().getMaxPlayerContrib()) { + return -1; + } + + currPlayer.set("red_stones", currPlayer.getInt("red_stones") + redCount); + currPlayer.set("green_stones", currPlayer.getInt("green_stones") + greenCount); + currPlayer.set("blue_stones", currPlayer.getInt("blue_stones") + blueCount); + currPlayer.set("ancient_adena_amount", totalAncientAdena); + currPlayer.set("contribution_score", totalContribScore); + _signsPlayerData.put(player.getObjectId(), currPlayer); + + switch (getPlayerCabal(player.getObjectId())) { + case CABAL_DAWN -> _dawnStoneScore += contribScore; + case CABAL_DUSK -> _duskStoneScore += contribScore; + } + + if (!sevenSigns().sevenSignsLazyUpdate()) { + saveSevenSignsData(player.getObjectId()); + saveSevenSignsStatus(); + } + + return contribScore; + } + + public static long calcContributionScore(long blueCount, long greenCount, long redCount) { + long contrib = blueCount * BLUE_CONTRIB_POINTS; + contrib += greenCount * GREEN_CONTRIB_POINTS; + contrib += redCount * RED_CONTRIB_POINTS; + + return contrib; + } + + public static long calcAncientAdenaReward(long blueCount, long greenCount, long redCount) { + long reward = blueCount * SEAL_STONE_BLUE_VALUE; + reward += greenCount * SEAL_STONE_GREEN_VALUE; + reward += redCount * SEAL_STONE_RED_VALUE; + + return reward; + } + + /** + * Adds the specified number of festival points to the specified cabal.
+ * Remember, the same number of points are deducted from the rival cabal to maintain proportionality. + * @param cabal + * @param amount + */ + public void addFestivalScore(int cabal, int amount) { + if (cabal == CABAL_DUSK) { + _duskFestivalScore += amount; + + // To prevent negative scores! + if (_dawnFestivalScore >= amount) { + _dawnFestivalScore -= amount; + } + } else { + _dawnFestivalScore += amount; + + if (_duskFestivalScore >= amount) { + _duskFestivalScore -= amount; + } + } + } + + /** + * Send info on the current Seven Signs period to the specified player. + * @param player + */ + public void sendCurrentPeriodMsg(L2PcInstance player) { + SystemMessage sm = switch (getCurrentPeriod()) { + case PERIOD_COMP_RECRUITING -> SystemMessage.getSystemMessage(PREPARATIONS_PERIOD_BEGUN); + case PERIOD_COMPETITION -> SystemMessage.getSystemMessage(COMPETITION_PERIOD_BEGUN); + case PERIOD_COMP_RESULTS -> SystemMessage.getSystemMessage(RESULTS_PERIOD_BEGUN); + case PERIOD_SEAL_VALIDATION -> SystemMessage.getSystemMessage(VALIDATION_PERIOD_BEGUN); + default -> null; + }; + player.sendPacket(sm); + } + + /** + * Sends the built-in system message specified by sysMsgId to all online players. + * @param sysMsgId + */ + public void sendMessageToAll(SystemMessageId sysMsgId) { + Broadcast.toAllOnlinePlayers(SystemMessage.getSystemMessage(sysMsgId)); + } + + /** + * Used to initialize the seals for each cabal.
+ * (Used at startup or at beginning of a new cycle).
+ * This method should be called after resetSeals() and calcNewSealOwners() on a new cycle. + */ + protected void initializeSeals() { + for (Entry e : _signsSealOwners.entrySet()) { + if (e.getValue() != CABAL_NULL) { + if (isSealValidationPeriod()) { + LOG.info("The {} have won the {}.", getCabalName(e.getValue()), getSealName(e.getKey(), false)); + } else { + LOG.info("The {} is currently owned by {}.", getSealName(e.getKey(), false), getCabalName(e.getValue())); + } + } else { + LOG.info("The {} remains unclaimed.", getSealName(e.getKey(), false)); + } + } + } + + /** + * Only really used at the beginning of a new cycle, this method resets all seal-related data. + */ + protected void resetSeals() { + _signsDawnSealTotals.put(SEAL_AVARICE, 0); + _signsDawnSealTotals.put(SEAL_GNOSIS, 0); + _signsDawnSealTotals.put(SEAL_STRIFE, 0); + _signsDuskSealTotals.put(SEAL_AVARICE, 0); + _signsDuskSealTotals.put(SEAL_GNOSIS, 0); + _signsDuskSealTotals.put(SEAL_STRIFE, 0); + } + + /** + * Calculates the ownership of the three Seals of the Seven Signs, based on various criterion.
+ *
+ * Should only ever called at the beginning of a new cycle. + */ + protected void calcNewSealOwners() { + for (Integer currSeal : _signsDawnSealTotals.keySet()) { + int prevSealOwner = _signsSealOwners.get(currSeal); + int newSealOwner = CABAL_NULL; + int dawnProportion = getSealProportion(currSeal, CABAL_DAWN); + int totalDawnMembers = getTotalMembers(CABAL_DAWN) == 0 ? 1 : getTotalMembers(CABAL_DAWN); + int dawnPercent = Math.round(((float) dawnProportion / (float) totalDawnMembers) * 100); + int duskProportion = getSealProportion(currSeal, CABAL_DUSK); + int totalDuskMembers = getTotalMembers(CABAL_DUSK) == 0 ? 1 : getTotalMembers(CABAL_DUSK); + int duskPercent = Math.round(((float) duskProportion / (float) totalDuskMembers) * 100); + + // If a Seal was already closed or owned by the opponent and the new winner wants to assume ownership of the Seal, 35% or more of the members of the Cabal must have chosen the Seal. + // If they chose less than 35%, they cannot own the Seal. + // If the Seal was owned by the winner in the previous Seven Signs, they can retain that seal if 10% or more members have chosen it. + // If they want to possess a new Seal, at least 35% of the members of the Cabal must have chosen the new Seal. + switch (prevSealOwner) { + case CABAL_NULL: + switch (getCabalHighestScore()) { + case CABAL_NULL: + newSealOwner = CABAL_NULL; + break; + case CABAL_DAWN: + if (dawnPercent >= 35) { + newSealOwner = CABAL_DAWN; + } else { + newSealOwner = CABAL_NULL; + } + break; + case CABAL_DUSK: + if (duskPercent >= 35) { + newSealOwner = CABAL_DUSK; + } else { + newSealOwner = CABAL_NULL; + } + break; + } + break; + case CABAL_DAWN: + switch (getCabalHighestScore()) { + case CABAL_NULL: + if (dawnPercent >= 10) { + newSealOwner = CABAL_DAWN; + } else { + newSealOwner = CABAL_NULL; + } + break; + case CABAL_DAWN: + if (dawnPercent >= 10) { + newSealOwner = CABAL_DAWN; + } else { + newSealOwner = CABAL_NULL; + } + break; + case CABAL_DUSK: + if (duskPercent >= 35) { + newSealOwner = CABAL_DUSK; + } else if (dawnPercent >= 10) { + newSealOwner = CABAL_DAWN; + } else { + newSealOwner = CABAL_NULL; + } + break; + } + break; + case CABAL_DUSK: + switch (getCabalHighestScore()) { + case CABAL_NULL: + if (duskPercent >= 10) { + newSealOwner = CABAL_DUSK; + } else { + newSealOwner = CABAL_NULL; + } + break; + case CABAL_DAWN: + if (dawnPercent >= 35) { + newSealOwner = CABAL_DAWN; + } else if (duskPercent >= 10) { + newSealOwner = CABAL_DUSK; + } else { + newSealOwner = CABAL_NULL; + } + break; + case CABAL_DUSK: + if (duskPercent >= 10) { + newSealOwner = CABAL_DUSK; + } else { + newSealOwner = CABAL_NULL; + } + break; + } + break; + } + + _signsSealOwners.put(currSeal, newSealOwner); + + // Alert all online players to new seal status. + switch (currSeal) { + case SEAL_AVARICE: + if (newSealOwner == CABAL_DAWN) { + sendMessageToAll(DAWN_OBTAINED_AVARICE); + } else if (newSealOwner == CABAL_DUSK) { + sendMessageToAll(DUSK_OBTAINED_AVARICE); + } + break; + case SEAL_GNOSIS: + if (newSealOwner == CABAL_DAWN) { + sendMessageToAll(DAWN_OBTAINED_GNOSIS); + } else if (newSealOwner == CABAL_DUSK) { + sendMessageToAll(DUSK_OBTAINED_GNOSIS); + } + break; + case SEAL_STRIFE: + if (newSealOwner == CABAL_DAWN) { + sendMessageToAll(DAWN_OBTAINED_STRIFE); + } else if (newSealOwner == CABAL_DUSK) { + sendMessageToAll(DUSK_OBTAINED_STRIFE); + } + + CastleManager.getInstance().validateTaxes(newSealOwner); + break; + } + } + } + + /** + * This method is called to remove all players from catacombs and necropolises, who belong to the losing cabal.
+ * Should only ever called at the beginning of Seal Validation. + * @param compWinner + */ + protected void teleLosingCabalFromDungeons(String compWinner) { + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + StatsSet currPlayer = _signsPlayerData.get(player.getObjectId()); + + if (isSealValidationPeriod() || isCompResultsPeriod()) { + if (!player.isGM() && player.isIn7sDungeon() && ((currPlayer == null) || !currPlayer.getString("cabal").equals(compWinner))) { + player.teleToLocation(TeleportWhereType.TOWN); + player.setIsIn7sDungeon(false); + } + } else { + if (!player.isGM() && player.isIn7sDungeon() && ((currPlayer == null) || !currPlayer.getString("cabal").isEmpty())) { + player.teleToLocation(TeleportWhereType.TOWN); + player.setIsIn7sDungeon(false); + } + } + } + } + + /** + * The primary controller of period change of the Seven Signs system.
+ * This runs all related tasks depending on the period that is about to begin. + * @author Tempy + */ + protected class SevenSignsPeriodChange implements Runnable { + @Override + public void run() { + // Remember the period check here refers to the period just ENDED! + final int periodEnded = getCurrentPeriod(); + _activePeriod++; + + switch (periodEnded) { + case PERIOD_COMP_RECRUITING: // Initialization + + // Start the Festival of Darkness cycle. + SevenSignsFestival.getInstance().startFestivalManager(); + + // Send message that Competition has begun. + sendMessageToAll(QUEST_EVENT_PERIOD_BEGUN); + break; + case PERIOD_COMPETITION: // Results Calculation + + // Send message that Competition has ended. + sendMessageToAll(QUEST_EVENT_PERIOD_ENDED); + + int compWinner = getCabalHighestScore(); + + // Schedule a stop of the festival engine and reward highest ranking members from cycle + SevenSignsFestival.getInstance().getFestivalManagerSchedule().cancel(false); + SevenSignsFestival.getInstance().rewardHighestRanked(); + + calcNewSealOwners(); + + switch (compWinner) { + case CABAL_DAWN -> sendMessageToAll(DAWN_WON); + case CABAL_DUSK -> sendMessageToAll(DUSK_WON); + } + + _previousWinner = compWinner; + // Reset Castle ticket buy count + List castles = CastleManager.getInstance().getCastles(); + for (Castle castle : castles) { + castle.setTicketBuyCount(0); + } + break; + case PERIOD_COMP_RESULTS: // Seal Validation + + // Perform initial Seal Validation set up. + initializeSeals(); + + // Buff/Debuff members of the event when Seal of Strife captured. + giveCPMult(getSealOwner(SEAL_STRIFE)); + + // Send message that Seal Validation has begun. + sendMessageToAll(SEAL_VALIDATION_PERIOD_BEGUN); + + LOG.info("The {} have won the competition with {} points!", getCabalName(_previousWinner), getCurrentScore(_previousWinner)); + break; + case PERIOD_SEAL_VALIDATION: // Reset for New Cycle + + // Ensure a cycle restart when this period ends. + _activePeriod = PERIOD_COMP_RECRUITING; + + // Send message that Seal Validation has ended. + sendMessageToAll(SEAL_VALIDATION_PERIOD_ENDED); + // Clear Seal of Strife influence. + removeCPMult(); + // Reset all data + resetPlayerData(); + resetSeals(); + + _currentCycle++; + // Reset all Festival-related data and remove any unused blood offerings. + // NOTE: A full update of Festival data in the database is also performed. + SevenSignsFestival.getInstance().resetFestivalData(false); + + _dawnStoneScore = 0; + _duskStoneScore = 0; + + _dawnFestivalScore = 0; + _duskFestivalScore = 0; + break; + } + + // Make sure all Seven Signs data is saved for future use. + saveSevenSignsData(); + saveSevenSignsStatus(); + + teleLosingCabalFromDungeons(getCabalShortName(getCabalHighestScore())); + + SSQInfo ss = new SSQInfo(); + + Broadcast.toAllOnlinePlayers(ss); + spawnSevenSignsNPC(); + + LOG.info("The {} period has begun!", getCurrentPeriodName()); + + setCalendarForNextPeriodChange(); + + SevenSignsPeriodChange sspc = new SevenSignsPeriodChange(); + ThreadPoolManager.getInstance().scheduleGeneral(sspc, getMilliToPeriodChange()); + } + } + + public boolean checkIsDawnPostingTicket(int itemId) { + // TODO: I think it should be some kind of a list in the datapack for compare. + if ((itemId > 6114) && (itemId < 6175)) { + return true; + } + if ((itemId > 6801) && (itemId < 6812)) { + return true; + } + if ((itemId > 7997) && (itemId < 8008)) { + return true; + } + if ((itemId > 7940) && (itemId < 7951)) { + return true; + } + if ((itemId > 6294) && (itemId < 6307)) { + return true; + } + if ((itemId > 6831) && (itemId < 6834)) { + return true; + } + if ((itemId > 8027) && (itemId < 8030)) { + return true; + } + return (itemId > 7970) && (itemId < 7973); + } + + public boolean checkIsRookiePostingTicket(int itemId) { + // TODO: I think it should be some kind of a list in the datapack for compare. + if ((itemId > 6174) && (itemId < 6295)) { + return true; + } + if ((itemId > 6811) && (itemId < 6832)) { + return true; + } + if ((itemId > 7950) && (itemId < 7971)) { + return true; + } + return (itemId > 8007) && (itemId < 8028); + } + + public void giveCPMult(int strifeOwner) { + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + // Gives "Victor of War" passive skill to all online characters with Cabal, which controls Seal of Strife + int cabal = getPlayerCabal(player.getObjectId()); + if (cabal != SevenSigns.CABAL_NULL) { + if (cabal == strifeOwner) { + player.addSkill(CommonSkill.THE_VICTOR_OF_WAR.getSkill()); + } else { + // Gives "The Vanquished of War" passive skill to all online characters with Cabal, which does not control Seal of Strife + player.addSkill(CommonSkill.THE_VANQUISHED_OF_WAR.getSkill()); + } + } + } + } + + public void removeCPMult() { + // Remove SevenSigns' buffs/debuffs. + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + player.removeSkill(CommonSkill.THE_VICTOR_OF_WAR.getSkill()); + player.removeSkill(CommonSkill.THE_VANQUISHED_OF_WAR.getSkill()); + } + } + + public boolean checkSummonConditions(L2PcInstance activeChar) { + if (activeChar == null) { + return true; + } + // Golems cannot be summoned by Dusk when the Seal of Strife is controlled by the Dawn + if (isSealValidationPeriod()) { + if (getSealOwner(SEAL_STRIFE) == CABAL_DAWN) { + if (getPlayerCabal(activeChar.getObjectId()) == CABAL_DUSK) { + activeChar.sendPacket(SEAL_OF_STRIFE_FORBIDS_SUMMONING); + return true; + } + } + } + + return false; + } + + public static SevenSigns getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SevenSigns INSTANCE = new SevenSigns(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/SevenSignsFestival.java b/src/main/java/com/l2jserver/gameserver/SevenSignsFestival.java new file mode 100644 index 00000000..088ba2c6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/SevenSignsFestival.java @@ -0,0 +1,2117 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.clan; +import static com.l2jserver.gameserver.config.Configuration.sevenSigns; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.L2Party.messageType; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.SpawnListener; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2FestivalMonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * Seven Signs Festival of Darkness Engine.
+ * TODO:
+ *
    + *
  • Archer mobs should target healer characters over other party members.
  • + *
  • Added 29 Sep: Players that leave a party during the Seven Signs Festival will now take damage and cannot be healed.
  • + *
+ * @author Tempy + */ +public class SevenSignsFestival implements SpawnListener { + protected static final Logger LOG = LoggerFactory.getLogger(SevenSignsFestival.class); + + private static final String GET_CLAN_NAME = "SELECT clan_name FROM clan_data WHERE clan_id = (SELECT clanid FROM characters WHERE char_name = ?)"; + + /** + * These length settings are important! :)
+ * All times are relative to the ELAPSED time (in ms) since a festival begins.
+ * Festival manager start is the time after the server starts to begin the first festival cycle.
+ * The cycle length should ideally be at least 2x longer than the festival length.
+ * This allows ample time for players to sign-up to participate in the festival.
+ * The intermission is the time between the festival participants being moved to the "arenas" and the spawning of the first set of mobs.
+ * The monster swarm time is the time before the monsters swarm to the center of the arena, after they are spawned.
+ * The chest spawn time is for when the bonus festival chests spawn, usually towards the end of the festival. + */ + public static final long FESTIVAL_SIGNUP_TIME = sevenSigns().getFestivalCycleLength() - sevenSigns().getFestivalLength() - 60000; + + // Key Constants \\ + private static final int FESTIVAL_MAX_OFFSET_X = 230; + private static final int FESTIVAL_MAX_OFFSET_Y = 230; + private static final int FESTIVAL_DEFAULT_RESPAWN = 60; // Specify in seconds! + + public static final int FESTIVAL_COUNT = 5; // do not change without correcting db and SevenSigns itself ! + public static final int FESTIVAL_LEVEL_MAX_31 = 0; + public static final int FESTIVAL_LEVEL_MAX_42 = 1; + public static final int FESTIVAL_LEVEL_MAX_53 = 2; + public static final int FESTIVAL_LEVEL_MAX_64 = 3; + public static final int FESTIVAL_LEVEL_MAX_NONE = 4; + // 500 maximum possible score + public static final int[] FESTIVAL_LEVEL_SCORES = { + 60, + 70, + 100, + 120, + 150 + }; + + public static final int FESTIVAL_OFFERING_ID = 5901; + public static final int FESTIVAL_OFFERING_VALUE = 5; + + /** + * The following contains all the necessary spawn data for:
+ *
    + *
  • Player Start Locations + *
  • + *
  • Witches + *
  • + *
  • Monsters + *
  • + *
  • Chests
  • + *
+ * All data is given by: X, Y, Z (coords), Heading, NPC ID (if necessary).
+ * This may be moved externally in time, but the data should not change.
+ */ + // @formatter:off + public static final int[][] FESTIVAL_DAWN_PLAYER_SPAWNS = + { + { -79187, 113186, -4895, 0 }, // 31 and below + { -75918, 110137, -4895, 0 }, // 42 and below + { -73835, 111969, -4895, 0 }, // 53 and below + { -76170, 113804, -4895, 0 }, // 64 and below + { -78927, 109528, -4895, 0 } // No level limit + }; + + public static final int[][] FESTIVAL_DUSK_PLAYER_SPAWNS = + { + { -77200, 88966, -5151, 0 }, // 31 and below + { -76941, 85307, -5151, 0 }, // 42 and below + { -74855, 87135, -5151, 0 }, // 53 and below + { -80208, 88222, -5151, 0 }, // 64 and below + { -79954, 84697, -5151, 0 } // No level limit + }; + + protected static final int[][] FESTIVAL_DAWN_WITCH_SPAWNS = + { + { -79183, 113052, -4891, 0, 31132 }, // 31 and below + { -75916, 110270, -4891, 0, 31133 }, // 42 and below + { -73979, 111970, -4891, 0, 31134 }, // 53 and below + { -76174, 113663, -4891, 0, 31135 }, // 64 and below + { -78930, 109664, -4891, 0, 31136 } // No level limit + }; + + protected static final int[][] FESTIVAL_DUSK_WITCH_SPAWNS = + { + { -77199, 88830, -5147, 0, 31142 }, // 31 and below + { -76942, 85438, -5147, 0, 31143 }, // 42 and below + { -74990, 87135, -5147, 0, 31144 }, // 53 and below + { -80207, 88222, -5147, 0, 31145 }, // 64 and below + { -79952, 84833, -5147, 0, 31146 } // No level limit + }; + + protected static final int[][][] FESTIVAL_DAWN_PRIMARY_SPAWNS = + { + { + /* Level 31 and Below - Offering of the Branded */ + { -78537, 113839, -4895, -1, 18009 }, + { -78466, 113852, -4895, -1, 18010 }, + { -78509, 113899, -4895, -1, 18010 }, + + { -78481, 112557, -4895, -1, 18009 }, + { -78559, 112504, -4895, -1, 18010 }, + { -78489, 112494, -4895, -1, 18010 }, + + { -79803, 112543, -4895, -1, 18012 }, + { -79854, 112492, -4895, -1, 18013 }, + { -79886, 112557, -4895, -1, 18014 }, + + { -79821, 113811, -4895, -1, 18015 }, + { -79857, 113896, -4895, -1, 18017 }, + { -79878, 113816, -4895, -1, 18018 }, + + // Archers and Marksmen \\ + { -79190, 113660, -4895, -1, 18011 }, + { -78710, 113188, -4895, -1, 18011 }, + { -79190, 112730, -4895, -1, 18016 }, + { -79656, 113188, -4895, -1, 18016 } + }, + { + /* Level 42 and Below - Apostate Offering */ + { -76558, 110784, -4895, -1, 18019 }, + { -76607, 110815, -4895, -1, 18020 }, // South West + { -76559, 110820, -4895, -1, 18020 }, + + { -75277, 110792, -4895, -1, 18019 }, + { -75225, 110801, -4895, -1, 18020 }, // South East + { -75262, 110832, -4895, -1, 18020 }, + + { -75249, 109441, -4895, -1, 18022 }, + { -75278, 109495, -4895, -1, 18023 }, // North East + { -75223, 109489, -4895, -1, 18024 }, + + { -76556, 109490, -4895, -1, 18025 }, + { -76607, 109469, -4895, -1, 18027 }, // North West + { -76561, 109450, -4895, -1, 18028 }, + // Archers and Marksmen \\ + { -76399, 110144, -4895, -1, 18021 }, + { -75912, 110606, -4895, -1, 18021 }, + { -75444, 110144, -4895, -1, 18026 }, + { -75930, 109665, -4895, -1, 18026 } + }, + { + /* Level 53 and Below - Witch's Offering */ + { -73184, 111319, -4895, -1, 18029 }, + { -73135, 111294, -4895, -1, 18030 }, // South West + { -73185, 111281, -4895, -1, 18030 }, + + { -74477, 111321, -4895, -1, 18029 }, + { -74523, 111293, -4895, -1, 18030 }, // South East + { -74481, 111280, -4895, -1, 18030 }, + + { -74489, 112604, -4895, -1, 18032 }, + { -74491, 112660, -4895, -1, 18033 }, // North East + { -74527, 112629, -4895, -1, 18034 }, + + { -73197, 112621, -4895, -1, 18035 }, + { -73142, 112631, -4895, -1, 18037 }, // North West + { -73182, 112656, -4895, -1, 18038 }, + + // Archers and Marksmen \\ + { -73834, 112430, -4895, -1, 18031 }, + { -74299, 111959, -4895, -1, 18031 }, + { -73841, 111491, -4895, -1, 18036 }, + { -73363, 111959, -4895, -1, 18036 } + }, + { + /* Level 64 and Below - Dark Omen Offering */ + { -75543, 114461, -4895, -1, 18039 }, + { -75514, 114493, -4895, -1, 18040 }, // South West + { -75488, 114456, -4895, -1, 18040 }, + + { -75521, 113158, -4895, -1, 18039 }, + { -75504, 113110, -4895, -1, 18040 }, // South East + { -75489, 113142, -4895, -1, 18040 }, + + { -76809, 113143, -4895, -1, 18042 }, + { -76860, 113138, -4895, -1, 18043 }, // North East + { -76831, 113112, -4895, -1, 18044 }, + + { -76831, 114441, -4895, -1, 18045 }, + { -76840, 114490, -4895, -1, 18047 }, // North West + { -76864, 114455, -4895, -1, 18048 }, + + // Archers and Marksmen \\ + { -75703, 113797, -4895, -1, 18041 }, + { -76180, 114263, -4895, -1, 18041 }, + { -76639, 113797, -4895, -1, 18046 }, + { -76180, 113337, -4895, -1, 18046 } + }, + { + /* No Level Limit - Offering of Forbidden Path */ + { -79576, 108881, -4895, -1, 18049 }, + { -79592, 108835, -4895, -1, 18050 }, // South West + { -79614, 108871, -4895, -1, 18050 }, + + { -79586, 110171, -4895, -1, 18049 }, + { -79589, 110216, -4895, -1, 18050 }, // South East + { -79620, 110177, -4895, -1, 18050 }, + + { -78825, 110182, -4895, -1, 18052 }, + { -78238, 110182, -4895, -1, 18053 }, // North East + { -78266, 110218, -4895, -1, 18054 }, + + { -78275, 108883, -4895, -1, 18055 }, + { -78267, 108839, -4895, -1, 18057 }, // North West + { -78241, 108871, -4895, -1, 18058 }, + + // Archers and Marksmen \\ + { -79394, 109538, -4895, -1, 18051 }, + { -78929, 109992, -4895, -1, 18051 }, + { -78454, 109538, -4895, -1, 18056 }, + { -78929, 109053, -4895, -1, 18056 } + } + }; + + protected static final int[][][] FESTIVAL_DUSK_PRIMARY_SPAWNS = + { + { + /* Level 31 and Below - Offering of the Branded */ + { -76542, 89653, -5151, -1, 18009 }, + { -76509, 89637, -5151, -1, 18010 }, + { -76548, 89614, -5151, -1, 18010 }, + + { -76539, 88326, -5151, -1, 18009 }, + { -76512, 88289, -5151, -1, 18010 }, + { -76546, 88287, -5151, -1, 18010 }, + + { -77879, 88308, -5151, -1, 18012 }, + { -77886, 88310, -5151, -1, 18013 }, + { -77879, 88278, -5151, -1, 18014 }, + + { -77857, 89605, -5151, -1, 18015 }, + { -77858, 89658, -5151, -1, 18017 }, + { -77891, 89633, -5151, -1, 18018 }, + + // Archers and Marksmen \\ + { -76728, 88962, -5151, -1, 18011 }, + { -77194, 88494, -5151, -1, 18011 }, + { -77660, 88896, -5151, -1, 18016 }, + { -77195, 89438, -5151, -1, 18016 } + }, + { + /* Level 42 and Below - Apostate's Offering */ + { -77585, 84650, -5151, -1, 18019 }, + { -77628, 84643, -5151, -1, 18020 }, + { -77607, 84613, -5151, -1, 18020 }, + + { -76603, 85946, -5151, -1, 18019 }, + { -77606, 85994, -5151, -1, 18020 }, + { -77638, 85959, -5151, -1, 18020 }, + + { -76301, 85960, -5151, -1, 18022 }, + { -76257, 85972, -5151, -1, 18023 }, + { -76286, 85992, -5151, -1, 18024 }, + + { -76281, 84667, -5151, -1, 18025 }, + { -76291, 84611, -5151, -1, 18027 }, + { -76257, 84616, -5151, -1, 18028 }, + + // Archers and Marksmen \\ + { -77419, 85307, -5151, -1, 18021 }, + { -76952, 85768, -5151, -1, 18021 }, + { -76477, 85312, -5151, -1, 18026 }, + { -76942, 84832, -5151, -1, 18026 } + }, + { + /* Level 53 and Below - Witch's Offering */ + { -74211, 86494, -5151, -1, 18029 }, + { -74200, 86449, -5151, -1, 18030 }, + { -74167, 86464, -5151, -1, 18030 }, + + { -75495, 86482, -5151, -1, 18029 }, + { -75540, 86473, -5151, -1, 18030 }, + { -75509, 86445, -5151, -1, 18030 }, + + { -75509, 87775, -5151, -1, 18032 }, + { -75518, 87826, -5151, -1, 18033 }, + { -75542, 87780, -5151, -1, 18034 }, + + { -74214, 87789, -5151, -1, 18035 }, + { -74169, 87801, -5151, -1, 18037 }, + { -74198, 87827, -5151, -1, 18038 }, + + // Archers and Marksmen \\ + { -75324, 87135, -5151, -1, 18031 }, + { -74852, 87606, -5151, -1, 18031 }, + { -74388, 87146, -5151, -1, 18036 }, + { -74856, 86663, -5151, -1, 18036 } + }, + { + /* Level 64 and Below - Dark Omen Offering */ + { -79560, 89007, -5151, -1, 18039 }, + { -79521, 89016, -5151, -1, 18040 }, + { -79544, 89047, -5151, -1, 18040 }, + + { -79552, 87717, -5151, -1, 18039 }, + { -79552, 87673, -5151, -1, 18040 }, + { -79510, 87702, -5151, -1, 18040 }, + + { -80866, 87719, -5151, -1, 18042 }, + { -80897, 87689, -5151, -1, 18043 }, + { -80850, 87685, -5151, -1, 18044 }, + + { -80848, 89013, -5151, -1, 18045 }, + { -80887, 89051, -5151, -1, 18047 }, + { -80891, 89004, -5151, -1, 18048 }, + + // Archers and Marksmen \\ + { -80205, 87895, -5151, -1, 18041 }, + { -80674, 88350, -5151, -1, 18041 }, + { -80209, 88833, -5151, -1, 18046 }, + { -79743, 88364, -5151, -1, 18046 } + }, + { + /* No Level Limit - Offering of Forbidden Path */ + { -80624, 84060, -5151, -1, 18049 }, + { -80621, 84007, -5151, -1, 18050 }, + { -80590, 84039, -5151, -1, 18050 }, + + { -80605, 85349, -5151, -1, 18049 }, + { -80639, 85363, -5151, -1, 18050 }, + { -80611, 85385, -5151, -1, 18050 }, + + { -79311, 85353, -5151, -1, 18052 }, + { -79277, 85384, -5151, -1, 18053 }, + { -79273, 85539, -5151, -1, 18054 }, + + { -79297, 84054, -5151, -1, 18055 }, + { -79285, 84006, -5151, -1, 18057 }, + { -79260, 84040, -5151, -1, 18058 }, + + // Archers and Marksmen \\ + { -79945, 85171, -5151, -1, 18051 }, + { -79489, 84707, -5151, -1, 18051 }, + { -79952, 84222, -5151, -1, 18056 }, + { -80423, 84703, -5151, -1, 18056 } + } + }; + + protected static final int[][][] FESTIVAL_DAWN_SECONDARY_SPAWNS = + { + { + /* 31 and Below */ + { -78757, 112834, -4895, -1, 18016 }, + { -78581, 112834, -4895, -1, 18016 }, + { -78822, 112526, -4895, -1, 18011 }, + + { -78822, 113702, -4895, -1, 18011 }, + { -78822, 113874, -4895, -1, 18011 }, + { -79524, 113546, -4895, -1, 18011 }, + + { -79693, 113546, -4895, -1, 18011 }, + { -79858, 113546, -4895, -1, 18011 }, + { -79545, 112757, -4895, -1, 18016 }, + { -79545, 112586, -4895, -1, 18016 }, + }, + { + /* 42 and Below */ + { -75565, 110580, -4895, -1, 18026 }, + { -75565, 110740, -4895, -1, 18026 }, + { -75577, 109776, -4895, -1, 18021 }, + + { -75413, 109776, -4895, -1, 18021 }, + { -75237, 109776, -4895, -1, 18021 }, + { -76274, 109468, -4895, -1, 18021 }, + + { -76274, 109635, -4895, -1, 18021 }, + { -76274, 109795, -4895, -1, 18021 }, + { -76351, 110500, -4895, -1, 18056 }, + { -76528, 110500, -4895, -1, 18056 }, + }, + { + /* 53 and Below */ + { -74191, 111527, -4895, -1, 18036 }, + { -74191, 111362, -4895, -1, 18036 }, + { -73495, 111611, -4895, -1, 18031 }, + + { -73327, 111611, -4895, -1, 18031 }, + { -73154, 111611, -4895, -1, 18031 }, + { -73473, 112301, -4895, -1, 18031 }, + + { -73473, 112475, -4895, -1, 18031 }, + { -73473, 112649, -4895, -1, 18031 }, + { -74270, 112326, -4895, -1, 18036 }, + { -74443, 112326, -4895, -1, 18036 }, + }, + { + /* 64 and Below */ + { -75738, 113439, -4895, -1, 18046 }, + { -75571, 113439, -4895, -1, 18046 }, + { -75824, 114141, -4895, -1, 18041 }, + + { -75824, 114309, -4895, -1, 18041 }, + { -75824, 114477, -4895, -1, 18041 }, + { -76513, 114158, -4895, -1, 18041 }, + + { -76683, 114158, -4895, -1, 18041 }, + { -76857, 114158, -4895, -1, 18041 }, + { -76535, 113357, -4895, -1, 18056 }, + { -76535, 113190, -4895, -1, 18056 }, + }, + { + /* No Level Limit */ + { -79350, 109894, -4895, -1, 18056 }, + { -79534, 109894, -4895, -1, 18056 }, + { -79285, 109187, -4895, -1, 18051 }, + + { -79285, 109019, -4895, -1, 18051 }, + { -79285, 108860, -4895, -1, 18051 }, + { -78587, 109172, -4895, -1, 18051 }, + + { -78415, 109172, -4895, -1, 18051 }, + { -78249, 109172, -4895, -1, 18051 }, + { -78575, 109961, -4895, -1, 18056 }, + { -78575, 110130, -4895, -1, 18056 }, + } + }; + + protected static final int[][][] FESTIVAL_DUSK_SECONDARY_SPAWNS = + { + { + /* 31 and Below */ + { -76844, 89304, -5151, -1, 18011 }, + { -76844, 89479, -5151, -1, 18011 }, + { -76844, 89649, -5151, -1, 18011 }, + + { -77544, 89326, -5151, -1, 18011 }, + { -77716, 89326, -5151, -1, 18011 }, + { -77881, 89326, -5151, -1, 18011 }, + + { -77561, 88530, -5151, -1, 18016 }, + { -77561, 88364, -5151, -1, 18016 }, + { -76762, 88615, -5151, -1, 18016 }, + { -76594, 88615, -5151, -1, 18016 }, + }, + { + /* 42 and Below */ + { -77307, 84969, -5151, -1, 18021 }, + { -77307, 84795, -5151, -1, 18021 }, + { -77307, 84623, -5151, -1, 18021 }, + + { -76614, 84944, -5151, -1, 18021 }, + { -76433, 84944, -5151, -1, 18021 }, + { -76251, 84944, -5151, -1, 18021 }, + + { -76594, 85745, -5151, -1, 18026 }, + { -76594, 85910, -5151, -1, 18026 }, + { -77384, 85660, -5151, -1, 18026 }, + { -77555, 85660, -5151, -1, 18026 }, + }, + { + /* 53 and Below */ + { -74517, 86782, -5151, -1, 18031 }, + { -74344, 86782, -5151, -1, 18031 }, + { -74185, 86782, -5151, -1, 18031 }, + + { -74496, 87464, -5151, -1, 18031 }, + { -74496, 87636, -5151, -1, 18031 }, + { -74496, 87815, -5151, -1, 18031 }, + + { -75298, 87497, -5151, -1, 18036 }, + { -75460, 87497, -5151, -1, 18036 }, + { -75219, 86712, -5151, -1, 18036 }, + { -75219, 86531, -5151, -1, 18036 }, + }, + { + /* 64 and Below */ + { -79851, 88703, -5151, -1, 18041 }, + { -79851, 88868, -5151, -1, 18041 }, + { -79851, 89040, -5151, -1, 18041 }, + + { -80548, 88722, -5151, -1, 18041 }, + { -80711, 88722, -5151, -1, 18041 }, + { -80883, 88722, -5151, -1, 18041 }, + + { -80565, 87916, -5151, -1, 18046 }, + { -80565, 87752, -5151, -1, 18046 }, + { -79779, 87996, -5151, -1, 18046 }, + { -79613, 87996, -5151, -1, 18046 }, + }, + { + /* No Level Limit */ + { -79271, 84330, -5151, -1, 18051 }, + { -79448, 84330, -5151, -1, 18051 }, + { -79601, 84330, -5151, -1, 18051 }, + + { -80311, 84367, -5151, -1, 18051 }, + { -80311, 84196, -5151, -1, 18051 }, + { -80311, 84015, -5151, -1, 18051 }, + + { -80556, 85049, -5151, -1, 18056 }, + { -80384, 85049, -5151, -1, 18056 }, + { -79598, 85127, -5151, -1, 18056 }, + { -79598, 85303, -5151, -1, 18056 }, + } + }; + + protected static final int[][][] FESTIVAL_DAWN_CHEST_SPAWNS = + { + { + /* Level 31 and Below */ + { -78999, 112957, -4927, -1, 18109 }, + { -79153, 112873, -4927, -1, 18109 }, + { -79256, 112873, -4927, -1, 18109 }, + + { -79368, 112957, -4927, -1, 18109 }, + { -79481, 113124, -4927, -1, 18109 }, + { -79481, 113275, -4927, -1, 18109 }, + + { -79364, 113398, -4927, -1, 18109 }, + { -79213, 113500, -4927, -1, 18109 }, + { -79099, 113500, -4927, -1, 18109 }, + + { -78960, 113398, -4927, -1, 18109 }, + { -78882, 113235, -4927, -1, 18109 }, + { -78882, 113099, -4927, -1, 18109 }, + }, + { + /* Level 42 and Below */ + { -76119, 110383, -4927, -1, 18110 }, + { -75980, 110442, -4927, -1, 18110 }, + { -75848, 110442, -4927, -1, 18110 }, + + { -75720, 110383, -4927, -1, 18110 }, + { -75625, 110195, -4927, -1, 18110 }, + { -75625, 110063, -4927, -1, 18110 }, + + { -75722, 109908, -4927, -1, 18110 }, + { -75863, 109832, -4927, -1, 18110 }, + { -75989, 109832, -4927, -1, 18110 }, + + { -76130, 109908, -4927, -1, 18110 }, + { -76230, 110079, -4927, -1, 18110 }, + { -76230, 110215, -4927, -1, 18110 }, + }, + { + /* Level 53 and Below */ + { -74055, 111781, -4927, -1, 18111 }, + { -74144, 111938, -4927, -1, 18111 }, + { -74144, 112075, -4927, -1, 18111 }, + + { -74055, 112173, -4927, -1, 18111 }, + { -73885, 112289, -4927, -1, 18111 }, + { -73756, 112289, -4927, -1, 18111 }, + + { -73574, 112141, -4927, -1, 18111 }, + { -73511, 112040, -4927, -1, 18111 }, + { -73511, 111912, -4927, -1, 18111 }, + + { -73574, 111772, -4927, -1, 18111 }, + { -73767, 111669, -4927, -1, 18111 }, + { -73899, 111669, -4927, -1, 18111 }, + }, + { + /* Level 64 and Below */ + { -76008, 113566, -4927, -1, 18112 }, + { -76159, 113485, -4927, -1, 18112 }, + { -76267, 113485, -4927, -1, 18112 }, + + { -76386, 113566, -4927, -1, 18112 }, + { -76482, 113748, -4927, -1, 18112 }, + { -76482, 113885, -4927, -1, 18112 }, + + { -76371, 114029, -4927, -1, 18112 }, + { -76220, 114118, -4927, -1, 18112 }, + { -76092, 114118, -4927, -1, 18112 }, + + { -75975, 114029, -4927, -1, 18112 }, + { -75861, 113851, -4927, -1, 18112 }, + { -75861, 113713, -4927, -1, 18112 }, + }, + { + /* No Level Limit */ + { -79100, 109782, -4927, -1, 18113 }, + { -78962, 109853, -4927, -1, 18113 }, + { -78851, 109853, -4927, -1, 18113 }, + + { -78721, 109782, -4927, -1, 18113 }, + { -78615, 109596, -4927, -1, 18113 }, + { -78615, 109453, -4927, -1, 18113 }, + + { -78746, 109300, -4927, -1, 18113 }, + { -78881, 109203, -4927, -1, 18113 }, + { -79027, 109203, -4927, -1, 18113 }, + + { -79159, 109300, -4927, -1, 18113 }, + { -79240, 109480, -4927, -1, 18113 }, + { -79240, 109615, -4927, -1, 18113 }, + } + }; + + protected static final int[][][] FESTIVAL_DUSK_CHEST_SPAWNS = + { + { + /* Level 31 and Below */ + { -77016, 88726, -5183, -1, 18114 }, + { -77136, 88646, -5183, -1, 18114 }, + { -77247, 88646, -5183, -1, 18114 }, + + { -77380, 88726, -5183, -1, 18114 }, + { -77512, 88883, -5183, -1, 18114 }, + { -77512, 89053, -5183, -1, 18114 }, + + { -77378, 89287, -5183, -1, 18114 }, + { -77254, 89238, -5183, -1, 18114 }, + { -77095, 89238, -5183, -1, 18114 }, + + { -76996, 89287, -5183, -1, 18114 }, + { -76901, 89025, -5183, -1, 18114 }, + { -76901, 88891, -5183, -1, 18114 }, + }, + { + /* Level 42 and Below */ + { -77128, 85553, -5183, -1, 18115 }, + { -77036, 85594, -5183, -1, 18115 }, + { -76919, 85594, -5183, -1, 18115 }, + + { -76755, 85553, -5183, -1, 18115 }, + { -76635, 85392, -5183, -1, 18115 }, + { -76635, 85216, -5183, -1, 18115 }, + + { -76761, 85025, -5183, -1, 18115 }, + { -76908, 85004, -5183, -1, 18115 }, + { -77041, 85004, -5183, -1, 18115 }, + + { -77138, 85025, -5183, -1, 18115 }, + { -77268, 85219, -5183, -1, 18115 }, + { -77268, 85410, -5183, -1, 18115 }, + }, + { + /* Level 53 and Below */ + { -75150, 87303, -5183, -1, 18116 }, + { -75150, 87175, -5183, -1, 18116 }, + { -75150, 87175, -5183, -1, 18116 }, + + { -75150, 87303, -5183, -1, 18116 }, + { -74943, 87433, -5183, -1, 18116 }, + { -74767, 87433, -5183, -1, 18116 }, + + { -74556, 87306, -5183, -1, 18116 }, + { -74556, 87184, -5183, -1, 18116 }, + { -74556, 87184, -5183, -1, 18116 }, + + { -74556, 87306, -5183, -1, 18116 }, + { -74757, 86830, -5183, -1, 18116 }, + { -74927, 86830, -5183, -1, 18116 }, + }, + { + /* Level 64 and Below */ + { -80010, 88128, -5183, -1, 18117 }, + { -80113, 88066, -5183, -1, 18117 }, + { -80220, 88066, -5183, -1, 18117 }, + + { -80359, 88128, -5183, -1, 18117 }, + { -80467, 88267, -5183, -1, 18117 }, + { -80467, 88436, -5183, -1, 18117 }, + + { -80381, 88639, -5183, -1, 18117 }, + { -80278, 88577, -5183, -1, 18117 }, + { -80142, 88577, -5183, -1, 18117 }, + + { -80028, 88639, -5183, -1, 18117 }, + { -79915, 88466, -5183, -1, 18117 }, + { -79915, 88322, -5183, -1, 18117 }, + }, + { + /* No Level Limit */ + { -80153, 84947, -5183, -1, 18118 }, + { -80003, 84962, -5183, -1, 18118 }, + { -79848, 84962, -5183, -1, 18118 }, + + { -79742, 84947, -5183, -1, 18118 }, + { -79668, 84772, -5183, -1, 18118 }, + { -79668, 84619, -5183, -1, 18118 }, + + { -79772, 84471, -5183, -1, 18118 }, + { -79888, 84414, -5183, -1, 18118 }, + { -80023, 84414, -5183, -1, 18118 }, + + { -80166, 84471, -5183, -1, 18118 }, + { -80253, 84600, -5183, -1, 18118 }, + { -80253, 84780, -5183, -1, 18118 }, + } + }; + // @formatter:on + + private FestivalManager _managerInstance; + protected ScheduledFuture _managerScheduledTask; + + protected int _signsCycle = SevenSigns.getInstance().getCurrentCycle(); + protected int _festivalCycle; + protected long _nextFestivalCycleStart; + protected long _nextFestivalStart; + protected boolean _festivalInitialized; + protected boolean _festivalInProgress; + protected List _accumulatedBonuses = new ArrayList<>(); // The total bonus available (in Ancient Adena) + + boolean _noPartyRegister; + private L2Npc _dawnChatGuide; + private L2Npc _duskChatGuide; + + protected Map> _dawnFestivalParticipants = new HashMap<>(); + protected Map> _duskFestivalParticipants = new HashMap<>(); + + protected Map> _dawnPreviousParticipants = new HashMap<>(); + protected Map> _duskPreviousParticipants = new HashMap<>(); + + private final Map _dawnFestivalScores = new HashMap<>(); + private final Map _duskFestivalScores = new HashMap<>(); + + /** + * _festivalData is essentially an instance of the seven_signs_festival table and should be treated as such. Data is initially accessed by the related Seven Signs cycle, with _signsCycle representing data for the current round of Festivals. The actual table data is stored as a series of StatsSet + * constructs. These are accessed by the use of an offset based on the number of festivals, thus: offset = FESTIVAL_COUNT + festivalId (Data for Dawn is always accessed by offset > FESTIVAL_COUNT) + */ + private final Map> _festivalData = new HashMap<>(); + + protected SevenSignsFestival() { + restoreFestivalData(); + + if (SevenSigns.getInstance().isSealValidationPeriod()) { + LOG.info("Initialization bypassed due to Seal Validation in effect."); + return; + } + + L2Spawn.addSpawnListener(this); + startFestivalManager(); + } + + public static SevenSignsFestival getInstance() { + return SingletonHolder._instance; + } + + /** + * Returns the associated name (level range) to a given festival ID. + * @param festivalID + * @return String festivalName + */ + public static String getFestivalName(int festivalID) { + return switch (festivalID) { + case FESTIVAL_LEVEL_MAX_31 -> "Level 31 or lower"; + case FESTIVAL_LEVEL_MAX_42 -> "Level 42 or lower"; + case FESTIVAL_LEVEL_MAX_53 -> "Level 53 or lower"; + case FESTIVAL_LEVEL_MAX_64 -> "Level 64 or lower"; + default -> "No Level Limit"; + }; + } + + /** + * Returns the maximum allowed player level for the given festival type. + * @param festivalId + * @return int maxLevel + */ + public static int getMaxLevelForFestival(int festivalId) { + return switch (festivalId) { + case SevenSignsFestival.FESTIVAL_LEVEL_MAX_31 -> 31; + case SevenSignsFestival.FESTIVAL_LEVEL_MAX_42 -> 42; + case SevenSignsFestival.FESTIVAL_LEVEL_MAX_53 -> 53; + case SevenSignsFestival.FESTIVAL_LEVEL_MAX_64 -> 64; + default -> character().getMaxPlayerLevel(); + }; + } + + /** + * Returns true if the monster ID given is of an archer/marksman type. + * @param npcId + * @return boolean isArcher + */ + protected static boolean isFestivalArcher(int npcId) { + if ((npcId < 18009) || (npcId > 18108)) { + return false; + } + + int identifier = npcId % 10; + return ((identifier == 4) || (identifier == 9)); + } + + /** + * Returns true if the monster ID given is a festival chest. + * @param npcId + * @return boolean isChest + */ + protected static boolean isFestivalChest(int npcId) { + return ((npcId < 18109) || (npcId > 18118)); + } + + /** + * Primarily used to terminate the Festival Manager, when the Seven Signs period changes. + * @return ScheduledFuture festManagerScheduler + */ + protected final ScheduledFuture getFestivalManagerSchedule() { + if (_managerScheduledTask == null) { + startFestivalManager(); + } + + return _managerScheduledTask; + } + + /** + * Used to start the Festival Manager, if the current period is not Seal Validation. + */ + protected void startFestivalManager() { + // Start the Festival Manager for the first time after the server has started + // at the specified time, then invoke it automatically after every cycle. + if (_managerInstance != null) { + return; // already started + } + _managerInstance = new FestivalManager(); + + setNextFestivalStart(sevenSigns().getFestivalManagerStart() + FESTIVAL_SIGNUP_TIME); + _managerScheduledTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(_managerInstance, sevenSigns().getFestivalManagerStart(), sevenSigns().getFestivalCycleLength()); + + LOG.info("The first Festival of Darkness cycle begins in {} minute(s).", MILLISECONDS.toMinutes(sevenSigns().getFestivalManagerStart())); + } + + /** + * Restores saved festival data, basic settings from the properties file and past high score data from the database. + */ + protected void restoreFestivalData() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT festivalId, cabal, cycle, date, score, members " + "FROM seven_signs_festival")) { + while (rs.next()) { + int festivalCycle = rs.getInt("cycle"); + int festivalId = rs.getInt("festivalId"); + String cabal = rs.getString("cabal"); + + StatsSet festivalDat = new StatsSet(); + festivalDat.set("festivalId", festivalId); + festivalDat.set("cabal", cabal); + festivalDat.set("cycle", festivalCycle); + festivalDat.set("date", rs.getString("date")); + festivalDat.set("score", rs.getInt("score")); + festivalDat.set("members", rs.getString("members")); + + if (cabal.equals("dawn")) { + festivalId += FESTIVAL_COUNT; + } + + final Map tempData = _festivalData.getOrDefault(festivalCycle, new HashMap<>()); + tempData.put(festivalId, festivalDat); + _festivalData.put(festivalCycle, tempData); + } + } catch (SQLException e) { + LOG.error("Failed to load configuration!", e); + } + + StringBuilder query = new StringBuilder(); + query.append("SELECT festival_cycle, "); + + for (int i = 0; i < (FESTIVAL_COUNT - 1); i++) { + query.append("accumulated_bonus"); + query.append(i); + query.append(", "); + } + + query.append("accumulated_bonus"); + query.append(FESTIVAL_COUNT - 1); + query.append(' '); + query.append("FROM seven_signs_status WHERE id=0"); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(query.toString())) { + while (rs.next()) { + _festivalCycle = rs.getInt("festival_cycle"); + + for (int i = 0; i < FESTIVAL_COUNT; i++) { + _accumulatedBonuses.add(i, rs.getInt("accumulated_bonus" + i)); + } + } + } catch (SQLException e) { + LOG.error("Failed to load configuration!", e); + } + } + + /** + * Stores current festival data, basic settings to the properties file and past high score data to the database. If updateSettings = true, then all Seven Signs data is updated in the database. + * @param updateSettings + */ + public void saveFestivalData(boolean updateSettings) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var psInsert = con.prepareStatement("REPLACE INTO seven_signs_festival (festivalId, cabal, cycle, date, score, members) VALUES (?,?,?,?,?,?)")) { + for (Map currCycleData : _festivalData.values()) { + for (StatsSet festivalDat : currCycleData.values()) { + psInsert.setInt(1, festivalDat.getInt("festivalId")); + psInsert.setString(2, festivalDat.getString("cabal")); + psInsert.setInt(3, festivalDat.getInt("cycle")); + psInsert.setLong(4, Long.parseLong(festivalDat.getString("date"))); + psInsert.setInt(5, festivalDat.getInt("score")); + psInsert.setString(6, festivalDat.getString("members")); + psInsert.addBatch(); + } + } + psInsert.executeBatch(); + } catch (SQLException e) { + LOG.error("Failed to save configuration!", e); + } + + // Updates Seven Signs DB data also, so call only if really necessary. + if (updateSettings) { + SevenSigns.getInstance().saveSevenSignsStatus(); + } + } + + /** + * If a clan member is a member of the highest-ranked party in the Festival of Darkness, 100 points are added per member + */ + protected void rewardHighestRanked() { + String[] partyMembers; + StatsSet overallData = getOverallHighestScoreData(FESTIVAL_LEVEL_MAX_31); + if (overallData != null) { + partyMembers = overallData.getString("members").split(","); + for (String partyMemberName : partyMembers) { + addReputationPointsForPartyMemberClan(partyMemberName); + } + } + + overallData = getOverallHighestScoreData(FESTIVAL_LEVEL_MAX_42); + if (overallData != null) { + partyMembers = overallData.getString("members").split(","); + for (String partyMemberName : partyMembers) { + addReputationPointsForPartyMemberClan(partyMemberName); + } + } + + overallData = getOverallHighestScoreData(FESTIVAL_LEVEL_MAX_53); + if (overallData != null) { + partyMembers = overallData.getString("members").split(","); + for (String partyMemberName : partyMembers) { + addReputationPointsForPartyMemberClan(partyMemberName); + } + } + + overallData = getOverallHighestScoreData(FESTIVAL_LEVEL_MAX_64); + if (overallData != null) { + partyMembers = overallData.getString("members").split(","); + for (String partyMemberName : partyMembers) { + addReputationPointsForPartyMemberClan(partyMemberName); + } + } + + overallData = getOverallHighestScoreData(FESTIVAL_LEVEL_MAX_NONE); + if (overallData != null) { + partyMembers = overallData.getString("members").split(","); + for (String partyMemberName : partyMembers) { + addReputationPointsForPartyMemberClan(partyMemberName); + } + } + } + + private void addReputationPointsForPartyMemberClan(String partyMemberName) { + L2PcInstance player = L2World.getInstance().getPlayer(partyMemberName); + if (player != null) { + if (player.getClan() != null) { + player.getClan().addReputationScore(clan().getFestivalOfDarknessWin(), true); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.CLAN_MEMBER_C1_WAS_IN_HIGHEST_RANKED_PARTY_IN_FESTIVAL_OF_DARKNESS_AND_GAINED_S2_REPUTATION); + sm.addString(partyMemberName); + sm.addInt(clan().getFestivalOfDarknessWin()); + player.getClan().broadcastToOnlineMembers(sm); + } + } else { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(GET_CLAN_NAME)) { + ps.setString(1, partyMemberName); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + String clanName = rs.getString("clan_name"); + if (clanName != null) { + L2Clan clan = ClanTable.getInstance().getClanByName(clanName); + if (clan != null) { + clan.addReputationScore(clan().getFestivalOfDarknessWin(), true); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.CLAN_MEMBER_C1_WAS_IN_HIGHEST_RANKED_PARTY_IN_FESTIVAL_OF_DARKNESS_AND_GAINED_S2_REPUTATION); + sm.addString(partyMemberName); + sm.addInt(clan().getFestivalOfDarknessWin()); + clan.broadcastToOnlineMembers(sm); + } + } + } + } + } catch (Exception e) { + LOG.warn("Could not get clan name of {}!", partyMemberName, e); + } + } + } + + /** + * Used to reset all festival data at the beginning of a new quest event period. + * @param updateSettings + */ + protected void resetFestivalData(boolean updateSettings) { + _festivalCycle = 0; + _signsCycle = SevenSigns.getInstance().getCurrentCycle(); + + // Set all accumulated bonuses back to 0. + for (int i = 0; i < FESTIVAL_COUNT; i++) { + _accumulatedBonuses.set(i, 0); + } + + _dawnFestivalParticipants.clear(); + _dawnPreviousParticipants.clear(); + _dawnFestivalScores.clear(); + + _duskFestivalParticipants.clear(); + _duskPreviousParticipants.clear(); + _duskFestivalScores.clear(); + + // Set up a new data set for the current cycle of festivals + Map newData = new HashMap<>(); + + for (int i = 0; i < (FESTIVAL_COUNT * 2); i++) { + int festivalId = i; + + if (i >= FESTIVAL_COUNT) { + festivalId -= FESTIVAL_COUNT; + } + + // Create a new StatsSet with "default" data for Dusk + StatsSet tempStats = new StatsSet(); + tempStats.set("festivalId", festivalId); + tempStats.set("cycle", _signsCycle); + tempStats.set("date", "0"); + tempStats.set("score", 0); + tempStats.set("members", ""); + + if (i >= FESTIVAL_COUNT) { + tempStats.set("cabal", SevenSigns.getCabalShortName(SevenSigns.CABAL_DAWN)); + } else { + tempStats.set("cabal", SevenSigns.getCabalShortName(SevenSigns.CABAL_DUSK)); + } + + newData.put(i, tempStats); + } + + // Add the newly created cycle data to the existing festival data, and + // subsequently save it to the database. + _festivalData.put(_signsCycle, newData); + + saveFestivalData(updateSettings); + + // Remove any unused blood offerings from online players. + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + final L2ItemInstance bloodOfferings = player.getInventory().getItemByItemId(FESTIVAL_OFFERING_ID); + if (bloodOfferings != null) { + player.destroyItem("SevenSigns", bloodOfferings, null, false); + } + } + + LOG.info("Reinitialized engine for next competition period."); + } + + public final int getCurrentFestivalCycle() { + return _festivalCycle; + } + + public final boolean isFestivalInitialized() { + return _festivalInitialized; + } + + public final boolean isFestivalInProgress() { + return _festivalInProgress; + } + + public void setNextCycleStart() { + _nextFestivalCycleStart = System.currentTimeMillis() + sevenSigns().getFestivalCycleLength(); + } + + public void setNextFestivalStart(long milliFromNow) { + _nextFestivalStart = System.currentTimeMillis() + milliFromNow; + } + + public final long getMinsToNextCycle() { + if (SevenSigns.getInstance().isSealValidationPeriod()) { + return -1; + } + + return (_nextFestivalCycleStart - System.currentTimeMillis()) / 60000; + } + + public final int getMinsToNextFestival() { + if (SevenSigns.getInstance().isSealValidationPeriod()) { + return -1; + } + + return (int) (((_nextFestivalStart - System.currentTimeMillis()) / 60000) + 1); + } + + public final String getTimeToNextFestivalStr() { + if (SevenSigns.getInstance().isSealValidationPeriod()) { + return "This is the Seal Validation period. Festivals will resume next week."; + } + + return "The next festival will begin in " + getMinsToNextFestival() + " minute(s)."; + } + + /** + * Returns the current festival ID and oracle ID that the specified player is in, but will return the default of {-1, -1} if the player is not found as a participant. + * @param player + * @return int[] playerFestivalInfo + */ + public final int[] getFestivalForPlayer(L2PcInstance player) { + int[] playerFestivalInfo = { + -1, + -1 + }; + int festivalId = 0; + + while (festivalId < FESTIVAL_COUNT) { + List participants = _dawnFestivalParticipants.get(festivalId); + + // If there are no participants in this festival, move on to the next. + if ((participants != null) && participants.contains(player.getObjectId())) { + playerFestivalInfo[0] = SevenSigns.CABAL_DAWN; + playerFestivalInfo[1] = festivalId; + + return playerFestivalInfo; + } + + participants = _duskFestivalParticipants.get(++festivalId); + + if ((participants != null) && participants.contains(player.getObjectId())) { + playerFestivalInfo[0] = SevenSigns.CABAL_DUSK; + playerFestivalInfo[1] = festivalId; + + return playerFestivalInfo; + } + + festivalId++; + } + + // Return default data if the player is not found as a participant. + return playerFestivalInfo; + } + + public final boolean isParticipant(L2PcInstance player) { + if (SevenSigns.getInstance().isSealValidationPeriod()) { + return false; + } + + if (_managerInstance == null) { + return false; + } + + for (List participants : _dawnFestivalParticipants.values()) { + if ((participants != null) && participants.contains(player.getObjectId())) { + return true; + } + } + + for (List participants : _duskFestivalParticipants.values()) { + if ((participants != null) && participants.contains(player.getObjectId())) { + return true; + } + } + + return false; + } + + public final List getParticipants(int oracle, int festivalId) { + if (oracle == SevenSigns.CABAL_DAWN) { + return _dawnFestivalParticipants.get(festivalId); + } + + return _duskFestivalParticipants.get(festivalId); + } + + public final List getPreviousParticipants(int oracle, int festivalId) { + if (oracle == SevenSigns.CABAL_DAWN) { + return _dawnPreviousParticipants.get(festivalId); + } + + return _duskPreviousParticipants.get(festivalId); + } + + public void setParticipants(int oracle, int festivalId, L2Party festivalParty) { + List participants = null; + + if (festivalParty != null) { + participants = new ArrayList<>(festivalParty.getMemberCount()); + for (L2PcInstance player : festivalParty.getMembers()) { + if (player == null) { + continue; + } + participants.add(player.getObjectId()); + } + } + + if (oracle == SevenSigns.CABAL_DAWN) { + _dawnFestivalParticipants.put(festivalId, participants); + } else { + _duskFestivalParticipants.put(festivalId, participants); + } + } + + public void updateParticipants(L2PcInstance player, L2Party festivalParty) { + if (!isParticipant(player)) { + return; + } + + final int[] playerFestInfo = getFestivalForPlayer(player); + final int oracle = playerFestInfo[0]; + final int festivalId = playerFestInfo[1]; + + if (festivalId > -1) { + if (_festivalInitialized) { + L2DarknessFestival festivalInst = _managerInstance.getFestivalInstance(oracle, festivalId); + + if (festivalParty == null) { + for (int partyMemberObjId : getParticipants(oracle, festivalId)) { + L2PcInstance partyMember = L2World.getInstance().getPlayer(partyMemberObjId); + if (partyMember == null) { + continue; + } + + festivalInst.relocatePlayer(partyMember, true); + } + } else { + festivalInst.relocatePlayer(player, true); + } + } + + setParticipants(oracle, festivalId, festivalParty); + + // Check on disconnect if min player in party + if ((festivalParty != null) && (festivalParty.getMemberCount() < sevenSigns().getFestivalMinPlayer())) { + updateParticipants(player, null); // under minimum count + festivalParty.removePartyMember(player, messageType.Expelled); + } + } + } + + public final long getFinalScore(int oracle, int festivalId) { + if (oracle == SevenSigns.CABAL_DAWN) { + return _dawnFestivalScores.get(festivalId); + } + + return _duskFestivalScores.get(festivalId); + } + + public final int getHighestScore(int oracle, int festivalId) { + return getHighestScoreData(oracle, festivalId).getInt("score"); + } + + /** + * Returns a stats set containing the highest score this cycle for the the specified cabal and associated festival ID. + * @param oracle + * @param festivalId + * @return StatsSet festivalDat + */ + public final StatsSet getHighestScoreData(int oracle, int festivalId) { + int offsetId = festivalId; + + if (oracle == SevenSigns.CABAL_DAWN) { + offsetId += 5; + } + + // Attempt to retrieve existing score data (if found), otherwise create a + // new blank data set and display a console warning. + StatsSet currData; + + try { + currData = _festivalData.get(_signsCycle).get(offsetId); + } catch (Exception e) { + currData = new StatsSet(); + currData.set("score", 0); + currData.set("members", ""); + } + + return currData; + } + + /** + * Returns a stats set containing the highest ever recorded score data for the specified festival. + * @param festivalId + * @return StatsSet result + */ + public final StatsSet getOverallHighestScoreData(int festivalId) { + StatsSet result = null; + int highestScore = 0; + + for (Map currCycleData : _festivalData.values()) { + for (StatsSet currFestData : currCycleData.values()) { + int currFestID = currFestData.getInt("festivalId"); + int festivalScore = currFestData.getInt("score"); + + if (currFestID != festivalId) { + continue; + } + + if (festivalScore > highestScore) { + highestScore = festivalScore; + result = currFestData; + } + } + } + + return result; + } + + /** + * Set the final score details for the last participants of the specified festival data. Returns true if the score is higher than that previously recorded this cycle. + * @param player + * @param oracle + * @param festivalId + * @param offeringScore + * @return boolean isHighestScore + */ + public boolean setFinalScore(L2PcInstance player, int oracle, int festivalId, long offeringScore) { + List partyMembers; + + int currDawnHighScore = getHighestScore(SevenSigns.CABAL_DAWN, festivalId); + int currDuskHighScore = getHighestScore(SevenSigns.CABAL_DUSK, festivalId); + + int thisCabalHighScore; + int otherCabalHighScore; + + if (oracle == SevenSigns.CABAL_DAWN) { + thisCabalHighScore = currDawnHighScore; + otherCabalHighScore = currDuskHighScore; + + _dawnFestivalScores.put(festivalId, offeringScore); + } else { + thisCabalHighScore = currDuskHighScore; + otherCabalHighScore = currDawnHighScore; + + _duskFestivalScores.put(festivalId, offeringScore); + } + + StatsSet currFestData = getHighestScoreData(oracle, festivalId); + + // Check if this is the highest score for this level range so far for the player's cabal. + if (offeringScore > thisCabalHighScore) { + // If the current score is greater than that for the other cabal, + // then they already have the points from this festival. + if (thisCabalHighScore > otherCabalHighScore) { + return false; + } + + List prevParticipants = getPreviousParticipants(oracle, festivalId); + partyMembers = new ArrayList<>(prevParticipants.size()); + + // Record a string list of the party members involved. + for (Integer partyMember : prevParticipants) { + partyMembers.add(CharNameTable.getInstance().getNameById(partyMember)); + } + + // Update the highest scores and party list. + currFestData.set("date", String.valueOf(System.currentTimeMillis())); + currFestData.set("score", offeringScore); + currFestData.set("members", Util.implodeString(partyMembers, ",")); + + // Only add the score to the cabal's overall if it's higher than the other cabal's score. + if (offeringScore > otherCabalHighScore) { + int contribPoints = FESTIVAL_LEVEL_SCORES[festivalId]; + + // Give this cabal the festival points, while deducting them from the other. + SevenSigns.getInstance().addFestivalScore(oracle, contribPoints); + } + + saveFestivalData(true); + + return true; + } + + return false; + } + + public final int getAccumulatedBonus(int festivalId) { + return _accumulatedBonuses.get(festivalId); + } + + public final int getTotalAccumulatedBonus() { + int totalAccumBonus = 0; + + for (int accumBonus : _accumulatedBonuses) { + totalAccumBonus += accumBonus; + } + + return totalAccumBonus; + } + + public void addAccumulatedBonus(int festivalId, int stoneType, int stoneAmount) { + int eachStoneBonus = switch (stoneType) { + case SevenSigns.SEAL_STONE_BLUE_ID -> SevenSigns.SEAL_STONE_BLUE_VALUE; + case SevenSigns.SEAL_STONE_GREEN_ID -> SevenSigns.SEAL_STONE_GREEN_VALUE; + case SevenSigns.SEAL_STONE_RED_ID -> SevenSigns.SEAL_STONE_RED_VALUE; + default -> 0; + }; + int newTotalBonus = _accumulatedBonuses.get(festivalId) + (stoneAmount * eachStoneBonus); + _accumulatedBonuses.set(festivalId, newTotalBonus); + } + + /** + * Calculate and return the proportion of the accumulated bonus for the festival where the player was in the winning party, if the winning party's cabal won the event. The accumulated bonus is then updated, with the player's share deducted. + * @param player + * @return playerBonus (the share of the bonus for the party) + */ + public final int distribAccumulatedBonus(L2PcInstance player) { + int playerBonus = 0; + String playerName = player.getName(); + int playerCabal = SevenSigns.getInstance().getPlayerCabal(player.getObjectId()); + + if (playerCabal != SevenSigns.getInstance().getCabalHighestScore()) { + return 0; + } + + final Map festivalDataMap = _festivalData.get(_signsCycle); + if (festivalDataMap != null) { + for (StatsSet festivalData : festivalDataMap.values()) { + if (festivalData.getString("members").contains(playerName)) { + int festivalId = festivalData.getInt("festivalId"); + int numPartyMembers = festivalData.getString("members").split(",").length; + int totalAccumBonus = _accumulatedBonuses.get(festivalId); + + playerBonus = totalAccumBonus / numPartyMembers; + _accumulatedBonuses.set(festivalId, totalAccumBonus - playerBonus); + break; + } + } + } + + return playerBonus; + } + + /** + * Used to send a "shout" message to all players currently present in an Oracle.
+ * Primarily used for Festival Guide and Witch related speech. + * @param senderName + * @param npcString + */ + public void sendMessageToAll(String senderName, NpcStringId npcString) { + if ((_dawnChatGuide == null) || (_duskChatGuide == null)) { + return; + } + + sendMessageToAll(senderName, npcString, _dawnChatGuide); + sendMessageToAll(senderName, npcString, _duskChatGuide); + } + + /** + * @param senderName + * @param npcString + * @param npc + */ + public void sendMessageToAll(String senderName, NpcStringId npcString, L2Npc npc) { + CreatureSay cs = new CreatureSay(npc.getObjectId(), Say2.NPC_SHOUT, senderName, npcString); + if (npcString.getParamCount() == 1) { + cs.addStringParameter(String.valueOf(getMinsToNextFestival())); + } + + npc.broadcastPacket(cs); + } + + /** + * Basically a wrapper-call to signal to increase the challenge of the specified festival. + * @param oracle + * @param festivalId + * @return boolean isChalIncreased + */ + public final boolean increaseChallenge(int oracle, int festivalId) { + L2DarknessFestival festivalInst = _managerInstance.getFestivalInstance(oracle, festivalId); + + return festivalInst.increaseChallenge(); + } + + /** + * Used with the SpawnListener, to update the required "chat guide" instances, for use with announcements in the oracles. + * @param npc + */ + @Override + public void npcSpawned(L2Npc npc) { + if (npc == null) { + return; + } + + int npcId = npc.getId(); + + // If the spawned NPC ID matches the ones we need, assign their instances. + if (npcId == 31127) { + _dawnChatGuide = npc; + } + + if (npcId == 31137) { + _duskChatGuide = npc; + } + } + + /** + * The FestivalManager class is the main runner of all the festivals. It is used for easier integration and management of all running festivals. + * @author Tempy + */ + private class FestivalManager implements Runnable { + protected Map _festivalInstances; + + public FestivalManager() { + _festivalInstances = new HashMap<>(); + + // Increment the cycle counter. + _festivalCycle++; + + // Set the next start timers. + setNextCycleStart(); + setNextFestivalStart(sevenSigns().getFestivalCycleLength() - FESTIVAL_SIGNUP_TIME); + } + + @Override + public synchronized void run() { + try { + // The manager shouldn't be running if Seal Validation is in effect. + if (SevenSigns.getInstance().isSealValidationPeriod()) { + return; + } + + // If the next period is due to start before the end of this + // festival cycle, then don't run it. + if (SevenSigns.getInstance().getMilliToPeriodChange() < sevenSigns().getFestivalCycleLength()) { + return; + } else if (getMinsToNextFestival() == 2) { + sendMessageToAll("Festival Guide", NpcStringId.THE_MAIN_EVENT_WILL_START_IN_2_MINUTES_PLEASE_REGISTER_NOW); + } + + // Stand by until the allowed signup period has elapsed. + try { + wait(FESTIVAL_SIGNUP_TIME); + } catch (InterruptedException e) { + } + + // Clear past participants, they can no longer register their score if not done so already. + _dawnPreviousParticipants.clear(); + _duskPreviousParticipants.clear(); + + // Get rid of random monsters that avoided deletion after last festival + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.unspawnMobs(); + } + + // Start only if participants signed up + _noPartyRegister = true; + + while (_noPartyRegister) { + if ((_duskFestivalParticipants.isEmpty() && _dawnFestivalParticipants.isEmpty())) { + try { + setNextCycleStart(); + setNextFestivalStart(sevenSigns().getFestivalCycleLength() - FESTIVAL_SIGNUP_TIME); + wait(sevenSigns().getFestivalCycleLength() - FESTIVAL_SIGNUP_TIME); + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + if (!festivalInst._npcInsts.isEmpty()) { + festivalInst.unspawnMobs(); + } + } + } catch (InterruptedException e) { + } + } else { + _noPartyRegister = false; + } + } + + /* INITIATION */ + // Set the festival timer to 0, as it is just beginning. + long elapsedTime; + + // Create the instances for the festivals in both Oracles, + // but only if they have participants signed up for them. + for (int i = 0; i < FESTIVAL_COUNT; i++) { + if (_duskFestivalParticipants.get(i) != null) { + _festivalInstances.put(10 + i, new L2DarknessFestival(SevenSigns.CABAL_DUSK, i)); + } + + if (_dawnFestivalParticipants.get(i) != null) { + _festivalInstances.put(20 + i, new L2DarknessFestival(SevenSigns.CABAL_DAWN, i)); + } + } + + // Prevent future signups while festival is in progress. + _festivalInitialized = true; + + setNextFestivalStart(sevenSigns().getFestivalCycleLength()); + sendMessageToAll("Festival Guide", NpcStringId.THE_MAIN_EVENT_IS_NOW_STARTING); + + // Stand by for a short length of time before starting the festival. + try { + wait(sevenSigns().getFestivalFirstSpawn()); + } catch (InterruptedException e) { + } + + elapsedTime = sevenSigns().getFestivalFirstSpawn(); + + // Participants can now opt to increase the challenge, if desired. + _festivalInProgress = true; + + /* PROPAGATION */ + // Sequentially set all festivals to begin, spawn the Festival Witch and notify participants. + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.festivalStart(); + festivalInst.sendMessageToParticipants(NpcStringId.THE_MAIN_EVENT_IS_NOW_STARTING); + } + + // After a short time period, move all idle spawns to the center of the arena. + try { + wait(sevenSigns().getFestivalFirstSwarm() - sevenSigns().getFestivalFirstSpawn()); + } catch (InterruptedException e) { + } + + elapsedTime += sevenSigns().getFestivalFirstSwarm() - sevenSigns().getFestivalFirstSpawn(); + + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.moveMonstersToCenter(); + } + + // Stand by until the time comes for the second spawn. + try { + wait(sevenSigns().getFestivalSecondSpawn() - sevenSigns().getFestivalFirstSwarm()); + } catch (InterruptedException e) { + } + + // Spawn an extra set of monsters (archers) on the free platforms with + // a faster respawn when killed. + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.spawnFestivalMonsters(FESTIVAL_DEFAULT_RESPAWN / 2, 2); + + long end = (sevenSigns().getFestivalLength() - sevenSigns().getFestivalSecondSpawn()) / 60000; + if (end == 2) { + festivalInst.sendMessageToParticipants(NpcStringId.THE_FESTIVAL_OF_DARKNESS_WILL_END_IN_TWO_MINUTES); + } else { + festivalInst.sendMessageToParticipants("The Festival of Darkness will end in " + end + " minute(s)."); + } + } + + elapsedTime += sevenSigns().getFestivalSecondSpawn() - sevenSigns().getFestivalFirstSwarm(); + + // After another short time period, again move all idle spawns to the center of the arena. + try { + wait(sevenSigns().getFestivalSecondSwarm() - sevenSigns().getFestivalSecondSpawn()); + } catch (InterruptedException e) { + } + + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.moveMonstersToCenter(); + } + + elapsedTime += sevenSigns().getFestivalSecondSwarm() - sevenSigns().getFestivalSecondSpawn(); + + // Stand by until the time comes for the chests to be spawned. + try { + wait(sevenSigns().getFestivalChestSpawn() - sevenSigns().getFestivalSecondSwarm()); + } catch (InterruptedException e) { + } + + // Spawn the festival chests, which enable the team to gain greater rewards + // for each chest they kill. + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.spawnFestivalMonsters(FESTIVAL_DEFAULT_RESPAWN, 3); + festivalInst.sendMessageToParticipants("The chests have spawned! Be quick, the festival will end soon."); // FIXME What is the correct npcString? + } + + elapsedTime += sevenSigns().getFestivalChestSpawn() - sevenSigns().getFestivalSecondSwarm(); + + // Stand by and wait until it's time to end the festival. + try { + wait(sevenSigns().getFestivalLength() - elapsedTime); + } catch (InterruptedException e) { + } + + // Participants can no longer opt to increase the challenge, as the festival will soon close. + _festivalInProgress = false; + + /* TERMINATION */ + // Sequentially begin the ending sequence for all running festivals. + for (L2DarknessFestival festivalInst : _festivalInstances.values()) { + festivalInst.festivalEnd(); + } + + // Clear the participants list for the next round of signups. + _dawnFestivalParticipants.clear(); + _duskFestivalParticipants.clear(); + + // Allow signups for the next festival cycle. + _festivalInitialized = false; + + sendMessageToAll("Festival Witch", NpcStringId.THAT_WILL_DO_ILL_MOVE_YOU_TO_THE_OUTSIDE_SOON); + } catch (Exception e) { + LOG.warn("Could not run festival task!", e); + } + } + + /** + * Returns the running instance of a festival for the given Oracle and festivalID.
+ * A null value is returned if there are no participants in that festival. + * @param oracle + * @param festivalId + * @return L2DarknessFestival festivalInst + */ + public final L2DarknessFestival getFestivalInstance(int oracle, int festivalId) { + if (!isFestivalInitialized()) { + return null; + } + + /* + * Compute the offset if a Dusk instance is required. ID: 0 1 2 3 4 Dusk 1:1011121314 Dawn 2:2021222324 + */ + + festivalId += (oracle == SevenSigns.CABAL_DUSK) ? 10 : 20; + return _festivalInstances.get(festivalId); + } + } + + /** + * Each running festival is represented by an L2DarknessFestival class. It contains all the spawn information and data for the running festival. All festivals are managed by the FestivalManager class, which must be initialized first. + * @author Tempy + */ + private class L2DarknessFestival { + protected final int _cabal; + protected final int _levelRange; + protected boolean _challengeIncreased; + + private final FestivalSpawn _startLocation; + private final FestivalSpawn _witchSpawn; + + private L2Npc _witchInst; + List _npcInsts = new ArrayList<>(); + + private List _participants; + private final Map _originalLocations = new ConcurrentHashMap<>(); + + protected L2DarknessFestival(int cabal, int levelRange) { + _cabal = cabal; + _levelRange = levelRange; + + if (cabal == SevenSigns.CABAL_DAWN) { + _participants = _dawnFestivalParticipants.get(levelRange); + _witchSpawn = new FestivalSpawn(FESTIVAL_DAWN_WITCH_SPAWNS[levelRange]); + _startLocation = new FestivalSpawn(FESTIVAL_DAWN_PLAYER_SPAWNS[levelRange]); + } else { + _participants = _duskFestivalParticipants.get(levelRange); + _witchSpawn = new FestivalSpawn(FESTIVAL_DUSK_WITCH_SPAWNS[levelRange]); + _startLocation = new FestivalSpawn(FESTIVAL_DUSK_PLAYER_SPAWNS[levelRange]); + } + + // FOR TESTING! + if (_participants == null) { + _participants = new ArrayList<>(); + } + + festivalInit(); + } + + protected void festivalInit() { + boolean isPositive; + + // Teleport all players to arena and notify them. + if ((_participants != null) && !_participants.isEmpty()) { + try { + for (int participantObjId : _participants) { + L2PcInstance participant = L2World.getInstance().getPlayer(participantObjId); + if (participant == null) { + continue; + } + + _originalLocations.put(participantObjId, new FestivalSpawn(participant.getX(), participant.getY(), participant.getZ(), participant.getHeading())); + + // Randomize the spawn point around the specific centerpoint for each player. + int x = _startLocation._x; + int y = _startLocation._y; + + isPositive = (Rnd.nextInt(2) == 1); + + if (isPositive) { + x += Rnd.nextInt(FESTIVAL_MAX_OFFSET_X); + y += Rnd.nextInt(FESTIVAL_MAX_OFFSET_Y); + } else { + x -= Rnd.nextInt(FESTIVAL_MAX_OFFSET_X); + y -= Rnd.nextInt(FESTIVAL_MAX_OFFSET_Y); + } + + participant.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + participant.teleToLocation(new Location(x, y, _startLocation._z), true); + + // Remove all buffs from all participants on entry. Works like the skill Cancel. + participant.stopAllEffectsExceptThoseThatLastThroughDeath(); + + // Remove any stray blood offerings in inventory + L2ItemInstance bloodOfferings = participant.getInventory().getItemByItemId(FESTIVAL_OFFERING_ID); + if (bloodOfferings != null) { + participant.destroyItem("SevenSigns", bloodOfferings, null, true); + } + } + } catch (NullPointerException e) { + // deleteMe handling should teleport party out in case of disconnect + } + } + + // Spawn the festival witch for this arena + try { + L2Spawn npcSpawn = new L2Spawn(_witchSpawn._npcId); + + npcSpawn.setX(_witchSpawn._x); + npcSpawn.setY(_witchSpawn._y); + npcSpawn.setZ(_witchSpawn._z); + npcSpawn.setHeading(_witchSpawn._heading); + npcSpawn.setAmount(1); + npcSpawn.setRespawnDelay(1); + + // Needed as doSpawn() is required to be called also for the NpcInstance it returns. + npcSpawn.startRespawn(); + + SpawnTable.getInstance().addNewSpawn(npcSpawn, false); + _witchInst = npcSpawn.doSpawn(); + } catch (Exception e) { + LOG.warn("Error while spawning Festival Witch ID {}!", _witchSpawn._npcId, e); + } + + // Make it appear as though the Witch has appeared there. + MagicSkillUse msu = new MagicSkillUse(_witchInst, _witchInst, 2003, 1, 1, 0); + _witchInst.broadcastPacket(msu); + + // And another one...:D + msu = new MagicSkillUse(_witchInst, _witchInst, 2133, 1, 1, 0); + _witchInst.broadcastPacket(msu); + + // Send a message to all participants from the witch. + sendMessageToParticipants(NpcStringId.THE_MAIN_EVENT_WILL_START_IN_2_MINUTES_PLEASE_REGISTER_NOW); + } + + protected void festivalStart() { + spawnFestivalMonsters(FESTIVAL_DEFAULT_RESPAWN, 0); + } + + protected void moveMonstersToCenter() { + boolean isPositive; + + for (L2FestivalMonsterInstance festivalMob : _npcInsts) { + if (festivalMob.isDead()) { + continue; + } + + // Only move monsters that are idle or doing their usual functions. + CtrlIntention currIntention = festivalMob.getAI().getIntention(); + + if ((currIntention != CtrlIntention.AI_INTENTION_IDLE) && (currIntention != CtrlIntention.AI_INTENTION_ACTIVE)) { + continue; + } + + int x = _startLocation._x; + int y = _startLocation._y; + + /* + * Random X and Y coords around the player start location, up to half of the maximum allowed offset are generated to prevent the mobs from all moving to the exact same place. + */ + isPositive = (Rnd.nextInt(2) == 1); + + if (isPositive) { + x += Rnd.nextInt(FESTIVAL_MAX_OFFSET_X); + y += Rnd.nextInt(FESTIVAL_MAX_OFFSET_Y); + } else { + x -= Rnd.nextInt(FESTIVAL_MAX_OFFSET_X); + y -= Rnd.nextInt(FESTIVAL_MAX_OFFSET_Y); + } + + festivalMob.setRunning(); + festivalMob.getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, new Location(x, y, _startLocation._z, Rnd.nextInt(65536))); + } + } + + /** + * Used to spawn monsters unique to the festival.
+ * Valid SpawnTypes:
+ * 0 - All Primary Monsters (starting monsters)
+ * 1 - Same as 0, but without archers/marksmen. (used for challenge increase)
+ * 2 - Secondary Monsters (archers)
+ * 3 - Festival Chests + * @param respawnDelay + * @param spawnType + */ + protected void spawnFestivalMonsters(int respawnDelay, int spawnType) { + int[][] _npcSpawns = switch (spawnType) { + case 0, 1 -> (_cabal == SevenSigns.CABAL_DAWN) ? FESTIVAL_DAWN_PRIMARY_SPAWNS[_levelRange] : FESTIVAL_DUSK_PRIMARY_SPAWNS[_levelRange]; + case 2 -> (_cabal == SevenSigns.CABAL_DAWN) ? FESTIVAL_DAWN_SECONDARY_SPAWNS[_levelRange] : FESTIVAL_DUSK_SECONDARY_SPAWNS[_levelRange]; + case 3 -> (_cabal == SevenSigns.CABAL_DAWN) ? FESTIVAL_DAWN_CHEST_SPAWNS[_levelRange] : FESTIVAL_DUSK_CHEST_SPAWNS[_levelRange]; + default -> new int[0][0]; + }; + + for (int[] _npcSpawn : _npcSpawns) { + FestivalSpawn currSpawn = new FestivalSpawn(_npcSpawn); + + // Only spawn archers/marksmen if specified to do so. + if ((spawnType == 1) && isFestivalArcher(currSpawn._npcId)) { + continue; + } + + try { + L2Spawn npcSpawn = new L2Spawn(currSpawn._npcId); + + npcSpawn.setX(currSpawn._x); + npcSpawn.setY(currSpawn._y); + npcSpawn.setZ(currSpawn._z); + npcSpawn.setHeading(Rnd.nextInt(65536)); + npcSpawn.setAmount(1); + npcSpawn.setRespawnDelay(respawnDelay); + + // Needed as doSpawn() is required to be called also for the NpcInstance it returns. + npcSpawn.startRespawn(); + + SpawnTable.getInstance().addNewSpawn(npcSpawn, false); + L2FestivalMonsterInstance festivalMob = (L2FestivalMonsterInstance) npcSpawn.doSpawn(); + + // Set the offering bonus to 2x or 5x the amount per kill, + // if this spawn is part of an increased challenge or is a festival chest. + if (spawnType == 1) { + festivalMob.setOfferingBonus(2); + } else if (spawnType == 3) { + festivalMob.setOfferingBonus(5); + } + + _npcInsts.add(festivalMob); + } catch (Exception e) { + LOG.warn("Error while spawning NPC ID {}!", currSpawn._npcId, e); + } + } + } + + protected boolean increaseChallenge() { + if (_challengeIncreased) { + return false; + } + + // Set this flag to true to make sure that this can only be done once. + _challengeIncreased = true; + + // Spawn more festival monsters, but this time with a twist. + spawnFestivalMonsters(FESTIVAL_DEFAULT_RESPAWN, 1); + return true; + } + + public void sendMessageToParticipants(NpcStringId npcStringId) { + if ((_participants != null) && !_participants.isEmpty()) { + _witchInst.broadcastPacket(new CreatureSay(_witchInst.getObjectId(), Say2.NPC_ALL, "Festival Witch", npcStringId)); + } + } + + public void sendMessageToParticipants(String npcString) { + if ((_participants != null) && !_participants.isEmpty()) { + _witchInst.broadcastPacket(new CreatureSay(_witchInst.getObjectId(), Say2.NPC_ALL, "Festival Witch", npcString)); + } + } + + protected void festivalEnd() { + if ((_participants != null) && !_participants.isEmpty()) { + for (int participantObjId : _participants) { + try { + L2PcInstance participant = L2World.getInstance().getPlayer(participantObjId); + if (participant == null) { + continue; + } + + relocatePlayer(participant, false); + participant.sendMessage("The festival has ended. Your party leader must now register your score before the next festival takes place."); + } catch (NullPointerException e) { + } + } + + if (_cabal == SevenSigns.CABAL_DAWN) { + _dawnPreviousParticipants.put(_levelRange, _participants); + } else { + _duskPreviousParticipants.put(_levelRange, _participants); + } + } + _participants = null; + + unspawnMobs(); + } + + protected void unspawnMobs() { + // Delete all the NPCs in the current festival arena. + if (_witchInst != null) { + _witchInst.getSpawn().stopRespawn(); + _witchInst.deleteMe(); + SpawnTable.getInstance().deleteSpawn(_witchInst.getSpawn(), false); + } + + for (L2FestivalMonsterInstance monsterInst : _npcInsts) { + if (monsterInst != null) { + monsterInst.getSpawn().stopRespawn(); + monsterInst.deleteMe(); + SpawnTable.getInstance().deleteSpawn(monsterInst.getSpawn(), false); + } + } + } + + public void relocatePlayer(L2PcInstance participant, boolean isRemoving) { + try { + FestivalSpawn origPosition = _originalLocations.get(participant.getObjectId()); + + if (isRemoving) { + _originalLocations.remove(participant.getObjectId()); + } + + participant.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + participant.teleToLocation(new Location(origPosition._x, origPosition._y, origPosition._z), true); + participant.sendMessage("You have been removed from the festival arena."); + } catch (Exception e) { + // If an exception occurs, just move the player to the nearest town. + try { + participant.teleToLocation(TeleportWhereType.TOWN); + participant.sendMessage("You have been removed from the festival arena."); + } catch (NullPointerException e2) { + } + } + } + } + + private static class FestivalSpawn { + protected final int _x; + protected final int _y; + protected final int _z; + protected final int _heading; + protected final int _npcId; + + protected FestivalSpawn(int x, int y, int z, int heading) { + _x = x; + _y = y; + _z = z; + + // Generate a random heading if no positive one given. + _heading = (heading < 0) ? Rnd.nextInt(65536) : heading; + + _npcId = -1; + } + + protected FestivalSpawn(int[] spawnData) { + _x = spawnData[0]; + _y = spawnData[1]; + _z = spawnData[2]; + + _heading = (spawnData[3] < 0) ? Rnd.nextInt(65536) : spawnData[3]; + + if (spawnData.length > 4) { + _npcId = spawnData[4]; + } else { + _npcId = -1; + } + } + } + + private static class SingletonHolder { + protected static final SevenSignsFestival _instance = new SevenSignsFestival(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/Shutdown.java b/src/main/java/com/l2jserver/gameserver/Shutdown.java new file mode 100644 index 00000000..2a8468cf --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/Shutdown.java @@ -0,0 +1,517 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.UPnPService; +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.sql.impl.OfflineTradersTable; +import com.l2jserver.gameserver.datatables.BotReportTable; +import com.l2jserver.gameserver.instancemanager.CastleManorManager; +import com.l2jserver.gameserver.instancemanager.ClanHallSiegeManager; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.instancemanager.GlobalVariablesManager; +import com.l2jserver.gameserver.instancemanager.GrandBossManager; +import com.l2jserver.gameserver.instancemanager.ItemAuctionManager; +import com.l2jserver.gameserver.instancemanager.ItemsOnGroundManager; +import com.l2jserver.gameserver.instancemanager.QuestManager; +import com.l2jserver.gameserver.instancemanager.RaidBossSpawnManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Hero; +import com.l2jserver.gameserver.model.olympiad.Olympiad; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.gameserverpackets.ServerStatus; +import com.l2jserver.gameserver.network.serverpackets.ServerClose; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * This class provides the functions for shutting down and restarting the server.
+ * It closes all open client connections and saves all data. + * @since 2005/03/27 15:29:09 + */ +public class Shutdown extends Thread { + + private static final Logger LOG = LoggerFactory.getLogger(Shutdown.class); + + private static Shutdown _counterInstance = null; + + private int _secondsShut; + private int _shutdownMode; + private static final int SIGTERM = 0; + private static final int GM_SHUTDOWN = 1; + private static final int GM_RESTART = 2; + private static final int ABORT = 3; + private static final String[] MODE_TEXT = { + "SIGTERM", + "shutting down", + "restarting", + "aborting" + }; + + /** + * This function starts a shutdown count down from Telnet (Copied from Function startShutdown()) + * @param seconds seconds until shutdown + */ + private void SendServerQuit(int seconds) { + SystemMessage sysm = SystemMessage.getSystemMessage(SystemMessageId.THE_SERVER_WILL_BE_COMING_DOWN_IN_S1_SECONDS); + sysm.addInt(seconds); + Broadcast.toAllOnlinePlayers(sysm); + } + + public void startTelnetShutdown(String IP, int seconds, boolean restart) { + LOG.warn("IP: {} issued shutdown command. {} in {} seconds!", IP, MODE_TEXT[_shutdownMode], seconds); + + if (restart) { + _shutdownMode = GM_RESTART; + } else { + _shutdownMode = GM_SHUTDOWN; + } + + switch (seconds) { + case 540: + case 480: + case 420: + case 360: + case 300: + case 240: + case 180: + case 120: + case 60: + case 30: + case 10: + case 5: + case 4: + case 3: + case 2: + case 1: + break; + default: + SendServerQuit(seconds); + } + + if (_counterInstance != null) { + _counterInstance._abort(); + } + _counterInstance = new Shutdown(seconds, restart); + _counterInstance.start(); + } + + /** + * This function aborts a running countdown + * @param IP IP Which Issued shutdown command + */ + public void telnetAbort(String IP) { + LOG.warn("IP: {} issued shutdown ABORT. {} has been stopped!", IP, MODE_TEXT[_shutdownMode]); + + if (_counterInstance != null) { + _counterInstance._abort(); + Broadcast.toAllOnlinePlayers("Server aborts " + MODE_TEXT[_shutdownMode] + " and continues normal operation!", false); + } + } + + /** + * Default constructor is only used internal to create the shutdown-hook instance + */ + protected Shutdown() { + _secondsShut = -1; + _shutdownMode = SIGTERM; + } + + /** + * This creates a countdown instance of Shutdown. + * @param seconds how many seconds until shutdown + * @param restart true is the server shall restart after shutdown + */ + public Shutdown(int seconds, boolean restart) { + if (seconds < 0) { + seconds = 0; + } + _secondsShut = seconds; + if (restart) { + _shutdownMode = GM_RESTART; + } else { + _shutdownMode = GM_SHUTDOWN; + } + } + + /** + * This function is called, when a new thread starts if this thread is the thread of getInstance, then this is the shutdown hook and we save all data and disconnect all clients.
+ * After this thread ends, the server will completely exit if this is not the thread of getInstance, then this is a countdown thread.
+ * We start the countdown, and when we finished it, and it was not aborted, we tell the shutdown-hook why we call exit, and then call exit when the exit status of the server is 1, startServer.sh / startServer.bat will restart the server. + */ + @Override + public void run() { + if (this == getInstance()) { + TimeCounter tc = new TimeCounter(); + TimeCounter tc1 = new TimeCounter(); + + try { + UPnPService.getInstance().removeAllPorts(); + LOG.info("UPnP Service: All ports mappings deleted ({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + LOG.warn("Error while removing UPnP port mappings!", e); + } + + try { + if ((customs().offlineTradeEnable() || customs().offlineCraftEnable()) && customs().restoreOffliners()) { + OfflineTradersTable.getInstance().storeOffliners(); + LOG.info("Offline Traders Table: Offline shops stored({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } + } catch (Exception e) { + LOG.warn("Error saving offline shops!", e); + } + + try { + disconnectAllCharacters(); + LOG.info("All players disconnected and saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + // ignore + } + + // ensure all services are stopped + try { + GameTimeController.getInstance().stopTimer(); + LOG.info("Game Time Controller: Timer stopped({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + // ignore + } + + // stop all thread pools + try { + ThreadPoolManager.getInstance().shutdown(); + LOG.info("Thread Pool Manager: Manager has been shut down({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + // ignore + } + + try { + LoginServerThread.getInstance().interrupt(); + LOG.info("Login Server Thread: Thread interrupted({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + // ignore + } + + // last bye bye, save all data and quit this server + saveData(); + tc.restartCounter(); + + // saveData sends messages to exit players, so shutdown selector after it + try { + GameServer.gameServer.getSelectorThread().shutdown(); + LOG.info("Game Server: Selector thread has been shut down({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } catch (Exception e) { + // ignore + } + + // commit data, last chance + ConnectionFactory.getInstance().close(); + LOG.info("ConnectionFactory: Database connection has been shutdown({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // server will quit, when this function ends. + if (getInstance()._shutdownMode == GM_RESTART) { + Runtime.getRuntime().halt(2); + } else { + Runtime.getRuntime().halt(0); + } + + LOG.info("The server has been successfully shut down in {}seconds.", TimeUnit.MILLISECONDS.toSeconds(tc1.getEstimatedTime())); + } else { + // gm shutdown: send warnings and then call exit to start shutdown sequence + countdown(); + // last point where logging is operational :( + LOG.warn("GM shutdown countdown is over. {} NOW!", MODE_TEXT[_shutdownMode]); + switch (_shutdownMode) { + case GM_SHUTDOWN -> { + getInstance().setMode(GM_SHUTDOWN); + System.exit(0); + } + case GM_RESTART -> { + getInstance().setMode(GM_RESTART); + System.exit(2); + } + case ABORT -> LoginServerThread.getInstance().setServerStatus(ServerStatus.STATUS_AUTO); + } + } + } + + /** + * This functions starts a shutdown countdown. + * @param activeChar GM who issued the shutdown command + * @param seconds seconds until shutdown + * @param restart true if the server will restart after shutdown + */ + public void startShutdown(L2PcInstance activeChar, int seconds, boolean restart) { + if (restart) { + _shutdownMode = GM_RESTART; + } else { + _shutdownMode = GM_SHUTDOWN; + } + + LOG.warn("GM: {}({}) issued shutdown command. {} in {} seconds!", activeChar.getName(), activeChar.getObjectId(), MODE_TEXT[_shutdownMode], seconds); + + if (_shutdownMode > 0) { + switch (seconds) { + case 540: + case 480: + case 420: + case 360: + case 300: + case 240: + case 180: + case 120: + case 60: + case 30: + case 10: + case 5: + case 4: + case 3: + case 2: + case 1: + break; + default: + SendServerQuit(seconds); + } + } + + if (_counterInstance != null) { + _counterInstance._abort(); + } + + // the main instance should only run for shutdown hook, so we start a new instance + _counterInstance = new Shutdown(seconds, restart); + _counterInstance.start(); + } + + /** + * This function aborts a running countdown. + * @param activeChar GM who issued the abort command + */ + public void abort(L2PcInstance activeChar) { + LOG.warn("GM: {}({}) issued shutdown ABORT. {} has been stopped!", activeChar.getName(), activeChar.getObjectId(), MODE_TEXT[_shutdownMode]); + if (_counterInstance != null) { + _counterInstance._abort(); + Broadcast.toAllOnlinePlayers("Server aborts " + MODE_TEXT[_shutdownMode] + " and continues normal operation!", false); + } + } + + /** + * Set the shutdown mode. + * @param mode what mode shall be set + */ + private void setMode(int mode) { + _shutdownMode = mode; + } + + /** + * Set shutdown mode to ABORT. + */ + private void _abort() { + _shutdownMode = ABORT; + } + + /** + * This counts the countdown and reports it to all players countdown is aborted if mode changes to ABORT. + */ + private void countdown() { + try { + while (_secondsShut > 0) { + switch (_secondsShut) { + case 540 -> SendServerQuit(540); + case 480 -> SendServerQuit(480); + case 420 -> SendServerQuit(420); + case 360 -> SendServerQuit(360); + case 300 -> SendServerQuit(300); + case 240 -> SendServerQuit(240); + case 180 -> SendServerQuit(180); + case 120 -> SendServerQuit(120); + case 60 -> { + LoginServerThread.getInstance().setServerStatus(ServerStatus.STATUS_DOWN); // avoids new players from logging in + SendServerQuit(60); + } + case 30 -> SendServerQuit(30); + case 10 -> SendServerQuit(10); + case 5 -> SendServerQuit(5); + case 4 -> SendServerQuit(4); + case 3 -> SendServerQuit(3); + case 2 -> SendServerQuit(2); + case 1 -> SendServerQuit(1); + } + + _secondsShut--; + + int delay = 1000; // milliseconds + Thread.sleep(delay); + + if (_shutdownMode == ABORT) { + break; + } + } + } catch (InterruptedException e) { + // this will never happen + } + } + + /** + * This sends a last bye bye, disconnects all players and saves data. + */ + private void saveData() { + switch (_shutdownMode) { + case SIGTERM -> LOG.info("SIGTERM received. Shutting down NOW!"); + case GM_SHUTDOWN -> LOG.info("GM shutdown received. Shutting down NOW!"); + case GM_RESTART -> LOG.info("GM restart received. Restarting NOW!"); + } + + TimeCounter tc = new TimeCounter(); + // Seven Signs data is now saved along with Festival data. + if (!SevenSigns.getInstance().isSealValidationPeriod()) { + SevenSignsFestival.getInstance().saveFestivalData(false); + LOG.info("SevenSignsFestival: Festival data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } + + // Save Seven Signs data before closing. :) + SevenSigns.getInstance().saveSevenSignsData(); + LOG.info("SevenSigns: Seven Signs data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + SevenSigns.getInstance().saveSevenSignsStatus(); + LOG.info("SevenSigns: Seven Signs status saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // Save all raidboss and GrandBoss status ^_^ + RaidBossSpawnManager.getInstance().cleanUp(); + LOG.info("RaidBossSpawnManager: All raidboss info saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + GrandBossManager.getInstance().cleanUp(); + LOG.info("GrandBossManager: All Grand Boss info saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + ItemAuctionManager.getInstance().shutdown(); + LOG.info("Item Auction Manager: All tasks stopped({}ms).", tc.getEstimatedTimeAndRestartCounter()); + Olympiad.getInstance().saveOlympiadStatus(); + LOG.info("Olympiad System: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + Hero.getInstance().shutdown(); + LOG.info("Hero System: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + ClanTable.getInstance().storeClanScore(); + LOG.info("Clan System: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // Save Cursed Weapons data before closing. + CursedWeaponsManager.getInstance().saveData(); + LOG.info("Cursed Weapons Manager: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // Save all manor data + if (!general().manorSaveAllActions()) { + CastleManorManager.getInstance().storeMe(); + LOG.info("Castle Manor Manager: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } + + ClanHallSiegeManager.getInstance().onServerShutDown(); + LOG.info("CHSiegeManager: Siegable hall attacker lists saved!"); + + // Save all global (non-player specific) Quest data that needs to persist after reboot + QuestManager.getInstance().save(); + LOG.info("Quest Manager: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // Save all global variables data + GlobalVariablesManager.getInstance().storeMe(); + LOG.info("Global Variables Manager: Variables saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + + // Save items on ground before closing + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().saveInDb(); + LOG.info("Items On Ground Manager: Data saved({}ms).", tc.getEstimatedTimeAndRestartCounter()); + ItemsOnGroundManager.getInstance().cleanUp(); + LOG.info("Items On Ground Manager: Cleaned up({}ms).", tc.getEstimatedTimeAndRestartCounter()); + } + + // Save bot reports to database + if (general().enableBotReportButton()) { + BotReportTable.getInstance().saveReportedCharData(); + LOG.info("Bot Report Table: Successfully saved reports to database!"); + } + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // never happens :p + } + } + + /** + * This disconnects all clients from the server. + */ + private void disconnectAllCharacters() { + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + // Logout Character + try { + L2GameClient client = player.getClient(); + if ((client != null) && !client.isDetached()) { + client.close(ServerClose.STATIC_PACKET); + client.setActiveChar(null); + player.setClient(null); + } + player.deleteMe(); + } catch (Exception e) { + LOG.warn("Failed logout char {}", player, e); + } + } + } + + /** + * A simple class used to track down the estimated time of method executions.
+ * Once this class is created, it saves the start time, and when you want to get the estimated time, use the getEstimatedTime() method. + */ + private static final class TimeCounter { + private long _startTime; + + protected TimeCounter() { + restartCounter(); + } + + protected void restartCounter() { + _startTime = System.currentTimeMillis(); + } + + protected long getEstimatedTimeAndRestartCounter() { + final long toReturn = System.currentTimeMillis() - _startTime; + restartCounter(); + return toReturn; + } + + protected long getEstimatedTime() { + return System.currentTimeMillis() - _startTime; + } + } + + /** + * Get the shutdown-hook instance the shutdown-hook instance is created by the first call of this function, but it has to be registered externally.
+ * @return instance of Shutdown, to be used as shutdown hook + */ + public static Shutdown getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final Shutdown _instance = new Shutdown(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ThreadPoolManager.java b/src/main/java/com/l2jserver/gameserver/ThreadPoolManager.java new file mode 100644 index 00000000..9f94c56e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ThreadPoolManager.java @@ -0,0 +1,630 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.config.Configuration; +import com.l2jserver.gameserver.util.StringUtil; + +/** + *

+ * This class is made to handle all the ThreadPools used in L2J. + *

+ *

+ * Scheduled Tasks can either be sent to a {@link #_generalScheduledThreadPool "general"} or {@link #_effectsScheduledThreadPool "effects"} {@link ScheduledThreadPoolExecutor ScheduledThreadPool}: The "effects" one is used for every effects (skills, hp/mp regen ...) while the "general" one is used + * for everything else that needs to be scheduled.
+ * There also is an {@link #_aiScheduledThreadPool "ai"} {@link ScheduledThreadPoolExecutor ScheduledThreadPool} used for AI Tasks. + *

+ *

+ * Tasks can be sent to {@link ScheduledThreadPoolExecutor ScheduledThreadPool} either with: + *

    + *
  • {@link #scheduleEffect(Runnable, long, TimeUnit)} and {@link #scheduleEffect(Runnable, long)} : for effects Tasks that needs to be executed only once.
  • + *
  • {@link #scheduleGeneral(Runnable, long, TimeUnit)} and {@link #scheduleGeneral(Runnable, long)} : for scheduled Tasks that needs to be executed once.
  • + *
  • {@link #scheduleAi(Runnable, long, TimeUnit)} and {@link #scheduleAi(Runnable, long)} : for AI Tasks that needs to be executed once
  • + *
+ * or + *
    + *
  • {@link #scheduleEffectAtFixedRate(Runnable, long, long, TimeUnit)} and {@link #scheduleEffectAtFixedRate(Runnable, long, long)} : for effects Tasks that needs to be executed periodically.
  • + *
  • {@link #scheduleGeneralAtFixedRate(Runnable, long, long, TimeUnit)} and {@link #scheduleGeneralAtFixedRate(Runnable, long, long)} : for scheduled Tasks that needs to be executed periodically.
  • + *
  • {@link #scheduleAiAtFixedRate(Runnable, long, long, TimeUnit)} and {@link #scheduleAiAtFixedRate(Runnable, long, long)} : for AI Tasks that needs to be executed periodically
  • + *
+ *

+ *

+ * For all Tasks that should be executed with no delay asynchronously in a ThreadPool there also are usual {@link ThreadPoolExecutor ThreadPools} that can grow/shrink according to their load.: + *

    + *
  • {@link #_generalPacketsThreadPool GeneralPackets} where most packets handler are executed.
  • + *
  • {@link #_ioPacketsThreadPool I/O Packets} where all the i/o packets are executed.
  • + *
  • There will be an AI ThreadPool where AI events should be executed
  • + *
  • A general ThreadPool where everything else that needs to run asynchronously with no delay should be executed ({@link com.l2jserver.gameserver.model.actor.knownlist KnownList} updates, SQL updates/inserts...)?
  • + *
+ *

+ * @author -Wooden- + */ +public class ThreadPoolManager { + protected static final Logger LOG = LoggerFactory.getLogger(ThreadPoolManager.class); + + private static final class RunnableWrapper implements Runnable { + private final Runnable _r; + + public RunnableWrapper(final Runnable r) { + _r = r; + } + + @Override + public void run() { + try { + _r.run(); + } catch (final Throwable e) { + final Thread t = Thread.currentThread(); + final UncaughtExceptionHandler h = t.getUncaughtExceptionHandler(); + if (h != null) { + h.uncaughtException(t, e); + } + } + } + } + + private final ScheduledThreadPoolExecutor _effectsScheduledThreadPool; + private final ScheduledThreadPoolExecutor _generalScheduledThreadPool; + private final ScheduledThreadPoolExecutor _aiScheduledThreadPool; + private final ScheduledThreadPoolExecutor _eventScheduledThreadPool; + private final ThreadPoolExecutor _generalPacketsThreadPool; + private final ThreadPoolExecutor _ioPacketsThreadPool; + private final ThreadPoolExecutor _generalThreadPool; + private final ThreadPoolExecutor _eventThreadPool; + + private boolean _shutdown; + + public static ThreadPoolManager getInstance() { + return SingletonHolder._instance; + } + + protected ThreadPoolManager() { + _effectsScheduledThreadPool = new ScheduledThreadPoolExecutor(general().getThreadPoolSizeEffects(), new PriorityThreadFactory("EffectsSTPool", Thread.NORM_PRIORITY)); + _generalScheduledThreadPool = new ScheduledThreadPoolExecutor(general().getThreadPoolSizeGeneral(), new PriorityThreadFactory("GeneralSTPool", Thread.NORM_PRIORITY)); + _eventScheduledThreadPool = new ScheduledThreadPoolExecutor(general().getThreadPoolSizeEvents(), new PriorityThreadFactory("EventSTPool", Thread.NORM_PRIORITY)); + _ioPacketsThreadPool = new ThreadPoolExecutor(general().getUrgentPacketThreadCoreSize(), Integer.MAX_VALUE, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new PriorityThreadFactory("I/O Packet Pool", Thread.NORM_PRIORITY + 1)); + _generalPacketsThreadPool = new ThreadPoolExecutor(general().getGeneralPacketThreadCoreSize(), general().getGeneralPacketThreadCoreSize() + 2, 15L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new PriorityThreadFactory("Normal Packet Pool", Thread.NORM_PRIORITY + 1)); + _generalThreadPool = new ThreadPoolExecutor(general().getGeneralThreadCoreSize(), general().getGeneralThreadCoreSize() + 2, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new PriorityThreadFactory("General Pool", Thread.NORM_PRIORITY)); + _aiScheduledThreadPool = new ScheduledThreadPoolExecutor(general().getAiMaxThread(), new PriorityThreadFactory("AISTPool", Thread.NORM_PRIORITY)); + _eventThreadPool = new ThreadPoolExecutor(general().getEventsMaxThread(), general().getEventsMaxThread() + 2, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new PriorityThreadFactory("Event Pool", Thread.NORM_PRIORITY)); + + scheduleGeneralAtFixedRate(new PurgeTask(_effectsScheduledThreadPool, _generalScheduledThreadPool, _aiScheduledThreadPool, _eventThreadPool), 10, 5, TimeUnit.MINUTES); + } + + /** + * Schedules an effect task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in the given time unit + * @param unit the time unit of the delay parameter + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEffect(Runnable task, long delay, TimeUnit unit) { + try { + return _effectsScheduledThreadPool.schedule(new RunnableWrapper(task), delay, unit); + } catch (RejectedExecutionException e) { + return null; + } + } + + /** + * Schedules an effect task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEffect(Runnable task, long delay) { + return scheduleEffect(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * Schedules an effect task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in the given time unit + * @param period the period between executions in the given time unit + * @param unit the time unit of the initialDelay and period parameters + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEffectAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) { + try { + return _effectsScheduledThreadPool.scheduleAtFixedRate(new RunnableWrapper(task), initialDelay, period, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules an effect task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in milliseconds + * @param period the period between executions in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEffectAtFixedRate(Runnable task, long initialDelay, long period) { + return scheduleEffectAtFixedRate(task, initialDelay, period, TimeUnit.MILLISECONDS); + } + + /** + * Schedules a general task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in the given time unit + * @param unit the time unit of the delay parameter + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleGeneral(Runnable task, long delay, TimeUnit unit) { + try { + return _generalScheduledThreadPool.schedule(new RunnableWrapper(task), delay, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules a general task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleGeneral(Runnable task, long delay) { + return scheduleGeneral(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * Schedules a general task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in the given time unit + * @param period the period between executions in the given time unit + * @param unit the time unit of the initialDelay and period parameters + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleGeneralAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) { + try { + return _generalScheduledThreadPool.scheduleAtFixedRate(new RunnableWrapper(task), initialDelay, period, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules a event task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in the given time unit + * @param unit the time unit of the delay parameter + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEvent(Runnable task, long delay, TimeUnit unit) { + try { + return _eventScheduledThreadPool.schedule(new RunnableWrapper(task), delay, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules a event task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEvent(Runnable task, long delay) { + return scheduleEvent(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * Schedules a event task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in the given time unit + * @param period the period between executions in the given time unit + * @param unit the time unit of the initialDelay and period parameters + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleEventAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) { + try { + return _eventScheduledThreadPool.scheduleAtFixedRate(new RunnableWrapper(task), initialDelay, period, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules a general task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in milliseconds + * @param period the period between executions in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleGeneralAtFixedRate(Runnable task, long initialDelay, long period) { + return scheduleGeneralAtFixedRate(task, initialDelay, period, TimeUnit.MILLISECONDS); + } + + /** + * Schedules an AI task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in the given time unit + * @param unit the time unit of the delay parameter + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleAi(Runnable task, long delay, TimeUnit unit) { + try { + return _aiScheduledThreadPool.schedule(new RunnableWrapper(task), delay, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules an AI task to be executed after the given delay. + * @param task the task to execute + * @param delay the delay in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleAi(Runnable task, long delay) { + return scheduleAi(task, delay, TimeUnit.MILLISECONDS); + } + + /** + * Schedules a general task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in the given time unit + * @param period the period between executions in the given time unit + * @param unit the time unit of the initialDelay and period parameters + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleAiAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) { + try { + return _aiScheduledThreadPool.scheduleAtFixedRate(new RunnableWrapper(task), initialDelay, period, unit); + } catch (RejectedExecutionException e) { + return null; /* shutdown, ignore */ + } + } + + /** + * Schedules a general task to be executed at fixed rate. + * @param task the task to execute + * @param initialDelay the initial delay in milliseconds + * @param period the period between executions in milliseconds + * @return a ScheduledFuture representing pending completion of the task, and whose get() method will throw an exception upon cancellation + */ + public ScheduledFuture scheduleAiAtFixedRate(Runnable task, long initialDelay, long period) { + return scheduleAiAtFixedRate(task, initialDelay, period, TimeUnit.MILLISECONDS); + } + + /** + * Executes a packet task sometime in future in another thread. + * @param task the task to execute + */ + public void executePacket(Runnable task) { + try { + _generalPacketsThreadPool.execute(task); + } catch (RejectedExecutionException e) { + /* shutdown, ignore */ + } + } + + /** + * Executes an IO packet task sometime in future in another thread. + * @param task the task to execute + */ + public void executeIOPacket(Runnable task) { + try { + _ioPacketsThreadPool.execute(task); + } catch (RejectedExecutionException e) { + /* shutdown, ignore */ + } + } + + /** + * Executes a general task sometime in future in another thread. + * @param task the task to execute + */ + public void executeGeneral(Runnable task) { + try { + _generalThreadPool.execute(new RunnableWrapper(task)); + } catch (RejectedExecutionException e) { + /* shutdown, ignore */ + } + } + + /** + * Executes an AI task sometime in future in another thread. + * @param task the task to execute + */ + public void executeAi(Runnable task) { + try { + _aiScheduledThreadPool.execute(new RunnableWrapper(task)); + } catch (RejectedExecutionException e) { + /* shutdown, ignore */ + } + } + + /** + * Executes an Event task sometime in future in another thread. + * @param task the task to execute + */ + public void executeEvent(Runnable task) { + try { + _eventThreadPool.execute(new RunnableWrapper(task)); + } catch (RejectedExecutionException e) { + /* shutdown, ignore */ + } + } + + public String[] getStats() { + return new String[] { + "STP:", + " + Effects:", + " |- ActiveThreads: " + _effectsScheduledThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _effectsScheduledThreadPool.getCorePoolSize(), + " |- PoolSize: " + _effectsScheduledThreadPool.getPoolSize(), + " |- MaximumPoolSize: " + _effectsScheduledThreadPool.getMaximumPoolSize(), + " |- CompletedTasks: " + _effectsScheduledThreadPool.getCompletedTaskCount(), + " |- ScheduledTasks: " + _effectsScheduledThreadPool.getQueue().size(), + " | -------", + " + General:", + " |- ActiveThreads: " + _generalScheduledThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _generalScheduledThreadPool.getCorePoolSize(), + " |- PoolSize: " + _generalScheduledThreadPool.getPoolSize(), + " |- MaximumPoolSize: " + _generalScheduledThreadPool.getMaximumPoolSize(), + " |- CompletedTasks: " + _generalScheduledThreadPool.getCompletedTaskCount(), + " |- ScheduledTasks: " + _generalScheduledThreadPool.getQueue().size(), + " | -------", + " + AI:", + " |- ActiveThreads: " + _aiScheduledThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _aiScheduledThreadPool.getCorePoolSize(), + " |- PoolSize: " + _aiScheduledThreadPool.getPoolSize(), + " |- MaximumPoolSize: " + _aiScheduledThreadPool.getMaximumPoolSize(), + " |- CompletedTasks: " + _aiScheduledThreadPool.getCompletedTaskCount(), + " |- ScheduledTasks: " + _aiScheduledThreadPool.getQueue().size(), + " | -------", + " + Event:", + " |- ActiveThreads: " + _eventScheduledThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _eventScheduledThreadPool.getCorePoolSize(), + " |- PoolSize: " + _eventScheduledThreadPool.getPoolSize(), + " |- MaximumPoolSize: " + _eventScheduledThreadPool.getMaximumPoolSize(), + " |- CompletedTasks: " + _eventScheduledThreadPool.getCompletedTaskCount(), + " |- ScheduledTasks: " + _eventScheduledThreadPool.getQueue().size(), + "TP:", + " + Packets:", + " |- ActiveThreads: " + _generalPacketsThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _generalPacketsThreadPool.getCorePoolSize(), + " |- MaximumPoolSize: " + _generalPacketsThreadPool.getMaximumPoolSize(), + " |- LargestPoolSize: " + _generalPacketsThreadPool.getLargestPoolSize(), + " |- PoolSize: " + _generalPacketsThreadPool.getPoolSize(), + " |- CompletedTasks: " + _generalPacketsThreadPool.getCompletedTaskCount(), + " |- QueuedTasks: " + _generalPacketsThreadPool.getQueue().size(), + " | -------", + " + I/O Packets:", + " |- ActiveThreads: " + _ioPacketsThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _ioPacketsThreadPool.getCorePoolSize(), + " |- MaximumPoolSize: " + _ioPacketsThreadPool.getMaximumPoolSize(), + " |- LargestPoolSize: " + _ioPacketsThreadPool.getLargestPoolSize(), + " |- PoolSize: " + _ioPacketsThreadPool.getPoolSize(), + " |- CompletedTasks: " + _ioPacketsThreadPool.getCompletedTaskCount(), + " |- QueuedTasks: " + _ioPacketsThreadPool.getQueue().size(), + " | -------", + " + General Tasks:", + " |- ActiveThreads: " + _generalThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _generalThreadPool.getCorePoolSize(), + " |- MaximumPoolSize: " + _generalThreadPool.getMaximumPoolSize(), + " |- LargestPoolSize: " + _generalThreadPool.getLargestPoolSize(), + " |- PoolSize: " + _generalThreadPool.getPoolSize(), + " |- CompletedTasks: " + _generalThreadPool.getCompletedTaskCount(), + " |- QueuedTasks: " + _generalThreadPool.getQueue().size(), + " | -------", + " + Event Tasks:", + " |- ActiveThreads: " + _eventThreadPool.getActiveCount(), + " |- getCorePoolSize: " + _eventThreadPool.getCorePoolSize(), + " |- MaximumPoolSize: " + _eventThreadPool.getMaximumPoolSize(), + " |- LargestPoolSize: " + _eventThreadPool.getLargestPoolSize(), + " |- PoolSize: " + _eventThreadPool.getPoolSize(), + " |- CompletedTasks: " + _eventThreadPool.getCompletedTaskCount(), + " |- QueuedTasks: " + _eventThreadPool.getQueue().size(), + " | -------" + }; + } + + private static class PriorityThreadFactory implements ThreadFactory { + private final int _priority; + private final String _name; + private final AtomicInteger _threadNumber = new AtomicInteger(1); + private final ThreadGroup _group; + + public PriorityThreadFactory(String name, int priority) { + _priority = priority; + _name = name; + _group = new ThreadGroup(_name); + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(_group, r, _name + "-" + _threadNumber.getAndIncrement()); + t.setPriority(_priority); + return t; + } + + public ThreadGroup getGroup() { + return _group; + } + } + + public void shutdown() { + _shutdown = true; + try { + _effectsScheduledThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _generalScheduledThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _generalPacketsThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _ioPacketsThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _generalThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _eventThreadPool.awaitTermination(1, TimeUnit.SECONDS); + _effectsScheduledThreadPool.shutdown(); + _generalScheduledThreadPool.shutdown(); + _generalPacketsThreadPool.shutdown(); + _ioPacketsThreadPool.shutdown(); + _generalThreadPool.shutdown(); + _eventThreadPool.shutdown(); + LOG.info("All ThreadPools are now stopped"); + + } catch (InterruptedException e) { + LOG.warn("There has been a problem shutting down the thread pool manager!", e); + } + } + + public boolean isShutdown() { + return _shutdown; + } + + public void purge() { + _effectsScheduledThreadPool.purge(); + _generalScheduledThreadPool.purge(); + _aiScheduledThreadPool.purge(); + _eventScheduledThreadPool.purge(); + _ioPacketsThreadPool.purge(); + _generalPacketsThreadPool.purge(); + _generalThreadPool.purge(); + _eventThreadPool.purge(); + } + + public String getPacketStats() { + final StringBuilder sb = new StringBuilder(1000); + ThreadFactory tf = _generalPacketsThreadPool.getThreadFactory(); + if (tf instanceof PriorityThreadFactory ptf) { + int count = ptf.getGroup().activeCount(); + Thread[] threads = new Thread[count + 2]; + ptf.getGroup().enumerate(threads); + StringUtil.append(sb, "General Packet Thread Pool:" + Configuration.EOL + "Tasks in the queue: ", String.valueOf(_generalPacketsThreadPool.getQueue().size()), Configuration.EOL + "Showing threads stack trace:" + Configuration.EOL + "There should be ", String.valueOf(count), " Threads" + + Configuration.EOL); + for (Thread t : threads) { + if (t == null) { + continue; + } + + StringUtil.append(sb, t.getName(), Configuration.EOL); + for (StackTraceElement ste : t.getStackTrace()) { + StringUtil.append(sb, ste.toString(), Configuration.EOL); + } + } + } + + sb.append("Packet Tp stack traces printed."); + sb.append(Configuration.EOL); + return sb.toString(); + } + + public String getIOPacketStats() { + final StringBuilder sb = new StringBuilder(1000); + ThreadFactory tf = _ioPacketsThreadPool.getThreadFactory(); + + if (tf instanceof PriorityThreadFactory ptf) { + int count = ptf.getGroup().activeCount(); + Thread[] threads = new Thread[count + 2]; + ptf.getGroup().enumerate(threads); + StringUtil.append(sb, "I/O Packet Thread Pool:" + Configuration.EOL + "Tasks in the queue: ", String.valueOf(_ioPacketsThreadPool.getQueue().size()), Configuration.EOL + "Showing threads stack trace:" + Configuration.EOL + "There should be ", String.valueOf(count), " Threads" + + Configuration.EOL); + + for (Thread t : threads) { + if (t == null) { + continue; + } + + StringUtil.append(sb, t.getName(), Configuration.EOL); + + for (StackTraceElement ste : t.getStackTrace()) { + StringUtil.append(sb, ste.toString(), Configuration.EOL); + } + } + } + + sb.append("Packet Tp stack traces printed."); + sb.append(Configuration.EOL); + return sb.toString(); + } + + public String getGeneralStats() { + final StringBuilder sb = new StringBuilder(1000); + ThreadFactory tf = _generalThreadPool.getThreadFactory(); + + if (tf instanceof PriorityThreadFactory ptf) { + int count = ptf.getGroup().activeCount(); + Thread[] threads = new Thread[count + 2]; + ptf.getGroup().enumerate(threads); + StringUtil.append(sb, "General Thread Pool:" + Configuration.EOL + "Tasks in the queue: ", String.valueOf(_generalThreadPool.getQueue().size()), Configuration.EOL + "Showing threads stack trace:" + Configuration.EOL + "There should be ", String.valueOf(count), " Threads" + + Configuration.EOL); + + for (Thread t : threads) { + if (t == null) { + continue; + } + + StringUtil.append(sb, t.getName(), Configuration.EOL); + + for (StackTraceElement ste : t.getStackTrace()) { + StringUtil.append(sb, ste.toString(), Configuration.EOL); + } + } + } + + sb.append("Packet Tp stack traces printed."); + sb.append(Configuration.EOL); + return sb.toString(); + } + + protected static class PurgeTask implements Runnable { + private final ScheduledThreadPoolExecutor _effectsScheduled; + + private final ScheduledThreadPoolExecutor _generalScheduled; + + private final ScheduledThreadPoolExecutor _aiScheduled; + + private final ThreadPoolExecutor _eventScheduled; + + PurgeTask(ScheduledThreadPoolExecutor effectsScheduledThreadPool, ScheduledThreadPoolExecutor generalScheduledThreadPool, // + ScheduledThreadPoolExecutor aiScheduledThreadPool, ThreadPoolExecutor eventScheduledThreadPool) { + _effectsScheduled = effectsScheduledThreadPool; + _generalScheduled = generalScheduledThreadPool; + _aiScheduled = aiScheduledThreadPool; + _eventScheduled = eventScheduledThreadPool; + } + + @Override + public void run() { + _effectsScheduled.purge(); + _generalScheduled.purge(); + _aiScheduled.purge(); + _eventScheduled.purge(); + } + } + + private static class SingletonHolder { + protected static final ThreadPoolManager _instance = new ThreadPoolManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/agathion/Agathion.java b/src/main/java/com/l2jserver/gameserver/agathion/Agathion.java new file mode 100644 index 00000000..16b4e4ef --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/agathion/Agathion.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.agathion; + +/** + * Agathion. + * @author Zoey76 + * @version 2.6.2.0 + */ +public record Agathion(int npcId, int id, int itemId, int energy, int maxEnergy) { +} diff --git a/src/main/java/com/l2jserver/gameserver/agathion/repository/AgathionRepository.java b/src/main/java/com/l2jserver/gameserver/agathion/repository/AgathionRepository.java new file mode 100644 index 00000000..270c4468 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/agathion/repository/AgathionRepository.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.agathion.repository; + +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.l2jserver.gameserver.agathion.Agathion; + +/** + * Agathion repository. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class AgathionRepository { + private static final Logger LOG = LoggerFactory.getLogger(AgathionRepository.class); + private static final Gson GSON = new Gson(); + private static final Type TYPE_LIST_AGATHION = new TypeToken>() { + }.getType(); + + private final Map agathions = new HashMap<>(); + private final Map itemIdToNpcId = new HashMap<>(); + + private AgathionRepository() { + load(); + } + + private void load() { + agathions.clear(); + itemIdToNpcId.clear(); + final var file = new File(server().getDatapackRoot(), "data/stats/agathions.json"); + try (var reader = new JsonReader(new FileReader(file))) { + final List loaded = GSON.fromJson(reader, TYPE_LIST_AGATHION); + if (loaded != null) { + for (var a : loaded) { + agathions.put(a.npcId(), a); + itemIdToNpcId.put(a.itemId(), a.npcId()); + } + } + } catch (FileNotFoundException ex) { + LOG.warn("data/stats/agathions.json not found!", ex); + } catch (IOException ex) { + LOG.warn("Failed to load agathions.json: ", ex); + } + } + + public Agathion getByNpcId(int npcId) { + return agathions.get(npcId); + } + + public Agathion getByItemId(int itemId) { + final var npcId = itemIdToNpcId.get(itemId); + return (npcId != null) ? agathions.get(npcId) : null; + } + + public static AgathionRepository getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + static final AgathionRepository INSTANCE = new AgathionRepository(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/AbstractAI.java b/src/main/java/com/l2jserver/gameserver/ai/AbstractAI.java new file mode 100644 index 00000000..04da324f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/AbstractAI.java @@ -0,0 +1,717 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_FOLLOW; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; + +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.AutoAttackStart; +import com.l2jserver.gameserver.network.serverpackets.AutoAttackStop; +import com.l2jserver.gameserver.network.serverpackets.Die; +import com.l2jserver.gameserver.network.serverpackets.MoveToLocation; +import com.l2jserver.gameserver.network.serverpackets.MoveToPawn; +import com.l2jserver.gameserver.network.serverpackets.StopMove; +import com.l2jserver.gameserver.network.serverpackets.StopRotation; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; + +/** + * Mother class of all objects AI in the world.
+ * AbstractAI :
+ *
  • L2CharacterAI
  • + */ +public abstract class AbstractAI implements Ctrl { + protected static final Logger LOG = LoggerFactory.getLogger(AbstractAI.class); + + private NextAction _nextAction; + + public NextAction getNextAction() { + return _nextAction; + } + + public void setNextAction(NextAction nextAction) { + _nextAction = nextAction; + } + + private class FollowTask implements Runnable { + protected int _range = 70; + + public FollowTask() { + } + + public FollowTask(int range) { + _range = range; + } + + @Override + public void run() { + try { + if (_followTask == null) { + return; + } + + L2Character followTarget = _followTarget; // copy to prevent NPE + if (followTarget == null) { + if (_actor instanceof L2Summon) { + ((L2Summon) _actor).setFollowStatus(false); + } + setIntention(AI_INTENTION_IDLE); + return; + } + + if (!_actor.isInsideRadius(followTarget, _range, true, false)) { + if (!_actor.isInsideRadius(followTarget, 3000, true, false)) { + // if the target is too far (maybe also teleported) + if (_actor instanceof L2Summon) { + ((L2Summon) _actor).setFollowStatus(false); + } + + setIntention(AI_INTENTION_IDLE); + return; + } + + moveToPawn(followTarget, _range); + } + } catch (Exception e) { + LOG.warn("{}: There has been a problem running the follow task!", getClass().getSimpleName(), e); + } + } + } + + /** The character that this AI manages */ + protected final L2Character _actor; + + /** Current long-term intention */ + protected CtrlIntention _intention = AI_INTENTION_IDLE; + /** Current long-term intention parameter */ + protected Object _intentionArg0 = null; + /** Current long-term intention parameter */ + protected Object _intentionArg1 = null; + + /** Flags about client's state, in order to know which messages to send */ + protected volatile boolean _clientMoving; + /** Flags about client's state, in order to know which messages to send */ + protected volatile boolean _clientAutoAttacking; + /** Flags about client's state, in order to know which messages to send */ + protected int _clientMovingToPawnOffset; + + /** Different targets this AI maintains */ + private L2Object _target; + private L2Character _castTarget; + protected L2Character _attackTarget; + protected L2Character _followTarget; + + /** The skill we are currently casting by INTENTION_CAST */ + protected Skill _skill; + + /** Different internal state flags */ + private int _moveToPawnTimeout; + + protected Future _followTask = null; + private static final int FOLLOW_INTERVAL = 1000; + private static final int ATTACK_FOLLOW_INTERVAL = 500; + + /** + * Constructor of AbstractAI. + * @param creature the creature + */ + protected AbstractAI(L2Character creature) { + _actor = creature; + } + + /** + * @return the L2Character managed by this Accessor AI. + */ + @Override + public L2Character getActor() { + return _actor; + } + + /** + * @return the current Intention. + */ + @Override + public CtrlIntention getIntention() { + return _intention; + } + + protected void setCastTarget(L2Character target) { + _castTarget = target; + } + + /** + * @return the current cast target. + */ + public L2Character getCastTarget() { + return _castTarget; + } + + protected void setAttackTarget(L2Character target) { + _attackTarget = target; + } + + /** + * @return current attack target. + */ + @Override + public L2Character getAttackTarget() { + return _attackTarget; + } + + /** + * Set the Intention of this AbstractAI.
    + * Caution : This method is USED by AI classes
    + * Overridden in
    :

    + * L2AttackableAI : Create an AI Task executed every 1s (if necessary)
    + * L2PlayerAI : Stores the current AI intention parameters to later restore it if necessary. + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention + * @param arg1 The second parameter of the Intention + */ + synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + _intention = intention; + _intentionArg0 = arg0; + _intentionArg1 = arg1; + } + + /** + * Launch the L2CharacterAI onIntention method corresponding to the new Intention.
    + * Caution : Stop the FOLLOW mode if necessary + * @param intention The new Intention to set to the AI + */ + @Override + public final void setIntention(CtrlIntention intention) { + setIntention(intention, null, null); + } + + /** + * Launch the L2CharacterAI onIntention method corresponding to the new Intention.
    + * Caution : Stop the FOLLOW mode if necessary + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention (optional target) + */ + @Override + public final void setIntention(CtrlIntention intention, Object arg0) { + setIntention(intention, arg0, null); + } + + @Override + public final void setIntention(CtrlIntention intention, Object arg0, Object arg1) { + // Stop the follow mode if necessary + if ((intention != AI_INTENTION_FOLLOW) && (intention != AI_INTENTION_ATTACK)) { + stopFollow(); + } + + // Launch the onIntention method of the L2CharacterAI corresponding to the new Intention + switch (intention) { + case AI_INTENTION_IDLE -> onIntentionIdle(); + case AI_INTENTION_ACTIVE -> onIntentionActive(); + case AI_INTENTION_REST -> onIntentionRest(); + case AI_INTENTION_ATTACK -> onIntentionAttack((L2Character) arg0); + case AI_INTENTION_CAST -> onIntentionCast((Skill) arg0, (L2Object) arg1); + case AI_INTENTION_MOVE_TO -> onIntentionMoveTo((Location) arg0); + case AI_INTENTION_FOLLOW -> onIntentionFollow((L2Character) arg0); + case AI_INTENTION_PICK_UP -> onIntentionPickUp((L2Object) arg0); + case AI_INTENTION_INTERACT -> onIntentionInteract((L2Object) arg0); + } + + // If do move or follow intention drop next action. + if ((_nextAction != null) && _nextAction.getIntentions().contains(intention)) { + _nextAction = null; + } + } + + /** + * Launch the L2CharacterAI onEvt method corresponding to the Event.
    + * Caution : The current general intention won't be change (ex : If the character attack and is stunned, he will attack again after the stunned period) + * @param evt The event whose the AI must be notified + */ + @Override + public final void notifyEvent(CtrlEvent evt) { + notifyEvent(evt, null, null); + } + + /** + * Launch the L2CharacterAI onEvt method corresponding to the Event. Caution : The current general intention won't be change (ex : If the character attack and is stunned, he will attack again after the stunned period) + * @param evt The event whose the AI must be notified + * @param arg0 The first parameter of the Event (optional target) + */ + @Override + public final void notifyEvent(CtrlEvent evt, Object arg0) { + notifyEvent(evt, arg0, null); + } + + /** + * Launch the L2CharacterAI onEvt method corresponding to the Event. Caution : The current general intention won't be change (ex : If the character attack and is stunned, he will attack again after the stunned period) + * @param evt The event whose the AI must be notified + */ + @Override + public final void notifyEvent(CtrlEvent evt, Object... args) { + if ((!_actor.isVisible() && !_actor.isTeleporting()) || !_actor.hasAI()) { + return; + } + + switch (evt) { + case EVT_THINK: + onEvtThink(); + break; + case EVT_ATTACKED: + onEvtAttacked((L2Character) args[0]); + break; + case EVT_AGGRESSION: + onEvtAggression((L2Character) args[0], ((Number) args[1]).intValue()); + break; + case EVT_STUNNED: + onEvtStunned((L2Character) args[0]); + break; + case EVT_SLEEPING: + onEvtSleeping((L2Character) args[0]); + break; + case EVT_ROOTED: + onEvtRooted((L2Character) args[0]); + break; + case EVT_CONFUSED: + onEvtConfused((L2Character) args[0]); + break; + case EVT_MUTED: + onEvtMuted((L2Character) args[0]); + break; + case EVT_EVADED: + onEvtEvaded((L2Character) args[0]); + break; + case EVT_READY_TO_ACT: + if (!_actor.isCastingNow() && !_actor.isCastingSimultaneouslyNow()) { + onEvtReadyToAct(); + } + break; + case EVT_USER_CMD: + onEvtUserCmd(args[0], args[1]); + break; + case EVT_ARRIVED: + // happens e.g. from stopmove but we don't process it if we're casting + if (!_actor.isCastingNow() && !_actor.isCastingSimultaneouslyNow()) { + onEvtArrived(); + } + break; + case EVT_ARRIVED_REVALIDATE: + // this is disregarded if the char is not moving any more + if (_actor.isMoving()) { + onEvtArrivedRevalidate(); + } + break; + case EVT_ARRIVED_BLOCKED: + onEvtArrivedBlocked((Location) args[0]); + break; + case EVT_FORGET_OBJECT: + onEvtForgetObject((L2Object) args[0]); + break; + case EVT_CANCEL: + onEvtCancel(); + break; + case EVT_DEAD: + onEvtDead(); + break; + case EVT_FAKE_DEATH: + onEvtFakeDeath(); + break; + case EVT_FINISH_CASTING: + onEvtFinishCasting(); + break; + case EVT_AFRAID: { + onEvtAfraid((L2Character) args[0], (Boolean) args[1]); + break; + } + } + + // Do next action. + if ((_nextAction != null) && _nextAction.getEvents().contains(evt)) { + _nextAction.doAction(); + } + } + + protected abstract void onIntentionIdle(); + + protected abstract void onIntentionActive(); + + protected abstract void onIntentionRest(); + + protected abstract void onIntentionAttack(L2Character target); + + protected abstract void onIntentionCast(Skill skill, L2Object target); + + protected abstract void onIntentionMoveTo(Location destination); + + protected abstract void onIntentionFollow(L2Character target); + + protected abstract void onIntentionPickUp(L2Object item); + + protected abstract void onIntentionInteract(L2Object object); + + protected abstract void onEvtThink(); + + protected abstract void onEvtAttacked(L2Character attacker); + + protected abstract void onEvtAggression(L2Character target, long aggro); + + protected abstract void onEvtStunned(L2Character attacker); + + protected abstract void onEvtSleeping(L2Character attacker); + + protected abstract void onEvtRooted(L2Character attacker); + + protected abstract void onEvtConfused(L2Character attacker); + + protected abstract void onEvtMuted(L2Character attacker); + + protected abstract void onEvtEvaded(L2Character attacker); + + protected abstract void onEvtReadyToAct(); + + protected abstract void onEvtUserCmd(Object arg0, Object arg1); + + protected abstract void onEvtArrived(); + + protected abstract void onEvtArrivedRevalidate(); + + protected abstract void onEvtArrivedBlocked(Location blocked_at_pos); + + protected abstract void onEvtForgetObject(L2Object object); + + protected abstract void onEvtCancel(); + + protected abstract void onEvtDead(); + + protected abstract void onEvtFakeDeath(); + + protected abstract void onEvtFinishCasting(); + + protected abstract void onEvtAfraid(L2Character effector, boolean start); + + /** + * Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor. Caution : Low level function, used by AI subclasses + */ + protected void clientActionFailed() { + if (_actor instanceof L2PcInstance) { + _actor.sendPacket(ActionFailed.STATIC_PACKET); + } + } + + /** + * Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast).
    + * Caution : Low level function, used by AI subclasses + * @param pawn + * @param offset + */ + protected void moveToPawn(L2Object pawn, int offset) { + // Check if actor can move + if (!_actor.isMovementDisabled()) { + if (offset < 10) { + offset = 10; + } + + // prevent possible extra calls to this function (there is none?), + // also don't send movetopawn packets too often + boolean sendPacket = true; + if (_clientMoving && (_target == pawn)) { + if (_clientMovingToPawnOffset == offset) { + if (GameTimeController.getInstance().getGameTicks() < _moveToPawnTimeout) { + return; + } + sendPacket = false; + } else if (_actor.isOnGeodataPath()) { + // minimum time to calculate new route is 2 seconds + if (GameTimeController.getInstance().getGameTicks() < (_moveToPawnTimeout + 10)) { + return; + } + } + } + + // Set AI movement data + _clientMoving = true; + _clientMovingToPawnOffset = offset; + _target = pawn; + _moveToPawnTimeout = GameTimeController.getInstance().getGameTicks(); + _moveToPawnTimeout += 1000 / GameTimeController.MILLIS_IN_TICK; + + if (pawn == null) { + return; + } + + // Calculate movement data for a move to location action and add the actor to movingObjects of GameTimeController + _actor.moveToLocation(pawn.getX(), pawn.getY(), pawn.getZ(), offset); + + if (!_actor.isMoving()) { + clientActionFailed(); + return; + } + + // Send a Server->Client packet MoveToPawn/CharMoveToLocation to the actor and all L2PcInstance in its _knownPlayers + if (pawn instanceof L2Character) { + if (_actor.isOnGeodataPath()) { + _actor.broadcastPacket(new MoveToLocation(_actor)); + _clientMovingToPawnOffset = 0; + } else if (sendPacket) { + _actor.broadcastPacket(new MoveToPawn(_actor, (L2Character) pawn, offset)); + } + } else { + _actor.broadcastPacket(new MoveToLocation(_actor)); + } + } else { + clientActionFailed(); + } + } + + /** + * Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast).
    + * Caution : Low level function, used by AI subclasses + * @param x + * @param y + * @param z + */ + protected void moveTo(int x, int y, int z) { + // Chek if actor can move + if (!_actor.isMovementDisabled()) { + // Set AI movement data + _clientMoving = true; + _clientMovingToPawnOffset = 0; + + // Calculate movement data for a move to location action and add the actor to movingObjects of GameTimeController + _actor.moveToLocation(x, y, z, 0); + + // Send a Server->Client packet CharMoveToLocation to the actor and all L2PcInstance in its _knownPlayers + _actor.broadcastPacket(new MoveToLocation(_actor)); + + } else { + clientActionFailed(); + } + } + + /** + * Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast).
    + * Caution : Low level function, used by AI subclasses + * @param loc + */ + protected void clientStopMoving(Location loc) { + // Stop movement of the L2Character + if (_actor.isMoving()) { + _actor.stopMove(loc); + } + + _clientMovingToPawnOffset = 0; + + if (_clientMoving || (loc != null)) { + _clientMoving = false; + + // Send a Server->Client packet StopMove to the actor and all L2PcInstance in its _knownPlayers + _actor.broadcastPacket(new StopMove(_actor)); + + if (loc != null) { + // Send a Server->Client packet StopRotation to the actor and all L2PcInstance in its _knownPlayers + _actor.broadcastPacket(new StopRotation(_actor.getObjectId(), loc.getHeading(), 0)); + } + } + } + + /** + * Client has already arrived to target, no need to force StopMove packet. + */ + protected void clientStoppedMoving() { + if (_clientMovingToPawnOffset > 0) // movetoPawn needs to be stopped + { + _clientMovingToPawnOffset = 0; + _actor.broadcastPacket(new StopMove(_actor)); + } + _clientMoving = false; + } + + public boolean isAutoAttacking() { + return _clientAutoAttacking; + } + + public void setAutoAttacking(boolean isAutoAttacking) { + if (_actor instanceof L2Summon summon) { + if (summon.getOwner() != null) { + summon.getOwner().getAI().setAutoAttacking(isAutoAttacking); + } + return; + } + _clientAutoAttacking = isAutoAttacking; + } + + /** + * Start the actor Auto Attack client side by sending Server->Client packet AutoAttackStart (broadcast).
    + * Caution : Low level function, used by AI subclasses + */ + public void clientStartAutoAttack() { + if (_actor instanceof L2Summon summon) { + if (summon.getOwner() != null) { + summon.getOwner().getAI().clientStartAutoAttack(); + } + return; + } + if (!isAutoAttacking()) { + if (_actor.isPlayer() && _actor.hasSummon()) { + _actor.getSummon().broadcastPacket(new AutoAttackStart(_actor.getSummon().getObjectId())); + } + // Send a Server->Client packet AutoAttackStart to the actor and all L2PcInstance in its _knownPlayers + _actor.broadcastPacket(new AutoAttackStart(_actor.getObjectId())); + setAutoAttacking(true); + } + AttackStanceTaskManager.getInstance().addAttackStanceTask(_actor); + } + + /** + * Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast).
    + * Caution : Low level function, used by AI subclasses + */ + public void clientStopAutoAttack() { + if (_actor instanceof L2Summon summon) { + if (summon.getOwner() != null) { + summon.getOwner().getAI().clientStopAutoAttack(); + } + return; + } + if (_actor instanceof L2PcInstance) { + if (!AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor) && isAutoAttacking()) { + AttackStanceTaskManager.getInstance().addAttackStanceTask(_actor); + } + } else if (isAutoAttacking()) { + _actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId())); + setAutoAttacking(false); + } + } + + /** + * Kill the actor client side by sending Server->Client packet AutoAttackStop, StopMove/StopRotation, Die (broadcast).
    + * Caution : Low level function, used by AI subclasses + */ + protected void clientNotifyDead() { + // Send a Server->Client packet Die to the actor and all L2PcInstance in its _knownPlayers + Die msg = new Die(_actor); + _actor.broadcastPacket(msg); + + // Init AI + _intention = AI_INTENTION_IDLE; + _target = null; + _castTarget = null; + _attackTarget = null; + + // Cancel the follow task if necessary + stopFollow(); + } + + /** + * Update the state of this actor client side by sending Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance player.
    + * Caution : Low level function, used by AI subclasses + * @param player The L2PcInstance to notify with state of this L2Character + */ + public void describeStateToPlayer(L2PcInstance player) { + if (getActor().isVisibleFor(player)) { + if (_clientMoving) { + if ((_clientMovingToPawnOffset != 0) && (_followTarget != null)) { + // Send a Server->Client packet MoveToPawn to the actor and all L2PcInstance in its _knownPlayers + player.sendPacket(new MoveToPawn(_actor, _followTarget, _clientMovingToPawnOffset)); + } else { + // Send a Server->Client packet CharMoveToLocation to the actor and all L2PcInstance in its _knownPlayers + player.sendPacket(new MoveToLocation(_actor)); + } + } + } + } + + /** + * Create and Launch an AI Follow Task to execute every 1s. + * @param target The L2Character to follow + */ + public synchronized void startFollow(L2Character target) { + if (_followTask != null) { + _followTask.cancel(false); + _followTask = null; + } + + // Create and Launch an AI Follow Task to execute every 1s + _followTarget = target; + _followTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new FollowTask(), 5, FOLLOW_INTERVAL); + } + + /** + * Create and Launch an AI Follow Task to execute every 0.5s, following at specified range. + * @param target The L2Character to follow + * @param range + */ + public synchronized void startFollow(L2Character target, int range) { + if (_followTask != null) { + _followTask.cancel(false); + _followTask = null; + } + + _followTarget = target; + _followTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new FollowTask(range), 5, ATTACK_FOLLOW_INTERVAL); + } + + /** + * Stop an AI Follow Task. + */ + public synchronized void stopFollow() { + if (_followTask != null) { + // Stop the Follow Task + _followTask.cancel(false); + _followTask = null; + } + _followTarget = null; + } + + protected L2Character getFollowTarget() { + return _followTarget; + } + + protected L2Object getTarget() { + return _target; + } + + protected void setTarget(L2Object target) { + _target = target; + } + + /** + * Stop all Ai tasks and futures. + */ + public void stopAITask() { + stopFollow(); + } + + @Override + public String toString() { + return "Actor: " + _actor; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/Ctrl.java b/src/main/java/com/l2jserver/gameserver/ai/Ctrl.java new file mode 100644 index 00000000..f3010e4b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/Ctrl.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Interface of AI and client state.
    + * To correctly send messages to client we need it's state.
    + * For example, if we've sent 'StartAutoAttack' message, we need to send 'StopAutoAttack' message before any other action.
    + * Or if we've sent 'MoveToPawn', we need to send 'StopMove' when the movement of a character is canceled (by Root spell or any other reason).
    + * Thus, we need to know the state of client, i.e. which messages we've sent and how the client will show the scene.
    + * Close to this task is the task of AI.
    + * If a player's character is attacking a mob, his ATTACK may be interrupted by an event, that temporary disables attacking.
    + * But when the possibility to ATTACK will be enabled, the character must continue the ATTACK.
    + * For mobs, it may be more complex, since we want them to decide when to use magic, or when to follow the player for physical combat, or when to escape, to help another mob, etc.
    + * This interface is hiding complexity of server<->client interaction and multiple states of a character.
    + * It allows to set a desired, simple "wish" of a character, and the implementation of this interface will take care about the rest.
    + * The goal of a character may be like "ATTACK", "random walk" and so on.
    + * To reach the goal implementation will split it into several small actions, several steps (possibly repeatable).
    + * Like "run to target" then "hit it", then if target is not dead - repeat.
    + * This flow of simpler steps may be interrupted by incoming events.
    + * Like a character's movement was disabled (by Root spell, for instance).
    + * Depending on character's ability AI may choose to wait, or to use magic ATTACK and so on.
    + * Additionally, incoming events are compared with client's state of the character,
    + * and required network messages are sent to client's, i.e. if we have incoming event that character's movement was disabled, it causes changing if its behavior,
    + * and if client's state for the character is "moving" we send messages to clients to stop the avatar/mob. + */ +public interface Ctrl { + /** + * Gets the actor. + * @return the actor + */ + L2Character getActor(); + + /** + * Gets the intention. + * @return the intention + */ + CtrlIntention getIntention(); + + /** + * Gets the attack target. + * @return the attack target + */ + L2Character getAttackTarget(); + + /** + * Set general state/intention for AI, with optional data. + * @param intention the new intention + */ + void setIntention(CtrlIntention intention); + + /** + * Sets the intention. + * @param intention the intention + * @param arg0 the arg0 + */ + void setIntention(CtrlIntention intention, Object arg0); + + /** + * Sets the intention. + * @param intention the intention + * @param arg0 the arg0 + * @param arg1 the arg1 + */ + void setIntention(CtrlIntention intention, Object arg0, Object arg1); + + /** + * Event, that notifies about previous step result, or user command, that does not change current general intention. + * @param evt the event + */ + void notifyEvent(CtrlEvent evt); + + /** + * Notify an event. + * @param evt the event + * @param arg0 the arg0 + */ + void notifyEvent(CtrlEvent evt, Object arg0); + + /** + * Notify an event. + * @param evt the event + * @param args the args + */ + void notifyEvent(CtrlEvent evt, Object... args); +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/CtrlEvent.java b/src/main/java/com/l2jserver/gameserver/ai/CtrlEvent.java new file mode 100644 index 00000000..6ab28856 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/CtrlEvent.java @@ -0,0 +1,87 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +/** + * This class contains an enum of each possible event that can happen on an AI character. + */ +public enum CtrlEvent { + /** + * Something has changed, usually a previous step has being completed or maybe was completed, the AI must think on next action. + */ + EVT_THINK, + /** + * The actor was attacked. This event comes each time a physical or magical
    + * attack was done on the actor. NPC may start attack in response, or ignore
    + * this event if they already attack someone, or change target and so on. + */ + EVT_ATTACKED, + /** Increase/decrease aggression towards a target, or reduce global aggression if target is null */ + EVT_AGGRESSION, + /** Actor is in stun state */ + EVT_STUNNED, + /** Actor starts/stops sleeping */ + EVT_SLEEPING, + /** Actor is in rooted state (cannot move) */ + EVT_ROOTED, + /** Actor evaded hit **/ + EVT_EVADED, + /** + * An event that previous action was completed. The action may be an attempt to physically/magically hit an enemy, or an action that discarded attack attempt has finished. + */ + EVT_READY_TO_ACT, + /** + * User's command, like using a combat magic or changing weapon, etc. The command is not intended to change final goal + */ + EVT_USER_CMD, + /** + * The actor arrived to assigned location, or it's a time to modify movement destination (follow, interact, random move and others intentions). + */ + EVT_ARRIVED, + /** + * The actor arrived to an intermediate point, and needs to revalidate destination. This is sent when follow/move to pawn if destination is far away. + */ + EVT_ARRIVED_REVALIDATE, + /** The actor cannot move anymore. */ + EVT_ARRIVED_BLOCKED, + /** Forgets an object (if it's used as attack target, follow target and so on */ + EVT_FORGET_OBJECT, + /** + * Attempt to cancel current step execution, but not change the intention.
    + * For example, the actor was put into a stun, so it's current attack
    + * or movement has to be canceled. But after the stun state expired,
    + * the actor may try to attack again. Another usage for CANCEL is a user's
    + * attempt to cancel a cast/bow attack and so on. + */ + EVT_CANCEL, + /** The character is dead */ + EVT_DEAD, + /** The character looks like dead */ + EVT_FAKE_DEATH, + /** The character attack anyone randomly **/ + EVT_CONFUSED, + /** The character cannot cast spells anymore **/ + EVT_MUTED, + /** The character flee in random directions **/ + EVT_AFRAID, + /** The character finish casting **/ + EVT_FINISH_CASTING, + /** The character betrayed its master */ + EVT_BETRAYED +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/CtrlIntention.java b/src/main/java/com/l2jserver/gameserver/ai/CtrlIntention.java new file mode 100644 index 00000000..85dec884 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/CtrlIntention.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +/** + * Enumeration of generic intentions of an NPC/PC, an intention may require several steps to be completed. + */ +public enum CtrlIntention { + /** Do nothing, disconnect AI of NPC if no players around */ + AI_INTENTION_IDLE, + /** Alerted state without goal : scan attackable targets, random walk, etc */ + AI_INTENTION_ACTIVE, + /** Rest (sit until attacked) */ + AI_INTENTION_REST, + /** Attack target (cast combat magic, go to target, combat), may be ignored, if target is locked on another character or a peaceful zone and so on. */ + AI_INTENTION_ATTACK, + /** Cast a spell, depending on the spell - may start or stop attacking */ + AI_INTENTION_CAST, + /** Just move to another location */ + AI_INTENTION_MOVE_TO, + /** Like move, but check target's movement and follow it */ + AI_INTENTION_FOLLOW, + /** PickUp and item, (got to item, pickup it, become idle */ + AI_INTENTION_PICK_UP, + /** Move to target, then interact */ + AI_INTENTION_INTERACT +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/FearTask.java b/src/main/java/com/l2jserver/gameserver/ai/FearTask.java new file mode 100644 index 00000000..cb7ffbd2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/FearTask.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Fear task. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class FearTask implements Runnable { + + protected static final int FEAR_TICKS = 5; + + private final L2AttackableAI ai; + + private final L2Character effector; + + private boolean start; + + public FearTask(L2AttackableAI ai, L2Character effector, boolean start) { + this.ai = ai; + this.effector = effector; + this.start = start; + } + + @Override + public void run() { + ai.setFearTime(ai.getFearTime() - FEAR_TICKS); + ai.onEvtAfraid(effector, start); + start = false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2AirShipAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2AirShipAI.java new file mode 100644 index 00000000..f0ee4368 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2AirShipAI.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.instance.L2AirShipInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.ExMoveToLocationAirShip; +import com.l2jserver.gameserver.network.serverpackets.ExStopMoveAirShip; + +/** + * @author DS + */ +public class L2AirShipAI extends L2VehicleAI { + public L2AirShipAI(L2AirShipInstance creature) { + super(creature); + } + + @Override + protected void moveTo(int x, int y, int z) { + if (!_actor.isMovementDisabled()) { + _clientMoving = true; + _actor.moveToLocation(x, y, z, 0); + _actor.broadcastPacket(new ExMoveToLocationAirShip(getActor())); + } + } + + @Override + protected void clientStopMoving(Location loc) { + if (_actor.isMoving()) { + _actor.stopMove(loc); + } + + if (_clientMoving || (loc != null)) { + _clientMoving = false; + _actor.broadcastPacket(new ExStopMoveAirShip(getActor())); + } + } + + @Override + public void describeStateToPlayer(L2PcInstance player) { + if (_clientMoving) { + player.sendPacket(new ExMoveToLocationAirShip(getActor())); + } + } + + @Override + public L2AirShipInstance getActor() { + return (L2AirShipInstance) _actor; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2AttackableAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2AttackableAI.java new file mode 100644 index 00000000..3a3d69d0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2AttackableAI.java @@ -0,0 +1,1967 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.GameTimeController.TICKS_PER_SECOND; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_MOVE_TO; +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.npc; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.TerritoryTable; +import com.l2jserver.gameserver.enums.AISkillScope; +import com.l2jserver.gameserver.enums.AIType; +import com.l2jserver.gameserver.instancemanager.DimensionalRiftManager; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2FestivalMonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FriendlyMobInstance; +import com.l2jserver.gameserver.model.actor.instance.L2GrandBossInstance; +import com.l2jserver.gameserver.model.actor.instance.L2GuardInstance; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2RaidBossInstance; +import com.l2jserver.gameserver.model.actor.instance.L2RiftInvaderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableHate; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.FactionCall; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.skills.AbnormalVisualEffect; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.skills.targets.TargetType; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.util.Util; + +/** + * Attackable creatures AI. + * @author Zoey76 + */ +public class L2AttackableAI extends L2CharacterAI { + + private static final Logger LOG = LoggerFactory.getLogger(L2AttackableAI.class); + + private static final int FEAR_TICKS = 5; + + private static final int RANDOM_WALK_RATE = 30; + + private static final int MAX_ATTACK_TIMEOUT = 120 * TICKS_PER_SECOND; + + /** The L2Attackable AI task executed every 1s (call onEvtThink method). */ + private Future _aiTask; + /** The delay after which the attacked is stopped. */ + private int _attackTimeout; + /** The L2Attackable aggro counter. */ + private int _globalAggro; + /** The flag used to indicate that a thinking action is in progress, to prevent recursive thinking. */ + private boolean _thinking; + private int _chaosTime = 0; + private int _lastBuffTick; + // Fear parameters + private int _fearTime; + private Future _fearTask = null; + + /** + * Constructor of L2AttackableAI. + * @param creature the creature + */ + public L2AttackableAI(L2Attackable creature) { + super(creature); + _attackTimeout = Integer.MAX_VALUE; + _globalAggro = -10; // 10 seconds timeout of ATTACK after respawn + } + + /** + * Actor is a L2GuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    • The L2MonsterInstance target is aggressive
    • + *
    + * Actor is a L2SiegeGuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • A siege is in progress
    • + *
    • The L2PcInstance target isn't a Defender
    • + *
    + * Actor is a L2FriendlyMobInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2Npc
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    + * Actor is a L2MonsterInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2Npc
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The actor is Aggressive
    • + *
    + * @param target The targeted L2Object + * @return True if the target is autoattackable (depends on the actor type). + */ + private boolean autoAttackCondition(L2Character target) { + if (target == null) { + return false; + } + + // Check if the target isn't invulnerable + if (target.isInvul()) { + // However EffectInvincible requires to check GMs specially + if (target.isPlayer() && target.isGM()) { + return false; + } + if (target.isSummon() && ((L2Summon) target).getOwner().isGM()) { + return false; + } + } + + // Check if the target isn't a Folk or a Door + if (target.isDoor()) { + return false; + } + + // Check if the target isn't dead, is in the Aggro range and is at the same height + final var actor = getActor(); + if (target.isAlikeDead() || ((target.isPlayable()) && !actor.isInsideRadius(target, actor.getAggroRange(), true, false))) { + return false; + } + + // Check if the target is a L2Playable + if (target.isPlayable()) { + // Check if the AI isn't a Raid Boss, can See Silent Moving players and the target isn't in silent move mode + if (!(actor.isRaid()) && !(actor.canSeeThroughSilentMove()) && ((L2Playable) target).isSilentMovingAffected()) { + return false; + } + } + + // Gets the player if there is any. + final var player = target.getActingPlayer(); + if (player != null) { + // Don't take the aggro if the GM has the access level below or equal to GM_DONT_TAKE_AGGRO + if (player.isGM() && !player.getAccessLevel().canTakeAggro()) { + return false; + } + + // check if the target is within the grace period for JUST getting up from fake death + if (player.isRecentFakeDeath()) { + return false; + } + + if (player.isInParty() && player.getParty().isInDimensionalRift()) { + var riftType = player.getParty().getDimensionalRift().getType(); + var riftRoom = player.getParty().getDimensionalRift().getCurrentRoom(); + + if ((actor instanceof L2RiftInvaderInstance) && !DimensionalRiftManager.getInstance().getRoom(riftType, riftRoom).checkIfInZone(actor.getX(), actor.getY(), actor.getZ())) { + return false; + } + } + } + + // Check if the actor is a L2GuardInstance + if (actor instanceof L2GuardInstance) { + // Check if the L2PcInstance target has karma (=PK) + if ((player != null) && (player.getKarma() > 0)) { + return GeoData.getInstance().canSeeTarget(actor, player); // Los Check + } + // Check if the L2MonsterInstance target is aggressive + if ((target instanceof L2MonsterInstance monster) && npc().guardAttackAggroMob()) { + return (monster.isAggressive() && GeoData.getInstance().canSeeTarget(actor, target)); + } + + return false; + } else if (actor instanceof L2FriendlyMobInstance) { + // Check if the target isn't another L2Npc + if (target instanceof L2Npc) { + return false; + } + + // Check if the L2PcInstance target has karma (=PK) + if ((target instanceof L2PcInstance targetPlayer) && (targetPlayer.getKarma() > 0)) { + return GeoData.getInstance().canSeeTarget(actor, target); // Los Check + } + return false; + } else { + if (target instanceof L2Attackable attackable) { + if (!target.isAutoAttackable(actor)) { + return false; + } + + if (actor.isChaos() && actor.isInsideRadius(target, actor.getAggroRange(), false, false)) { + if (attackable.isInMyClan(actor)) { + return false; + } + // Los Check + return GeoData.getInstance().canSeeTarget(actor, target); + } + } + + if (target instanceof L2Npc) { + return false; + } + + // depending on config, do not allow mobs to attack _new_ players in peace zones, + // unless they are already following those players from outside the peace zone. + if (!npc().mobAggroInPeaceZone() && target.isInsideZone(ZoneId.PEACE)) { + return false; + } + + if (actor.isChampion() && customs().championPassive()) { + return false; + } + + // Check if the actor is Aggressive + return (actor.isAggressive() && GeoData.getInstance().canSeeTarget(actor, target)); + } + } + + public void startAITask() { + // If not idle - create an AI task (schedule onEvtThink repeatedly) + if ((_aiTask == null) && (_actor.isInActiveRegion())) { + _aiTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(this::onEvtThink, 1000, 1000); + } + } + + @Override + public void stopAITask() { + if (_aiTask != null) { + _aiTask.cancel(false); + _aiTask = null; + } + super.stopAITask(); + } + + /** + * Set the Intention of this L2CharacterAI and create an AI Task executed every 1s (call onEvtThink method) for this L2Attackable.
    + * Caution : If actor _knowPlayer isn't EMPTY, AI_INTENTION_IDLE will be change in AI_INTENTION_ACTIVE + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention + * @param arg1 The second parameter of the Intention + */ + @Override + synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + if ((intention == AI_INTENTION_IDLE) || (intention == AI_INTENTION_ACTIVE)) { + // Check if actor is not dead + final var actor = getActor(); + if (!actor.isAlikeDead()) { + // If its _knownPlayer isn't empty set the Intention to AI_INTENTION_ACTIVE + if (!actor.getKnownList().getKnownPlayers().isEmpty()) { + intention = AI_INTENTION_ACTIVE; + } else { + if (actor.getSpawn() != null) { + final var loc = actor.getSpawn().getLocation(actor); + final int range = npc().getMaxDriftRange(); + + if (!actor.isInsideRadius(loc, range + range, true, false)) { + intention = AI_INTENTION_ACTIVE; + } + } + } + } + + if (intention == AI_INTENTION_IDLE) { + // Set the Intention of this L2AttackableAI to AI_INTENTION_IDLE + super.changeIntention(AI_INTENTION_IDLE, null, null); + + // Stop AI task and detach AI from NPC + stopAITask(); + + // Cancel the AI + _actor.detachAI(); + + return; + } + } + + // Set the Intention of this L2AttackableAI to intention + super.changeIntention(intention, arg0, arg1); + + // If not idle - create an AI task (schedule onEvtThink repeatedly) + startAITask(); + } + + /** + * Manage the Attack Intention : Stop current Attack (if necessary), Calculate attack timeout, Start a new Attack and Launch Think Event. + * @param target The L2Character to attack + */ + @Override + protected void onIntentionAttack(L2Character target) { + // Calculate the attack timeout + final var currentTick = GameTimeController.getInstance().getGameTicks(); + _attackTimeout = MAX_ATTACK_TIMEOUT + currentTick; + + // self and buffs + if ((_lastBuffTick + (3 * TICKS_PER_SECOND)) < currentTick) { + for (var buff : getActor().getTemplate().getAISkills(AISkillScope.BUFF)) { + if (checkSkillCastConditions(getActor(), buff)) { + if (!_actor.isAffectedBySkill(buff.getId())) { + _actor.setTarget(_actor); + _actor.doCast(buff); + _actor.setTarget(target); + LOG.debug("{} used buff skill {} on {}", this, buff, _actor); + break; + } + } + } + _lastBuffTick = currentTick; + } + + // Manage the Attack Intention : Stop current Attack (if necessary), Start a new Attack and Launch Think Event + super.onIntentionAttack(target); + } + + @Override + protected void onEvtAfraid(L2Character effector, boolean start) { + if ((_fearTime > 0) && (_fearTask == null)) { + _fearTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new FearTask(this, effector, start), 0, FEAR_TICKS, TimeUnit.SECONDS); + _actor.startAbnormalVisualEffect(true, AbnormalVisualEffect.TURN_FLEE); + } else { + super.onEvtAfraid(effector, start); + + if ((_actor.isDead() || (_fearTime <= 0)) && (_fearTask != null)) { + _fearTask.cancel(true); + _fearTask = null; + _actor.stopAbnormalVisualEffect(true, AbnormalVisualEffect.TURN_FLEE); + + final L2Character mostHated = getActor().getMostHated(); + if (mostHated != null) { + setIntention(AI_INTENTION_ATTACK, mostHated); + } else { + setIntention(AI_INTENTION_ACTIVE); + } + onEvtThink(); + } + } + } + + protected void thinkCast() { + if (checkTargetLost(getCastTarget())) { + setCastTarget(null); + return; + } + if (maybeMoveToPawn(getCastTarget(), _actor.getMagicalAttackRange(_skill))) { + return; + } + clientStopMoving(null); + setIntention(AI_INTENTION_ACTIVE); + _actor.doCast(_skill); + } + + /** + * Manage AI standard thinks of a L2Attackable (called by onEvtThink). Actions : + *
      + *
    • Update every 1s the _globalAggro counter to come close to 0
    • + *
    • If the actor is Aggressive and can attack, add all autoAttackable L2Character in its Aggro Range to its _aggroList, chose a target and order to attack it
    • + *
    • If the actor is a L2GuardInstance that can't attack, order to it to return to its home location
    • + *
    • If the actor is a L2MonsterInstance that can't attack, order to it to random walk (1/100)
    • + *
    + */ + protected void thinkActive() { + final var actor = getActor(); + + // Update every 1s the _globalAggro counter to come close to 0 + if (_globalAggro != 0) { + if (_globalAggro < 0) { + _globalAggro++; + } else { + _globalAggro--; + } + } + + // Add all autoAttackable L2Character in L2Attackable Aggro Range to its _aggroList with 0 damage and 1 hate + // A L2Attackable isn't aggressive during 10s after its spawn because _globalAggro is set to -10 + if (_globalAggro >= 0) { + // Get all visible objects inside its Aggro Range + for (var obj : actor.getKnownList().getKnownObjects().values()) { + if (!(obj instanceof L2Character target) || (obj instanceof L2StaticObjectInstance)) { + continue; + } + + // Check to see if this is a festival mob spawn. + // If it is, then check to see if the aggro trigger is a festival participant...if so, move to attack it. + if ((actor instanceof L2FestivalMonsterInstance) && (obj instanceof L2PcInstance targetPlayer)) { + if (!(targetPlayer.isFestivalParticipant())) { + continue; + } + } + + // For each L2Character check if the target is autoattackable, check aggression + if (autoAttackCondition(target)) { + if (target.isPlayable()) { + final var term = EventDispatcher.getInstance().notifyEvent(new AttackableHate(getActor(), target.getActingPlayer(), target.isSummon()), getActor(), TerminateReturn.class); + if ((term != null) && term.terminate()) { + continue; + } + } + + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + var hating = actor.getHating(target); + + // Add the attacker to the L2Attackable _aggroList with 0 damage and 1 hate + if (hating == 0) { + actor.addDamageHate(target, 0, 0); + } + } + } + + // Chose a target from its aggroList + final var hated = actor.isConfused() ? getAttackTarget() : actor.getMostHated(); + // Order to the L2Attackable to attack the target + if ((hated != null) && !actor.isCoreAIDisabled()) { + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + final var aggro = actor.getHating(hated); + if ((aggro + _globalAggro) > 0) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!actor.isRunning()) { + actor.setRunning(); + } + + // Set the AI Intention to AI_INTENTION_ATTACK + setIntention(AI_INTENTION_ATTACK, hated); + } + return; + } + } + + // Chance to forget attackers after some time + if ((actor.getCurrentHp() == actor.getMaxHp()) && (actor.getCurrentMp() == actor.getMaxMp()) && !actor.getAttackByList().isEmpty() && (Rnd.nextInt(500) == 0)) { + actor.clearAggroList(); + actor.getAttackByList().clear(); + if (actor instanceof L2MonsterInstance monster) { + if (monster.hasMinions()) { + monster.getMinionList().deleteReusedMinions(); + } + } + } + + // Check if the mob should not return to spawn point + if (!actor.canReturnToSpawnPoint()) { + return; + } + + // Check if the actor is a L2GuardInstance + if ((actor instanceof L2GuardInstance) && !actor.isWalker()) { + // Order to the L2GuardInstance to return to its home location because there's no target to attack + actor.returnHome(); + } + + // If this is a festival monster, then it remains in the same location. + if (actor instanceof L2FestivalMonsterInstance) { + return; + } + + // Minions following leader + final var leader = actor.getLeader(); + if ((leader != null) && !leader.isAlikeDead()) { + final int offset; + final var minRadius = 30; + + if (actor.isRaidMinion()) { + offset = 500; // for Raids - need correction + } else { + offset = 200; // for normal minions - need correction :) + } + + if (leader.isRunning()) { + actor.setRunning(); + } else { + actor.setWalking(); + } + + if (actor.calculateDistance(leader, false, true) > (offset * offset)) { + int x1, y1, z1; + x1 = Rnd.get(minRadius * 2, offset * 2); // x + y1 = Rnd.get(x1, offset * 2); // distance + y1 = (int) Math.sqrt((y1 * y1) - (x1 * x1)); // y + if (x1 > (offset + minRadius)) { + x1 = (leader.getX() + x1) - offset; + } else { + x1 = (leader.getX() - x1) + minRadius; + } + if (y1 > (offset + minRadius)) { + y1 = (leader.getY() + y1) - offset; + } else { + y1 = (leader.getY() - y1) + minRadius; + } + + z1 = leader.getZ(); + // Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast) + moveTo(x1, y1, z1); + } else if (Rnd.nextInt(RANDOM_WALK_RATE) == 0) { + for (var sk : actor.getTemplate().getAISkills(AISkillScope.BUFF)) { + if (cast(sk)) { + return; + } + } + } + } + // Order to the L2MonsterInstance to random walk (1/100) + else if ((actor.getSpawn() != null) && (Rnd.nextInt(RANDOM_WALK_RATE) == 0) && !actor.isNoRndWalk()) { + var x1 = 0; + var y1 = 0; + var z1 = 0; + final int range = npc().getMaxDriftRange(); + + if (actor.isWalker()) { + return; + } + + for (var sk : actor.getTemplate().getAISkills(AISkillScope.BUFF)) { + if (cast(sk)) { + return; + } + } + + // If me with random coord in territory - old method (for backward compatibility) + if ((actor.getSpawn().getX() == 0) && (actor.getSpawn().getY() == 0) && (actor.getSpawn().getSpawnTerritory() == null)) { + // Calculate a destination point in the spawn area + final var location = TerritoryTable.getInstance().getRandomPoint(actor.getSpawn().getLocationId()); + if (location != null) { + x1 = location.getX(); + y1 = location.getY(); + z1 = location.getZ(); + } + + // Calculate the distance between the current position of the L2Character and the target (x,y) + var distance2 = actor.calculateDistance(x1, y1, 0, false, true); + + if (distance2 > ((range + range) * (range + range))) { + actor.setisReturningToSpawnPoint(true); + var delay = (float) Math.sqrt(distance2) / range; + x1 = actor.getX() + (int) ((x1 - actor.getX()) / delay); + y1 = actor.getY() + (int) ((y1 - actor.getY()) / delay); + } + + // If me with random fixed coord, don't move (unless needs to return to spawnpoint) + if (!actor.isReturningToSpawnPoint() && (TerritoryTable.getInstance().getProcMax(actor.getSpawn().getLocationId()) > 0)) { + return; + } + } else { + x1 = actor.getSpawn().getX(actor); + y1 = actor.getSpawn().getY(actor); + z1 = actor.getSpawn().getZ(actor); + + if (!actor.isInsideRadius(x1, y1, 0, range, false, false)) { + actor.setisReturningToSpawnPoint(true); + } else { + var deltaX = Rnd.nextInt(range * 2); // x + var deltaY = Rnd.get(deltaX, range * 2); // distance + deltaY = (int) Math.sqrt((deltaY * deltaY) - (deltaX * deltaX)); // y + x1 = (deltaX + x1) - range; + y1 = (deltaY + y1) - range; + z1 = actor.getZ(); + } + } + // Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast) + final var moveLoc = GeoData.getInstance().moveCheck(actor.getX(), actor.getY(), actor.getZ(), x1, y1, z1, actor.getInstanceId()); + + moveTo(moveLoc.getX(), moveLoc.getY(), moveLoc.getZ()); + } + } + + /** + * Manage AI attack thinks of a L2Attackable (called by onEvtThink).
    + * Actions: + *
      + *
    • Update the attack timeout if actor is running
    • + *
    • If target is dead or timeout is expired, stop this attack and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • Call all L2Object of its Faction inside the Faction Range
    • + *
    • Chose a target and order to attack it with magic skill or physical attack
    • + *
    + */ + protected void thinkAttack() { + final var actor = getActor(); + if (actor.isCastingNow() || actor.isAttackingNow()) { + return; + } + + if (actor.isCoreAIDisabled()) { + return; + } + + if (actor.isOutOfControl()) { + return; + } + + final var mostHated = actor.getMostHated(); + if (mostHated == null) { + setIntention(AI_INTENTION_ACTIVE); + return; + } + + setAttackTarget(mostHated); + actor.setTarget(mostHated); + + // Immobilize condition + if (actor.isMovementDisabled()) { + movementDisable(); + return; + } + + // Check if target is dead or if timeout is expired to stop this attack + final var originalAttackTarget = getAttackTarget(); + if ((originalAttackTarget == null) || originalAttackTarget.isAlikeDead() || (_attackTimeout < GameTimeController.getInstance().getGameTicks())) { + // Stop hating this target after the attack timeout or if target is dead + actor.stopHating(originalAttackTarget); + + // Set the AI Intention to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + + actor.setWalking(); + return; + } + + if (actor.isSevenNpc()) { + final var target = originalAttackTarget.getActingPlayer(); + if (SevenSigns.getInstance().isSealValidationPeriod() || SevenSigns.getInstance().isCompResultsPeriod()) { + if (!target.isGM() && target.isIn7sDungeon() && (SevenSigns.getInstance().getPlayerCabal(target.getObjectId()) != SevenSigns.getInstance().getCabalHighestScore())) { + target.teleToLocation(TeleportWhereType.TOWN); + target.setIsIn7sDungeon(false); + target.sendMessage("You have been teleported to the nearest town due to the beginning of the Seal Validation period."); + } + } else { + if (!target.isGM() && target.isIn7sDungeon() && (SevenSigns.getInstance().getPlayerCabal(target.getObjectId()) == SevenSigns.CABAL_NULL)) { + target.teleToLocation(TeleportWhereType.TOWN); + target.setIsIn7sDungeon(false); + target.sendMessage("You have been teleported to the nearest town because you have not signed for any cabal."); + } + } + } + + final var collision = actor.getTemplate().getCollisionRadius(); + + // Handle all L2Object of its Faction inside the Faction Range + + final var clans = getActor().getTemplate().getClans(); + if ((clans != null) && !clans.isEmpty()) { + final var factionRange = actor.getTemplate().getClanHelpRange() + collision; + // Go through all L2Object that belong to its faction + try { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(factionRange)) { + if (obj instanceof L2Npc called) { + if (!getActor().getTemplate().isClan(called.getTemplate().getClans())) { + continue; + } + + // Check if the L2Object is inside the Faction Range of the actor + if (called.hasAI()) { + if ((Math.abs(originalAttackTarget.getZ() - called.getZ()) < 600) && actor.getAttackByList().contains(originalAttackTarget) && ((called.getAI()._intention == AI_INTENTION_IDLE) || (called.getAI()._intention == AI_INTENTION_ACTIVE)) + && (called.getInstanceId() == actor.getInstanceId())) { + if (originalAttackTarget.isPlayable()) { + if (originalAttackTarget.isInParty() && originalAttackTarget.getParty().isInDimensionalRift()) { + var riftType = originalAttackTarget.getParty().getDimensionalRift().getType(); + var riftRoom = originalAttackTarget.getParty().getDimensionalRift().getCurrentRoom(); + + if ((actor instanceof L2RiftInvaderInstance) && !DimensionalRiftManager.getInstance().getRoom(riftType, riftRoom).checkIfInZone(actor.getX(), actor.getY(), actor.getZ())) { + continue; + } + } + + // By default, when a faction member calls for help, attack the caller's attacker. + // Notify the AI with EVT_AGGRESSION + called.getAI().notifyEvent(CtrlEvent.EVT_AGGRESSION, originalAttackTarget, 1); + EventDispatcher.getInstance().notifyEventAsync(new FactionCall(called, getActor(), originalAttackTarget.getActingPlayer(), originalAttackTarget.isSummon()), called); + } else if ((called instanceof L2Attackable attackable) && (getAttackTarget() != null) && (called.getAI()._intention != AI_INTENTION_ATTACK)) { + attackable.addDamageHate(getAttackTarget(), 0, actor.getHating(getAttackTarget())); + called.getAI().setIntention(AI_INTENTION_ATTACK, getAttackTarget()); + } + } + } + } + } + } catch (NullPointerException e) { + LOG.warn("{}: There has been a problem trying to think the attack!", getClass().getSimpleName(), e); + } + } + + // Initialize data + final var combinedCollision = collision + mostHated.getTemplate().getCollisionRadius(); + + final var aiSuicideSkills = actor.getTemplate().getAISkills(AISkillScope.SUICIDE); + if (!aiSuicideSkills.isEmpty() && ((int) ((actor.getCurrentHp() / actor.getMaxHp()) * 100) < 30)) { + final var skill = aiSuicideSkills.get(Rnd.get(aiSuicideSkills.size())); + if (Util.checkIfInRange(skill.getAffectRange(), getActor(), mostHated, false) && actor.hasSkillChance()) { + if (cast(skill)) { + LOG.debug("{} used suicide skill {}", this, skill); + return; + } + } + } + + // ------------------------------------------------------ + // In case many mobs are trying to hit from same place, move a bit, circling around the target + // Note from Gnacik: + // On l2js because of that sometimes mobs don't attack player only running + // around player without any sense, so decrease chance for now + if (!actor.isMovementDisabled() && (Rnd.nextInt(100) <= 3)) { + for (var nearby : actor.getKnownList().getKnownObjects().values()) { + if ((nearby instanceof L2Attackable) && actor.isInsideRadius(nearby, collision, false, false) && (nearby != mostHated)) { + var newX = combinedCollision + Rnd.get(40); + if (Rnd.nextBoolean()) { + newX = mostHated.getX() + newX; + } else { + newX = mostHated.getX() - newX; + } + var newY = combinedCollision + Rnd.get(40); + if (Rnd.nextBoolean()) { + newY = mostHated.getY() + newY; + } else { + newY = mostHated.getY() - newY; + } + + if (!actor.isInsideRadius(newX, newY, 0, collision, false, false)) { + var newZ = actor.getZ() + 30; + if (GeoData.getInstance().canMove(actor.getX(), actor.getY(), actor.getZ(), newX, newY, newZ, actor.getInstanceId())) { + moveTo(newX, newY, newZ); + } + } + return; + } + } + } + // Calculate Archer movement + if (!actor.isMovementDisabled() && (actor.getTemplate().getBaseAttackRange() >= 700)) { + if (Rnd.get(100) <= 15) { + var distance2 = actor.calculateDistance(mostHated, false, true); + if (Math.sqrt(distance2) <= (60 + combinedCollision)) { + var posX = actor.getX(); + var posY = actor.getY(); + var posZ = actor.getZ() + 30; + + if (originalAttackTarget.getX() < posX) { + posX = posX + 300; + } else { + posX = posX - 300; + } + + if (originalAttackTarget.getY() < posY) { + posY = posY + 300; + } else { + posY = posY - 300; + } + + if (GeoData.getInstance().canMove(actor.getX(), actor.getY(), actor.getZ(), posX, posY, posZ, actor.getInstanceId())) { + setIntention(AI_INTENTION_MOVE_TO, new Location(posX, posY, posZ, 0)); + } + return; + } + } + } + + // BOSS/Raid Minion Target Reconsider + if (actor.isRaid() || actor.isRaidMinion()) { + _chaosTime++; + if (actor instanceof L2RaidBossInstance) { + if (!((L2MonsterInstance) actor).hasMinions()) { + if (_chaosTime > npc().getRaidChaosTime()) { + if (Rnd.get(100) <= (100 - ((actor.getCurrentHp() * 100) / actor.getMaxHp()))) { + aggroReconsider(); + _chaosTime = 0; + return; + } + } + } else { + if (_chaosTime > npc().getRaidChaosTime()) { + if (Rnd.get(100) <= (100 - ((actor.getCurrentHp() * 200) / actor.getMaxHp()))) { + aggroReconsider(); + _chaosTime = 0; + return; + } + } + } + } else if (actor instanceof L2GrandBossInstance) { + if (_chaosTime > npc().getGrandChaosTime()) { + var chaosRate = 100 - ((actor.getCurrentHp() * 300) / actor.getMaxHp()); + if (((chaosRate <= 10) && (Rnd.get(100) <= 10)) || ((chaosRate > 10) && (Rnd.get(100) <= chaosRate))) { + aggroReconsider(); + _chaosTime = 0; + return; + } + } + } else { + if (_chaosTime > npc().getMinionChaosTime()) { + if (Rnd.get(100) <= (100 - ((actor.getCurrentHp() * 200) / actor.getMaxHp()))) { + aggroReconsider(); + _chaosTime = 0; + return; + } + } + } + } + + final var generalSkills = actor.getTemplate().getAISkills(AISkillScope.GENERAL); + if (!generalSkills.isEmpty()) { + // Heal Condition + final var aiHealSkills = actor.getTemplate().getAISkills(AISkillScope.HEAL); + if (!aiHealSkills.isEmpty()) { + var percentage = (actor.getCurrentHp() / actor.getMaxHp()) * 100; + if (actor.isMinion()) { + final var leader = actor.getLeader(); + if ((leader != null) && !leader.isDead() && (Rnd.get(100) > ((leader.getCurrentHp() / leader.getMaxHp()) * 100))) { + for (var healSkill : aiHealSkills) { + if (healSkill.getTargetType() == TargetType.SELF) { + continue; + } + + if (!checkSkillCastConditions(actor, healSkill)) { + continue; + } + + if (!Util.checkIfInRange((healSkill.getCastRange() + collision + leader.getTemplate().getCollisionRadius()), actor, leader, false) && !isParty(healSkill) && !actor.isMovementDisabled()) { + moveToPawn(leader, healSkill.getCastRange() + collision + leader.getTemplate().getCollisionRadius()); + return; + } + + if (GeoData.getInstance().canSeeTarget(actor, leader)) { + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(leader); + actor.doCast(healSkill); + actor.setTarget(target); + LOG.debug("{} used heal skill {} on leader {}", this, healSkill, leader); + return; + } + } + } + } + + if (Rnd.get(100) < ((100 - percentage) / 3)) { + for (var sk : aiHealSkills) { + if (!checkSkillCastConditions(actor, sk)) { + continue; + } + + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(target); + LOG.debug("{} used heal skill {} on itself", this, sk); + return; + } + } + + for (var sk : aiHealSkills) { + if (!checkSkillCastConditions(actor, sk)) { + continue; + } + + if (sk.getTargetType() == TargetType.ONE) { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getCastRange() + collision)) { + if (!(obj instanceof L2Attackable targets) || obj.isDead()) { + continue; + } + + if (!targets.isInMyClan(actor)) { + continue; + } + + percentage = (targets.getCurrentHp() / targets.getMaxHp()) * 100; + if (Rnd.get(100) < ((100 - percentage) / 10)) { + if (GeoData.getInstance().canSeeTarget(actor, targets)) { + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(obj); + actor.doCast(sk); + actor.setTarget(target); + LOG.debug("{} used heal skill {} on {}", this, sk, obj); + return; + } + } + } + } + + if (isParty(sk)) { + clientStopMoving(null); + actor.doCast(sk); + return; + } + } + } + + // Res Skill Condition + final var aiResSkills = actor.getTemplate().getAISkills(AISkillScope.RES); + if (!aiResSkills.isEmpty()) { + if (actor.isMinion()) { + final var leader = actor.getLeader(); + if ((leader != null) && leader.isDead()) { + for (var sk : aiResSkills) { + if (sk.getTargetType() == TargetType.SELF) { + continue; + } + + if (!checkSkillCastConditions(actor, sk)) { + continue; + } + + if (!Util.checkIfInRange((sk.getCastRange() + collision + leader.getTemplate().getCollisionRadius()), actor, leader, false) && !isParty(sk) && !actor.isMovementDisabled()) { + moveToPawn(leader, sk.getCastRange() + collision + leader.getTemplate().getCollisionRadius()); + return; + } + + if (GeoData.getInstance().canSeeTarget(actor, leader)) { + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(leader); + actor.doCast(sk); + actor.setTarget(target); + LOG.debug("{} used resurrection skill {} on leader {}", this, sk, leader); + return; + } + } + } + } + + for (var sk : aiResSkills) { + if (!checkSkillCastConditions(actor, sk)) { + continue; + } + if (sk.getTargetType() == TargetType.ONE) { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getCastRange() + collision)) { + if (!(obj instanceof L2Attackable targets) || !obj.isDead()) { + continue; + } + + if (!actor.isInMyClan(targets)) { + continue; + } + if (Rnd.get(100) < 10) { + if (GeoData.getInstance().canSeeTarget(actor, targets)) { + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(obj); + actor.doCast(sk); + actor.setTarget(target); + LOG.debug("{} used heal skill {} on clan member {}", this, sk, obj); + return; + } + } + } + } + + if (isParty(sk)) { + clientStopMoving(null); + final var target = actor.getTarget(); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(target); + LOG.debug("{} used heal skill {} on party", this, sk); + return; + } + } + } + + // Short Range Skill Condition + final var aiShortRangeSkills = actor.getTemplate().getAISkills(AISkillScope.SHORT_RANGE); + if (!aiShortRangeSkills.isEmpty()) { + for (var shortSkill : aiShortRangeSkills) { + if (!checkSkillCastConditions(actor, shortSkill)) { + continue; + } + + final var skill = aiShortRangeSkills.get(Rnd.get(aiShortRangeSkills.size())); + if (Util.checkIfInRange(skill.getCastRange(), getActor(), mostHated, false) && actor.hasSkillChance()) { + if (GeoData.getInstance().canSeeTarget(actor, mostHated)) { + clientStopMoving(null); + actor.doCast(skill); + LOG.debug("{} used short range skill {} on {}", this, skill, actor.getTarget()); + return; + } + } + } + } + + // Long Range Skill Condition + final var aiLongRangeSkills = actor.getTemplate().getAISkills(AISkillScope.LONG_RANGE); + if (!aiLongRangeSkills.isEmpty()) { + for (var longSkill : aiLongRangeSkills) { + if (!checkSkillCastConditions(actor, longSkill)) { + continue; + } + + final var skill = aiLongRangeSkills.get(Rnd.get(aiLongRangeSkills.size())); + if (Util.checkIfInRange(skill.getCastRange(), getActor(), mostHated, false) && actor.hasSkillChance()) { + if (GeoData.getInstance().canSeeTarget(actor, mostHated)) { + clientStopMoving(null); + actor.doCast(skill); + LOG.debug("{} used long range skill {} on {}", this, skill, actor.getTarget()); + return; + } + } + } + } + } + + var dist = actor.calculateDistance(mostHated, false, false); + var dist2 = (int) dist - collision; + var range = actor.getPhysicalAttackRange() + combinedCollision; + if (actor.getTemplate().getBaseAttackRange() >= 700) { + range = actor.getTemplate().getBaseAttackRange(); // Base Bow Range NPC + } + + if (mostHated.isMoving()) { + range = range + 50; + if (actor.isMoving()) { + range = range + 50; + } + } + + // Starts melee attack + if ((dist2 > range) || !GeoData.getInstance().canSeeTarget(actor, mostHated)) { + if (actor.isMovementDisabled()) { + targetReconsider(); + } else { + final var target = getAttackTarget(); + if (target != null) { + if (target.isMoving()) { + range -= 100; + } + moveToPawn(target, Math.max(range, 5)); + } + } + return; + } + + clientStopMoving(null); + // Attacks target + _actor.doAttack(getAttackTarget()); + } + + private boolean cast(Skill sk) { + final var actor = getActor(); + if (!checkSkillCastConditions(actor, sk)) { + return false; + } + + if (getAttackTarget() == null) { + if (actor.getMostHated() != null) { + setAttackTarget(actor.getMostHated()); + } + } + + final var attackTarget = getAttackTarget(); + if (attackTarget == null) { + return false; + } + + var dist = actor.calculateDistance(attackTarget, false, false); + var dist2 = dist - attackTarget.getTemplate().getCollisionRadius(); + double range = actor.getPhysicalAttackRange() + actor.getTemplate().getCollisionRadius() + attackTarget.getTemplate().getCollisionRadius(); + double srange = sk.getCastRange() + actor.getTemplate().getCollisionRadius(); + if (attackTarget.isMoving()) { + dist2 = dist2 - 30; + } + + if (sk.isContinuous()) { + if (!sk.isDebuff()) { + if (!actor.isAffectedBySkill(sk.getId())) { + clientStopMoving(null); + actor.setTarget(actor); + actor.doCast(sk); + _actor.setTarget(attackTarget); + return true; + } + // If actor already have buff, start looking at others same faction mob to cast + if (sk.getTargetType() == TargetType.SELF) { + return false; + } + if (sk.getTargetType() == TargetType.ONE) { + var target = effectTargetReconsider(sk, true); + if (target != null) { + clientStopMoving(null); + actor.setTarget(target); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + if (canParty(sk)) { + clientStopMoving(null); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } else { + if (GeoData.getInstance().canSeeTarget(actor, attackTarget) && !canAOE(sk) && !attackTarget.isDead() && (dist2 <= srange)) { + if (!attackTarget.isAffectedBySkill(sk.getId())) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } else if (canAOE(sk)) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance on living targets. + } else if (sk.getTargetType() == TargetType.ONE) { + var target = effectTargetReconsider(sk, false); + if (target != null) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } + } + } + + if (sk.hasEffectType(L2EffectType.DISPEL)) { + if (sk.getTargetType() == TargetType.ONE) { + if ((attackTarget.getEffectList().getFirstEffect(L2EffectType.BUFF) != null) && GeoData.getInstance().canSeeTarget(actor, attackTarget) && !attackTarget.isDead() && (dist2 <= srange)) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + var target = effectTargetReconsider(sk, false); + if (target != null) { + clientStopMoving(null); + actor.setTarget(target); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } else if (canAOE(sk)) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance on living targets. + } + } + + if (sk.hasEffectType(L2EffectType.HP)) { + var percentage = (actor.getCurrentHp() / actor.getMaxHp()) * 100; + if (actor.isMinion() && (sk.getTargetType() != TargetType.SELF)) { + L2Character leader = actor.getLeader(); + if ((leader != null) && !leader.isDead() && (Rnd.get(100) > ((leader.getCurrentHp() / leader.getMaxHp()) * 100))) { + if (!Util.checkIfInRange((sk.getCastRange() + actor.getTemplate().getCollisionRadius() + leader.getTemplate().getCollisionRadius()), actor, leader, false) && !isParty(sk) && !actor.isMovementDisabled()) { + moveToPawn(leader, sk.getCastRange() + actor.getTemplate().getCollisionRadius() + leader.getTemplate().getCollisionRadius()); + } + if (GeoData.getInstance().canSeeTarget(actor, leader)) { + clientStopMoving(null); + actor.setTarget(leader); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + + if (Rnd.get(100) < ((100 - percentage) / 3)) { + clientStopMoving(null); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + + if (sk.getTargetType() == TargetType.ONE) { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getCastRange() + actor.getTemplate().getCollisionRadius())) { + if (!(obj instanceof L2Attackable targets) || obj.isDead()) { + continue; + } + + if (!actor.isInMyClan(targets)) { + continue; + } + + percentage = (targets.getCurrentHp() / targets.getMaxHp()) * 100; + if (Rnd.get(100) < ((100 - percentage) / 10)) { + if (GeoData.getInstance().canSeeTarget(actor, targets)) { + clientStopMoving(null); + actor.setTarget(obj); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + } + if (isParty(sk)) { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getAffectRange() + actor.getTemplate().getCollisionRadius())) { + if (!(obj instanceof L2Attackable targets)) { + continue; + } + + if (targets.isInMyClan(actor)) { + if ((obj.getCurrentHp() < obj.getMaxHp()) && (Rnd.get(100) <= 20)) { + clientStopMoving(null); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + } + } + + if (sk.hasEffectType(L2EffectType.PHYSICAL_ATTACK, L2EffectType.MAGICAL_ATTACK, L2EffectType.HP_DRAIN)) { + if (!canAura(sk)) { + if (GeoData.getInstance().canSeeTarget(actor, attackTarget) && !attackTarget.isDead() && (dist2 <= srange)) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + + var target = skillTargetReconsider(sk); + if (target != null) { + clientStopMoving(null); + actor.setTarget(target); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } else { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } + + if (sk.hasEffectType(L2EffectType.SLEEP)) { + if (sk.getTargetType() == TargetType.ONE) { + if (!attackTarget.isDead() && (dist2 <= srange)) { + if ((dist2 > range) || attackTarget.isMoving()) { + if (!attackTarget.isAffectedBySkill(sk.getId())) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } + } + + var target = effectTargetReconsider(sk, false); + if (target != null) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } else if (canAOE(sk)) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance on living targets. + } + } + + if (sk.hasEffectType(L2EffectType.STUN, L2EffectType.ROOT, L2EffectType.MUTE, L2EffectType.FEAR)) { + if (GeoData.getInstance().canSeeTarget(actor, attackTarget) && !canAOE(sk) && (dist2 <= srange)) { + if (!attackTarget.isAffectedBySkill(sk.getId())) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } else if (canAOE(sk)) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance on living targets. + } else if (sk.getTargetType() == TargetType.ONE) { + var target = effectTargetReconsider(sk, false); + if (target != null) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } + } + + if (sk.hasEffectType(L2EffectType.DMG_OVER_TIME)) { + if (GeoData.getInstance().canSeeTarget(actor, attackTarget) && !canAOE(sk) && !attackTarget.isDead() && (dist2 <= srange)) { + if (!attackTarget.isAffectedBySkill(sk.getId())) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } else if (canAOE(sk)) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance on living targets. + } else if (sk.getTargetType() == TargetType.ONE) { + var target = effectTargetReconsider(sk, false); + if (target != null) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + } + } + + if (sk.hasEffectType(L2EffectType.RESURRECTION)) { + if (!isParty(sk)) { + if (actor.isMinion() && (sk.getTargetType() != TargetType.SELF)) { + L2Character leader = actor.getLeader(); + if (leader != null) { + if (leader.isDead()) { + if (!Util.checkIfInRange((sk.getCastRange() + actor.getTemplate().getCollisionRadius() + leader.getTemplate().getCollisionRadius()), actor, leader, false) && !isParty(sk) && !actor.isMovementDisabled()) { + moveToPawn(leader, sk.getCastRange() + actor.getTemplate().getCollisionRadius() + leader.getTemplate().getCollisionRadius()); + } + } + if (GeoData.getInstance().canSeeTarget(actor, leader)) { + clientStopMoving(null); + actor.setTarget(leader); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getCastRange() + actor.getTemplate().getCollisionRadius())) { + if (!(obj instanceof L2Attackable targets) || !obj.isDead()) { + continue; + } + + if (!actor.isInMyClan(targets)) { + continue; + } + + if (Rnd.get(100) < 10) { + if (GeoData.getInstance().canSeeTarget(actor, targets)) { + clientStopMoving(null); + actor.setTarget(obj); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + } else if (isParty(sk)) { + for (var obj : actor.getKnownList().getKnownCharactersInRadius(sk.getAffectRange() + actor.getTemplate().getCollisionRadius())) { + if (!(obj instanceof L2Attackable targets)) { + continue; + } + + if (actor.isInMyClan(targets)) { + if ((obj.getCurrentHp() < obj.getMaxHp()) && (Rnd.get(100) <= 20)) { + clientStopMoving(null); + actor.setTarget(actor); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } + } + } + } + + if (!canAura(sk)) { + if (GeoData.getInstance().canSeeTarget(actor, attackTarget) && !attackTarget.isDead() && (dist2 <= srange)) { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + + var target = skillTargetReconsider(sk); + if (target != null) { + clientStopMoving(null); + actor.setTarget(target); + actor.doCast(sk); + actor.setTarget(attackTarget); + return true; + } + } else { + clientStopMoving(null); + actor.doCast(sk); + return true; + } + return false; + } + + private void movementDisable() { + final var target = getAttackTarget(); + if (target == null) { + return; + } + + final var actor = getActor(); + if (actor.getTarget() == null) { + actor.setTarget(target); + } + + final var dist = actor.calculateDistance(target, false, false); + final var range = actor.getPhysicalAttackRange() + actor.getTemplate().getCollisionRadius() + target.getTemplate().getCollisionRadius(); + // TODO(Zoey76): Review this "magic changes". + final var random = Rnd.get(100); + if (!target.isImmobilized() && (random < 15)) { + if (tryCast(actor, target, AISkillScope.IMMOBILIZE, dist)) { + return; + } + } + + if (random < 20) { + if (tryCast(actor, target, AISkillScope.COT, dist)) { + return; + } + } + + if (random < 30) { + if (tryCast(actor, target, AISkillScope.DEBUFF, dist)) { + return; + } + } + + if (random < 40) { + if (tryCast(actor, target, AISkillScope.NEGATIVE, dist)) { + return; + } + } + + if (actor.isMovementDisabled() || (actor.getAiType() == AIType.MAGE) || (actor.getAiType() == AIType.HEALER)) { + if (tryCast(actor, target, AISkillScope.ATTACK, dist)) { + return; + } + } + + if (tryCast(actor, target, AISkillScope.UNIVERSAL, dist)) { + return; + } + + // If cannot cast, try to attack. + if ((dist <= range) && GeoData.getInstance().canSeeTarget(actor, target)) { + _actor.doAttack(target); + return; + } + + // If cannot cast nor attack, find a new target. + targetReconsider(); + } + + private boolean tryCast(L2Attackable me, L2Character target, AISkillScope aiSkillScope, double dist) { + for (var sk : me.getTemplate().getAISkills(aiSkillScope)) { + if (!checkSkillCastConditions(me, sk) || (((sk.getCastRange() + target.getTemplate().getCollisionRadius()) <= dist) && !canAura(sk))) { + continue; + } + + if (!GeoData.getInstance().canSeeTarget(me, target)) { + continue; + } + + clientStopMoving(null); + me.doCast(sk); + return true; + } + return false; + } + + /** + * @param caster the caster + * @param skill the skill to check. + * @return {@code true} if the skill is available for casting {@code false} otherwise. + */ + private static boolean checkSkillCastConditions(L2Attackable caster, Skill skill) { + if (skill == null) { + return false; + } + + if (caster.isCastingNow() && !skill.isSimultaneousCast()) { + return false; + } + // Not enough MP. + if (skill.getMpConsume2() >= caster.getCurrentMp()) { + return false; + } + // Character is in "skill disabled" mode. + if (caster.isSkillDisabled(skill)) { + return false; + } + // If is a static skill and magic skill and character is muted or is a physical skill muted and character is physically muted. + return skill.isStatic() || ((!skill.isMagic() || !caster.isMuted()) && !caster.isPhysicalMuted()); + } + + private L2Character effectTargetReconsider(Skill sk, boolean positive) { + if (sk == null) { + return null; + } + final var actor = getActor(); + if (!sk.hasEffectType(L2EffectType.DISPEL)) { + if (!positive) { + double dist; + double dist2; + var range = 0; + + for (var creature : actor.getAttackByList()) { + if ((creature == null) || creature.isDead() || !GeoData.getInstance().canSeeTarget(actor, creature) || (creature == getAttackTarget())) { + continue; + } + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(creature, false, false); + dist2 = dist - actor.getTemplate().getCollisionRadius(); + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + creature.getTemplate().getCollisionRadius(); + if (creature.isMoving()) { + dist2 = dist2 - 70; + } + } catch (NullPointerException e) { + continue; + } + if (dist2 <= range) { + if (!getAttackTarget().isAffectedBySkill(sk.getId())) { + return creature; + } + } + } + + // ---------------------------------------------------------------------- + // If there is nearby Target with aggro, start going on random target that is attackable + for (var creature : actor.getKnownList().getKnownCharactersInRadius(range)) { + if (creature.isDead() || !GeoData.getInstance().canSeeTarget(actor, creature)) { + continue; + } + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(creature, false, false); + dist2 = dist; + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + creature.getTemplate().getCollisionRadius(); + if (creature.isMoving()) { + dist2 = dist2 - 70; + } + } catch (NullPointerException e) { + continue; + } + + if ((creature instanceof L2PcInstance) || (creature instanceof L2Summon)) { + if (dist2 <= range) { + if (!getAttackTarget().isAffectedBySkill(sk.getId())) { + return creature; + } + } + } + } + } else { + double dist; + double dist2; + var range = 0; + for (var obj : actor.getKnownList().getKnownCharactersInRadius(range)) { + if (!(obj instanceof L2Attackable targets) || obj.isDead() || !GeoData.getInstance().canSeeTarget(actor, obj)) { + continue; + } + + if (targets.isInMyClan(actor)) { + continue; + } + + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(obj, false, false); + dist2 = dist - actor.getTemplate().getCollisionRadius(); + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + obj.getTemplate().getCollisionRadius(); + if (obj.isMoving()) { + dist2 = dist2 - 70; + } + } catch (NullPointerException e) { + continue; + } + if (dist2 <= range) { + if (!obj.isAffectedBySkill(sk.getId())) { + return obj; + } + } + } + } + } else { + double dist; + double dist2; + int range; + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + getAttackTarget().getTemplate().getCollisionRadius(); + for (var obj : actor.getKnownList().getKnownCharactersInRadius(range)) { + if ((obj == null) || obj.isDead() || !GeoData.getInstance().canSeeTarget(actor, obj)) { + continue; + } + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(obj, false, false); + dist2 = dist - actor.getTemplate().getCollisionRadius(); + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + obj.getTemplate().getCollisionRadius(); + if (obj.isMoving()) { + dist2 = dist2 - 70; + } + } catch (NullPointerException e) { + continue; + } + + if ((obj instanceof L2PcInstance) || (obj instanceof L2Summon)) { + if (dist2 <= range) { + if (getAttackTarget().getEffectList().getFirstEffect(L2EffectType.BUFF) != null) { + return obj; + } + } + } + } + } + return null; + } + + private L2Character skillTargetReconsider(Skill sk) { + double dist; + double dist2; + int range; + final var actor = getActor(); + if (actor.getHateList() != null) { + for (var obj : actor.getHateList()) { + if ((obj == null) || !GeoData.getInstance().canSeeTarget(actor, obj) || obj.isDead()) { + continue; + } + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(obj, false, false); + dist2 = dist - actor.getTemplate().getCollisionRadius(); + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + getAttackTarget().getTemplate().getCollisionRadius(); + // if(obj.isMoving()) + // dist2 = dist2 - 40; + } catch (NullPointerException e) { + continue; + } + if (dist2 <= range) { + return obj; + } + } + } + + if (!(actor instanceof L2GuardInstance)) { + for (var target : actor.getKnownList().getKnownObjects().values()) { + try { + actor.setTarget(getAttackTarget()); + dist = actor.calculateDistance(target, false, false); + dist2 = dist; + range = sk.getCastRange() + actor.getTemplate().getCollisionRadius() + getAttackTarget().getTemplate().getCollisionRadius(); + // if(obj.isMoving()) + // dist2 = dist2 - 40; + } catch (NullPointerException e) { + continue; + } + + if (!(target instanceof L2Character obj)) { + continue; + } + + if (!GeoData.getInstance().canSeeTarget(actor, obj) || (dist2 > range)) { + continue; + } + + if (obj instanceof L2PcInstance) { + return obj; + } + + if (obj instanceof L2Attackable attackable) { + if (actor.isChaos()) { + if (attackable.isInMyClan(actor)) { + continue; + } + return obj; + } + } + + if (obj instanceof L2Summon) { + return obj; + } + } + } + return null; + } + + private void targetReconsider() { + double dist; + double dist2; + int range; + final var actor = getActor(); + final var mostHated = actor.getMostHated(); + if (actor.getHateList() != null) { + for (var obj : actor.getHateList()) { + if ((obj == null) || !GeoData.getInstance().canSeeTarget(actor, obj) || obj.isDead() || (obj != mostHated) || (obj == actor)) { + continue; + } + try { + dist = actor.calculateDistance(obj, false, false); + dist2 = dist - actor.getTemplate().getCollisionRadius(); + range = actor.getPhysicalAttackRange() + actor.getTemplate().getCollisionRadius() + obj.getTemplate().getCollisionRadius(); + if (obj.isMoving()) { + dist2 = dist2 - 70; + } + } catch (NullPointerException e) { + continue; + } + + if (dist2 <= range) { + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + return; + } + } + } + if (!(actor instanceof L2GuardInstance)) { + for (var target : actor.getKnownList().getKnownObjects().values()) { + if (!(target instanceof L2Character obj)) { + continue; + } + + if (!GeoData.getInstance().canSeeTarget(actor, obj) || obj.isDead() || (obj != mostHated) || (obj == actor) || (obj == getAttackTarget())) { + continue; + } + + if (obj instanceof L2PcInstance) { + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } else if (obj instanceof L2Attackable attackable) { + if (actor.isChaos()) { + if (attackable.isInMyClan(actor)) { + continue; + } + + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } + } else if (obj instanceof L2Summon) { + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } + } + } + } + + private void aggroReconsider() { + final var actor = getActor(); + final var mostHated = actor.getMostHated(); + if (actor.getHateList() != null) { + var rand = Rnd.get(actor.getHateList().size()); + var count = 0; + for (var creature : actor.getHateList()) { + if (count < rand) { + count++; + continue; + } + + if ((creature == null) || !GeoData.getInstance().canSeeTarget(actor, creature) || creature.isDead() || (creature == getAttackTarget()) || (creature == actor)) { + continue; + } + + try { + actor.setTarget(getAttackTarget()); + } catch (NullPointerException e) { + continue; + } + if (mostHated != null) { + actor.addDamageHate(creature, 0, actor.getHating(mostHated)); + } else { + actor.addDamageHate(creature, 0, 2000); + } + actor.setTarget(creature); + setAttackTarget(creature); + return; + } + } + + if (!(actor instanceof L2GuardInstance)) { + for (var target : actor.getKnownList().getKnownObjects().values()) { + if (!(target instanceof L2Character obj)) { + continue; + } + + if (!GeoData.getInstance().canSeeTarget(actor, obj) || obj.isDead() || (obj != mostHated) || (obj == actor)) { + continue; + } + + if (obj instanceof L2PcInstance) { + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } else if (obj instanceof L2Attackable attackable) { + if (actor.isChaos()) { + if (attackable.isInMyClan(actor)) { + continue; + } + + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } + } else if (obj instanceof L2Summon) { + actor.addDamageHate(obj, 0, actor.getHating(mostHated)); + actor.setTarget(obj); + setAttackTarget(obj); + } + } + } + } + + /** + * Manage AI thinking actions of a L2Attackable. + */ + @Override + protected void onEvtThink() { + // Check if the actor can't use skills and if a thinking action isn't already in progress + if (_thinking || getActor().isAllSkillsDisabled() || (_fearTime > 0)) { + return; + } + + // Start thinking action + _thinking = true; + + try { + // Manage AI thinks of a L2Attackable + switch (getIntention()) { + case AI_INTENTION_ACTIVE -> thinkActive(); + case AI_INTENTION_ATTACK -> thinkAttack(); + case AI_INTENTION_CAST -> thinkCast(); + } + } catch (Exception e) { + LOG.warn("{}: {} - onEvtThink() for {} failed!", getClass().getSimpleName(), this, getIntention(), e); + } finally { + // Stop thinking action + _thinking = false; + } + } + + /** + * Launch actions corresponding to the Event Attacked.
    + * Actions : + *
      + *
    • Init the attack : Calculate the attack timeout, Set the _globalAggro to 0, Add the attacker to the actor _aggroList
    • + *
    • Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance
    • + *
    • Set the Intention to AI_INTENTION_ATTACK
    • + *
    + * @param attacker The L2Character that attacks the actor + */ + @Override + protected void onEvtAttacked(L2Character attacker) { + final var actor = getActor(); + if (attacker == actor) { + return; + } + + // Calculate the attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // Set the _globalAggro to 0 to permit attack even just after spawn + if (_globalAggro < 0) { + _globalAggro = 0; + } + + // Add the attacker to the _aggroList of the actor + actor.addDamageHate(attacker, 0, 1); + + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!actor.isRunning()) { + actor.setRunning(); + } + + // Set the Intention to AI_INTENTION_ATTACK + if (_fearTime <= 0) { + if (getIntention() != AI_INTENTION_ATTACK) { + setIntention(AI_INTENTION_ATTACK, attacker); + } else if (actor.getMostHated() != getAttackTarget()) { + setIntention(AI_INTENTION_ATTACK, attacker); + } + } + + if (actor instanceof L2MonsterInstance master) { + if (master.hasMinions()) { + master.getMinionList().onAssist(actor, attacker); + } + + master = master.getLeader(); + if ((master != null) && master.hasMinions()) { + master.getMinionList().onAssist(actor, attacker); + } + } + + super.onEvtAttacked(attacker); + } + + /** + * Launch actions corresponding to the Event Aggression.
    + * Actions : + *
      + *
    • Add the target to the actor _aggroList or update hate if already present
    • + *
    • Set the actor Intention to AI_INTENTION_ATTACK (if actor is L2GuardInstance check if it isn't too far from its home location)
    • + *
    + * @param aggro The value of hate to add to the actor against the target + */ + @Override + protected void onEvtAggression(L2Character target, long aggro) { + final var actor = getActor(); + if (actor.isDead()) { + return; + } + + if (target != null) { + // Add the target to the actor _aggroList or update hate if already present + actor.addDamageHate(target, 0, aggro); + + // Set the actor AI Intention to AI_INTENTION_ATTACK + if ((_fearTime <= 0) && (getIntention() != AI_INTENTION_ATTACK)) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!actor.isRunning()) { + actor.setRunning(); + } + + setIntention(AI_INTENTION_ATTACK, target); + } + + if (actor instanceof L2MonsterInstance master) { + if (master.hasMinions()) { + master.getMinionList().onAssist(actor, target); + } + + master = master.getLeader(); + if ((master != null) && master.hasMinions()) { + master.getMinionList().onAssist(actor, target); + } + } + } + } + + @Override + protected void onIntentionActive() { + // Cancel attack timeout + _attackTimeout = Integer.MAX_VALUE; + super.onIntentionActive(); + } + + public void setGlobalAggro(int value) { + _globalAggro = value; + } + + @Override + public final L2Attackable getActor() { + return (L2Attackable) _actor; + } + + public int getFearTime() { + return _fearTime; + } + + public void setFearTime(int fearTime) { + _fearTime = fearTime; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2BoatAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2BoatAI.java new file mode 100644 index 00000000..7c8dc4bd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2BoatAI.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.instance.L2BoatInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.VehicleDeparture; +import com.l2jserver.gameserver.network.serverpackets.VehicleInfo; +import com.l2jserver.gameserver.network.serverpackets.VehicleStarted; + +/** + * @author DS + */ +public class L2BoatAI extends L2VehicleAI { + public L2BoatAI(L2BoatInstance creature) { + super(creature); + } + + @Override + protected void moveTo(int x, int y, int z) { + if (!_actor.isMovementDisabled()) { + if (!_clientMoving) { + _actor.broadcastPacket(new VehicleStarted(getActor(), 1)); + } + + _clientMoving = true; + _actor.moveToLocation(x, y, z, 0); + _actor.broadcastPacket(new VehicleDeparture(getActor())); + } + } + + @Override + protected void clientStopMoving(Location loc) { + if (_actor.isMoving()) { + _actor.stopMove(loc); + } + + if (_clientMoving || (loc != null)) { + _clientMoving = false; + _actor.broadcastPacket(new VehicleStarted(getActor(), 0)); + _actor.broadcastPacket(new VehicleInfo(getActor())); + } + } + + @Override + public void describeStateToPlayer(L2PcInstance player) { + if (_clientMoving) { + player.sendPacket(new VehicleDeparture(getActor())); + } + } + + @Override + public L2BoatInstance getActor() { + return (L2BoatInstance) _actor; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2CharacterAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2CharacterAI.java new file mode 100644 index 00000000..cd1d3a9c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2CharacterAI.java @@ -0,0 +1,1312 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_CAST; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_FOLLOW; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_INTERACT; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_MOVE_TO; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_PICK_UP; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_REST; +import static com.l2jserver.gameserver.config.Configuration.geodata; +import static com.l2jserver.gameserver.model.skills.targets.AffectScope.PARTY; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.ItemLocation; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcMoveFinished; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.AutoAttackStop; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; +import com.l2jserver.gameserver.util.Util; + +/** + * Creatures AI. + */ +public class L2CharacterAI extends AbstractAI { + public static class IntentionCommand { + protected final CtrlIntention _crtlIntention; + protected final Object _arg0, _arg1; + + protected IntentionCommand(CtrlIntention pIntention, Object pArg0, Object pArg1) { + _crtlIntention = pIntention; + _arg0 = pArg0; + _arg1 = pArg1; + } + + public CtrlIntention getCtrlIntention() { + return _crtlIntention; + } + } + + protected static final int FEAR_RANGE = 500; + + /** + * Cast Task + * @author Zoey76 + */ + public static class CastTask implements Runnable { + private final L2Character _activeChar; + private final L2Object _target; + private final Skill _skill; + + public CastTask(L2Character actor, Skill skill, L2Object target) { + _activeChar = actor; + _target = target; + _skill = skill; + } + + @Override + public void run() { + if (_activeChar.isAttackingNow()) { + _activeChar.abortAttack(); + } + _activeChar.getAI().changeIntentionToCast(_skill, _target); + } + } + + /** + * Constructor of L2CharacterAI. + * @param creature the creature + */ + public L2CharacterAI(L2Character creature) { + super(creature); + } + + public IntentionCommand getNextIntention() { + return null; + } + + @Override + protected void onEvtAttacked(L2Character attacker) { + if ((attacker instanceof L2Attackable) && !attacker.isCoreAIDisabled()) { + clientStartAutoAttack(); + } + } + + /** + * Manage the Idle Intention : Stop Attack, Movement and Stand Up the actor.
    + * Actions : + *
      + *
    • Set the AI Intention to AI_INTENTION_IDLE
    • + *
    • Init cast and attack target
    • + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Stand up the actor server side AND client side by sending Server->Client packet ChangeWaitType (broadcast)
    • + *
    + */ + @Override + protected void onIntentionIdle() { + // Set the AI Intention to AI_INTENTION_IDLE + changeIntention(AI_INTENTION_IDLE, null, null); + + // Init cast and attack target + setCastTarget(null); + setAttackTarget(null); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + } + + /** + * Manage the Active Intention : Stop Attack, Movement and Launch Think Event.
    + * Actions : if the Intention is not already Active + *
      + *
    • Set the AI Intention to AI_INTENTION_ACTIVE
    • + *
    • Init cast and attack target
    • + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Launch the Think Event
    • + *
    + */ + @Override + protected void onIntentionActive() { + // Check if the Intention is not already Active + if (getIntention() != AI_INTENTION_ACTIVE) { + // Set the AI Intention to AI_INTENTION_ACTIVE + changeIntention(AI_INTENTION_ACTIVE, null, null); + + // Init cast and attack target + setCastTarget(null); + setAttackTarget(null); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + // Also enable random animations for this L2Character if allowed + // This is only for mobs - town npcs are handled in their constructor + if (_actor instanceof L2Attackable attackable) { + attackable.startRandomAnimationTimer(); + } + + // Launch the Think Event + onEvtThink(); + } + } + + /** + * Manage the Rest Intention.
    + * Actions : + *
      + *
    • Set the AI Intention to AI_INTENTION_IDLE
    • + *
    + */ + @Override + protected void onIntentionRest() { + // Set the AI Intention to AI_INTENTION_IDLE + setIntention(AI_INTENTION_IDLE); + } + + /** + * Manage the Attack Intention : Stop current Attack (if necessary), Start a new Attack and Launch Think Event.
    + * Actions : + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Set the Intention of this AI to AI_INTENTION_ATTACK
    • + *
    • Set or change the AI attack target
    • + *
    • Start the actor Auto Attack client side by sending Server->Client packet AutoAttackStart (broadcast)
    • + *
    • Launch the Think Event
    • + *
    + * Overridden in : + *
      + *
    • L2AttackableAI : Calculate attack timeout
    • + *
    + */ + @Override + protected void onIntentionAttack(L2Character target) { + if (target == null) { + clientActionFailed(); + return; + } + + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAfraid()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + if (_actor.isPlayer()) { + ((L2PlayerAI) this).saveNextIntention(AI_INTENTION_ATTACK, target, null); + } + return; + } + + // Check if the Intention is already AI_INTENTION_ATTACK + if (getIntention() == AI_INTENTION_ATTACK) { + // Check if the AI already targets the L2Character + if (getAttackTarget() != target) { + // Set the AI attack target (change target) + setAttackTarget(target); + + stopFollow(); + + // Launch the Think Event + notifyEvent(CtrlEvent.EVT_THINK); + + } else { + clientActionFailed(); // else client freezes until cancel target + } + } else { + // Set the Intention of this AbstractAI to AI_INTENTION_ATTACK + changeIntention(AI_INTENTION_ATTACK, target, null); + + // Set the AI attack target + setAttackTarget(target); + + stopFollow(); + + // Launch the Think Event + notifyEvent(CtrlEvent.EVT_THINK); + } + } + + /** + * Manage the Cast Intention : Stop current Attack, Init the AI in order to cast and Launch Think Event.
    + * Actions : + *
      + *
    • Set the AI cast target
    • + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor
    • + *
    • Set the AI skill used by INTENTION_CAST
    • + *
    • Set the Intention of this AI to AI_INTENTION_CAST
    • + *
    • Launch the Think Event
    • + *
    + */ + @Override + protected void onIntentionCast(Skill skill, L2Object target) { + if ((getIntention() == AI_INTENTION_REST) && skill.isMagic()) { + clientActionFailed(); + _actor.setIsCastingNow(false); + return; + } + + if (_actor.isAttackingNow()) { + ThreadPoolManager.getInstance().scheduleGeneral(new CastTask(_actor, skill, target), MILLISECONDS.convert(_actor.getAttackEndTime() - System.nanoTime(), NANOSECONDS)); + } else { + changeIntentionToCast(skill, target); + } + } + + protected void changeIntentionToCast(Skill skill, L2Object target) { + // Set the AI cast target + setCastTarget((L2Character) target); + // Set the AI skill used by INTENTION_CAST + _skill = skill; + // Change the Intention of this AbstractAI to AI_INTENTION_CAST + changeIntention(AI_INTENTION_CAST, skill, target); + + // Launch the Think Event + notifyEvent(CtrlEvent.EVT_THINK); + } + + /** + * Manage the Move To Intention : Stop current Attack and Launch a Move to Location Task.
    + * Actions : + *
      + *
    • Stop the actor auto-attack server side AND client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Set the Intention of this AI to AI_INTENTION_MOVE_TO
    • + *
    • Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast)
    • + *
    + */ + @Override + protected void onIntentionMoveTo(Location loc) { + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + // Set the Intention of this AbstractAI to AI_INTENTION_MOVE_TO + changeIntention(AI_INTENTION_MOVE_TO, loc, null); + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + // Abort the attack of the L2Character and send Server->Client ActionFailed packet + _actor.abortAttack(); + + // Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast) + moveTo(loc.getX(), loc.getY(), loc.getZ()); + } + + /** + * Manage the Follow Intention : Stop current Attack and Launch a Follow Task.
    + * Actions : + *
      + *
    • Stop the actor auto-attack server side AND client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Set the Intention of this AI to AI_INTENTION_FOLLOW
    • + *
    • Create and Launch an AI Follow Task to execute every 1s
    • + *
    + */ + @Override + protected void onIntentionFollow(L2Character target) { + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isMovementDisabled()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + // Dead actors can`t follow + if (_actor.isDead()) { + clientActionFailed(); + return; + } + + // do not follow yourself + if (_actor == target) { + clientActionFailed(); + return; + } + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + // Set the Intention of this AbstractAI to AI_INTENTION_FOLLOW + changeIntention(AI_INTENTION_FOLLOW, target, null); + + // Create and Launch an AI Follow Task to execute every 1s + startFollow(target); + } + + /** + * Manage the PickUp Intention : Set the pick up target and Launch a Move To Pawn Task (offset=20).
    + * Actions : + *
      + *
    • Set the AI pick up target
    • + *
    • Set the Intention of this AI to AI_INTENTION_PICK_UP
    • + *
    • Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast)
    • + *
    + */ + @Override + protected void onIntentionPickUp(L2Object object) { + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + if (_actor.isPlayer()) { + ((L2PlayerAI) this).saveNextIntention(AI_INTENTION_PICK_UP, object, null); + } + return; + } + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + if ((object instanceof L2ItemInstance) && (((L2ItemInstance) object).getItemLocation() != ItemLocation.VOID)) { + return; + } + + // Set the Intention of this AbstractAI to AI_INTENTION_PICK_UP + changeIntention(AI_INTENTION_PICK_UP, object, null); + + // Set the AI pick up target + setTarget(object); + if ((object.getX() == 0) && (object.getY() == 0)) // TODO: Find the drop&spawn bug + { + LOG.warn("Object in coords 0,0 - using a temporary fix"); + object.setXYZ(getActor().getX(), getActor().getY(), getActor().getZ() + 5); + } + + // Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast) + moveToPawn(object, 20); + } + + /** + * Manage the Interact Intention : Set the interact target and Launch a Move To Pawn Task (offset=60).
    + * Actions : + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Set the AI interact target
    • + *
    • Set the Intention of this AI to AI_INTENTION_INTERACT
    • + *
    • Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast)
    • + *
    + */ + @Override + protected void onIntentionInteract(L2Object object) { + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow()) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + if (_actor.isPlayer()) { + ((L2PlayerAI) this).saveNextIntention(AI_INTENTION_INTERACT, object, null); + } + return; + } + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + if (getIntention() != AI_INTENTION_INTERACT) { + // Set the Intention of this AbstractAI to AI_INTENTION_INTERACT + changeIntention(AI_INTENTION_INTERACT, object, null); + + // Set the AI interact target + setTarget(object); + + // Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast) + moveToPawn(object, 60); + } + } + + /** + * Do nothing. + */ + @Override + protected void onEvtThink() { + // do nothing + } + + /** + * Do nothing. + */ + @Override + protected void onEvtAggression(L2Character target, long aggro) { + // do nothing + } + + /** + * Launch actions corresponding to the Event Stunned then onAttacked Event.
    + * Actions : + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Break an attack and send Server->Client ActionFailed packet and a System Message to the L2Character
    • + *
    • Break a cast and send Server->Client ActionFailed packet and a System Message to the L2Character
    • + *
    • Launch actions corresponding to the Event onAttacked (only for L2AttackableAI after the stunning period)
    • + *
    + */ + @Override + protected void onEvtStunned(L2Character attacker) { + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + _actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId())); + if (AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor)) { + AttackStanceTaskManager.getInstance().removeAttackStanceTask(_actor); + } + + // Stop Server AutoAttack also + setAutoAttacking(false); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Launch actions corresponding to the Event onAttacked (only for L2AttackableAI after the stunning period) + onEvtAttacked(attacker); + } + + /** + * Launch actions corresponding to the Event Sleeping.
    + * Actions : + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Break an attack and send Server->Client ActionFailed packet and a System Message to the L2Character
    • + *
    • Break a cast and send Server->Client ActionFailed packet and a System Message to the L2Character
    • + *
    + */ + @Override + protected void onEvtSleeping(L2Character attacker) { + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + _actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId())); + if (AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor)) { + AttackStanceTaskManager.getInstance().removeAttackStanceTask(_actor); + } + + // stop Server AutoAttack also + setAutoAttacking(false); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + } + + /** + * Launch actions corresponding to the Event Rooted.
    + * Actions : + *
      + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Launch actions corresponding to the Event onAttacked
    • + *
    + */ + @Override + protected void onEvtRooted(L2Character attacker) { + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + // _actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId())); + // if (AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor)) + // AttackStanceTaskManager.getInstance().removeAttackStanceTask(_actor); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Launch actions corresponding to the Event onAttacked + onEvtAttacked(attacker); + + } + + /** + * Launch actions corresponding to the Event Confused.
    + * Actions : + *
      + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Launch actions corresponding to the Event onAttacked
    • + *
    + */ + @Override + protected void onEvtConfused(L2Character attacker) { + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Launch actions corresponding to the Event onAttacked + onEvtAttacked(attacker); + } + + /** + * Launch actions corresponding to the Event Muted.
    + * Actions : + *
      + *
    • Break a cast and send Server->Client ActionFailed packet and a System Message to the L2Character
    • + *
    + */ + @Override + protected void onEvtMuted(L2Character attacker) { + // Break a cast and send Server->Client ActionFailed packet and a System Message to the L2Character + onEvtAttacked(attacker); + } + + /** + * Do nothing. + */ + @Override + protected void onEvtEvaded(L2Character attacker) { + // do nothing + } + + /** + * Launch actions corresponding to the Event ReadyToAct.
    + * Actions : + *
      + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtReadyToAct() { + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Do nothing. + */ + @Override + protected void onEvtUserCmd(Object arg0, Object arg1) { + // do nothing + } + + /** + * Launch actions corresponding to the Event Arrived.
    + * Actions : + *
      + *
    • If the Intention was AI_INTENTION_MOVE_TO, set the Intention to AI_INTENTION_ACTIVE
    • + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtArrived() { + _actor.revalidateZone(true); + + if (_actor.moveToNextRoutePoint()) { + return; + } + + if (_actor instanceof L2Attackable attackable) { + attackable.setisReturningToSpawnPoint(false); + } + clientStoppedMoving(); + + if (_actor instanceof L2Npc npc) { + WalkingManager.getInstance().onArrived(npc); // Walking Manager support + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new NpcMoveFinished(npc), npc); + } + + // If the Intention was AI_INTENTION_MOVE_TO, set the Intention to AI_INTENTION_ACTIVE + if (getIntention() == AI_INTENTION_MOVE_TO) { + setIntention(AI_INTENTION_ACTIVE); + } + + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Launch actions corresponding to the Event ArrivedRevalidate.
    + * Actions : + *
      + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtArrivedRevalidate() { + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Launch actions corresponding to the Event ArrivedBlocked.
    + * Actions : + *
      + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • If the Intention was AI_INTENTION_MOVE_TO, set the Intention to AI_INTENTION_ACTIVE
    • + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtArrivedBlocked(Location blocked_at_loc) { + // If the Intention was AI_INTENTION_MOVE_TO, set the Intention to AI_INTENTION_ACTIVE + if ((getIntention() == AI_INTENTION_MOVE_TO) || (getIntention() == AI_INTENTION_CAST)) { + setIntention(AI_INTENTION_ACTIVE); + } + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(blocked_at_loc); + + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Launch actions corresponding to the Event ForgetObject.
    + * Actions : + *
      + *
    • If the object was targeted and the Intention was AI_INTENTION_INTERACT or AI_INTENTION_PICK_UP, set the Intention to AI_INTENTION_ACTIVE
    • + *
    • If the object was targeted to attack, stop the auto-attack, cancel target and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • If the object was targeted to cast, cancel target and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • If the object was targeted to follow, stop the movement, cancel AI Follow Task and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • If the targeted object was the actor , cancel AI target, stop AI Follow Task, stop the movement and set the Intention to AI_INTENTION_IDLE
    • + *
    + */ + @Override + protected void onEvtForgetObject(L2Object object) { + // If the object was targeted and the Intention was AI_INTENTION_INTERACT or AI_INTENTION_PICK_UP, set the Intention to AI_INTENTION_ACTIVE + if (getTarget() == object) { + setTarget(null); + + if ((getIntention() == AI_INTENTION_INTERACT) || (getIntention() == AI_INTENTION_PICK_UP)) { + setIntention(AI_INTENTION_ACTIVE); + } + } + + // Check if the object was targeted to attack + if (getAttackTarget() == object) { + // Cancel attack target + setAttackTarget(null); + + // Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + } + + // Check if the object was targeted to cast + if (getCastTarget() == object) { + // Cancel cast target + setCastTarget(null); + + // Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + } + + // Check if the object was targeted to follow + if (getFollowTarget() == object) { + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Stop an AI Follow Task + stopFollow(); + + // Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + } + + // Check if the targeted object was the actor + if (_actor == object) { + // Cancel AI target + setTarget(null); + setAttackTarget(null); + setCastTarget(null); + + // Stop an AI Follow Task + stopFollow(); + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Set the Intention of this AbstractAI to AI_INTENTION_IDLE + changeIntention(AI_INTENTION_IDLE, null, null); + } + } + + /** + * Launch actions corresponding to the Event Cancel.
    + * Actions : + *
      + *
    • Stop an AI Follow Task
    • + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtCancel() { + _actor.abortCast(); + + // Stop an AI Follow Task + stopFollow(); + + if (!AttackStanceTaskManager.getInstance().hasAttackStanceTask(_actor)) { + _actor.broadcastPacket(new AutoAttackStop(_actor.getObjectId())); + } + + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Launch actions corresponding to the Event Dead.
    + * Actions : + *
      + *
    • Stop an AI Follow Task
    • + *
    • Kill the actor client side by sending Server->Client packet AutoAttackStop, StopMove/StopRotation, Die (broadcast)
    • + *
    + */ + @Override + protected void onEvtDead() { + // Stop an AI Tasks + stopAITask(); + + // Kill the actor client side by sending Server->Client packet AutoAttackStop, StopMove/StopRotation, Die (broadcast) + clientNotifyDead(); + + if (!(_actor instanceof L2Playable)) { + _actor.setWalking(); + } + } + + /** + * Launch actions corresponding to the Event Fake Death.
    + * Actions : + *
      + *
    • Stop an AI Follow Task
    • + *
    + */ + @Override + protected void onEvtFakeDeath() { + // Stop an AI Follow Task + stopFollow(); + + // Stop the actor movement and send Server->Client packet StopMove/StopRotation (broadcast) + clientStopMoving(null); + + // Init AI + _intention = AI_INTENTION_IDLE; + setTarget(null); + setCastTarget(null); + setAttackTarget(null); + } + + /** + * Do nothing. + */ + @Override + protected void onEvtFinishCasting() { + // do nothing + } + + @Override + protected void onEvtAfraid(L2Character effector, boolean start) { + double radians = Math.toRadians(Util.calculateAngleFrom(effector, _actor)); + + int posX = (int) (_actor.getX() + (FEAR_RANGE * Math.cos(radians))); + int posY = (int) (_actor.getY() + (FEAR_RANGE * Math.sin(radians))); + int posZ = _actor.getZ(); + + if (!_actor.isPet()) { + _actor.setRunning(); + } + + // If pathfinding enabled the creature will go to the destination or it will go to the nearest obstacle. + final Location destination; + if (geodata().getPathFinding() > 0) { + destination = GeoData.getInstance().moveCheck(_actor.getX(), _actor.getY(), _actor.getZ(), posX, posY, posZ, _actor.getInstanceId()); + } else { + destination = new Location(posX, posY, posZ, _actor.getInstanceId()); + } + setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, destination); + } + + protected boolean maybeMoveToPosition(ILocational worldPosition, int offset) { + if (worldPosition == null) { + LOG.warn("maybeMoveToPosition: worldPosition == NULL!"); + return false; + } + + if (offset < 0) { + return false; // skill radius -1 + } + + if (!_actor.isInsideRadius(worldPosition, offset + _actor.getTemplate().getCollisionRadius(), false, false)) { + if (_actor.isMovementDisabled()) { + return true; + } + + if (!_actor.isRunning() && !(this instanceof L2PlayerAI) && !(this instanceof L2SummonAI)) { + _actor.setRunning(); + } + + stopFollow(); + + int x = _actor.getX(); + int y = _actor.getY(); + + double dx = worldPosition.getX() - x; + double dy = worldPosition.getY() - y; + + double dist = Math.hypot(dx, dy); + + double sin = dy / dist; + double cos = dx / dist; + + dist -= offset - 5; + + x += (int) (dist * cos); + y += (int) (dist * sin); + + moveTo(x, y, worldPosition.getZ()); + return true; + } + + if (getFollowTarget() != null) { + stopFollow(); + } + + return false; + } + + /** + * Manage the Move to Pawn action in function of the distance and of the Interact area.
    + * Actions : + *
      + *
    • Get the distance between the current position of the L2Character and the target (x,y)
    • + *
    • If the distance > offset+20, move the actor (by running) to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast)
    • + *
    • If the distance <= offset+20, Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    + * Example of use : + *
      + *
    • L2PLayerAI, L2SummonAI
    • + *
    + * @param target The targeted L2Object + * @param offset The Interact area radius + * @return True if a movement must be done + */ + protected boolean maybeMoveToPawn(L2Object target, int offset) { + // Get the distance between the current position of the L2Character and the target (x,y) + if (target == null) { + LOG.warn("maybeMoveToPawn: target == NULL!"); + return false; + } + if (offset < 0) { + return false; // skill radius -1 + } + + offset += _actor.getTemplate().getCollisionRadius(); + if (target instanceof L2Character creature) { + offset += creature.getTemplate().getCollisionRadius(); + } + + final boolean needToMove; + + if (target.isDoor()) { + L2DoorInstance dor = (L2DoorInstance) target; + int xPoint = 0; + int yPoint = 0; + for (int i : dor.getTemplate().getNodeX()) { + xPoint += i; + } + for (int i : dor.getTemplate().getNodeY()) { + yPoint += i; + } + xPoint /= 4; + yPoint /= 4; + needToMove = !_actor.isInsideRadius(xPoint, yPoint, dor.getTemplate().getNodeZ(), offset, false, false); + } else { + needToMove = !_actor.isInsideRadius(target, offset, false, false); + } + + if (needToMove) { + // Caller should be L2Playable and thinkAttack/thinkCast/thinkInteract/thinkPickUp + if (getFollowTarget() != null) { + // allow larger hit range when the target is moving (check is run only once per second) + if (!_actor.isInsideRadius(target, offset + 100, false, false)) { + return true; + } + stopFollow(); + return false; + } + + if (_actor.isMovementDisabled()) { + // If player is trying attack target but he cannot move to attack target + // change his intention to idle + if (_actor.getAI().getIntention() == CtrlIntention.AI_INTENTION_ATTACK) { + _actor.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + } + + return true; + } + + // while flying there is no move to cast + if ((_actor.getAI().getIntention() == CtrlIntention.AI_INTENTION_CAST) && (_actor instanceof L2PcInstance) && _actor.isTransformed()) { + if (!_actor.getTransformation().isCombat()) { + _actor.sendPacket(SystemMessageId.DIST_TOO_FAR_CASTING_STOPPED); + _actor.sendPacket(ActionFailed.STATIC_PACKET); + return true; + } + } + + // If not running, set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning() && !(this instanceof L2PlayerAI) && !(this instanceof L2SummonAI)) { + _actor.setRunning(); + } + + stopFollow(); + if ((target instanceof L2Character creature) && !(target instanceof L2DoorInstance)) { + if (creature.isMoving()) { + offset -= 100; + } + if (offset < 5) { + offset = 5; + } + + startFollow((L2Character) target, offset); + } else { + // Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast) + moveToPawn(target, offset); + } + return true; + } + + if (getFollowTarget() != null) { + stopFollow(); + } + + // Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast) + // clientStopMoving(null); + return false; + } + + /** + * Modify current Intention and actions if the target is lost or dead.
    + * Actions : If the target is lost or dead + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE
    • + *
    + * Example of use : + *
      + *
    • L2PLayerAI, L2SummonAI
    • + *
    + * @param target The targeted L2Object + * @return True if the target is lost or dead (false if fake death) + */ + protected boolean checkTargetLostOrDead(L2Character target) { + if ((target == null) || target.isAlikeDead()) { + // check if player is fake death + if ((target instanceof L2PcInstance) && ((L2PcInstance) target).isFakeDeath()) { + target.stopFakeDeath(true); + return false; + } + + // Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + return true; + } + return false; + } + + /** + * Modify current Intention and actions if the target is lost.
    + * Actions : If the target is lost + *
      + *
    • Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Stop the actor movement server side AND client side by sending Server->Client packet StopMove/StopRotation (broadcast)
    • + *
    • Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE
    • + *
    + * Example of use : + *
      + *
    • L2PLayerAI, L2SummonAI
    • + *
    + * @param target The targeted L2Object + * @return True if the target is lost + */ + protected boolean checkTargetLost(L2Object target) { + // check if player is fake death + if (target instanceof L2PcInstance targetPlayer) { + if (targetPlayer.isFakeDeath()) { + targetPlayer.stopFakeDeath(true); + return false; + } + } + if (target == null) { + // Set the Intention of this AbstractAI to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE); + return true; + } + if ((_actor != null) && (_skill != null) && _skill.isBad() && (_skill.getAffectRange() > 0) && !GeoData.getInstance().canSeeTarget(_actor, target)) { + setIntention(AI_INTENTION_ACTIVE); + return true; + } + return false; + } + + protected class SelfAnalysis { + public boolean isMage = false; + public boolean isBalanced; + public boolean isArcher = false; + public boolean isHealer = false; + public boolean isFighter = false; + public boolean cannotMoveOnLand = false; + public List generalSkills = new ArrayList<>(); + public List buffSkills = new ArrayList<>(); + public List debuffSkills = new ArrayList<>(); + public int lastDebuffTick = 0; + public List cancelSkills = new ArrayList<>(); + public List healSkills = new ArrayList<>(); + public List generalDisablers = new ArrayList<>(); + public List sleepSkills = new ArrayList<>(); + public List rootSkills = new ArrayList<>(); + public List muteSkills = new ArrayList<>(); + public List resurrectSkills = new ArrayList<>(); + public boolean hasHealOrResurrect = false; + public boolean hasLongRangeSkills = false; + public boolean hasLongRangeDamageSkills = false; + public int maxCastRange = 0; + + public SelfAnalysis() { + } + + public void init() { + switch (((L2NpcTemplate) _actor.getTemplate()).getAIType()) { + case MAGE -> isMage = true; + case CORPSE, BALANCED -> isBalanced = true; + case ARCHER -> isArcher = true; + case HEALER -> isHealer = true; + default -> isFighter = true; + } + // water movement analysis + if (_actor.isNpc()) { + switch (_actor.getId()) { + case 20314, 20849 -> cannotMoveOnLand = true; + default -> cannotMoveOnLand = false; + } + } + // skill analysis + for (Skill sk : _actor.getAllSkills()) { + if (sk.isPassive()) { + continue; + } + int castRange = sk.getCastRange(); + boolean hasLongRangeDamageSkill = false; + + if (sk.isContinuous()) { + if (!sk.isDebuff()) { + buffSkills.add(sk); + } else { + debuffSkills.add(sk); + } + continue; + } + + if (sk.hasEffectType(L2EffectType.DISPEL)) { + cancelSkills.add(sk); + } else if (sk.hasEffectType(L2EffectType.HP)) { + healSkills.add(sk); + hasHealOrResurrect = true; + } else if (sk.hasEffectType(L2EffectType.SLEEP)) { + sleepSkills.add(sk); + } else if (sk.hasEffectType(L2EffectType.STUN)) { + // hardcoding petrification until improvements are made to + // EffectTemplate... petrification is totally different for + // AI than paralyze + switch (sk.getId()) { + case 367, 4111, 4383, 4616, 4578 -> sleepSkills.add(sk); + default -> generalDisablers.add(sk); + } + } else if (sk.hasEffectType(L2EffectType.ROOT)) { + rootSkills.add(sk); + } else if (sk.hasEffectType(L2EffectType.FEAR)) { + debuffSkills.add(sk); + } else if (sk.hasEffectType(L2EffectType.MUTE)) { + muteSkills.add(sk); + } else if (sk.hasEffectType(L2EffectType.RESURRECTION)) { + resurrectSkills.add(sk); + hasHealOrResurrect = true; + } else { + generalSkills.add(sk); + hasLongRangeDamageSkill = true; + } + + if (castRange > 70) { + hasLongRangeSkills = true; + if (hasLongRangeDamageSkill) { + hasLongRangeDamageSkills = true; + } + } + if (castRange > maxCastRange) { + maxCastRange = castRange; + } + + } + // Because of missing skills, some mages/balanced cannot play like mages + if (!hasLongRangeDamageSkills && isMage) { + isBalanced = true; + isMage = false; + isFighter = false; + } + if (!hasLongRangeSkills && (isMage || isBalanced)) { + isBalanced = false; + isMage = false; + isFighter = true; + } + if (generalSkills.isEmpty() && isMage) { + isBalanced = true; + isMage = false; + } + } + } + + protected class TargetAnalysis { + public L2Character character; + public boolean isMage; + public boolean isBalanced; + public boolean isArcher; + public boolean isFighter; + public boolean isCanceled; + public boolean isSlower; + public boolean isMagicResistant; + + public TargetAnalysis() { + } + + public void update(L2Character target) { + // update status once in 4 seconds + if ((target == character) && (Rnd.nextInt(100) > 25)) { + return; + } + character = target; + if (target == null) { + return; + } + isMage = false; + isBalanced = false; + isArcher = false; + isFighter = false; + isCanceled = false; + + if (target.getMAtk(null, null) > (1.5 * target.getPAtk(null))) { + isMage = true; + } else if (((target.getPAtk(null) * 0.8) < target.getMAtk(null, null)) || ((target.getMAtk(null, null) * 0.8) > target.getPAtk(null))) { + isBalanced = true; + } else { + L2Weapon weapon = target.getActiveWeaponItem(); + if ((weapon != null) && ((weapon.getItemType() == WeaponType.BOW) || (weapon.getItemType() == WeaponType.CROSSBOW))) { + isArcher = true; + } else { + isFighter = true; + } + } + + isSlower = target.getRunSpeed() < (_actor.getRunSpeed() - 3); + + isMagicResistant = (target.getMDef(null, null) * 1.2) > _actor.getMAtk(null, null); + + if (target.getBuffCount() < 4) { + isCanceled = true; + } + } + } + + @Deprecated + public boolean canAura(Skill sk) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance. + return false; + } + + @Deprecated + public boolean canAOE(Skill sk) { + // TODO(Zoey76): Handle scope RANGE skills, include Geodata check and distance. + return false; + } + + public boolean canParty(Skill sk) { + if (isParty(sk)) { + int count = 0; + int ccount = 0; + final var actors = ((L2Npc) _actor); + for (L2Character target : _actor.getKnownList().getKnownCharactersInRadius(sk.getAffectRange())) { + if (!(target instanceof L2Attackable targets) || !GeoData.getInstance().canSeeTarget(_actor, target)) { + continue; + } + if (targets.isInMyClan(actors)) { + count++; + if (target.isAffectedBySkill(sk.getId())) { + ccount++; + } + } + } + if (ccount < count) { + return true; + } + } + return false; + } + + public boolean isParty(Skill sk) { + return sk.getAffectScope() == PARTY; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2ControllableMobAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2ControllableMobAI.java new file mode 100644 index 00000000..93f29145 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2ControllableMobAI.java @@ -0,0 +1,437 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; + +import java.util.Collection; +import java.util.stream.Collectors; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.MobGroup; +import com.l2jserver.gameserver.model.MobGroupTable; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.instance.L2ControllableMobInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2NpcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.Util; + +/** + * AI for controllable mobs + * @author littlecrow + */ +public final class L2ControllableMobAI extends L2AttackableAI { + public static final int AI_IDLE = 1; + public static final int AI_NORMAL = 2; + public static final int AI_FORCE_ATTACK = 3; + public static final int AI_FOLLOW = 4; + public static final int AI_CAST = 5; + public static final int AI_ATTACK_GROUP = 6; + + private int _alternateAI; + + private boolean _isThinking; // to prevent thinking recursively + private boolean _isNotMoving; + + private L2Character _forcedTarget; + private MobGroup _targetGroup; + + public L2ControllableMobAI(L2ControllableMobInstance creature) { + super(creature); + setAlternateAI(AI_IDLE); + } + + protected void thinkFollow() { + L2Attackable me = (L2Attackable) _actor; + + if (!Util.checkIfInRange(MobGroupTable.FOLLOW_RANGE, me, getForcedTarget(), true)) { + int signX = (Rnd.nextInt(2) == 0) ? -1 : 1; + int signY = (Rnd.nextInt(2) == 0) ? -1 : 1; + int randX = Rnd.nextInt(MobGroupTable.FOLLOW_RANGE); + int randY = Rnd.nextInt(MobGroupTable.FOLLOW_RANGE); + + moveTo(getForcedTarget().getX() + (signX * randX), getForcedTarget().getY() + (signY * randY), getForcedTarget().getZ()); + } + } + + @Override + protected void onEvtThink() { + if (_isThinking) { + return; + } + + _isThinking = true; + + try { + switch (getAlternateAI()) { + case AI_IDLE: + if (getIntention() != CtrlIntention.AI_INTENTION_ACTIVE) { + setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + break; + case AI_FOLLOW: + thinkFollow(); + break; + case AI_CAST: + thinkCast(); + break; + case AI_FORCE_ATTACK: + thinkForceAttack(); + break; + case AI_ATTACK_GROUP: + thinkAttackGroup(); + break; + default: + if (getIntention() == AI_INTENTION_ACTIVE) { + thinkActive(); + } else if (getIntention() == AI_INTENTION_ATTACK) { + thinkAttack(); + } + break; + } + } finally { + _isThinking = false; + } + } + + @Override + protected void thinkCast() { + final var attackTarget = getAttackTarget(); + if ((attackTarget == null) || attackTarget.isAlikeDead()) { + setAttackTarget(findNextRndTarget()); + clientStopMoving(null); + + if (attackTarget == null) { + return; + } + } + + final var actor = getActor(); + actor.setTarget(attackTarget); + + if (!_actor.isMuted()) { + int max_range = 0; + // check distant skills + + for (Skill sk : _actor.getAllSkills()) { + if (Util.checkIfInRange(sk.getCastRange(), _actor, attackTarget, true) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() > _actor.getStat().getMpConsume2(sk))) { + _actor.doCast(sk); + return; + } + + max_range = Math.max(max_range, sk.getCastRange()); + } + + if (!isNotMoving()) { + moveToPawn(attackTarget, max_range); + } + } + } + + protected void thinkAttackGroup() { + L2Character target = getForcedTarget(); + if ((target == null) || target.isAlikeDead()) { + // try to get next group target + setForcedTarget(findNextGroupTarget()); + clientStopMoving(null); + } + + if (target == null) { + return; + } + + _actor.setTarget(target); + // as a response, we put the target in a forced attack mode + L2ControllableMobInstance theTarget = (L2ControllableMobInstance) target; + L2ControllableMobAI ctrlAi = (L2ControllableMobAI) theTarget.getAI(); + ctrlAi.forceAttack(_actor); + + double dist2 = _actor.calculateDistance(target, false, true); + int range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + target.getTemplate().getCollisionRadius(); + int max_range = range; + + if (!_actor.isMuted() && (dist2 > ((range + 20) * (range + 20)))) { + // check distant skills + for (Skill sk : _actor.getAllSkills()) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist2) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() > _actor.getStat().getMpConsume2(sk))) { + _actor.doCast(sk); + return; + } + + max_range = Math.max(max_range, castRange); + } + + if (!isNotMoving()) { + moveToPawn(target, range); + } + + return; + } + _actor.doAttack(target); + } + + protected void thinkForceAttack() { + if ((getForcedTarget() == null) || getForcedTarget().isAlikeDead()) { + clientStopMoving(null); + setIntention(AI_INTENTION_ACTIVE); + setAlternateAI(AI_IDLE); + } + + _actor.setTarget(getForcedTarget()); + double dist2 = _actor.calculateDistance(getForcedTarget(), false, true); + int range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + getForcedTarget().getTemplate().getCollisionRadius(); + int max_range = range; + + if (!_actor.isMuted() && (dist2 > ((range + 20) * (range + 20)))) { + // check distant skills + for (Skill sk : _actor.getAllSkills()) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist2) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() > _actor.getStat().getMpConsume2(sk))) { + _actor.doCast(sk); + return; + } + + max_range = Math.max(max_range, castRange); + } + + if (!isNotMoving()) { + moveToPawn(getForcedTarget(), _actor.getPhysicalAttackRange()/* range */); + } + + return; + } + + _actor.doAttack(getForcedTarget()); + } + + @Override + protected void thinkAttack() { + if ((getAttackTarget() == null) || getAttackTarget().isAlikeDead()) { + if (getAttackTarget() != null) { + // stop hating + L2Attackable npc = (L2Attackable) _actor; + npc.stopHating(getAttackTarget()); + } + + setIntention(AI_INTENTION_ACTIVE); + } else { + // notify aggression + if (((L2Npc) _actor).getTemplate().getClans() != null) { + Collection objs = _actor.getKnownList().getKnownObjects().values(); + for (L2Object obj : objs) { + if (!(obj instanceof L2Npc npc)) { + continue; + } + + if (!npc.isInMyClan((L2Npc) _actor)) { + continue; + } + + if (_actor.isInsideRadius(npc, npc.getTemplate().getClanHelpRange(), false, true) && (Math.abs(getAttackTarget().getZ() - npc.getZ()) < 200)) { + npc.getAI().notifyEvent(CtrlEvent.EVT_AGGRESSION, getAttackTarget(), 1); + } + } + } + + _actor.setTarget(getAttackTarget()); + double dist2 = _actor.calculateDistance(getAttackTarget(), false, true); + int range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + getAttackTarget().getTemplate().getCollisionRadius(); + int max_range = range; + + if (!_actor.isMuted() && (dist2 > ((range + 20) * (range + 20)))) { + // check distant skills + for (Skill sk : _actor.getAllSkills()) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist2) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() > _actor.getStat().getMpConsume2(sk))) { + _actor.doCast(sk); + return; + } + + max_range = Math.max(max_range, castRange); + } + + moveToPawn(getAttackTarget(), range); + return; + } + + // Force mobs to attack anybody if confused. + L2Character hated; + + if (_actor.isConfused()) { + hated = findNextRndTarget(); + } else { + hated = getAttackTarget(); + } + + if (hated == null) { + setIntention(AI_INTENTION_ACTIVE); + return; + } + + if (hated != getAttackTarget()) { + setAttackTarget(hated); + } + + if (!_actor.isMuted() && (Rnd.nextInt(5) == 3)) { + for (Skill sk : _actor.getAllSkills()) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist2) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() < _actor.getStat().getMpConsume2(sk))) { + _actor.doCast(sk); + return; + } + } + } + + _actor.doAttack(getAttackTarget()); + } + } + + @Override + protected void thinkActive() { + setAttackTarget(findNextRndTarget()); + L2Character hated; + + if (_actor.isConfused()) { + hated = findNextRndTarget(); + } else { + hated = getAttackTarget(); + } + + if (hated != null) { + _actor.setRunning(); + setIntention(CtrlIntention.AI_INTENTION_ATTACK, hated); + } + } + + private boolean checkAutoAttackCondition(L2Character target) { + if ((target == null) || (target instanceof L2NpcInstance) || (target instanceof L2DoorInstance)) { + return false; + } + + // TODO(Zoey76)[#112]: This check must change if summon fall in L2Npc hierarchy. + if (target.isNpc()) { + return false; + } + + // Check if the target isn't invulnerable + if (target.isInvul() || target.isAlikeDead()) { + return false; + } + + // Spawn protection (only against mobs) + if (target.isPlayer() && ((L2PcInstance) target).isSpawnProtected()) { + return false; + } + + final var actor = getActor(); + if (!_actor.isInsideRadius(target, actor.getAggroRange(), false, false) || (Math.abs(_actor.getZ() - target.getZ()) > 100)) { + return false; + } + + // Check if the target is a L2Playable + if (target.isPlayable()) { + // Check if the target isn't in silent move mode + if (((L2Playable) target).isSilentMovingAffected()) { + return false; + } + } + return actor.isAggressive(); + } + + private L2Character findNextRndTarget() { + final var potentialTarget = _actor.getKnownList().getKnownCharactersInRadius(getActor().getAggroRange()).stream().filter(this::checkAutoAttackCondition).collect(Collectors.toList()); + if (potentialTarget.isEmpty()) { + return null; + } + return potentialTarget.get(Rnd.nextInt(potentialTarget.size())); + } + + private L2ControllableMobInstance findNextGroupTarget() { + return getGroupTarget().getRandomMob(); + } + + public int getAlternateAI() { + return _alternateAI; + } + + public void setAlternateAI(int alternateAI) { + _alternateAI = alternateAI; + } + + public void forceAttack(L2Character target) { + setAlternateAI(AI_FORCE_ATTACK); + setForcedTarget(target); + } + + public void forceAttackGroup(MobGroup group) { + setForcedTarget(null); + setGroupTarget(group); + setAlternateAI(AI_ATTACK_GROUP); + } + + public void stop() { + setAlternateAI(AI_IDLE); + clientStopMoving(null); + } + + public void move(int x, int y, int z) { + moveTo(x, y, z); + } + + public void follow(L2Character target) { + setAlternateAI(AI_FOLLOW); + setForcedTarget(target); + } + + public boolean isNotMoving() { + return _isNotMoving; + } + + public void setNotMoving(boolean isNotMoving) { + _isNotMoving = isNotMoving; + } + + private L2Character getForcedTarget() { + return _forcedTarget; + } + + private MobGroup getGroupTarget() { + return _targetGroup; + } + + private void setForcedTarget(L2Character forcedTarget) { + _forcedTarget = forcedTarget; + } + + private void setGroupTarget(MobGroup targetGroup) { + _targetGroup = targetGroup; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2DoorAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2DoorAI.java new file mode 100644 index 00000000..b77fa00e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2DoorAI.java @@ -0,0 +1,149 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * @author mkizub + */ +public class L2DoorAI extends L2CharacterAI { + public L2DoorAI(L2DoorInstance creature) { + super(creature); + } + + @Override + protected void onIntentionIdle() { + } + + @Override + protected void onIntentionActive() { + } + + @Override + protected void onIntentionRest() { + } + + @Override + protected void onIntentionAttack(L2Character target) { + } + + @Override + protected void onIntentionCast(Skill skill, L2Object target) { + } + + @Override + protected void onIntentionMoveTo(Location destination) { + } + + @Override + protected void onIntentionFollow(L2Character target) { + } + + @Override + protected void onIntentionPickUp(L2Object item) { + } + + @Override + protected void onIntentionInteract(L2Object object) { + } + + @Override + protected void onEvtThink() { + } + + @Override + protected void onEvtAttacked(L2Character attacker) { + ThreadPoolManager.getInstance().executeGeneral(new onEventAttackedDoorTask((L2DoorInstance) _actor, attacker)); + } + + @Override + protected void onEvtAggression(L2Character target, long aggro) { + } + + @Override + protected void onEvtStunned(L2Character attacker) { + } + + @Override + protected void onEvtSleeping(L2Character attacker) { + } + + @Override + protected void onEvtRooted(L2Character attacker) { + } + + @Override + protected void onEvtReadyToAct() { + } + + @Override + protected void onEvtUserCmd(Object arg0, Object arg1) { + } + + @Override + protected void onEvtArrived() { + } + + @Override + protected void onEvtArrivedRevalidate() { + } + + @Override + protected void onEvtArrivedBlocked(Location blocked_at_loc) { + } + + @Override + protected void onEvtForgetObject(L2Object object) { + } + + @Override + protected void onEvtCancel() { + } + + @Override + protected void onEvtDead() { + } + + private class onEventAttackedDoorTask implements Runnable { + private final L2DoorInstance _door; + private final L2Character _attacker; + + public onEventAttackedDoorTask(L2DoorInstance door, L2Character attacker) { + _door = door; + _attacker = attacker; + } + + @Override + public void run() { + for (L2DefenderInstance guard : _door.getKnownDefenders()) { + if (_actor.isInsideRadius(guard, guard.getTemplate().getClanHelpRange(), false, true) && (Math.abs(_attacker.getZ() - guard.getZ()) < 200)) { + guard.getAI().notifyEvent(CtrlEvent.EVT_AGGRESSION, _attacker, 15); + } + } + } + } + +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2FortSiegeGuardAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2FortSiegeGuardAI.java new file mode 100644 index 00000000..27211acc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2FortSiegeGuardAI.java @@ -0,0 +1,806 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; + +import java.util.Collection; +import java.util.concurrent.Future; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FortCommanderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2NpcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.Util; + +/** + * Fort siege guards AI. + */ +public class L2FortSiegeGuardAI extends L2CharacterAI implements Runnable { + private static final int MAX_ATTACK_TIMEOUT = 30 * GameTimeController.TICKS_PER_SECOND; // 30s converted to ticks + + /** The L2Attackable AI task executed every 1s (call onEvtThink method) */ + private Future _aiTask; + + /** For attack AI, analysis of mob and its targets */ + private final SelfAnalysis _selfAnalysis = new SelfAnalysis(); + + /** The delay after which the attacked is stopped */ + private int _attackTimeout; + + /** The L2Attackable aggro counter */ + private int _globalAggro; + + /** The flag used to indicate that a thinking action is in progress */ + private boolean _thinking; // to prevent recursive thinking + + private final int _attackRange; + + /** + * Constructor of L2AttackableAI. + * @param creature the creature + */ + public L2FortSiegeGuardAI(L2DefenderInstance creature) { + super(creature); + _selfAnalysis.init(); + _attackTimeout = Integer.MAX_VALUE; + _globalAggro = -10; // 10 seconds timeout of ATTACK after respawn + _attackRange = _actor.getPhysicalAttackRange(); + } + + @Override + public void run() { + // Launch actions corresponding to the Event Think + onEvtThink(); + } + + /** + * Actor is a L2GuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    • The L2MonsterInstance target is aggressive
    • + *
    + * Actor is a L2SiegeGuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • A siege is in progress
    • + *
    • The L2PcInstance target isn't a Defender
    • + *
    + * Actor is a L2FriendlyMobInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2NpcInstance
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    + * Actor is a L2MonsterInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2NpcInstance
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The actor is Aggressive
    • + *
    + * @param target The targeted L2Object + * @return True if the target is autoattackable (depends on the actor type). + */ + private boolean autoAttackCondition(L2Character target) { + // Check if the target isn't another guard, folk or a door + if ((target == null) || (target instanceof L2DefenderInstance) || (target instanceof L2NpcInstance) || (target instanceof L2DoorInstance) || target.isAlikeDead() || (target instanceof L2FortCommanderInstance) || (target instanceof L2Playable)) { + L2PcInstance player = null; + if (target instanceof L2PcInstance) { + player = ((L2PcInstance) target); + } else if (target instanceof L2Summon) { + player = ((L2Summon) target).getOwner(); + } + if ((player == null) || ((player.getClan() != null) && (player.getClan().getFortId() == ((L2Npc) _actor).getFort().getResidenceId()))) { + return false; + } + } + + // Check if the target isn't invulnerable + if (target.isInvul()) { + // However EffectInvincible requires to check GMs specially + if (target.isPlayer() && target.isGM()) { + return false; + } + if (target.isSummon() && ((L2Summon) target).getOwner().isGM()) { + return false; + } + } + + // Get the owner if the target is a summon + if (target instanceof L2Summon) { + L2PcInstance owner = ((L2Summon) target).getOwner(); + if (_actor.isInsideRadius(owner, 1000, true, false)) { + target = owner; + } + } + + // Check if the target is a L2PcInstance + if (target instanceof L2Playable playable) { + // Check if the target isn't in silent move mode AND too far (>100) + if (playable.isSilentMovingAffected() && !_actor.isInsideRadius(target, 250, false, false)) { + return false; + } + } + // Los Check Here + return (_actor.isAutoAttackable(target) && GeoData.getInstance().canSeeTarget(_actor, target)); + + } + + /** + * Set the Intention of this L2CharacterAI and create an AI Task executed every 1s (call onEvtThink method) for this L2Attackable.
    + * Caution : If actor _knowPlayer isn't EMPTY, AI_INTENTION_IDLE will be change in AI_INTENTION_ACTIVE + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention + * @param arg1 The second parameter of the Intention + */ + @Override + synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + LOG.debug("{}: changeIntention({}, {}, {})", getClass().getSimpleName(), intention, arg0, arg1); + + if (intention == AI_INTENTION_IDLE /* || intention == AI_INTENTION_ACTIVE */) // active becomes idle if only a summon is present + { + // Check if actor is not dead + if (!_actor.isAlikeDead()) { + L2Attackable npc = (L2Attackable) _actor; + + // If its _knownPlayer isn't empty set the Intention to AI_INTENTION_ACTIVE + if (!npc.getKnownList().getKnownPlayers().isEmpty()) { + intention = AI_INTENTION_ACTIVE; + } else { + intention = AI_INTENTION_IDLE; + } + } + + if (intention == AI_INTENTION_IDLE) { + // Set the Intention of this L2AttackableAI to AI_INTENTION_IDLE + super.changeIntention(AI_INTENTION_IDLE, null, null); + + // Stop AI task and detach AI from NPC + if (_aiTask != null) { + _aiTask.cancel(true); + _aiTask = null; + } + + // Cancel the AI + _actor.detachAI(); + + return; + } + } + + // Set the Intention of this L2AttackableAI to intention + super.changeIntention(intention, arg0, arg1); + + // If not idle - create an AI task (schedule onEvtThink repeatedly) + if ((_aiTask == null) && (_actor.isInActiveRegion())) { + _aiTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(this, 1000, 1000); + } + } + + /** + * Manage the Attack Intention : Stop current Attack (if necessary), Calculate attack timeout, Start a new Attack and Launch Think Event. + * @param target The L2Character to attack + */ + @Override + protected void onIntentionAttack(L2Character target) { + // Calculate the attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // Manage the Attack Intention : Stop current Attack (if necessary), Start a new Attack and Launch Think Event + // if (_actor.getTarget() != null) + super.onIntentionAttack(target); + } + + /** + * Manage AI standard thinks of a L2Attackable (called by onEvtThink).
    + * Actions : + *
      + *
    • Update every 1s the _globalAggro counter to come close to 0
    • + *
    • If the actor is Aggressive and can attack, add all autoAttackable L2Character in its Aggro Range to its _aggroList, chose a target and order to attack it
    • + *
    • If the actor can't attack, order to it to return to its home location
    • + *
    + */ + private void thinkActive() { + L2Attackable npc = (L2Attackable) _actor; + + // Update every 1s the _globalAggro counter to come close to 0 + if (_globalAggro != 0) { + if (_globalAggro < 0) { + _globalAggro++; + } else { + _globalAggro--; + } + } + + // Add all autoAttackable L2Character in L2Attackable Aggro Range to its _aggroList with 0 damage and 1 hate + // A L2Attackable isn't aggressive during 10s after its spawn because _globalAggro is set to -10 + if (_globalAggro >= 0) { + for (L2Character target : npc.getKnownList().getKnownCharactersInRadius(_attackRange)) { + if (target == null) { + continue; + } + if (autoAttackCondition(target)) // check aggression + { + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + long hating = npc.getHating(target); + + // Add the attacker to the L2Attackable _aggroList with 0 damage and 1 hate + if (hating == 0) { + npc.addDamageHate(target, 0, 1); + } + } + } + + // Chose a target from its aggroList + L2Character hated; + if (_actor.isConfused()) { + hated = getAttackTarget(); // Force mobs to attack anybody if confused + } else { + hated = npc.getMostHated(); + // _mostHatedAnalysis.Update(hated); + } + + // Order to the L2Attackable to attack the target + if (hated != null) { + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + long aggro = npc.getHating(hated); + if ((aggro + _globalAggro) > 0) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + // Set the AI Intention to AI_INTENTION_ATTACK + setIntention(CtrlIntention.AI_INTENTION_ATTACK, hated, null); + } + + return; + } + + } + // Order to the L2SiegeGuardInstance to return to its home location because there's no target to attack + if (_actor.getWalkSpeed() >= 0) { + if (_actor instanceof L2DefenderInstance defender) { + defender.returnHome(); + } else { + ((L2FortCommanderInstance) _actor).returnHome(); + } + } + } + + /** + * Manage AI attack thinks of a L2Attackable (called by onEvtThink).
    + * Actions : + *
      + *
    • Update the attack timeout if actor is running
    • + *
    • If target is dead or timeout is expired, stop this attack and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • Call all L2Object of its Faction inside the Faction Range
    • + *
    • Chose a target and order to attack it with magic skill or physical attack
    • + *
    + * TODO: Manage casting rules to healer mobs (like Ant Nurses) + */ + private void thinkAttack() { + LOG.debug("{}: thinkAttack(); timeout={}", getClass().getSimpleName(), (_attackTimeout - GameTimeController.getInstance().getGameTicks())); + + if (_attackTimeout < GameTimeController.getInstance().getGameTicks()) { + // Check if the actor is running + if (_actor.isRunning()) { + // Set the actor movement type to walk and send Server->Client packet ChangeMoveType to all others L2PcInstance + _actor.setWalking(); + + // Calculate a new attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + } + } + + L2Character attackTarget = getAttackTarget(); + // Check if target is dead or if timeout is expired to stop this attack + if ((attackTarget == null) || attackTarget.isAlikeDead() || (_attackTimeout < GameTimeController.getInstance().getGameTicks())) { + // Stop hating this target after the attack timeout or if target is dead + if (attackTarget != null) { + L2Attackable npc = (L2Attackable) _actor; + npc.stopHating(attackTarget); + } + + // Cancel target and timeout + _attackTimeout = Integer.MAX_VALUE; + setAttackTarget(null); + + // Set the AI Intention to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE, null, null); + + _actor.setWalking(); + return; + } + + factionNotifyAndSupport(); + attackPrepare(); + } + + private void factionNotifyAndSupport() { + L2Character target = getAttackTarget(); + // Call all L2Object of its Faction inside the Faction Range + if ((((L2Npc) _actor).getTemplate().getClans() == null) || (target == null)) { + return; + } + + if (target.isInvul()) { + return; // speeding it up for siege guards + } + + // Go through all L2Character that belong to its faction + // for (L2Character cha : _actor.getKnownList().getKnownCharactersInRadius(((L2NpcInstance) _actor).getFactionRange()+_actor.getTemplate().collisionRadius)) + for (L2Character cha : _actor.getKnownList().getKnownCharactersInRadius(1000)) { + if (cha == null) { + continue; + } + + if (!(cha instanceof L2Npc npc)) { + if (_selfAnalysis.hasHealOrResurrect && (cha instanceof L2PcInstance) && ((L2Npc) _actor).getFort().getSiege().checkIsDefender(cha.getClan())) { + // heal friends + if (!_actor.isAttackingDisabled() && (cha.getCurrentHp() < (cha.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2.0)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2.0)) && cha.isInCombat()) { + for (Skill sk : _selfAnalysis.healSkills) { + if (_actor.getCurrentMp() < sk.getMpConsume2()) { + continue; + } + if (_actor.isSkillDisabled(sk)) { + continue; + } + if (!Util.checkIfInRange(sk.getCastRange(), _actor, cha, true)) { + continue; + } + + int chance = 5; + if (chance >= Rnd.get(100)) { + continue; + } + if (!GeoData.getInstance().canSeeTarget(_actor, cha)) { + break; + } + + L2Object oldTarget = _actor.getTarget(); + _actor.setTarget(cha); + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(oldTarget); + return; + } + } + } + continue; + } + + if (!npc.isInMyClan((L2Npc) _actor)) { + continue; + } + + if (npc.getAI() != null) // TODO: possibly check not needed + { + if (!npc.isDead() && (Math.abs(target.getZ() - npc.getZ()) < 600) + // && _actor.getAttackByList().contains(getAttackTarget()) + && ((npc.getAI()._intention == CtrlIntention.AI_INTENTION_IDLE) || (npc.getAI()._intention == CtrlIntention.AI_INTENTION_ACTIVE)) + // limiting aggro for siege guards + && target.isInsideRadius(npc, 1500, true, false) && GeoData.getInstance().canSeeTarget(npc, target)) { + // Notify the L2Object AI with EVT_AGGRESSION + npc.getAI().notifyEvent(CtrlEvent.EVT_AGGRESSION, getAttackTarget(), 1); + return; + } + // heal friends + if (_selfAnalysis.hasHealOrResurrect && !_actor.isAttackingDisabled() && (npc.getCurrentHp() < (npc.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2.0)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2.0)) && npc.isInCombat()) { + for (Skill sk : _selfAnalysis.healSkills) { + if (_actor.getCurrentMp() < sk.getMpConsume2()) { + continue; + } + if (_actor.isSkillDisabled(sk)) { + continue; + } + if (!Util.checkIfInRange(sk.getCastRange(), _actor, npc, true)) { + continue; + } + + int chance = 4; + if (chance >= Rnd.get(100)) { + continue; + } + if (!GeoData.getInstance().canSeeTarget(_actor, npc)) { + break; + } + + L2Object OldTarget = _actor.getTarget(); + _actor.setTarget(npc); + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + } + } + } + + private void attackPrepare() { + // Get all information needed to choose between physical or magical attack + Collection skills; + double dist_2; + int range; + L2DefenderInstance sGuard; + if (_actor instanceof L2FortCommanderInstance) { + sGuard = (L2FortCommanderInstance) _actor; + } else { + sGuard = (L2DefenderInstance) _actor; + } + L2Character attackTarget = getAttackTarget(); + + try { + _actor.setTarget(attackTarget); + skills = _actor.getAllSkills(); + dist_2 = _actor.calculateDistance(attackTarget, false, true); + range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + attackTarget.getTemplate().getCollisionRadius(); + if (attackTarget.isMoving()) { + range += 50; + } + } catch (NullPointerException e) { + LOG.warn("{}: Attack target is NULL.", getClass().getSimpleName()); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + + // never attack defenders + if ((attackTarget instanceof L2PcInstance) && sGuard.getFort().getSiege().checkIsDefender(attackTarget.getClan())) { + // Cancel the target + sGuard.stopHating(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + + if (!GeoData.getInstance().canSeeTarget(_actor, attackTarget)) { + // Siege guards differ from normal mobs currently: + // If target cannot seen, don't attack any more + sGuard.stopHating(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + + // Check if the actor isn't muted and if it is far from target + if (!_actor.isMuted() && (dist_2 > (range * range))) { + // check for long ranged skills and heal/buff skills + for (Skill sk : skills) { + int castRange = sk.getCastRange(); + + if ((dist_2 <= (castRange * castRange)) && (castRange > 70) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume2(sk)) && !sk.isPassive()) { + + L2Object OldTarget = _actor.getTarget(); + if ((sk.isContinuous() && !sk.isDebuff()) || (sk.hasEffectType(L2EffectType.HP))) { + boolean useSkillSelf = true; + if ((sk.hasEffectType(L2EffectType.HP)) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5))) { + useSkillSelf = false; + break; + } + + if ((sk.isContinuous() && !sk.isDebuff()) && _actor.isAffectedBySkill(sk.getId())) { + useSkillSelf = false; + } + + if (useSkillSelf) { + _actor.setTarget(_actor); + } + } + + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + + // Check if the L2SiegeGuardInstance is attacking, knows the target and can't run + if (!(_actor.isAttackingNow()) && (_actor.getRunSpeed() == 0) && (_actor.getKnownList().knowsObject(attackTarget))) { + // Cancel the target + _actor.getKnownList().removeKnownObject(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + } else { + double dx = _actor.getX() - attackTarget.getX(); + double dy = _actor.getY() - attackTarget.getY(); + double dz = _actor.getZ() - attackTarget.getZ(); + double homeX = attackTarget.getX() - sGuard.getSpawn().getX(); + double homeY = attackTarget.getY() - sGuard.getSpawn().getY(); + + // Check if the L2SiegeGuardInstance isn't too far from it's home location + if ((((dx * dx) + (dy * dy)) > 10000) && (((homeX * homeX) + (homeY * homeY)) > 3240000) // 1800 * 1800 + && (_actor.getKnownList().knowsObject(attackTarget))) { + // Cancel the target + _actor.getKnownList().removeKnownObject(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + } else + // Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast) + { + // Temporary hack for preventing guards jumping off towers, + // before replacing this with effective geodata checks and AI modification + if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct + { + if (_selfAnalysis.isMage) { + range = _selfAnalysis.maxCastRange - 50; + } + if (_actor.getWalkSpeed() <= 0) { + return; + } + if (attackTarget.isMoving()) { + moveToPawn(attackTarget, range - 70); + } else { + moveToPawn(attackTarget, range); + } + } + } + } + } + // Else, if the actor is muted and far from target, just "move to pawn" + else if (_actor.isMuted() && (dist_2 > (range * range))) { + // Temporary hack for preventing guards jumping off towers, + // before replacing this with effective geodata checks and AI modification + double dz = _actor.getZ() - attackTarget.getZ(); + if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct + { + if (_selfAnalysis.isMage) { + range = _selfAnalysis.maxCastRange - 50; + } + if (_actor.getWalkSpeed() <= 0) { + return; + } + if (attackTarget.isMoving()) { + moveToPawn(attackTarget, range - 70); + } else { + moveToPawn(attackTarget, range); + } + } + } + // Else, if this is close enough to attack + else if (dist_2 <= (range * range)) { + // Force mobs to attack anybody if confused + L2Character hated; + if (_actor.isConfused()) { + hated = attackTarget; + } else { + hated = ((L2Attackable) _actor).getMostHated(); + } + + if (hated == null) { + setIntention(AI_INTENTION_ACTIVE, null, null); + return; + } + if (hated != attackTarget) { + attackTarget = hated; + } + + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // check for close combat skills && heal/buff skills + if (!_actor.isMuted() && (Rnd.nextInt(100) <= 5)) { + for (Skill sk : skills) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist_2) && !sk.isPassive() && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume2(sk)) && !_actor.isSkillDisabled(sk)) { + L2Object OldTarget = _actor.getTarget(); + if ((sk.isContinuous() && !sk.isDebuff()) || (sk.hasEffectType(L2EffectType.HP))) { + boolean useSkillSelf = true; + if ((sk.hasEffectType(L2EffectType.HP)) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5))) { + useSkillSelf = false; + break; + } + + if ((sk.isContinuous() && !sk.isDebuff()) && _actor.isAffectedBySkill(sk.getId())) { + useSkillSelf = false; + } + + if (useSkillSelf) { + _actor.setTarget(_actor); + } + } + + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + } + // Finally, do the physical attack itself + _actor.doAttack(attackTarget); + } + } + + /** + * Manage AI thinking actions of a L2Attackable. + */ + @Override + protected void onEvtThink() { + // if(getIntention() != AI_INTENTION_IDLE && (!_actor.isVisible() || !_actor.hasAI() || !_actor.isKnownPlayers())) + // setIntention(AI_INTENTION_IDLE); + + // Check if the actor can't use skills and if a thinking action isn't already in progress + if (_thinking || _actor.isCastingNow() || _actor.isAllSkillsDisabled()) { + return; + } + + // Start thinking action + _thinking = true; + + try { + // Manage AI thinks of a L2Attackable + if (getIntention() == AI_INTENTION_ACTIVE) { + thinkActive(); + } else if (getIntention() == AI_INTENTION_ATTACK) { + thinkAttack(); + } + } finally { + // Stop thinking action + _thinking = false; + } + } + + /** + * Launch actions corresponding to the Event Attacked.
    + * Actions : + *
      + *
    • Init the attack : Calculate the attack timeout, Set the _globalAggro to 0, Add the attacker to the actor _aggroList
    • + *
    • Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance
    • + *
    • Set the Intention to AI_INTENTION_ATTACK
    • + *
    + * @param attacker The L2Character that attacks the actor + */ + @Override + protected void onEvtAttacked(L2Character attacker) { + // Calculate the attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // Set the _globalAggro to 0 to permit attack even just after spawn + if (_globalAggro < 0) { + _globalAggro = 0; + } + + // Add the attacker to the _aggroList of the actor + ((L2Attackable) _actor).addDamageHate(attacker, 0, 1); + + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + // Set the Intention to AI_INTENTION_ATTACK + if (getIntention() != AI_INTENTION_ATTACK) { + setIntention(CtrlIntention.AI_INTENTION_ATTACK, attacker, null); + } + + super.onEvtAttacked(attacker); + } + + /** + * Launch actions corresponding to the Event Aggression.
    + * Actions : + *
      + *
    • Add the target to the actor _aggroList or update hate if already present
    • + *
    • Set the actor Intention to AI_INTENTION_ATTACK (if actor is L2GuardInstance check if it isn't too far from its home location)
    • + *
    + * @param aggro The value of hate to add to the actor against the target + */ + @Override + protected void onEvtAggression(L2Character target, long aggro) { + if (_actor == null) { + return; + } + L2Attackable me = (L2Attackable) _actor; + + if (target != null) { + // Add the target to the actor _aggroList or update hate if already present + me.addDamageHate(target, 0, aggro); + + // Get the hate of the actor against the target + aggro = me.getHating(target); + + if (aggro <= 0) { + if (me.getMostHated() == null) { + _globalAggro = -25; + me.clearAggroList(); + setIntention(AI_INTENTION_IDLE, null, null); + } + return; + } + + // Set the actor AI Intention to AI_INTENTION_ATTACK + if (getIntention() != CtrlIntention.AI_INTENTION_ATTACK) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + L2DefenderInstance sGuard; + if (_actor instanceof L2FortCommanderInstance) { + sGuard = (L2FortCommanderInstance) _actor; + } else { + sGuard = (L2DefenderInstance) _actor; + } + double homeX = target.getX() - sGuard.getSpawn().getX(); + double homeY = target.getY() - sGuard.getSpawn().getY(); + + // Check if the L2SiegeGuardInstance is not too far from its home location + if (((homeX * homeX) + (homeY * homeY)) < 3240000) { + setIntention(CtrlIntention.AI_INTENTION_ATTACK, target, null); + } + } + } else { + // currently only for setting lower general aggro + if (aggro >= 0) { + return; + } + + L2Character mostHated = me.getMostHated(); + if (mostHated == null) { + _globalAggro = -25; + return; + } + + for (var attacker : me.getAggroList().keySet()) { + me.addDamageHate(attacker, 0, aggro); + } + + aggro = me.getHating(mostHated); + if (aggro <= 0) { + _globalAggro = -25; + me.clearAggroList(); + setIntention(AI_INTENTION_IDLE, null, null); + } + } + } + + @Override + public void stopAITask() { + if (_aiTask != null) { + _aiTask.cancel(false); + _aiTask = null; + } + _actor.detachAI(); + super.stopAITask(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2PlayableAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2PlayableAI.java new file mode 100644 index 00000000..fd470675 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2PlayableAI.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Playable creatures AI. + * @author JIV + */ +public abstract class L2PlayableAI extends L2CharacterAI { + + public L2PlayableAI(L2Playable creature) { + super(creature); + } + + @Override + protected void onIntentionAttack(L2Character target) { + if (target instanceof L2Playable) { + if (target.getActingPlayer().isProtectionBlessingAffected() && ((_actor.getActingPlayer().getLevel() - target.getActingPlayer().getLevel()) >= 10) && (_actor.getActingPlayer().getKarma() > 0) && !(target.isInsideZone(ZoneId.PVP))) { + // If attacker have karma and have level >= 10 than his target and target have + // Newbie Protection Buff, + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + return; + } + + if (_actor.getActingPlayer().isProtectionBlessingAffected() && ((target.getActingPlayer().getLevel() - _actor.getActingPlayer().getLevel()) >= 10) && (target.getActingPlayer().getKarma() > 0) && !(target.isInsideZone(ZoneId.PVP))) { + // If target have karma and have level >= 10 than his target and actor have + // Newbie Protection Buff, + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + return; + } + + if (target.getActingPlayer().isCursedWeaponEquipped() && (_actor.getActingPlayer().getLevel() <= 20)) { + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + return; + } + + if (_actor.getActingPlayer().isCursedWeaponEquipped() && (target.getActingPlayer().getLevel() <= 20)) { + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + return; + } + } + + super.onIntentionAttack(target); + } + + @Override + protected void onIntentionCast(Skill skill, L2Object target) { + if ((target instanceof L2Playable) && skill.isBad()) { + if (target.getActingPlayer().isProtectionBlessingAffected() && ((_actor.getActingPlayer().getLevel() - target.getActingPlayer().getLevel()) >= 10) && (_actor.getActingPlayer().getKarma() > 0) && !target.isInsideZone(ZoneId.PVP)) { + // If attacker have karma and have level >= 10 than his target and target have + // Newbie Protection Buff, + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + _actor.setIsCastingNow(false); + return; + } + + if (_actor.getActingPlayer().isProtectionBlessingAffected() && ((target.getActingPlayer().getLevel() - _actor.getActingPlayer().getLevel()) >= 10) && (target.getActingPlayer().getKarma() > 0) && !target.isInsideZone(ZoneId.PVP)) { + // If target have karma and have level >= 10 than his target and actor have + // Newbie Protection Buff, + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + _actor.setIsCastingNow(false); + return; + } + + if (target.getActingPlayer().isCursedWeaponEquipped() && ((_actor.getActingPlayer().getLevel() <= 20) || (target.getActingPlayer().getLevel() <= 20))) { + _actor.getActingPlayer().sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + clientActionFailed(); + _actor.setIsCastingNow(false); + return; + } + } + super.onIntentionCast(skill, target); + } + +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2PlayerAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2PlayerAI.java new file mode 100644 index 00000000..0a8b9c15 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2PlayerAI.java @@ -0,0 +1,305 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_CAST; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_INTERACT; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_MOVE_TO; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_PICK_UP; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_REST; + +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.skills.targets.TargetType; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class L2PlayerAI extends L2PlayableAI { + private boolean _thinking; // to prevent recursive thinking + + private IntentionCommand _nextIntention = null; + + public L2PlayerAI(L2PcInstance creature) { + super(creature); + } + + void saveNextIntention(CtrlIntention intention, Object arg0, Object arg1) { + _nextIntention = new IntentionCommand(intention, arg0, arg1); + } + + @Override + public IntentionCommand getNextIntention() { + return _nextIntention; + } + + /** + * Saves the current Intention for this L2PlayerAI if necessary and calls changeIntention in AbstractAI. + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention + * @param arg1 The second parameter of the Intention + */ + @Override + protected synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + // Forget next if it's not cast or it's cast and skill is toggle. + if ((intention != AI_INTENTION_CAST) || ((arg0 != null) && !((Skill) arg0).isToggle())) { + _nextIntention = null; + super.changeIntention(intention, arg0, arg1); + return; + } + + // do nothing if next intention is same as current one. + if ((intention == _intention) && (arg0 == _intentionArg0) && (arg1 == _intentionArg1)) { + super.changeIntention(intention, arg0, arg1); + return; + } + + // save current intention so it can be used after cast + saveNextIntention(_intention, _intentionArg0, _intentionArg1); + super.changeIntention(intention, arg0, arg1); + } + + /** + * Launch actions corresponding to the Event ReadyToAct.
    + * Actions : + *
      + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtReadyToAct() { + // Launch actions corresponding to the Event Think + if (_nextIntention != null) { + setIntention(_nextIntention._crtlIntention, _nextIntention._arg0, _nextIntention._arg1); + _nextIntention = null; + } + super.onEvtReadyToAct(); + } + + /** + * Launch actions corresponding to the Event Cancel.
    + * Actions : + *
      + *
    • Stop an AI Follow Task
    • + *
    • Launch actions corresponding to the Event Think
    • + *
    + */ + @Override + protected void onEvtCancel() { + _nextIntention = null; + super.onEvtCancel(); + } + + /** + * Finalize the casting of a skill. This method overrides L2CharacterAI method.
    + * What it does:
    + * Check if actual intention is set to CAST and, if so, retrieves latest intention before the actual CAST and set it as the current intention for the player. + */ + @Override + protected void onEvtFinishCasting() { + if (getIntention() == AI_INTENTION_CAST) { + // run interrupted or next intention + + IntentionCommand nextIntention = _nextIntention; + if (nextIntention != null) { + if (nextIntention._crtlIntention != AI_INTENTION_CAST) // previous state shouldn't be casting + { + setIntention(nextIntention._crtlIntention, nextIntention._arg0, nextIntention._arg1); + } else { + setIntention(AI_INTENTION_IDLE); + } + } else { + // set intention to idle if skill doesn't change intention. + setIntention(AI_INTENTION_IDLE); + } + } + } + + @Override + protected void onIntentionRest() { + if (getIntention() != AI_INTENTION_REST) { + changeIntention(AI_INTENTION_REST, null, null); + setTarget(null); + if (getAttackTarget() != null) { + setAttackTarget(null); + } + clientStopMoving(null); + } + } + + @Override + protected void onIntentionActive() { + setIntention(AI_INTENTION_IDLE); + } + + /** + * Manage the Move To Intention : Stop current Attack and Launch a Move to Location Task.
    + * Actions : + *
      + *
    • Stop the actor auto-attack server side AND client side by sending Server->Client packet AutoAttackStop (broadcast)
    • + *
    • Set the Intention of this AI to AI_INTENTION_MOVE_TO
    • + *
    • Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast)
    • + *
    + */ + @Override + protected void onIntentionMoveTo(Location loc) { + if (getIntention() == AI_INTENTION_REST) { + // Cancel action client side by sending Server->Client packet ActionFailed to the L2PcInstance actor + clientActionFailed(); + return; + } + if (_actor.getActingPlayer().getDuelState() == DuelState.DEAD) { + clientActionFailed(); + _actor.getActingPlayer().sendPacket(SystemMessage.getSystemMessage(SystemMessageId.CANNOT_MOVE_FROZEN)); + return; + } + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow() || _actor.isAttackingNow()) { + clientActionFailed(); + saveNextIntention(AI_INTENTION_MOVE_TO, loc, null); + return; + } + + // Stop the actor auto-attack client side by sending Server->Client packet AutoAttackStop (broadcast) + clientStopAutoAttack(); + + // Set the Intention of this AbstractAI to AI_INTENTION_MOVE_TO + changeIntention(AI_INTENTION_MOVE_TO, loc, null); + + // Abort the attack of the L2Character and send Server->Client ActionFailed packet + _actor.abortAttack(); + + // Move the actor to Location (x,y,z) server side AND client side by sending Server->Client packet CharMoveToLocation (broadcast) + moveTo(loc.getX(), loc.getY(), loc.getZ()); + } + + @Override + protected void clientNotifyDead() { + _clientMovingToPawnOffset = 0; + _clientMoving = false; + + super.clientNotifyDead(); + } + + private void thinkAttack() { + L2Character target = getAttackTarget(); + if (target == null) { + return; + } + if (checkTargetLostOrDead(target)) { + // Notify the target + setAttackTarget(null); + return; + } + if (maybeMoveToPawn(target, _actor.getPhysicalAttackRange())) { + return; + } + + clientStopMoving(null); + _actor.doAttack(target); + } + + private void thinkCast() { + L2Character target = getCastTarget(); + if ((_skill.getTargetType() == TargetType.GROUND) && (_actor instanceof L2PcInstance)) { + if (maybeMoveToPosition(((L2PcInstance) _actor).getCurrentSkillWorldPosition(), _actor.getMagicalAttackRange(_skill))) { + _actor.setIsCastingNow(false); + return; + } + } else { + if (checkTargetLost(target)) { + if (_skill.isBad() && (getAttackTarget() != null)) { + // Notify the target + setCastTarget(null); + } + _actor.setIsCastingNow(false); + return; + } + if ((target != null) && maybeMoveToPawn(target, _actor.getMagicalAttackRange(_skill))) { + _actor.setIsCastingNow(false); + return; + } + } + + if ((_skill.getHitTime() > 50) && !_skill.isSimultaneousCast()) { + clientStopMoving(null); + } + + _actor.doCast(_skill); + } + + private void thinkPickUp() { + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow()) { + return; + } + L2Object target = getTarget(); + if (checkTargetLost(target)) { + return; + } + if (maybeMoveToPawn(target, 36)) { + return; + } + setIntention(AI_INTENTION_IDLE); + _actor.getActingPlayer().doPickupItem(target); + } + + private void thinkInteract() { + if (_actor.isAllSkillsDisabled() || _actor.isCastingNow()) { + return; + } + L2Object target = getTarget(); + if (checkTargetLost(target)) { + return; + } + if (maybeMoveToPawn(target, 36)) { + return; + } + if (!(target instanceof L2StaticObjectInstance)) { + _actor.getActingPlayer().doInteract((L2Character) target); + } + setIntention(AI_INTENTION_IDLE); + } + + @Override + protected void onEvtThink() { + if (_thinking && (getIntention() != AI_INTENTION_CAST)) { + return; + } + + _thinking = true; + try { + if (getIntention() == AI_INTENTION_ATTACK) { + thinkAttack(); + } else if (getIntention() == AI_INTENTION_CAST) { + thinkCast(); + } else if (getIntention() == AI_INTENTION_PICK_UP) { + thinkPickUp(); + } else if (getIntention() == AI_INTENTION_INTERACT) { + thinkInteract(); + } + } finally { + _thinking = false; + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2SiegeGuardAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2SiegeGuardAI.java new file mode 100644 index 00000000..0dabaf63 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2SiegeGuardAI.java @@ -0,0 +1,784 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; + +import java.util.Collection; +import java.util.concurrent.Future; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2NpcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.Util; + +/** + * Siege guard AI. + */ +public class L2SiegeGuardAI extends L2CharacterAI implements Runnable { + private static final int MAX_ATTACK_TIMEOUT = 30 * GameTimeController.TICKS_PER_SECOND; // 30s converted to ticks + + /** The L2Attackable AI task executed every 1s (call onEvtThink method) */ + private Future _aiTask; + + /** For attack AI, analysis of mob and its targets */ + private final SelfAnalysis _selfAnalysis = new SelfAnalysis(); + // private TargetAnalysis _mostHatedAnalysis = new TargetAnalysis(); + + /** The delay after which the attacked is stopped */ + private int _attackTimeout; + + /** The L2Attackable aggro counter */ + private int _globalAggro; + + /** The flag used to indicate that a thinking action is in progress */ + private boolean _thinking; // to prevent recursive thinking + + private final int _attackRange; + + /** + * Constructor of L2AttackableAI. + * @param creature the creature + */ + public L2SiegeGuardAI(L2DefenderInstance creature) { + super(creature); + _selfAnalysis.init(); + _attackTimeout = Integer.MAX_VALUE; + _globalAggro = -10; // 10 seconds timeout of ATTACK after respawn + _attackRange = _actor.getPhysicalAttackRange(); + } + + @Override + public void run() { + // Launch actions corresponding to the Event Think + onEvtThink(); + + } + + /** + * Actor is a L2GuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    • The L2MonsterInstance target is aggressive
    • + *
    + * Actor is a L2SiegeGuardInstance : + *
      + *
    • The target isn't a Folk or a Door
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • A siege is in progress
    • + *
    • The L2PcInstance target isn't a Defender
    • + *
    + * Actor is a L2FriendlyMobInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2NpcInstance
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The L2PcInstance target has karma (=PK)
    • + *
    + * Actor is a L2MonsterInstance : + *
      + *
    • The target isn't a Folk, a Door or another L2NpcInstance
    • + *
    • The target isn't dead, isn't invulnerable, isn't in silent moving mode AND too far (>100)
    • + *
    • The target is in the actor Aggro range and is at the same height
    • + *
    • The actor is Aggressive
    • + *
    + * @param target The targeted L2Object + * @return True if the target is autoattackable (depends on the actor type). + */ + protected boolean autoAttackCondition(L2Character target) { + // Check if the target isn't another guard, folk or a door + if ((target == null) || (target instanceof L2DefenderInstance) || (target instanceof L2NpcInstance) || (target instanceof L2DoorInstance) || target.isAlikeDead()) { + return false; + } + + // Check if the target isn't invulnerable + if (target.isInvul()) { + // However EffectInvincible requires to check GMs specially + if (target.isPlayer() && target.isGM()) { + return false; + } + if (target.isSummon() && ((L2Summon) target).getOwner().isGM()) { + return false; + } + } + + // Get the owner if the target is a summon + if (target instanceof L2Summon summon) { + L2PcInstance owner = summon.getOwner(); + if (_actor.isInsideRadius(owner, 1000, true, false)) { + target = owner; + } + } + + // Check if the target is a L2PcInstance + if (target instanceof L2Playable playable) { + // Check if the target isn't in silent move mode AND too far (>100) + if (playable.isSilentMovingAffected() && !_actor.isInsideRadius(target, 250, false, false)) { + return false; + } + } + // Los Check Here + return (_actor.isAutoAttackable(target) && GeoData.getInstance().canSeeTarget(_actor, target)); + + } + + /** + * Set the Intention of this L2CharacterAI and create an AI Task executed every 1s (call onEvtThink method) for this L2Attackable.
    + * Caution : If actor _knowPlayer isn't EMPTY, AI_INTENTION_IDLE will be change in AI_INTENTION_ACTIVE + * @param intention The new Intention to set to the AI + * @param arg0 The first parameter of the Intention + * @param arg1 The second parameter of the Intention + */ + @Override + synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + LOG.debug("{}: changeIntention({}, {}, {})", getClass().getSimpleName(), intention, arg0, arg1); + + if (intention == AI_INTENTION_IDLE /* || intention == AI_INTENTION_ACTIVE */) // active becomes idle if only a summon is present + { + // Check if actor is not dead + if (!_actor.isAlikeDead()) { + L2Attackable npc = (L2Attackable) _actor; + + // If its _knownPlayer isn't empty set the Intention to AI_INTENTION_ACTIVE + if (!npc.getKnownList().getKnownPlayers().isEmpty()) { + intention = AI_INTENTION_ACTIVE; + } else { + intention = AI_INTENTION_IDLE; + } + } + + if (intention == AI_INTENTION_IDLE) { + // Set the Intention of this L2AttackableAI to AI_INTENTION_IDLE + super.changeIntention(AI_INTENTION_IDLE, null, null); + + // Stop AI task and detach AI from NPC + if (_aiTask != null) { + _aiTask.cancel(true); + _aiTask = null; + } + + // Cancel the AI + _actor.detachAI(); + + return; + } + } + + // Set the Intention of this L2AttackableAI to intention + super.changeIntention(intention, arg0, arg1); + + // If not idle - create an AI task (schedule onEvtThink repeatedly) + if ((_aiTask == null) && (_actor.isInActiveRegion())) { + _aiTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(this, 1000, 1000); + } + } + + /** + * Manage the Attack Intention : Stop current Attack (if necessary), Calculate attack timeout, Start a new Attack and Launch Think Event. + * @param target The L2Character to attack + */ + @Override + protected void onIntentionAttack(L2Character target) { + // Calculate the attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // Manage the Attack Intention : Stop current Attack (if necessary), Start a new Attack and Launch Think Event + // if (_actor.getTarget() != null) + super.onIntentionAttack(target); + } + + /** + * Manage AI standard thinks of a L2Attackable (called by onEvtThink).
    + * Actions : + *
      + *
    • Update every 1s the _globalAggro counter to come close to 0
    • + *
    • If the actor is Aggressive and can attack, add all autoAttackable L2Character in its Aggro Range to its _aggroList, chose a target and order to attack it
    • + *
    • If the actor can't attack, order to it to return to its home location
    • + *
    + */ + private void thinkActive() { + L2Attackable npc = (L2Attackable) _actor; + + // Update every 1s the _globalAggro counter to come close to 0 + if (_globalAggro != 0) { + if (_globalAggro < 0) { + _globalAggro++; + } else { + _globalAggro--; + } + } + + // Add all autoAttackable L2Character in L2Attackable Aggro Range to its _aggroList with 0 damage and 1 hate + // A L2Attackable isn't aggressive during 10s after its spawn because _globalAggro is set to -10 + if (_globalAggro >= 0) { + for (L2Character target : npc.getKnownList().getKnownCharactersInRadius(_attackRange)) { + if (target == null) { + continue; + } + if (autoAttackCondition(target)) // check aggression + { + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + long hating = npc.getHating(target); + + // Add the attacker to the L2Attackable _aggroList with 0 damage and 1 hate + if (hating == 0) { + npc.addDamageHate(target, 0, 1); + } + } + } + + // Chose a target from its aggroList + L2Character hated; + if (_actor.isConfused()) { + hated = getAttackTarget(); // Force mobs to attack anybody if confused + } else { + hated = npc.getMostHated(); + // _mostHatedAnalysis.Update(hated); + } + + // Order to the L2Attackable to attack the target + if (hated != null) { + // Get the hate level of the L2Attackable against this L2Character target contained in _aggroList + long aggro = npc.getHating(hated); + + if ((aggro + _globalAggro) > 0) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + // Set the AI Intention to AI_INTENTION_ATTACK + setIntention(CtrlIntention.AI_INTENTION_ATTACK, hated, null); + } + + return; + } + + } + // Order to the L2DefenderInstance to return to its home location because there's no target to attack + ((L2DefenderInstance) _actor).returnHome(); + } + + /** + * Manage AI attack thinks of a L2Attackable (called by onEvtThink).
    + * Actions : + *
      + *
    • Update the attack timeout if actor is running
    • + *
    • If target is dead or timeout is expired, stop this attack and set the Intention to AI_INTENTION_ACTIVE
    • + *
    • Call all L2Object of its Faction inside the Faction Range
    • + *
    • Chose a target and order to attack it with magic skill or physical attack
    • + *
    + * TODO: Manage casting rules to healer mobs (like Ant Nurses) + */ + private void thinkAttack() { + LOG.debug("{}: thinkAttack(); timeout={}", getClass().getSimpleName(), (_attackTimeout - GameTimeController.getInstance().getGameTicks())); + + if (_attackTimeout < GameTimeController.getInstance().getGameTicks()) { + // Check if the actor is running + if (_actor.isRunning()) { + // Set the actor movement type to walk and send Server->Client packet ChangeMoveType to all others L2PcInstance + _actor.setWalking(); + + // Calculate a new attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + } + } + + L2Character attackTarget = getAttackTarget(); + // Check if target is dead or if timeout is expired to stop this attack + if ((attackTarget == null) || attackTarget.isAlikeDead() || (_attackTimeout < GameTimeController.getInstance().getGameTicks())) { + // Stop hating this target after the attack timeout or if target is dead + if (attackTarget != null) { + L2Attackable npc = (L2Attackable) _actor; + npc.stopHating(attackTarget); + } + + // Cancel target and timeout + _attackTimeout = Integer.MAX_VALUE; + setAttackTarget(null); + + // Set the AI Intention to AI_INTENTION_ACTIVE + setIntention(AI_INTENTION_ACTIVE, null, null); + + _actor.setWalking(); + return; + } + + factionNotifyAndSupport(); + attackPrepare(); + } + + private void factionNotifyAndSupport() { + L2Character target = getAttackTarget(); + final var siegeGuard = (L2Npc) _actor; + // Call all L2Object of its Faction inside the Faction Range + if ((siegeGuard.getTemplate().getClans() == null) || (target == null)) { + return; + } + + if (target.isInvul()) { + return; // speeding it up for siege guards + } + + // Go through all L2Character that belong to its faction + for (var cha : _actor.getKnownList().getKnownCharactersInRadius(1000)) { + if (cha == null) { + continue; + } + + if (!(cha instanceof L2Npc npc)) { + if (_selfAnalysis.hasHealOrResurrect && (cha instanceof L2PcInstance) && (siegeGuard.getCastle().getSiege().checkIsDefender(cha.getClan()))) { + // heal friends + if (!_actor.isAttackingDisabled() && (cha.getCurrentHp() < (cha.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2.0)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2.0)) && cha.isInCombat()) { + for (Skill sk : _selfAnalysis.healSkills) { + if (_actor.getCurrentMp() < sk.getMpConsume2()) { + continue; + } + if (_actor.isSkillDisabled(sk)) { + continue; + } + if (!Util.checkIfInRange(sk.getCastRange(), _actor, cha, true)) { + continue; + } + + int chance = 5; + if (chance >= Rnd.get(100)) { + continue; + } + if (!GeoData.getInstance().canSeeTarget(_actor, cha)) { + break; + } + + L2Object OldTarget = _actor.getTarget(); + _actor.setTarget(cha); + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + } + continue; + } + + if (!npc.isInMyClan(siegeGuard)) { + continue; + } + + if (npc.getAI() != null) // TODO: possibly check not needed + { + if (!npc.isDead() && (Math.abs(target.getZ() - npc.getZ()) < 600) + // && _actor.getAttackByList().contains(getAttackTarget()) + && ((npc.getAI()._intention == CtrlIntention.AI_INTENTION_IDLE) || (npc.getAI()._intention == CtrlIntention.AI_INTENTION_ACTIVE)) + // limiting aggro for siege guards + && target.isInsideRadius(npc, 1500, true, false) && GeoData.getInstance().canSeeTarget(npc, target)) { + // Notify the L2Object AI with EVT_AGGRESSION + npc.getAI().notifyEvent(CtrlEvent.EVT_AGGRESSION, getAttackTarget(), 1); + return; + } + // heal friends + if (_selfAnalysis.hasHealOrResurrect && !_actor.isAttackingDisabled() && (npc.getCurrentHp() < (npc.getMaxHp() * 0.6)) && (_actor.getCurrentHp() > (_actor.getMaxHp() / 2.0)) && (_actor.getCurrentMp() > (_actor.getMaxMp() / 2.0)) && npc.isInCombat()) { + for (Skill sk : _selfAnalysis.healSkills) { + if (_actor.getCurrentMp() < sk.getMpConsume2()) { + continue; + } + if (_actor.isSkillDisabled(sk)) { + continue; + } + if (!Util.checkIfInRange(sk.getCastRange(), _actor, npc, true)) { + continue; + } + + int chance = 4; + if (chance >= Rnd.get(100)) { + continue; + } + if (!GeoData.getInstance().canSeeTarget(_actor, npc)) { + break; + } + + L2Object OldTarget = _actor.getTarget(); + _actor.setTarget(npc); + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + } + } + } + + private void attackPrepare() { + // Get all information needed to choose between physical or magical attack + Collection skills; + double dist_2; + int range; + L2DefenderInstance sGuard = (L2DefenderInstance) _actor; + L2Character attackTarget = getAttackTarget(); + + try { + _actor.setTarget(attackTarget); + skills = _actor.getAllSkills(); + dist_2 = _actor.calculateDistance(attackTarget, false, true); + range = _actor.getPhysicalAttackRange() + _actor.getTemplate().getCollisionRadius() + attackTarget.getTemplate().getCollisionRadius(); + if (attackTarget.isMoving()) { + range += 50; + } + } catch (NullPointerException e) { + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + + // never attack defenders + if (attackTarget instanceof L2PcInstance) { + if ((sGuard.getConquerableHall() == null) && sGuard.getCastle().getSiege().checkIsDefender(attackTarget.getClan())) { + // Cancel the target + sGuard.stopHating(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + } + + if (!GeoData.getInstance().canSeeTarget(_actor, attackTarget)) { + // Siege guards differ from normal mobs currently: + // If target cannot seen, don't attack any more + sGuard.stopHating(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + return; + } + + // Check if the actor isn't muted and if it is far from target + if (!_actor.isMuted() && (dist_2 > (range * range))) { + // check for long ranged skills and heal/buff skills + for (Skill sk : skills) { + int castRange = sk.getCastRange(); + + if ((dist_2 <= (castRange * castRange)) && (castRange > 70) && !_actor.isSkillDisabled(sk) && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume2(sk)) && !sk.isPassive()) { + + L2Object OldTarget = _actor.getTarget(); + if ((sk.isContinuous() && !sk.isDebuff()) || (sk.hasEffectType(L2EffectType.HP))) { + boolean useSkillSelf = true; + if ((sk.hasEffectType(L2EffectType.HP)) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5))) { + useSkillSelf = false; + break; + } + + if ((sk.isContinuous() && !sk.isDebuff()) && _actor.isAffectedBySkill(sk.getId())) { + useSkillSelf = false; + } + + if (useSkillSelf) { + _actor.setTarget(_actor); + } + } + + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + + // Check if the L2SiegeGuardInstance is attacking, knows the target and can't run + if (!(_actor.isAttackingNow()) && (_actor.getRunSpeed() == 0) && (_actor.getKnownList().knowsObject(attackTarget))) { + // Cancel the target + _actor.getKnownList().removeKnownObject(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + } else { + double dx = _actor.getX() - attackTarget.getX(); + double dy = _actor.getY() - attackTarget.getY(); + double dz = _actor.getZ() - attackTarget.getZ(); + double homeX = attackTarget.getX() - sGuard.getSpawn().getX(); + double homeY = attackTarget.getY() - sGuard.getSpawn().getY(); + + // Check if the L2SiegeGuardInstance isn't too far from it's home location + if ((((dx * dx) + (dy * dy)) > 10000) && (((homeX * homeX) + (homeY * homeY)) > 3240000) // 1800 * 1800 + && (_actor.getKnownList().knowsObject(attackTarget))) { + // Cancel the target + _actor.getKnownList().removeKnownObject(attackTarget); + _actor.setTarget(null); + setIntention(AI_INTENTION_IDLE, null, null); + } else + // Move the actor to Pawn server side AND client side by sending Server->Client packet MoveToPawn (broadcast) + { + // Temporary hack for preventing guards jumping off towers, + // before replacing this with effective geodata checks and AI modification + if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct + { + if (_selfAnalysis.isHealer) { + return; + } + if (_selfAnalysis.isMage) { + range = _selfAnalysis.maxCastRange - 50; + } + if (attackTarget.isMoving()) { + moveToPawn(attackTarget, range - 70); + } else { + moveToPawn(attackTarget, range); + } + } + } + } + } + // Else, if the actor is muted and far from target, just "move to pawn" + else if (_actor.isMuted() && (dist_2 > (range * range)) && !_selfAnalysis.isHealer) { + // Temporary hack for preventing guards jumping off towers, + // before replacing this with effective geodata checks and AI modification + double dz = _actor.getZ() - attackTarget.getZ(); + if ((dz * dz) < (170 * 170)) // normally 130 if guard z coordinates correct + { + if (_selfAnalysis.isMage) { + range = _selfAnalysis.maxCastRange - 50; + } + if (attackTarget.isMoving()) { + moveToPawn(attackTarget, range - 70); + } else { + moveToPawn(attackTarget, range); + } + } + } + // Else, if this is close enough to attack + else if (dist_2 <= (range * range)) { + // Force mobs to attack anybody if confused + L2Character hated; + if (_actor.isConfused()) { + hated = attackTarget; + } else { + hated = ((L2Attackable) _actor).getMostHated(); + } + + if (hated == null) { + setIntention(AI_INTENTION_ACTIVE, null, null); + return; + } + if (hated != attackTarget) { + attackTarget = hated; + } + + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // check for close combat skills && heal/buff skills + if (!_actor.isMuted() && (Rnd.nextInt(100) <= 5)) { + for (Skill sk : skills) { + int castRange = sk.getCastRange(); + + if (((castRange * castRange) >= dist_2) && !sk.isPassive() && (_actor.getCurrentMp() >= _actor.getStat().getMpConsume2(sk)) && !_actor.isSkillDisabled(sk)) { + L2Object OldTarget = _actor.getTarget(); + if ((sk.isContinuous() && !sk.isDebuff()) || (sk.hasEffectType(L2EffectType.HP))) { + boolean useSkillSelf = true; + if ((sk.hasEffectType(L2EffectType.HP)) && (_actor.getCurrentHp() > (int) (_actor.getMaxHp() / 1.5))) { + useSkillSelf = false; + break; + } + + if ((sk.isContinuous() && !sk.isDebuff()) && _actor.isAffectedBySkill(sk.getId())) { + useSkillSelf = false; + } + + if (useSkillSelf) { + _actor.setTarget(_actor); + } + } + + clientStopMoving(null); + _actor.doCast(sk); + _actor.setTarget(OldTarget); + return; + } + } + } + // Finally, do the physical attack itself + if (!_selfAnalysis.isHealer) { + _actor.doAttack(attackTarget); + } + } + } + + /** + * Manage AI thinking actions of a L2Attackable. + */ + @Override + protected void onEvtThink() { + // if(getIntention() != AI_INTENTION_IDLE && (!_actor.isVisible() || !_actor.hasAI() || !_actor.isKnownPlayers())) + // setIntention(AI_INTENTION_IDLE); + + // Check if the thinking action is already in progress + if (_thinking || _actor.isCastingNow() || _actor.isAllSkillsDisabled()) { + return; + } + + // Start thinking action + _thinking = true; + + try { + // Manage AI thinks of a L2Attackable + if (getIntention() == AI_INTENTION_ACTIVE) { + thinkActive(); + } else if (getIntention() == AI_INTENTION_ATTACK) { + thinkAttack(); + } + } finally { + // Stop thinking action + _thinking = false; + } + } + + /** + * Launch actions corresponding to the Event Attacked.
    + * Actions : + *
      + *
    • Init the attack : Calculate the attack timeout, Set the _globalAggro to 0, Add the attacker to the actor _aggroList
    • + *
    • Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance
    • + *
    • Set the Intention to AI_INTENTION_ATTACK
    • + *
    + * @param attacker The L2Character that attacks the actor + */ + @Override + protected void onEvtAttacked(L2Character attacker) { + // Calculate the attack timeout + _attackTimeout = MAX_ATTACK_TIMEOUT + GameTimeController.getInstance().getGameTicks(); + + // Set the _globalAggro to 0 to permit attack even just after spawn + if (_globalAggro < 0) { + _globalAggro = 0; + } + + // Add the attacker to the _aggroList of the actor + ((L2Attackable) _actor).addDamageHate(attacker, 0, 1); + + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + // Set the Intention to AI_INTENTION_ATTACK + if (getIntention() != AI_INTENTION_ATTACK) { + setIntention(CtrlIntention.AI_INTENTION_ATTACK, attacker, null); + } + + super.onEvtAttacked(attacker); + } + + /** + * Launch actions corresponding to the Event Aggression.
    + * Actions : + *
      + *
    • Add the target to the actor _aggroList or update hate if already present
    • + *
    • Set the actor Intention to AI_INTENTION_ATTACK (if actor is L2GuardInstance check if it isn't too far from its home location)
    • + *
    + * @param aggro The value of hate to add to the actor against the target + */ + @Override + protected void onEvtAggression(L2Character target, long aggro) { + if (_actor == null) { + return; + } + L2Attackable me = (L2Attackable) _actor; + + if (target != null) { + // Add the target to the actor _aggroList or update hate if already present + me.addDamageHate(target, 0, aggro); + + // Get the hate of the actor against the target + aggro = me.getHating(target); + + if (aggro <= 0) { + if (me.getMostHated() == null) { + _globalAggro = -25; + me.clearAggroList(); + setIntention(AI_INTENTION_IDLE, null, null); + } + return; + } + + // Set the actor AI Intention to AI_INTENTION_ATTACK + if (getIntention() != CtrlIntention.AI_INTENTION_ATTACK) { + // Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance + if (!_actor.isRunning()) { + _actor.setRunning(); + } + + L2DefenderInstance sGuard = (L2DefenderInstance) _actor; + double homeX = target.getX() - sGuard.getSpawn().getX(); + double homeY = target.getY() - sGuard.getSpawn().getY(); + + // Check if the L2SiegeGuardInstance is not too far from its home location + if (((homeX * homeX) + (homeY * homeY)) < 3240000) { + setIntention(CtrlIntention.AI_INTENTION_ATTACK, target, null); + } + } + } else { + // currently only for setting lower general aggro + if (aggro >= 0) { + return; + } + + L2Character mostHated = me.getMostHated(); + if (mostHated == null) { + _globalAggro = -25; + return; + } + + for (L2Character aggroed : me.getAggroList().keySet()) { + me.addDamageHate(aggroed, 0, aggro); + } + + aggro = me.getHating(mostHated); + if (aggro <= 0) { + _globalAggro = -25; + me.clearAggroList(); + setIntention(AI_INTENTION_IDLE, null, null); + } + } + } + + @Override + public void stopAITask() { + if (_aiTask != null) { + _aiTask.cancel(false); + _aiTask = null; + } + _actor.detachAI(); + super.stopAITask(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2SpecialSiegeGuardAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2SpecialSiegeGuardAI.java new file mode 100644 index 00000000..5831a3e8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2SpecialSiegeGuardAI.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; + +/** + * @author BiggBoss + */ +public final class L2SpecialSiegeGuardAI extends L2SiegeGuardAI { + private final List _allied = new ArrayList<>(); + + public L2SpecialSiegeGuardAI(L2DefenderInstance creature) { + super(creature); + } + + public List getAlly() { + return _allied; + } + + @Override + protected boolean autoAttackCondition(L2Character target) { + if (_allied.contains(target.getObjectId())) { + return false; + } + + return super.autoAttackCondition(target); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2SummonAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2SummonAI.java new file mode 100644 index 00000000..72d06745 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2SummonAI.java @@ -0,0 +1,243 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_FOLLOW; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; +import static com.l2jserver.gameserver.config.Configuration.geodata; + +import java.util.concurrent.Future; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.pathfinding.PathFinding; + +public class L2SummonAI extends L2PlayableAI implements Runnable { + private static final int AVOID_RADIUS = 70; + + private volatile boolean _thinking; // to prevent recursive thinking + private volatile boolean _startFollow = ((L2Summon) _actor).getFollowStatus(); + private L2Character _lastAttack = null; + + private volatile boolean _startAvoid = false; + private Future _avoidTask = null; + + public L2SummonAI(L2Summon creature) { + super(creature); + } + + @Override + protected void onIntentionAttack(L2Character target) { + if ((geodata().getPathFinding() > 0) && // + (PathFinding.getInstance().findPath(_actor.getX(), _actor.getY(), _actor.getZ(), target.getX(), target.getY(), target.getZ(), _actor.getInstanceId(), true) == null)) { + return; + } + super.onIntentionAttack(target); + } + + @Override + protected void onIntentionIdle() { + stopFollow(); + _startFollow = false; + onIntentionActive(); + } + + @Override + protected void onIntentionActive() { + L2Summon summon = (L2Summon) _actor; + if (_startFollow) { + setIntention(AI_INTENTION_FOLLOW, summon.getOwner()); + } else { + super.onIntentionActive(); + } + } + + @Override + synchronized void changeIntention(CtrlIntention intention, Object arg0, Object arg1) { + switch (intention) { + case AI_INTENTION_ACTIVE, AI_INTENTION_FOLLOW -> startAvoidTask(); + default -> stopAvoidTask(); + } + super.changeIntention(intention, arg0, arg1); + } + + private void thinkAttack() { + if (checkTargetLostOrDead(getAttackTarget())) { + setAttackTarget(null); + return; + } + if (maybeMoveToPawn(getAttackTarget(), _actor.getPhysicalAttackRange())) { + return; + } + clientStopMoving(null); + _actor.doAttack(getAttackTarget()); + } + + private void thinkCast() { + L2Summon summon = (L2Summon) _actor; + if (checkTargetLost(getCastTarget())) { + setCastTarget(null); + return; + } + boolean val = _startFollow; + if (maybeMoveToPawn(getCastTarget(), _actor.getMagicalAttackRange(_skill))) { + return; + } + clientStopMoving(null); + summon.setFollowStatus(false); + setIntention(AI_INTENTION_IDLE); + _startFollow = val; + _actor.doCast(_skill); + } + + private void thinkPickUp() { + if (checkTargetLost(getTarget())) { + return; + } + if (maybeMoveToPawn(getTarget(), 36)) { + return; + } + setIntention(AI_INTENTION_IDLE); + ((L2Summon) _actor).doPickupItem(getTarget()); + } + + private void thinkInteract() { + if (checkTargetLost(getTarget())) { + return; + } + if (maybeMoveToPawn(getTarget(), 36)) { + return; + } + setIntention(AI_INTENTION_IDLE); + } + + @Override + protected void onEvtThink() { + if (_thinking || _actor.isCastingNow() || _actor.isAllSkillsDisabled()) { + return; + } + _thinking = true; + try { + switch (getIntention()) { + case AI_INTENTION_ATTACK -> thinkAttack(); + case AI_INTENTION_CAST -> thinkCast(); + case AI_INTENTION_PICK_UP -> thinkPickUp(); + case AI_INTENTION_INTERACT -> thinkInteract(); + } + } finally { + _thinking = false; + } + } + + @Override + protected void onEvtFinishCasting() { + if (_lastAttack == null) { + ((L2Summon) _actor).setFollowStatus(_startFollow); + } else { + setIntention(CtrlIntention.AI_INTENTION_ATTACK, _lastAttack); + _lastAttack = null; + } + } + + @Override + protected void onEvtAttacked(L2Character attacker) { + super.onEvtAttacked(attacker); + + avoidAttack(attacker); + } + + @Override + protected void onEvtEvaded(L2Character attacker) { + super.onEvtEvaded(attacker); + + avoidAttack(attacker); + } + + private void avoidAttack(L2Character attacker) { + // trying to avoid if summon near owner + if ((((L2Summon) _actor).getOwner() != null) && (((L2Summon) _actor).getOwner() != attacker) && ((L2Summon) _actor).getOwner().isInsideRadius(_actor, 2 * AVOID_RADIUS, true, false)) { + _startAvoid = true; + } + } + + @Override + public void run() { + if (_startAvoid) { + _startAvoid = false; + + if (!_clientMoving && !_actor.isDead() && !_actor.isMovementDisabled()) { + final int ownerX = ((L2Summon) _actor).getOwner().getX(); + final int ownerY = ((L2Summon) _actor).getOwner().getY(); + final double angle = Math.toRadians(Rnd.get(-90, 90)) + Math.atan2(ownerY - _actor.getY(), ownerX - _actor.getX()); + + final int targetX = ownerX + (int) (AVOID_RADIUS * Math.cos(angle)); + final int targetY = ownerY + (int) (AVOID_RADIUS * Math.sin(angle)); + if (GeoData.getInstance().canMove(_actor.getX(), _actor.getY(), _actor.getZ(), targetX, targetY, _actor.getZ(), _actor.getInstanceId())) { + moveTo(targetX, targetY, _actor.getZ()); + } + } + } + } + + public void notifyFollowStatusChange() { + _startFollow = !_startFollow; + switch (getIntention()) { + case AI_INTENTION_ACTIVE, AI_INTENTION_FOLLOW, AI_INTENTION_IDLE, AI_INTENTION_MOVE_TO, AI_INTENTION_PICK_UP -> ((L2Summon) _actor).setFollowStatus(_startFollow); + } + } + + public void setStartFollowController(boolean val) { + _startFollow = val; + } + + @Override + protected void onIntentionCast(Skill skill, L2Object target) { + if (getIntention() == AI_INTENTION_ATTACK) { + _lastAttack = getAttackTarget(); + } else { + _lastAttack = null; + } + super.onIntentionCast(skill, target); + } + + private void startAvoidTask() { + if (_avoidTask == null) { + _avoidTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(this, 100, 100); + } + } + + private void stopAvoidTask() { + if (_avoidTask != null) { + _avoidTask.cancel(false); + _avoidTask = null; + } + } + + @Override + public void stopAITask() { + stopAvoidTask(); + super.stopAITask(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/ai/L2VehicleAI.java b/src/main/java/com/l2jserver/gameserver/ai/L2VehicleAI.java new file mode 100644 index 00000000..c98ffa8c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/L2VehicleAI.java @@ -0,0 +1,106 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Vehicle; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * @author DS + */ +public abstract class L2VehicleAI extends L2CharacterAI { + + public L2VehicleAI(L2Vehicle creature) { + super(creature); + } + + @Override + protected void onIntentionAttack(L2Character target) { + } + + @Override + protected void onIntentionCast(Skill skill, L2Object target) { + } + + @Override + protected void onIntentionFollow(L2Character target) { + } + + @Override + protected void onIntentionPickUp(L2Object item) { + } + + @Override + protected void onIntentionInteract(L2Object object) { + } + + @Override + protected void onEvtAttacked(L2Character attacker) { + } + + @Override + protected void onEvtAggression(L2Character target, long aggro) { + } + + @Override + protected void onEvtStunned(L2Character attacker) { + } + + @Override + protected void onEvtSleeping(L2Character attacker) { + } + + @Override + protected void onEvtRooted(L2Character attacker) { + } + + @Override + protected void onEvtForgetObject(L2Object object) { + } + + @Override + protected void onEvtCancel() { + } + + @Override + protected void onEvtDead() { + } + + @Override + protected void onEvtFakeDeath() { + } + + @Override + protected void onEvtFinishCasting() { + } + + @Override + protected void clientActionFailed() { + } + + @Override + protected void moveToPawn(L2Object pawn, int offset) { + } + + @Override + protected void clientStoppedMoving() { + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/ai/NextAction.java b/src/main/java/com/l2jserver/gameserver/ai/NextAction.java new file mode 100644 index 00000000..d6d88ed6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/ai/NextAction.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.ai; + +import java.util.List; + +/** + * Class for AI action after some event.
    + * Has 2 lists for "work" and "break". + * @author Yaroslav + * @author Zoey76 + */ +public class NextAction { + public interface NextActionCallback { + void doWork(); + } + + private final List _events; + private final List _intentions; + private final NextActionCallback _callback; + + public NextAction(CtrlEvent event, CtrlIntention intention, NextActionCallback callback) { + _events = List.of(event); + _intentions = List.of(intention); + _callback = callback; + } + + public void doAction() { + _callback.doWork(); + } + + public List getEvents() { + return _events; + } + + public List getIntentions() { + return _intentions; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/Forum.java b/src/main/java/com/l2jserver/gameserver/bbs/model/Forum.java new file mode 100644 index 00000000..96ffaaed --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/Forum.java @@ -0,0 +1,134 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Forum. + * @author Zoey76 + * @version 2.6.2.0 + */ +public final class Forum { + + private int id; + private String name; + private ForumType type; + private int post; + private ForumVisibility visibility; + private final Forum parent; + private int ownerId; + private final Map children = new ConcurrentHashMap<>(); + private final Map topics = new ConcurrentHashMap<>(); + + public Forum(int id, String name, Forum parent, ForumType type, ForumVisibility visibility, int ownerId) { + this.name = name; + this.id = id; + this.type = type; + post = 0; + this.visibility = visibility; + this.parent = parent; + this.ownerId = ownerId; + } + + public int getTopicSize() { + return topics.size(); + } + + public Topic getTopic(int topicId) { + return topics.get(topicId); + } + + public void addTopic(Topic t) { + topics.put(t.getId(), t); + } + + public void removeTopic(int topicId) { + topics.remove(topicId); + } + + public Forum getChildByName(String name) { + return children.get(name); + } + + public void addChild(Forum child) { + children.put(child.getName(), child); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ForumType getType() { + return type; + } + + public void setType(ForumType type) { + this.type = type; + } + + public int getPost() { + return post; + } + + public void setPost(int post) { + this.post = post; + } + + public ForumVisibility getVisibility() { + return visibility; + } + + public void setVisibility(ForumVisibility visibility) { + this.visibility = visibility; + } + + public int getOwnerId() { + return ownerId; + } + + public void setOwnerId(int ownerId) { + this.ownerId = ownerId; + } + + public Forum getParent() { + return parent; + } + + public Map getChildren() { + return children; + } + + public Map getTopics() { + return topics; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/ForumType.java b/src/main/java/com/l2jserver/gameserver/bbs/model/ForumType.java new file mode 100644 index 00000000..17c155d8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/ForumType.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +/** + * Forum Type. + * @author Zoey76 + * @version 2.6.2.0 + */ +public enum ForumType { + ROOT, + NORMAL, + CLAN, + MEMO, + MAIL +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/ForumVisibility.java b/src/main/java/com/l2jserver/gameserver/bbs/model/ForumVisibility.java new file mode 100644 index 00000000..1f70c2fa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/ForumVisibility.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +/** + * Forum Visibility. + * @author Zoey76 + * @version 2.6.2.0 + */ +public enum ForumVisibility { + INVISIBLE, + ALL, + CLAN_MEMBER_ONLY, + OWNER_ONLY +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/Post.java b/src/main/java/com/l2jserver/gameserver/bbs/model/Post.java new file mode 100644 index 00000000..82252690 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/Post.java @@ -0,0 +1,107 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +/** + * Post. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class Post { + + private int id; + + private String ownerName; + + private int ownerId; + + private long date; + + private int topicId; + + private int forumId; + + private String txt; + + public Post(int id, String ownerName, int ownerId, long date, int topicId, int forumId, String txt) { + this.id = id; + this.ownerName = ownerName; + this.ownerId = ownerId; + this.date = date; + this.topicId = topicId; + this.forumId = forumId; + this.txt = txt; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getOwnerName() { + return ownerName; + } + + public void setOwnerName(String ownerName) { + this.ownerName = ownerName; + } + + public int getOwnerId() { + return ownerId; + } + + public void setOwnerId(int ownerId) { + this.ownerId = ownerId; + } + + public long getDate() { + return date; + } + + public void setDate(long date) { + this.date = date; + } + + public int getTopicId() { + return topicId; + } + + public void setTopicId(int topicId) { + this.topicId = topicId; + } + + public int getForumId() { + return forumId; + } + + public void setForumId(int forumId) { + this.forumId = forumId; + } + + public String getTxt() { + return txt; + } + + public void setTxt(String txt) { + this.txt = txt; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/Topic.java b/src/main/java/com/l2jserver/gameserver/bbs/model/Topic.java new file mode 100644 index 00000000..5cd41ae9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/Topic.java @@ -0,0 +1,86 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +/** + * Topic. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class Topic { + + private final int id; + + private final int forumId; + + private final String name; + + private final long date; + + private final String ownerName; + + private final int ownerId; + + private final TopicType type; + + private final int reply; + + public Topic(int id, int forumId, String name, long date, String ownerName, int ownerId, TopicType type, int reply) { + this.id = id; + this.forumId = forumId; + this.name = name; + this.date = date; + this.ownerName = ownerName; + this.ownerId = ownerId; + this.type = type; + this.reply = reply; + } + + public int getId() { + return id; + } + + public int getForumId() { + return forumId; + } + + public String getName() { + return name; + } + + public long getDate() { + return date; + } + + public String getOwnerName() { + return ownerName; + } + + public int getOwnerId() { + return ownerId; + } + + public TopicType getType() { + return type; + } + + public int getReply() { + return reply; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/model/TopicType.java b/src/main/java/com/l2jserver/gameserver/bbs/model/TopicType.java new file mode 100644 index 00000000..6ce33dd7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/model/TopicType.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.model; + +/** + * TopicType. + * @author Zoey76 + * @version 2.6.2.0 + */ +public enum TopicType { + NORMAL, + MEMO +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/ForumRepository.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/ForumRepository.java new file mode 100644 index 00000000..665ec9e7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/ForumRepository.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository; + +import java.util.Map; + +import com.l2jserver.gameserver.bbs.model.Forum; + +/** + * Forum repository. + * @author Zoey76 + * @version 2.6.2.0 + */ +public interface ForumRepository { + + Map getForums(); + + void save(Forum forum); +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/PostRepository.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/PostRepository.java new file mode 100644 index 00000000..df65b070 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/PostRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository; + +import java.util.List; + +import com.l2jserver.gameserver.bbs.model.Post; +import com.l2jserver.gameserver.bbs.model.Topic; + +/** + * Post repository. + * @author Zoey76 + * @version 2.6.2.0 + */ +public interface PostRepository { + + void delete(Topic topic); + + void update(Post post); + + List load(Topic topic); +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/TopicRepository.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/TopicRepository.java new file mode 100644 index 00000000..c3c8628d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/TopicRepository.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository; + +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.Topic; + +/** + * Topic repository. + * @author Zoey76 + * @version 2.6.2.0 + */ +public interface TopicRepository { + + void load(Forum forum); + + void save(Topic topic); + + void delete(Topic topic, Forum forum); +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/ForumRepositoryMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/ForumRepositoryMySQLImpl.java new file mode 100644 index 00000000..d2e24991 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/ForumRepositoryMySQLImpl.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository.impl; + +import static java.sql.Statement.RETURN_GENERATED_KEYS; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.ForumType; +import com.l2jserver.gameserver.bbs.model.ForumVisibility; +import com.l2jserver.gameserver.bbs.repository.ForumRepository; +import com.l2jserver.gameserver.bbs.service.ForumsBBSManager; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; + +/** + * Forum repository MySQL implementation. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ForumRepositoryMySQLImpl implements ForumRepository { + + private static final Logger LOG = LoggerFactory.getLogger(ForumRepositoryMySQLImpl.class); + + private static final String SELECT_FORUMS = "SELECT forum_id, forum_name, forum_post, forum_type, forum_perm, forum_owner_id FROM forums WHERE forum_type = 0"; + + private static final String SELECT_FORUM_CHILDREN = "SELECT forum_id, forum_name, forum_post, forum_type, forum_perm, forum_owner_id FROM forums WHERE forum_parent=?"; + + private static final String INSERT_FORUM = "INSERT INTO forums (forum_name, forum_parent, forum_post, forum_type, forum_perm, forum_owner_id) VALUES (?,?,?,?,?,?)"; + + @Override + public Map getForums() { + final var forums = new HashMap(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(SELECT_FORUMS)) { + while (rs.next()) { + final var forum = new Forum(rs.getInt("forum_id"), // + rs.getString("forum_name"), // + null, // + ForumType.values()[rs.getInt("forum_type")], // + ForumVisibility.values()[rs.getInt("forum_perm")], // + rs.getInt("forum_owner_id")); + forums.put(forum.getName(), forum); + + // Load topic + DAOFactory.getInstance().getTopicRepository().load(forum); + + // Load children + loadChildren(forum); + } + } catch (Exception ex) { + LOG.warn("Data error on Forum (root)!", ex); + } + return forums; + } + + private void loadChildren(Forum parent) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_FORUM_CHILDREN)) { + ps.setInt(1, parent.getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + ForumsBBSManager.getInstance().load(rs.getInt("forum_id"), // + rs.getString("forum_name"), // + parent, // + ForumType.values()[rs.getInt("forum_type")], // + ForumVisibility.values()[rs.getInt("forum_perm")], // + rs.getInt("forum_owner_id")); + } + } + } catch (Exception ex) { + LOG.warn("Error loading child forums for forum Id {}!", parent.getId(), ex); + } + } + + @Override + public void save(Forum forum) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_FORUM, RETURN_GENERATED_KEYS)) { + ps.setString(1, forum.getName()); + ps.setInt(2, forum.getParent().getId()); + ps.setInt(3, forum.getPost()); + ps.setInt(4, forum.getType().ordinal()); + ps.setInt(5, forum.getVisibility().ordinal()); + ps.setInt(6, forum.getOwnerId()); + ps.executeUpdate(); + + try (var rs = ps.getGeneratedKeys()) { + if (rs.next()) { + forum.setId(rs.getInt(1)); + } + } + } catch (Exception ex) { + LOG.error("Error saving forum Id {} in database!", forum.getId(), ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/PostRepositoryMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/PostRepositoryMySQLImpl.java new file mode 100644 index 00000000..476c7432 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/PostRepositoryMySQLImpl.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository.impl; + +import java.util.LinkedList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.bbs.model.Post; +import com.l2jserver.gameserver.bbs.model.Topic; +import com.l2jserver.gameserver.bbs.repository.PostRepository; + +/** + * Post repository MySQL implementation. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class PostRepositoryMySQLImpl implements PostRepository { + + private static final Logger LOG = LoggerFactory.getLogger(PostRepositoryMySQLImpl.class); + + private static final String INSERT_POST = "INSERT INTO posts (post_id,post_owner_name,post_ownerid,post_date,post_topic_id,post_forum_id,post_txt) values (?,?,?,?,?,?,?)"; + + private static final String DELETE_POST = "DELETE FROM posts WHERE post_forum_id=? AND post_topic_id=?"; + + private static final String SELECT_POSTS = "SELECT * FROM posts WHERE post_forum_id=? AND post_topic_id=? ORDER BY post_id"; + + private static final String UPDATE_POST = "UPDATE posts SET post_txt=? WHERE post_id=? AND post_topic_id=? AND post_forum_id=?"; + + public void save(Post cp) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_POST)) { + ps.setInt(1, cp.getId()); + ps.setString(2, cp.getOwnerName()); + ps.setInt(3, cp.getOwnerId()); + ps.setLong(4, cp.getDate()); + ps.setInt(5, cp.getTopicId()); + ps.setInt(6, cp.getForumId()); + ps.setString(7, cp.getTxt()); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Could not save post Id {} in database!", cp.getId(), ex); + } + } + + @Override + public void delete(Topic topic) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_POST)) { + ps.setInt(1, topic.getForumId()); + ps.setInt(2, topic.getId()); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Unable to delete post for topic Id {} in forum Id {} from database!", topic.getForumId(), topic.getId(), ex); + } + } + + @Override + public List load(Topic topic) { + final var posts = new LinkedList(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_POSTS)) { + ps.setInt(1, topic.getForumId()); + ps.setInt(2, topic.getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final Post cp = new Post(rs.getInt("post_id"), rs.getString("post_owner_name"), rs.getInt("post_ownerid"), // + rs.getLong("post_date"), rs.getInt("post_topic_id"), rs.getInt("post_forum_id"), rs.getString("post_txt")); + posts.add(cp); + } + } + } catch (Exception ex) { + LOG.warn("Unable to get post from topic Id {} in forum Id {} from database!", topic.getForumId(), topic.getId(), ex); + } + return posts; + } + + @Override + public void update(Post cp) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_POST)) { + ps.setString(1, cp.getTxt()); + ps.setInt(2, cp.getId()); + ps.setInt(3, cp.getTopicId()); + ps.setInt(4, cp.getForumId()); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Unable to store post Id {} in database!", cp.getId(), ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/TopicRepositoryMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/TopicRepositoryMySQLImpl.java new file mode 100644 index 00000000..c5c11d40 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/repository/impl/TopicRepositoryMySQLImpl.java @@ -0,0 +1,102 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.repository.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.Topic; +import com.l2jserver.gameserver.bbs.model.TopicType; +import com.l2jserver.gameserver.bbs.repository.TopicRepository; +import com.l2jserver.gameserver.bbs.service.TopicBBSManager; + +/** + * Topic Repository MySQL implementation. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class TopicRepositoryMySQLImpl implements TopicRepository { + + private static final Logger LOG = LoggerFactory.getLogger(TopicRepositoryMySQLImpl.class); + + private static final String SELECT_TOPICS = "SELECT * FROM topic WHERE topic_forum_id=? ORDER BY topic_id DESC"; + + private static final String DELETE_TOPIC = "DELETE FROM topic WHERE topic_id=? AND topic_forum_id=?"; + + private static final String INSERT_TOPIC = "INSERT INTO topic (topic_id,topic_forum_id,topic_name,topic_date,topic_ownername,topic_ownerid,topic_type,topic_reply) values (?,?,?,?,?,?,?,?)"; + + @Override + public void load(Forum forum) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_TOPICS)) { + ps.setInt(1, forum.getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final var topic = new Topic(rs.getInt("topic_id"), // + rs.getInt("topic_forum_id"), rs.getString("topic_name"), rs.getLong("topic_date"), // + rs.getString("topic_ownername"), rs.getInt("topic_ownerid"), TopicType.values()[rs.getInt("topic_type")], // + rs.getInt("topic_reply")); + + TopicBBSManager.getInstance().addTopic(topic); + + forum.getTopics().put(topic.getId(), topic); + if (topic.getId() > TopicBBSManager.getInstance().getMaxId(forum)) { + TopicBBSManager.getInstance().setMaxId(topic.getId(), forum); + } + } + } + } catch (Exception ex) { + LOG.warn("Could not get from database topics for forum Id {}!", forum.getId(), ex); + } + } + + @Override + public void save(Topic topic) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_TOPIC)) { + ps.setInt(1, topic.getId()); + ps.setInt(2, topic.getForumId()); + ps.setString(3, topic.getName()); + ps.setLong(4, topic.getDate()); + ps.setString(5, topic.getOwnerName()); + ps.setInt(6, topic.getOwnerId()); + ps.setInt(7, topic.getType().ordinal()); + ps.setInt(8, topic.getReply()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error while saving new Topic to database!", e); + } + } + + @Override + public void delete(Topic topic, Forum forum) { + TopicBBSManager.getInstance().delTopic(topic); + forum.removeTopic(topic.getId()); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_TOPIC)) { + ps.setInt(1, topic.getId()); + ps.setInt(2, forum.getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error while deleting topic ID {} from database!", topic.getId(), e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/service/BaseBBSManager.java b/src/main/java/com/l2jserver/gameserver/bbs/service/BaseBBSManager.java new file mode 100644 index 00000000..4ea399d1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/service/BaseBBSManager.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.service; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.ShowBoard; + +public abstract class BaseBBSManager { + public abstract void parsecmd(String command, L2PcInstance activeChar); + + public abstract void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, L2PcInstance activeChar); + + protected void send1001(String html, L2PcInstance player) { + if (html.length() < 8192) { + player.sendPacket(new ShowBoard(html, "1001")); + } + } + + protected void send1002(L2PcInstance player) { + send1002(player, " ", " ", "0"); + } + + protected void send1002(L2PcInstance activeChar, String string, String string2, String string3) { + List arg = new ArrayList<>(); + arg.add("0"); + arg.add("0"); + arg.add("0"); + arg.add("0"); + arg.add("0"); + arg.add("0"); + arg.add(activeChar.getName()); + arg.add(Integer.toString(activeChar.getObjectId())); + arg.add(activeChar.getAccountName()); + arg.add("9"); + arg.add(string2); // subject? + arg.add(string2); // subject? + arg.add(string); // text + arg.add(string3); // date? + arg.add(string3); // date? + arg.add("0"); + arg.add("0"); + activeChar.sendPacket(new ShowBoard(arg)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/service/ForumsBBSManager.java b/src/main/java/com/l2jserver/gameserver/bbs/service/ForumsBBSManager.java new file mode 100644 index 00000000..a4da2552 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/service/ForumsBBSManager.java @@ -0,0 +1,107 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.service; + +import static com.l2jserver.gameserver.bbs.model.ForumType.CLAN; +import static com.l2jserver.gameserver.bbs.model.ForumVisibility.CLAN_MEMBER_ONLY; +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.ForumType; +import com.l2jserver.gameserver.bbs.model.ForumVisibility; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.model.L2Clan; + +/** + * Forums BBS Manager. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ForumsBBSManager { + + private static final Logger LOG = LoggerFactory.getLogger(ForumsBBSManager.class); + + private static final Map FORUMS_BY_NAME = new ConcurrentHashMap<>(); + + private static final Map FORUMS_BY_ID = new ConcurrentHashMap<>(); + + protected ForumsBBSManager() { + // Do nothing. + } + + public void load() { + FORUMS_BY_NAME.putAll(DAOFactory.getInstance().getForumRepository().getForums()); + FORUMS_BY_ID.putAll(FORUMS_BY_NAME.values().stream().collect(Collectors.toMap(Forum::getId, f -> f))); + LOG.info("Loaded {} forums.", FORUMS_BY_NAME.size()); + } + + public Forum getForumByName(String name) { + return FORUMS_BY_NAME.get(name); + } + + public Forum getForumById(Integer id) { + return FORUMS_BY_ID.get(id); + } + + public Forum create(String name, Forum parent, ForumType type, ForumVisibility visibility, int ownerId) { + final var forum = new Forum(0, name, parent, type, visibility, ownerId); + parent.addChild(forum); + + DAOFactory.getInstance().getForumRepository().save(forum); + + FORUMS_BY_NAME.put(forum.getName(), forum); + FORUMS_BY_ID.put(forum.getId(), forum); + return forum; + } + + public Forum load(int id, String name, Forum parent, ForumType type, ForumVisibility visibility, int ownerId) { + final var forum = new Forum(id, name, parent, type, visibility, ownerId); + parent.addChild(forum); + FORUMS_BY_NAME.put(forum.getName(), forum); + FORUMS_BY_ID.put(forum.getId(), forum); + return forum; + } + + public void onClanLevel(L2Clan clan) { + if ((clan.getLevel() >= 2) && general().enableCommunityBoard()) { + final var clanRootForum = ForumsBBSManager.getInstance().getForumByName("ClanRoot"); + if (clanRootForum != null) { + var forum = clanRootForum.getChildByName(clan.getName()); + if (forum == null) { + ForumsBBSManager.getInstance().create(clan.getName(), clanRootForum, CLAN, CLAN_MEMBER_ONLY, clan.getId()); + } + } + } + } + + public static ForumsBBSManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ForumsBBSManager INSTANCE = new ForumsBBSManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/bbs/service/PostBBSManager.java b/src/main/java/com/l2jserver/gameserver/bbs/service/PostBBSManager.java new file mode 100644 index 00000000..ca6f95da --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/service/PostBBSManager.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.service; + +import java.text.DateFormat; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.ForumType; +import com.l2jserver.gameserver.bbs.model.Post; +import com.l2jserver.gameserver.bbs.model.Topic; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.handler.CommunityBoardHandler; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Post BBS Manager. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class PostBBSManager extends BaseBBSManager { + + private final Map> _postByTopic = new ConcurrentHashMap<>(); + + protected PostBBSManager() { + // Do nothing. + } + + public List getGPostByTopic(Topic topic) { + var posts = _postByTopic.get(topic); + if (posts == null) { + posts = DAOFactory.getInstance().getPostRepository().load(topic); + _postByTopic.put(topic, posts); + } + return posts; + } + + public void delPostByTopic(Topic t) { + _postByTopic.remove(t); + } + + public void addPostByTopic(Topic topic, List posts) { + _postByTopic.putIfAbsent(topic, posts); + } + + @Override + public void parsecmd(String command, L2PcInstance activeChar) { + if (command.startsWith("_bbsposts;read;")) { + final var st = new StringTokenizer(command, ";"); + st.nextToken(); + st.nextToken(); + final var forumId = Integer.parseInt(st.nextToken()); + final var topicId = Integer.parseInt(st.nextToken()); + final var index = st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 1; + + final var forum = ForumsBBSManager.getInstance().getForumById(forumId); + final var topic = TopicBBSManager.getInstance().getTopicById(topicId); + showPost(topic, forum, activeChar, index); + } else if (command.startsWith("_bbsposts;edit;")) { + final var st = new StringTokenizer(command, ";"); + st.nextToken(); + st.nextToken(); + final var forumId = Integer.parseInt(st.nextToken()); + final var topicId = Integer.parseInt(st.nextToken()); + final var postId = Integer.parseInt(st.nextToken()); + + final var forum = ForumsBBSManager.getInstance().getForumById(forumId); + final var topic = TopicBBSManager.getInstance().getTopicById(topicId); + showEditPost(topic, forum, activeChar, postId); + } else { + CommunityBoardHandler.separateAndSend("

    the command: " + command + " is not implemented yet


    ", activeChar); + } + } + + private void showEditPost(Topic topic, Forum forum, L2PcInstance activeChar, int idp) { + if (topic == null) { + CommunityBoardHandler.separateAndSend("

    Error: This topic does not exist!
    ", activeChar); + } else { + final List p = getGPostByTopic(topic); + if ((forum == null) || (p == null)) { + CommunityBoardHandler.separateAndSend("

    Error: This forum or post does not exist!
    ", activeChar); + } else { + showHtmlEditPost(topic, activeChar, forum, p); + } + } + } + + private void showPost(Topic topic, Forum forum, L2PcInstance activeChar, int ind) { + if ((forum == null) || (topic == null)) { + CommunityBoardHandler.separateAndSend("

    Error: This forum is not implemented yet!
    ", activeChar); + } else if (forum.getType() == ForumType.MEMO) { + showMemoPost(topic, activeChar, forum); + } else { + CommunityBoardHandler.separateAndSend("

    The forum: " + forum.getName() + " is not implemented yet!
    ", activeChar); + } + } + + private void showHtmlEditPost(Topic topic, L2PcInstance activeChar, Forum forum, List posts) { + final String html = StringUtil.concat("

    HOME > ", forum + .getName(), " Form
    &$413;", topic + .getName(), "
    &$427;
      
    "); + send1001(html, activeChar); + send1002(activeChar, posts.get(0).getTxt(), topic.getName(), DateFormat.getInstance().format(new Date(topic.getDate()))); + } + + private void showMemoPost(Topic topic, L2PcInstance activeChar, Forum forum) { + final var posts = getGPostByTopic(topic); + Locale locale = Locale.getDefault(); + DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, locale); + + String mes = posts.get(0).getTxt().replace(">", ">"); + mes = mes.replace("<", "<"); + // TODO(Zoey76): Replace this with a template. + final String html = StringUtil + .concat("

    HOME > Memo Form
    &$413; :  ", topic + .getName(), "
    &$417; :  ", topic.getOwnerName() + + "&$418; :", dateFormat.format(posts.get(0) + .getDate()), "

    ", mes, "

       



    "); + CommunityBoardHandler.separateAndSend(html, activeChar); + } + + @Override + public void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, L2PcInstance activeChar) { + StringTokenizer st = new StringTokenizer(ar1, ";"); + int idf = Integer.parseInt(st.nextToken()); + int idt = Integer.parseInt(st.nextToken()); + int idp = Integer.parseInt(st.nextToken()); + + Forum f = ForumsBBSManager.getInstance().getForumById(idf); + if (f == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + idf + " does not exist !


    ", activeChar); + } else { + Topic t = f.getTopic(idt); + if (t == null) { + CommunityBoardHandler.separateAndSend("

    the topic: " + idt + " does not exist !


    ", activeChar); + } else { + final var posts = getGPostByTopic(t); + if (!posts.isEmpty()) { + final Post post = posts.get(idp); + if (post == null) { + CommunityBoardHandler.separateAndSend("

    the post: " + idp + " does not exist !


    ", activeChar); + } else { + post.setTxt(ar4); + DAOFactory.getInstance().getPostRepository().update(post); + parsecmd("_bbsposts;read;" + f.getId() + ";" + t.getId(), activeChar); + } + } + } + } + } + + public static PostBBSManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PostBBSManager INSTANCE = new PostBBSManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/bbs/service/TopicBBSManager.java b/src/main/java/com/l2jserver/gameserver/bbs/service/TopicBBSManager.java new file mode 100644 index 00000000..0c3cf554 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/bbs/service/TopicBBSManager.java @@ -0,0 +1,278 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.bbs.service; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.model.ForumType; +import com.l2jserver.gameserver.bbs.model.Post; +import com.l2jserver.gameserver.bbs.model.Topic; +import com.l2jserver.gameserver.bbs.model.TopicType; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.handler.CommunityBoardHandler; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Topic BBS Manager. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class TopicBBSManager extends BaseBBSManager { + + private final List table = new CopyOnWriteArrayList<>(); + + private final Map maxId = new HashMap<>(); + + protected TopicBBSManager() { + // Do nothing. + } + + public void addTopic(Topic topic) { + table.add(topic); + } + + public void delTopic(Topic topic) { + table.remove(topic); + } + + public void setMaxId(int id, Forum forum) { + maxId.put(forum, id); + } + + public int getMaxId(Forum f) { + var i = maxId.get(f); + if (i == null) { + return 0; + } + return i; + } + + public Topic getTopicById(int id) { + for (var topic : table) { + if (topic.getId() == id) { + return topic; + } + } + return null; + } + + @Override + public void parsewrite(String ar1, String ar2, String ar3, String ar4, String ar5, L2PcInstance activeChar) { + if (ar1.equals("crea")) { + final var forum = ForumsBBSManager.getInstance().getForumById(Integer.parseInt(ar2)); + if (forum == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + ar2 + " is not implemented yet


    ", activeChar); + return; + } + + final var id = TopicBBSManager.getInstance().getMaxId(forum) + 1; + final var date = Calendar.getInstance().getTimeInMillis(); + final var topic = new Topic(id, Integer.parseInt(ar2), ar5, date, activeChar.getName(), activeChar.getObjectId(), TopicType.MEMO, 0); + + TopicBBSManager.getInstance().addTopic(topic); + + DAOFactory.getInstance().getTopicRepository().save(topic); + + forum.addTopic(topic); + TopicBBSManager.getInstance().setMaxId(topic.getId(), forum); + + final var posts = new LinkedList(); + posts.add(new Post(0, activeChar.getName(), activeChar.getObjectId(), Calendar.getInstance().getTimeInMillis(), topic.getId(), forum.getId(), ar4)); + PostBBSManager.getInstance().addPostByTopic(topic, posts); + parsecmd("_bbsmemo", activeChar); + } else if (ar1.equals("del")) { + final var forum = ForumsBBSManager.getInstance().getForumById(Integer.parseInt(ar2)); + if (forum == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + ar2 + " does not exist !


    ", activeChar); + return; + } + + final var topic = forum.getTopic(Integer.parseInt(ar3)); + if (topic == null) { + CommunityBoardHandler.separateAndSend("

    the topic: " + ar3 + " does not exist !


    ", activeChar); + return; + } + + final var posts = PostBBSManager.getInstance().getGPostByTopic(topic); + if (!posts.isEmpty()) { + PostBBSManager.getInstance().delPostByTopic(topic); + DAOFactory.getInstance().getPostRepository().delete(topic); + } + + DAOFactory.getInstance().getTopicRepository().delete(topic, forum); + + parsecmd("_bbsmemo", activeChar); + } else { + CommunityBoardHandler.separateAndSend("

    the command: " + ar1 + " is not implemented yet


    ", activeChar); + } + } + + @Override + public void parsecmd(String command, L2PcInstance activeChar) { + if (command.equals("_bbsmemo")) { + showTopics(activeChar.getMemo(), activeChar, 1, activeChar.getMemo().getId()); + } else if (command.startsWith("_bbstopics;read")) { + StringTokenizer st = new StringTokenizer(command, ";"); + st.nextToken(); + st.nextToken(); + int idf = Integer.parseInt(st.nextToken()); + String index = null; + if (st.hasMoreTokens()) { + index = st.nextToken(); + } + int ind; + if (index == null) { + ind = 1; + } else { + ind = Integer.parseInt(index); + } + showTopics(ForumsBBSManager.getInstance().getForumById(idf), activeChar, ind, idf); + } else if (command.startsWith("_bbstopics;crea")) { + StringTokenizer st = new StringTokenizer(command, ";"); + st.nextToken(); + st.nextToken(); + int idf = Integer.parseInt(st.nextToken()); + showNewTopic(ForumsBBSManager.getInstance().getForumById(idf), activeChar, idf); + } else if (command.startsWith("_bbstopics;del")) { + StringTokenizer st = new StringTokenizer(command, ";"); + st.nextToken(); + st.nextToken(); + int idf = Integer.parseInt(st.nextToken()); + int idt = Integer.parseInt(st.nextToken()); + Forum f = ForumsBBSManager.getInstance().getForumById(idf); + if (f == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + idf + " does not exist !


    ", activeChar); + } else { + Topic t = f.getTopic(idt); + if (t == null) { + CommunityBoardHandler.separateAndSend("

    the topic: " + idt + " does not exist !


    ", activeChar); + } else { + final var p = PostBBSManager.getInstance().getGPostByTopic(t); + if (p != null) { + PostBBSManager.getInstance().delPostByTopic(t); + DAOFactory.getInstance().getPostRepository().delete(t); + } + + DAOFactory.getInstance().getTopicRepository().delete(t, f); + + parsecmd("_bbsmemo", activeChar); + } + } + } else { + CommunityBoardHandler.separateAndSend("

    the command: " + command + " is not implemented yet


    ", activeChar); + } + } + + private void showNewTopic(Forum forum, L2PcInstance activeChar, int idf) { + if (forum == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + idf + " is not implemented yet


    ", activeChar); + } else if (forum.getType() == ForumType.MEMO) { + showMemoNewTopics(forum, activeChar); + } else { + CommunityBoardHandler.separateAndSend("

    the forum: " + forum.getName() + " is not implemented yet


    ", activeChar); + } + } + + private void showMemoNewTopics(Forum forum, L2PcInstance activeChar) { + final String html = StringUtil + .concat("

    HOME > Memo Form
    &$413;
    &$427;
      
    "); + send1001(html, activeChar); + send1002(activeChar); + } + + private void showTopics(Forum forum, L2PcInstance activeChar, int index, int idf) { + if (forum == null) { + CommunityBoardHandler.separateAndSend("

    the forum: " + idf + " is not implemented yet


    ", activeChar); + } else if (forum.getType() == ForumType.MEMO) { + showMemoTopics(forum, activeChar, index); + } else { + CommunityBoardHandler.separateAndSend("

    the forum: " + forum.getName() + " is not implemented yet


    ", activeChar); + } + } + + private void showMemoTopics(Forum forum, L2PcInstance activeChar, int index) { + final StringBuilder html = StringUtil + .startAppend(2000, "

    HOME > Memo Form
    &$413;&$418;
    "); + final DateFormat dateFormat = DateFormat.getInstance(); + + for (int i = 0, j = getMaxId(forum) + 1; i < (12 * index); j--) { + if (j < 0) { + break; + } + Topic t = forum.getTopic(j); + if (t != null) { + if (i++ >= (12 * (index - 1))) { + StringUtil.append(html, "
    ", t + .getName(), "", dateFormat.format(new Date(t.getDate())), "
    "); + } + } + } + + html.append("
    "); + + if (index == 1) { + html.append(""); + } else { + StringUtil.append(html, ""); + } + + int nbp; + nbp = forum.getTopicSize() / 8; + if ((nbp * 8) != ClanTable.getInstance().getClanCount()) { + nbp++; + } + for (int i = 1; i <= nbp; i++) { + if (i == index) { + StringUtil.append(html, ""); + } else { + StringUtil.append(html, ""); + } + } + if (index == nbp) { + html.append(""); + } else { + StringUtil.append(html, ""); + } + + StringUtil.append(html, "
    ", String.valueOf(i), " ", String.valueOf(i), "



    "); + CommunityBoardHandler.separateAndSend(html.toString(), activeChar); + } + + public static TopicBBSManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final TopicBBSManager _instance = new TopicBBSManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/cache/HtmCache.java b/src/main/java/com/l2jserver/gameserver/cache/HtmCache.java new file mode 100644 index 00000000..d0a92d1b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/cache/HtmCache.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.cache; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.util.file.filter.HTMLFilter; + +/** + * HTML Cache. + * @author Layane + * @author Zoey76 + */ +public class HtmCache { + + private static final Logger LOG = LoggerFactory.getLogger(HtmCache.class); + + private static final HTMLFilter HTML_FILTER = new HTMLFilter(); + + private static final Map HTML_CACHE = general().lazyCache() ? new ConcurrentHashMap<>() : new HashMap<>(); + + private int _loadedFiles; + + private long _bytesBuffLen; + + protected HtmCache() { + reload(); + } + + public void reload() { + reload(server().getDatapackRoot()); + } + + public void reload(File f) { + if (!general().lazyCache()) { + LOG.info("Html cache start..."); + parseDir(f); + LOG.info(String.format("%.3f", getMemoryUsage()) + " megabytes on " + getLoadedFiles() + " files loaded."); + } else { + HTML_CACHE.clear(); + _loadedFiles = 0; + _bytesBuffLen = 0; + LOG.info("Running lazy cache."); + } + } + + public void reloadPath(File f) { + parseDir(f); + LOG.info("Reloaded specified path."); + } + + public double getMemoryUsage() { + return ((float) _bytesBuffLen / 1048576); + } + + public int getLoadedFiles() { + return _loadedFiles; + } + + private void parseDir(File dir) { + final File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (!file.isDirectory()) { + loadFile(file); + } else { + parseDir(file); + } + } + } + } + + public String loadFile(File file) { + if (!HTML_FILTER.accept(file)) { + return null; + } + + String content = null; + try (var fis = new FileInputStream(file); + var bis = new BufferedInputStream(fis)) { + final int bytes = bis.available(); + byte[] raw = new byte[bytes]; + + bis.read(raw); + content = new String(raw, UTF_8); + content = content.replaceAll("(?s)", ""); // Remove html comments + + String oldContent = HTML_CACHE.put(file.getCanonicalPath(), content); + if (oldContent == null) { + _bytesBuffLen += bytes; + _loadedFiles++; + } else { + _bytesBuffLen = (_bytesBuffLen - oldContent.length()) + bytes; + } + } catch (Exception e) { + LOG.warn("Problem with htm file {}!", file, e); + } + return content; + } + + public String getHtm(String prefix, String path) { + final var newPath = Objects.requireNonNullElse(prefix, "") + path; + var content = HTML_CACHE.get(newPath); + if (general().lazyCache() && (content == null)) { + content = loadFile(new File(server().getDatapackRoot(), newPath)); + if (content == null) { + content = loadFile(new File(server().getScriptRoot(), newPath)); + } + + // If multilanguage content is not present, try default location. + if ((prefix != null) && (content == null)) { + content = loadFile(new File(server().getDatapackRoot(), path)); + if (content == null) { + content = loadFile(new File(server().getScriptRoot(), path)); + } + } + } + return content; + } + + public boolean contains(String path) { + return HTML_CACHE.containsKey(path); + } + + /** + * @param path The path to the HTM + * @return {@code true} if the path targets a HTM or HTML file, {@code false} otherwise. + */ + public boolean isLoadable(String path) { + return HTML_FILTER.accept(new File(server().getDatapackRoot(), path)); + } + + public static HtmCache getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final HtmCache INSTANCE = new HtmCache(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/cache/WarehouseCacheManager.java b/src/main/java/com/l2jserver/gameserver/cache/WarehouseCacheManager.java new file mode 100644 index 00000000..a49d0f59 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/cache/WarehouseCacheManager.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.cache; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author -Nemesiss- + */ +public class WarehouseCacheManager { + + protected final Map _cachedWh = new ConcurrentHashMap<>(); + + protected WarehouseCacheManager() { + ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new CacheScheduler(), 120000, 60000); + } + + public void addCacheTask(L2PcInstance pc) { + _cachedWh.put(pc, System.currentTimeMillis()); + } + + public void remCacheTask(L2PcInstance pc) { + _cachedWh.remove(pc); + } + + public class CacheScheduler implements Runnable { + @Override + public void run() { + long cTime = System.currentTimeMillis(); + for (L2PcInstance pc : _cachedWh.keySet()) { + if ((cTime - _cachedWh.get(pc)) > general().getWarehouseCacheTime()) { + pc.clearWarehouse(); + _cachedWh.remove(pc); + } + } + } + } + + public static WarehouseCacheManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final WarehouseCacheManager _instance = new WarehouseCacheManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/BufferServiceConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/BufferServiceConfiguration.java new file mode 100644 index 00000000..d752b41e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/BufferServiceConfiguration.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Seconds2MillisecondsConverter; +import com.l2jserver.gameserver.model.zone.ZoneId; + +/** + * Buffer Service Configuration. + * @author HorridoJoho + * @version 2.6.2.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/bufferservice.properties", + "file:./config/bufferservice.properties", + "classpath:config/bufferservice.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface BufferServiceConfiguration extends Reloadable { + + @Key("Enable") + boolean enable(); + + @Key("HealCooldown") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getHealCooldown(); + + @Key("MaxUniqueLists") + int getMaxUniqueLists(); + + @Key("Debug") + boolean getDebug(); + + @Key("ForbidInZones") + ZoneId[] getForbidInZones(); + + @Key("ForbidInEvents") + boolean getForbidInEvents(); + + @Key("ForbidInDuel") + boolean getForbidInDuel(); + + @Key("ForbidInFight") + boolean getForbidInFight(); + + @Key("ForbidInPvp") + boolean getForbidInPvp(); + + @Key("ForbidForChaoticPlayers") + boolean getForbidForChaoticPlayers(); + + @Key("VoicedEnable") + boolean getVoicedEnable(); + + @Key("VoicedCommand") + String getVoicedCommand(); + + @Key("VoicedName") + String getVoicedName(); + + @Key("VoicedRequiredItem") + int getVoicedRequiredItem(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/CastleConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/CastleConfiguration.java new file mode 100644 index 00000000..5e53d463 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/CastleConfiguration.java @@ -0,0 +1,129 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.HOURS; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Castle Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/castle.properties", + "file:./config/castle.properties", + "classpath:config/castle.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 1, unit = HOURS, type = ASYNC) +public interface CastleConfiguration extends Reloadable { + + @Key("SiegeHourList") + List getSiegeHourList(); + + @Key("FunctionFeeDay7") + long getFunctionFeeDay7(); + + @Key("TeleportFunctionFeeLvl11") + int getTeleportFunctionFeeLvl11(); + + @Key("TeleportFunctionFeeLvl12") + int getTeleportFunctionFeeLvl12(); + + @Key("SupportFeeLvl15") + int getSupportFeeLvl15(); + + @Key("SupportFeeLvl18") + int getSupportFeeLvl18(); + + @Key("MpRegenerationFeeLvl18") + int getMpRegenerationFeeLvl18(); + + @Key("MpRegenerationFeeLvl20") + int getMpRegenerationFeeLvl20(); + + @Key("HpRegenerationFeeLvl25") + int getHpRegenerationFeeLvl25(); + + @Key("HpRegenerationFeeLvl30") + int getHpRegenerationFeeLvl30(); + + @Key("ExpRegenerationFeeLvl19") + int getExpRegenerationFeeLvl19(); + + @Key("ExpRegenerationFeeLvl20") + int getExpRegenerationFeeLvl20(); + + @Key("OuterDoorUpgradePriceLvl2") + int getOuterDoorUpgradePriceLvl2(); + + @Key("OuterDoorUpgradePriceLvl3") + int getOuterDoorUpgradePriceLvl3(); + + @Key("OuterDoorUpgradePriceLvl5") + int getOuterDoorUpgradePriceLvl5(); + + @Key("InnerDoorUpgradePriceLvl2") + int getInnerDoorUpgradePriceLvl2(); + + @Key("InnerDoorUpgradePriceLvl3") + int getInnerDoorUpgradePriceLvl3(); + + @Key("InnerDoorUpgradePriceLvl5") + int getInnerDoorUpgradePriceLvl5(); + + @Key("WallUpgradePriceLvl2") + int getWallUpgradePriceLvl2(); + + @Key("WallUpgradePriceLvl3") + int getWallUpgradePriceLvl3(); + + @Key("WallUpgradePriceLvl5") + int getWallUpgradePriceLvl5(); + + @Key("TrapUpgradePriceLvl1") + int getTrapUpgradePriceLvl1(); + + @Key("TrapUpgradePriceLvl2") + int getTrapUpgradePriceLvl2(); + + @Key("TrapUpgradePriceLvl3") + int getTrapUpgradePriceLvl3(); + + @Key("TrapUpgradePriceLvl4") + int getTrapUpgradePriceLvl4(); + + @Key("AllowRideWyvernAlways") + boolean allowRideWyvernAlways(); + + @Key("AllowRideWyvernDuringSiege") + boolean allowRideWyvernDuringSiege(); + + @Key("MpBuffFree") + boolean mpBuffFree(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/CharacterConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/CharacterConfiguration.java new file mode 100644 index 00000000..9c983f20 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/CharacterConfiguration.java @@ -0,0 +1,663 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.ClassMasterSetting; +import com.l2jserver.gameserver.config.converter.ClassMasterSettingConverter; +import com.l2jserver.gameserver.config.converter.Days2MillisecondsConverter; +import com.l2jserver.gameserver.config.converter.MapIntegerIntegerConverter; +import com.l2jserver.gameserver.config.converter.PatternConverter; +import com.l2jserver.gameserver.config.converter.Seconds2MillisecondsConverter; + +/** + * Character Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/character.properties", + "file:./config/character.properties", + "classpath:config/character.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 5, unit = MINUTES, type = ASYNC) +public interface CharacterConfiguration extends Reloadable { + + // Statistics + @Key("Delevel") + boolean delevel(); + + @Key("DecreaseSkillOnDelevel") + boolean decreaseSkillOnDelevel(); + + @Key("WeightLimit") + int getWeightLimit(); + + @Key("RunSpeedBoost") + int getRunSpeedBoost(); + + @Key("DeathPenaltyChance") + int getDeathPenaltyChance(); + + @Key("RespawnRestoreCP") + double getRespawnRestoreCP(); + + @Key("RespawnRestoreHP") + double getRespawnRestoreHP(); + + @Key("RespawnRestoreMP") + double getRespawnRestoreMP(); + + @Key("HpRegenMultiplier") + double getHpRegenMultiplier(); + + @Key("MpRegenMultiplier") + double getMpRegenMultiplier(); + + @Key("CpRegenMultiplier") + double getCpRegenMultiplier(); + + // Skills & Effects + + @Key("ModifySkillDuration") + boolean modifySkillDuration(); + + @Key("SkillDuration") + @ConverterClass(MapIntegerIntegerConverter.class) + Map getSkillDuration(); + + @Key("ModifySkillReuse") + boolean modifySkillReuse(); + + @Key("SkillReuse") + @ConverterClass(MapIntegerIntegerConverter.class) + Map getSkillReuse(); + + @Key("AutoLearnSkills") + boolean autoLearnSkills(); + + @Key("AutoLearnForgottenScrollSkills") + boolean autoLearnForgottenScrollSkills(); + + @Key("AutoLootHerbs") + boolean autoLootHerbs(); + + @Key("MaxBuffAmount") + int getMaxBuffAmount(); + + @Key("MaxTriggeredBuffAmount") + int getMaxTriggeredBuffAmount(); + + @Key("MaxDanceAmount") + int getMaxDanceAmount(); + + @Key("DanceCancelBuff") + boolean danceCancelBuff(); + + @Key("DanceConsumeAdditionalMP") + boolean danceConsumeAdditionalMP(); + + @Key("StoreDances") + boolean storeDances(); + + @Key("AutoLearnDivineInspiration") + boolean autoLearnDivineInspiration(); + + @Key("CancelByHit") + String cancelByHit(); + + default boolean cancelBow() { + return cancelByHit().equalsIgnoreCase("all") || cancelByHit().equalsIgnoreCase("bow"); + } + + default boolean cancelCast() { + return cancelByHit().equalsIgnoreCase("all") || cancelByHit().equalsIgnoreCase("cast"); + } + + @Key("MagicFailures") + boolean magicFailures(); + + @Key("PlayerFakeDeathUpProtection") + int getPlayerFakeDeathUpProtection(); + + @Key("StoreSkillCooltime") + boolean storeSkillCooltime(); + + @Key("SubclassStoreSkillCooltime") + boolean subclassStoreSkillCooltime(); + + @Key("ShieldBlocks") + boolean shieldBlocks(); + + @Key("PerfectShieldBlockRate") + int getPerfectShieldBlockRate(); + + @Key("EffectTickRatio") + int getEffectTickRatio(); + + // Class, Sub-class and skill learning + + @Key("AllowClassMasters") + boolean allowClassMasters(); + + @Key("ConfigClassMaster") + @ConverterClass(ClassMasterSettingConverter.class) + ClassMasterSetting getClassMaster(); + + @Key("AllowEntireTree") + boolean allowEntireTree(); + + @Key("AlternateClassMaster") + boolean alternateClassMaster(); + + @Key("LifeCrystalNeeded") + boolean lifeCrystalNeeded(); + + @Key("EnchantSkillSpBookNeeded") + boolean enchantSkillSpBookNeeded(); + + @Key("DivineInspirationSpBookNeeded") + boolean divineInspirationSpBookNeeded(); + + @Key("SkillLearn") + boolean skillLearn(); + + @Key("SubclassWithoutQuests") + boolean subclassWithoutQuests(); + + @Key("SubclassEverywhere") + boolean subclassEverywhere(); + + @Key("TransformationWithoutQuest") + boolean transformationWithoutQuest(); + + @Key("FeeDeleteTransferSkills") + int getFeeDeleteTransferSkills(); + + @Key("FeeDeleteSubClassSkills") + int getFeeDeleteSubClassSkills(); + + // Summons + + @Key("SummonStoreSkillCooltime") + boolean summonStoreSkillCooltime(); + + @Key("RestoreServitorOnReconnect") + boolean restoreServitorOnReconnect(); + + @Key("RestorePetOnReconnect") + boolean restorePetOnReconnect(); + + // Limits + + @Key("MaxExpBonus") + double getMaxExpBonus(); + + @Key("MaxSpBonus") + double getMaxSpBonus(); + + @Key("MaxRunSpeed") + int getMaxRunSpeed(); + + @Key("MaxPCritRate") + int getMaxPCritRate(); + + @Key("MaxMCritRate") + int getMaxMCritRate(); + + @Key("MaxPAtkSpeed") + int getMaxPAtkSpeed(); + + @Key("MaxMAtkSpeed") + int getMaxMAtkSpeed(); + + @Key("MaxEvasion") + int getMaxEvasion(); + + @Key("MinAbnormalStateSuccessRate") + int getMinAbnormalStateSuccessRate(); + + @Key("MaxAbnormalStateSuccessRate") + int getMaxAbnormalStateSuccessRate(); + + @Key("MaxPlayerLevel") + int getMaxPlayerLevel(); + + @Key("MaxPetLevel") + int getMaxPetLevel(); + + @Key("MaxSubclass") + int getMaxSubclass(); + + @Key("BaseSubclassLevel") + int getBaseSubclassLevel(); + + @Key("MaxSubclassLevel") + int getMaxSubclassLevel(); + + @Key("MaxPvtStoreSellSlotsDwarf") + int getMaxPvtStoreSellSlotsDwarf(); + + @Key("MaxPvtStoreSellSlotsOther") + int getMaxPvtStoreSellSlotsOther(); + + @Key("MaxPvtStoreBuySlotsDwarf") + int getMaxPvtStoreBuySlotsDwarf(); + + @Key("MaxPvtStoreBuySlotsOther") + int getMaxPvtStoreBuySlotsOther(); + + @Key("MaximumSlotsForNoDwarf") + int getMaximumSlotsForNoDwarf(); + + @Key("MaximumSlotsForDwarf") + int getMaximumSlotsForDwarf(); + + @Key("MaximumSlotsForGMPlayer") + int getMaximumSlotsForGMPlayer(); + + @Key("MaximumSlotsForQuestItems") + int getMaximumSlotsForQuestItems(); + + @Key("MaximumWarehouseSlotsForDwarf") + int getMaximumWarehouseSlotsForDwarf(); + + @Key("MaximumWarehouseSlotsForNoDwarf") + int getMaximumWarehouseSlotsForNoDwarf(); + + @Key("MaximumWarehouseSlotsForClan") + int getMaximumWarehouseSlotsForClan(); + + @Key("MaximumFreightSlots") + int getMaximumFreightSlots(); + + @Key("FreightPrice") + int getFreightPrice(); + + @Key("NpcTalkBlockingTime") + int getNpcTalkBlockingTime(); + + @Key("FriendListLimit") + int getFriendListLimit(); + + @Key("BlockListLimit") + int getBlockListLimit(); + + // Enchanting + + @Key("EnchantChanceElementStone") + int getEnchantChanceElementStone(); + + @Key("EnchantChanceElementCrystal") + int getEnchantChanceElementCrystal(); + + @Key("EnchantChanceElementJewel") + int getEnchantChanceElementJewel(); + + @Key("EnchantChanceElementEnergy") + int getEnchantChanceElementEnergy(); + + @Key("EnchantBlacklist") + Set getEnchantBlacklist(); + + // Augmenting + + @Key("AugmentationNGSkillChance") + int getAugmentationNGSkillChance(); + + @Key("AugmentationMidSkillChance") + int getAugmentationMidSkillChance(); + + @Key("AugmentationHighSkillChance") + int getAugmentationHighSkillChance(); + + @Key("AugmentationTopSkillChance") + int getAugmentationTopSkillChance(); + + @Key("AugmentationAccSkillChance") + int getAugmentationAccSkillChance(); + + @Key("AugmentationBaseStatChance") + int getAugmentationBaseStatChance(); + + @Key("AugmentationNGGlowChance") + int getAugmentationNGGlowChance(); + + @Key("AugmentationMidGlowChance") + int getAugmentationMidGlowChance(); + + @Key("AugmentationHighGlowChance") + int getAugmentationHighGlowChance(); + + @Key("AugmentationTopGlowChance") + int getAugmentationTopGlowChance(); + + @Key("RetailLikeAugmentation") + boolean retailLikeAugmentation(); + + @Key("RetailLikeAugmentationNoGradeChance") + List getRetailLikeAugmentationNoGradeChance(); + + @Key("RetailLikeAugmentationMidGradeChance") + List getRetailLikeAugmentationMidGradeChance(); + + @Key("RetailLikeAugmentationHighGradeChance") + List getRetailLikeAugmentationHighGradeChance(); + + @Key("RetailLikeAugmentationTopGradeChance") + List getRetailLikeAugmentationTopGradeChance(); + + @Key("RetailLikeAugmentationAccessory") + boolean retailLikeAugmentationAccessory(); + + @Key("AugmentationBlacklist") + Set getAugmentationBlacklist(); + + @Key("AllowAugmentPvPItems") + boolean allowAugmentPvPItems(); + + // Karma + + @Key("KarmaPlayerCanBeKilledInPeaceZone") + boolean karmaPlayerCanBeKilledInPeaceZone(); + + @Key("KarmaPlayerCanUseGK") + boolean karmaPlayerCanUseGK(); + + @Key("KarmaPlayerCanTeleport") + boolean karmaPlayerCanTeleport(); + + @Key("KarmaPlayerCanShop") + boolean karmaPlayerCanShop(); + + @Key("KarmaPlayerCanTrade") + boolean karmaPlayerCanTrade(); + + @Key("KarmaPlayerCanUseWareHouse") + boolean karmaPlayerCanUseWareHouse(); + + // Fame + + @Key("MaxPersonalFamePoints") + int getMaxPersonalFamePoints(); + + @Key("FortressZoneFameTaskFrequency") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getFortressZoneFameTaskFrequency(); + + @Key("FortressZoneFameAcquirePoints") + int getFortressZoneFameAcquirePoints(); + + @Key("CastleZoneFameTaskFrequency") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getCastleZoneFameTaskFrequency(); + + @Key("CastleZoneFameAcquirePoints") + int getCastleZoneFameAcquirePoints(); + + @Key("FameForDeadPlayers") + boolean fameForDeadPlayers(); + + // Crafting + + @Key("Crafting") + boolean crafting(); + + @Key("CraftMasterwork") + boolean craftMasterwork(); + + @Key("DwarfRecipeLimit") + int getDwarfRecipeLimit(); + + @Key("CommonRecipeLimit") + int getCommonRecipeLimit(); + + @Key("AlternativeCrafting") + boolean alternativeCrafting(); + + @Key("CraftingSpeed") + double getCraftingSpeed(); + + @Key("CraftingXpRate") + double getCraftingXpRate(); + + @Key("CraftingSpRate") + double getCraftingSpRate(); + + @Key("CraftingRareXpRate") + double getCraftingRareXpRate(); + + @Key("CraftingRareSpRate") + double getCraftingRareSpRate(); + + @Key("BlacksmithUseRecipes") + boolean blacksmithUseRecipes(); + + @Key("StoreRecipeShopList") + boolean storeRecipeShopList(); + + // Clan + + @Key("ClanLeaderDateChange") + int getClanLeaderDateChange(); + + @Key("ClanLeaderHourChange") + String getClanLeaderHourChange(); + + @Key("ClanLeaderInstantActivation") + boolean clanLeaderInstantActivation(); + + @Key("DaysBeforeJoinAClan") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeJoinAClan(); + + @Key("DaysBeforeCreateAClan") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeCreateAClan(); + + @Key("DaysToPassToDissolveAClan") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysToPassToDissolveAClan(); + + @Key("DaysBeforeJoiningAllianceAfterLeaving") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeJoiningAllianceAfterLeaving(); + + @Key("DaysBeforeJoinAllyWhenDismissed") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeJoinAllyWhenDismissed(); + + @Key("DaysBeforeAcceptNewClanWhenDismissed") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeAcceptNewClanWhenDismissed(); + + @Key("DaysBeforeCreateNewAllyWhenDissolved") + @ConverterClass(Days2MillisecondsConverter.class) + long getDaysBeforeCreateNewAllyWhenDissolved(); + + @Key("MaxNumOfClansInAlly") + int getMaxNumOfClansInAlly(); + + @Key("MembersCanWithdrawFromClanWH") + boolean membersCanWithdrawFromClanWH(); + + @Key("RemoveCastleCirclets") + boolean removeCastleCirclets(); + + @Key("ClanMembersForWar") + int getClanMembersForWar(); + + // Party + + @Key("PartyRange") + int getPartyRange(); + + @Key("PartyRange2") + int getPartyRange2(); + + @Key("PartyEvenlyDistributeAllStackableItems") + boolean getPartyEvenlyDistributeAllStackableItems(); + + @Key("PartyEvenlyDistributeAllOtherItems") + boolean getPartyEvenlyDistributeAllOtherItems(); + + @Key("PartyEvenlyDistributeItems") + List getPartyEvenlyDistributeItems(); + + @Key("LeavePartyLeader") + boolean leavePartyLeader(); + + // Initial + + @Key("InitialEquipmentEvent") + boolean initialEquipmentEvent(); + + @Key("StartingAdena") + int getStartingAdena(); + + @Key("StartingLevel") + int getStartingLevel(); + + @Key("StartingSP") + int getStartingSP(); + + // Other + + @Key("MaxAdena") + long getMaxAdena(); + + @Key("AutoLoot") + boolean autoLoot(); + + @Key("AutoLootRaids") + boolean autoLootRaids(); + + @Key("RaidLootRightsInterval") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getRaidLootRightsInterval(); + + @Key("RaidLootRightsCCSize") + int getRaidLootRightsCCSize(); + + @Key("UnstuckInterval") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getUnstuckInterval(); + + @Key("TeleportWatchdogTimeout") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getTeleportWatchdogTimeout(); + + @Key("PlayerSpawnProtection") + int getPlayerSpawnProtection(); + + @Key("PlayerSpawnProtectionAllowedItems") + Set getPlayerSpawnProtectionAllowedItems(); + + @Key("PlayerTeleportProtection") + int getPlayerTeleportProtection(); + + @Key("RandomRespawnInTown") + boolean randomRespawnInTown(); + + @Key("OffsetOnTeleport") + boolean offsetOnTeleport(); + + @Key("MaxOffsetOnTeleport") + int getMaxOffsetOnTeleport(); + + @Key("PetitioningAllowed") + boolean petitioningAllowed(); + + @Key("MaxPetitionsPerPlayer") + int getMaxPetitionsPerPlayer(); + + @Key("MaxPetitionsPending") + int getMaxPetitionsPending(); + + @Key("FreeTeleporting") + boolean freeTeleporting(); + + @Key("DeleteCharAfterDays") + int getDeleteCharAfterDays(); + + @Key("ExponentXp") + int getExponentXp(); + + @Key("ExponentSp") + int getExponentSp(); + + @Key("PartyXpCutoffMethod") + String getPartyXpCutoffMethod(); + + @Key("PartyXpCutoffPercent") + double getPartyXpCutoffPercent(); + + @Key("PartyXpCutoffLevel") + int getPartyXpCutoffLevel(); + + @Key("PartyXpCutoffGaps") + @ConverterClass(MapIntegerIntegerConverter.class) + Map getPartyXpCutoffGaps(); + + @Key("PartyXpCutoffGapPercent") + List getPartyXpCutoffGapPercent(); + + @Key("Tutorial") + boolean tutorial(); + + @Key("ExpertisePenalty") + boolean expertisePenalty(); + + @Key("StoreUISettings") + boolean storeUISettings(); + + @Key("SilenceModeExclude") + boolean silenceModeExclude(); + + @Key("ValidateTriggerSkills") + boolean validateTriggerSkills(); + + @Key("PlayerNameTemplate") + @ConverterClass(PatternConverter.class) + Pattern getPlayerNameTemplate(); + + @Key("PetNameTemplate") + @ConverterClass(PatternConverter.class) + Pattern getPetNameTemplate(); + + @Key("ForbiddenNames") + Set getForbiddenNames(); + + @Key("CharMaxNumber") + int getCharMaxNumber(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/ClanConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/ClanConfiguration.java new file mode 100644 index 00000000..3f770e3e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/ClanConfiguration.java @@ -0,0 +1,175 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.regex.Pattern; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.PatternConverter; + +/** + * Clan Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/clan.properties", + "file:./config/clan.properties", + "classpath:config/clan.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface ClanConfiguration extends Reloadable { + + @Key("ClanNameTemplate") + @ConverterClass(PatternConverter.class) + Pattern getClanNameTemplate(); + + @Key("TakeFortPoints") + int getTakeFortPoints(); + + @Key("TakeCastlePoints") + int getTakeCastlePoints(); + + @Key("CastleDefendedPoints") + int getCastleDefendedPoints(); + + @Key("FestivalOfDarknessWin") + int getFestivalOfDarknessWin(); + + @Key("HeroPoints") + int getHeroPoints(); + + @Key("CompleteAcademyMinPoints") + int getCompleteAcademyMinPoints(); + + @Key("CompleteAcademyMaxPoints") + int getCompleteAcademyMaxPoints(); + + @Key("KillBallistaPoints") + int getKillBallistaPoints(); + + @Key("BloodAlliancePoints") + int getBloodAlliancePoints(); + + @Key("BloodOathPoints") + int getBloodOathPoints(); + + @Key("KnightsEpaulettePoints") + int getKnightsEpaulettePoints(); + + @Key("1stRaidRankingPoints") + int get1stRaidRankingPoints(); + + @Key("2ndRaidRankingPoints") + int get2ndRaidRankingPoints(); + + @Key("3rdRaidRankingPoints") + int get3rdRaidRankingPoints(); + + @Key("4thRaidRankingPoints") + int get4thRaidRankingPoints(); + + @Key("5thRaidRankingPoints") + int get5thRaidRankingPoints(); + + @Key("6thRaidRankingPoints") + int get6thRaidRankingPoints(); + + @Key("7thRaidRankingPoints") + int get7thRaidRankingPoints(); + + @Key("8thRaidRankingPoints") + int get8thRaidRankingPoints(); + + @Key("9thRaidRankingPoints") + int get9thRaidRankingPoints(); + + @Key("10thRaidRankingPoints") + int get10thRaidRankingPoints(); + + @Key("UpTo50thRaidRankingPoints") + int getUpTo50thRaidRankingPoints(); + + @Key("UpTo100thRaidRankingPoints") + int getUpTo100thRaidRankingPoints(); + + @Key("ReputationScorePerKill") + int getReputationScorePerKill(); + + @Key("LoseFortPoints") + int getLoseFortPoints(); + + @Key("LoseCastlePoints") + int getLoseCastlePoints(); + + @Key("CreateRoyalGuardCost") + int getCreateRoyalGuardCost(); + + @Key("CreateKnightUnitCost") + int getCreateKnightUnitCost(); + + // TODO(Zoey76): Implement ReinforceKnightUnitCost configuration. + @Key("ReinforceKnightUnitCost") + int getReinforceKnightUnitCost(); + + @Key("ClanLevel6Cost") + int getClanLevel6Cost(); + + @Key("ClanLevel7Cost") + int getClanLevel7Cost(); + + @Key("ClanLevel8Cost") + int getClanLevel8Cost(); + + @Key("ClanLevel9Cost") + int getClanLevel9Cost(); + + @Key("ClanLevel10Cost") + int getClanLevel10Cost(); + + @Key("ClanLevel11Cost") + int getClanLevel11Cost(); + + @Key("ClanLevel6Requirement") + int getClanLevel6Requirement(); + + @Key("ClanLevel7Requirement") + int getClanLevel7Requirement(); + + @Key("ClanLevel8Requirement") + int getClanLevel8Requirement(); + + @Key("ClanLevel9Requirement") + int getClanLevel9Requirement(); + + @Key("ClanLevel10Requirement") + int getClanLevel10Requirement(); + + @Key("ClanLevel11Requirement") + int getClanLevel11Requirement(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/ClanHallConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/ClanHallConfiguration.java new file mode 100644 index 00000000..99701e59 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/ClanHallConfiguration.java @@ -0,0 +1,239 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Clan Hall Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/clanhall.properties", + "file:./config/clanhall.properties", + "classpath:config/clanhall.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface ClanHallConfiguration extends Reloadable { + + @Key("FunctionFeeDay1") + long getFunctionFeeDay1(); + + @Key("FunctionFeeDay2") + long getFunctionFeeDay2(); + + @Key("FunctionFeeDay3") + long getFunctionFeeDay3(); + + @Key("FunctionFeeDay7") + long getFunctionFeeDay7(); + + @Key("TeleportFunctionFeeLvl1") + int getTeleportFunctionFeeLvl1(); + + @Key("TeleportFunctionFeeLvl2") + int getTeleportFunctionFeeLvl2(); + + @Key("TeleportFunctionFeeLvl11") + int getTeleportFunctionFeeLvl11(); + + @Key("TeleportFunctionFeeLvl12") + int getTeleportFunctionFeeLvl12(); + + @Key("SupportFeeLvl1") + int getSupportFeeLvl1(); + + @Key("SupportFeeLvl2") + int getSupportFeeLvl2(); + + @Key("SupportFeeLvl3") + int getSupportFeeLvl3(); + + @Key("SupportFeeLvl4") + int getSupportFeeLvl4(); + + @Key("SupportFeeLvl5") + int getSupportFeeLvl5(); + + @Key("SupportFeeLvl7") + int getSupportFeeLvl7(); + + @Key("SupportFeeLvl8") + int getSupportFeeLvl8(); + + @Key("SupportFeeLvl15") + int getSupportFeeLvl15(); + + @Key("SupportFeeLvl18") + int getSupportFeeLvl18(); + + @Key("MpRegenerationFeeLvl1") + int getMpRegenerationFeeLvl1(); + + @Key("MpRegenerationFeeLvl3") + int getMpRegenerationFeeLvl3(); + + @Key("MpRegenerationFeeLvl5") + int getMpRegenerationFeeLvl5(); + + @Key("MpRegenerationFeeLvl6") + int getMpRegenerationFeeLvl6(); + + @Key("MpRegenerationFeeLvl8") + int getMpRegenerationFeeLvl8(); + + @Key("MpRegenerationFeeLvl18") + int getMpRegenerationFeeLvl18(); + + @Key("MpRegenerationFeeLvl20") + int getMpRegenerationFeeLvl20(); + + @Key("HpRegenerationFeeLvl2") + int getHpRegenerationFeeLvl2(); + + @Key("HpRegenerationFeeLvl4") + int getHpRegenerationFeeLvl4(); + + @Key("HpRegenerationFeeLvl5") + int getHpRegenerationFeeLvl5(); + + @Key("HpRegenerationFeeLvl6") + int getHpRegenerationFeeLvl6(); + + @Key("HpRegenerationFeeLvl7") + int getHpRegenerationFeeLvl7(); + + @Key("HpRegenerationFeeLvl8") + int getHpRegenerationFeeLvl8(); + + @Key("HpRegenerationFeeLvl9") + int getHpRegenerationFeeLvl9(); + + @Key("HpRegenerationFeeLvl10") + int getHpRegenerationFeeLvl10(); + + @Key("HpRegenerationFeeLvl12") + int getHpRegenerationFeeLvl12(); + + @Key("HpRegenerationFeeLvl13") + int getHpRegenerationFeeLvl13(); + + @Key("HpRegenerationFeeLvl15") + int getHpRegenerationFeeLvl15(); + + @Key("HpRegenerationFeeLvl25") + int getHpRegenerationFeeLvl25(); + + @Key("HpRegenerationFeeLvl30") + int getHpRegenerationFeeLvl30(); + + @Key("ExpRegenerationFeeLvl1") + int getExpRegenerationFeeLvl1(); + + @Key("ExpRegenerationFeeLvl3") + int getExpRegenerationFeeLvl3(); + + @Key("ExpRegenerationFeeLvl5") + int getExpRegenerationFeeLvl5(); + + @Key("ExpRegenerationFeeLvl6") + int getExpRegenerationFeeLvl6(); + + @Key("ExpRegenerationFeeLvl7") + int getExpRegenerationFeeLvl7(); + + @Key("ExpRegenerationFeeLvl8") + int getExpRegenerationFeeLvl8(); + + @Key("ExpRegenerationFeeLvl10") + int getExpRegenerationFeeLvl10(); + + @Key("ExpRegenerationFeeLvl19") + int getExpRegenerationFeeLvl19(); + + @Key("ExpRegenerationFeeLvl20") + int getExpRegenerationFeeLvl20(); + + @Key("ItemCreationFunctionFeeLvl1") + int getItemCreationFunctionFeeLvl1(); + + @Key("ItemCreationFunctionFeeLvl2") + int getItemCreationFunctionFeeLvl2(); + + @Key("ItemCreationFunctionFeeLvl3") + int getItemCreationFunctionFeeLvl3(); + + @Key("ItemCreationFunctionFeeLvl11") + int getItemCreationFunctionFeeLvl11(); + + @Key("ItemCreationFunctionFeeLvl12") + int getItemCreationFunctionFeeLvl12(); + + @Key("ItemCreationFunctionFeeLvl13") + int getItemCreationFunctionFeeLvl13(); + + @Key("CurtainFunctionFeeLvl1") + int getCurtainFunctionFeeLvl1(); + + @Key("CurtainFunctionFeeLvl2") + int getCurtainFunctionFeeLvl2(); + + @Key("FrontPlatformFunctionFeeLvl1") + int getFrontPlatformFunctionFeeLvl1(); + + @Key("FrontPlatformFunctionFeeLvl2") + int getFrontPlatformFunctionFeeLvl2(); + + @Key("BroadCastFunctionFeeLvl1") + int getBroadCastFunctionFeeLvl1(); + + @Key("BroadCastFunctionFeeLvl2") + int getBroadCastFunctionFeeLvl2(); + + @Key("MpBuffFree") + boolean mpBuffFree(); + + @Key("MinClanLevel") + int getMinClanLevel(); + + @Key("MaxAttackers") + int getMaxAttackers(); + + // TODO(Zoey76): Implement MaxFlagsPerClan configuration. + @Key("MaxFlagsPerClan") + int getMaxFlagsPerClan(); + + @Key("EnableFame") + boolean enableFame(); + + @Key("FameAmount") + int getFameAmount(); + + @Key("FameFrequency") + int getFameFrequency(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/Configuration.java b/src/main/java/com/l2jserver/gameserver/config/Configuration.java new file mode 100644 index 00000000..16d7f222 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/Configuration.java @@ -0,0 +1,249 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import java.nio.file.Path; + +import org.aeonbits.owner.ConfigFactory; + +/** + * Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class Configuration { + + public static final String DEFAULT_PATH = "config/"; + + public static final String CUSTOM_SUBPATH = "custom/game/config/"; + + public static final String EOL = System.lineSeparator(); + + private static final IPConfigData ipConfigData = new IPConfigData(); + + private static final ServerConfiguration server = ConfigFactory.create(ServerConfiguration.class); + + private static final HexIdConfiguration hexId = ConfigFactory.create(HexIdConfiguration.class); + + private static final DatabaseConfiguration database = ConfigFactory.create(DatabaseConfiguration.class); + + private static final MMOConfiguration mmo = ConfigFactory.create(MMOConfiguration.class); + + private static final TelnetConfiguration telnet = ConfigFactory.create(TelnetConfiguration.class); + + private static final GeneralConfiguration general = ConfigFactory.create(GeneralConfiguration.class); + + private static final GeodataConfiguration geodata = ConfigFactory.create(GeodataConfiguration.class); + + private static final CharacterConfiguration character = ConfigFactory.create(CharacterConfiguration.class); + + private static final CastleConfiguration castle = ConfigFactory.create(CastleConfiguration.class); + + private static final ClanConfiguration clan = ConfigFactory.create(ClanConfiguration.class); + + private static final ClanHallConfiguration clanhall = ConfigFactory.create(ClanHallConfiguration.class); + + private static final FortressConfiguration fortress = ConfigFactory.create(FortressConfiguration.class); + + private static final SevenSignsConfiguration sevenSigns = ConfigFactory.create(SevenSignsConfiguration.class); + + private static final TvTConfiguration tvt = ConfigFactory.create(TvTConfiguration.class); + + private static final NPCConfiguration npc = ConfigFactory.create(NPCConfiguration.class); + + private static final OlympiadConfiguration olympiad = ConfigFactory.create(OlympiadConfiguration.class); + + private static final SiegeConfiguration siege = ConfigFactory.create(SiegeConfiguration.class); + + private static final TerritoryWarConfiguration territoryWar = ConfigFactory.create(TerritoryWarConfiguration.class); + + private static final FortSiegeConfiguration fortSiege = ConfigFactory.create(FortSiegeConfiguration.class); + + private static final GrandBossConfiguration grandBoss = ConfigFactory.create(GrandBossConfiguration.class); + + private static final GraciaSeedsConfiguration graciaSeeds = ConfigFactory.create(GraciaSeedsConfiguration.class); + + private static final RatesConfiguration rates = ConfigFactory.create(RatesConfiguration.class); + + private static final VitalityConfiguration vitality = ConfigFactory.create(VitalityConfiguration.class); + + private static final PvPConfiguration pvp = ConfigFactory.create(PvPConfiguration.class); + + private static final CustomsConfiguration customs = ConfigFactory.create(CustomsConfiguration.class); + + private static final HuntingSystemConfiguration hunting = ConfigFactory.create(HuntingSystemConfiguration.class); + + private static final FloodProtectorConfiguration floodProtector = ConfigFactory.create(FloodProtectorConfiguration.class); + + private static final BufferServiceConfiguration bufferService = ConfigFactory.create(BufferServiceConfiguration.class); + + private static final TeleporterServiceConfiguration teleporterService = ConfigFactory.create(TeleporterServiceConfiguration.class); + + private static final DiscordConfiguration discord = ConfigFactory.create(DiscordConfiguration.class); + + private Configuration() { + // Do nothing. + } + + public static ServerConfiguration server() { + return server; + } + + public static HexIdConfiguration hexId() { + return hexId; + } + + public static DatabaseConfiguration database() { + return database; + } + + public static MMOConfiguration mmo() { + return mmo; + } + + public static TelnetConfiguration telnet() { + return telnet; + } + + public static GeneralConfiguration general() { + return general; + } + + public static GeodataConfiguration geodata() { + return geodata; + } + + public static CharacterConfiguration character() { + return character; + } + + public static CastleConfiguration castle() { + return castle; + } + + public static ClanConfiguration clan() { + return clan; + } + + public static ClanHallConfiguration clanhall() { + return clanhall; + } + + public static FortressConfiguration fortress() { + return fortress; + } + + public static SevenSignsConfiguration sevenSigns() { + return sevenSigns; + } + + public static TvTConfiguration tvt() { + return tvt; + } + + public static NPCConfiguration npc() { + return npc; + } + + public static OlympiadConfiguration olympiad() { + return olympiad; + } + + public static SiegeConfiguration siege() { + return siege; + } + + public static TerritoryWarConfiguration territoryWar() { + return territoryWar; + } + + public static FortSiegeConfiguration fortSiege() { + return fortSiege; + } + + public static GrandBossConfiguration grandBoss() { + return grandBoss; + } + + public static GraciaSeedsConfiguration graciaSeeds() { + return graciaSeeds; + } + + public static RatesConfiguration rates() { + return rates; + } + + public static VitalityConfiguration vitality() { + return vitality; + } + + public static PvPConfiguration pvp() { + return pvp; + } + + public static CustomsConfiguration customs() { + return customs; + } + + public static HuntingSystemConfiguration hunting() { + return hunting; + } + + public static FloodProtectorConfiguration floodProtector() { + return floodProtector; + } + + public static IPConfigData ip() { + return ipConfigData; + } + + public static BufferServiceConfiguration bufferService() { + return bufferService; + } + + public static TeleporterServiceConfiguration teleporterService() { + return teleporterService; + } + + public static DiscordConfiguration discord() { + return discord; + } + + public static String getDefaultPath(String filename) { + return DEFAULT_PATH + filename; + } + + public static String getCustomSubpath(String filename) { + return CUSTOM_SUBPATH + filename; + } + + /** + * Returns either the custom or default path of the config file.
    + * When the L2J_HOME variable is defined, the custom path to the config file is returned. If not, the default path to the config file is returned. + * @param filename the filename + * @return either the custom or default path to the config file + */ + public static Path getCustomOrDefaultPath(String filename) { + var l2jHome = server().getL2jHome(); + if (l2jHome == null) { + return Path.of(getDefaultPath(filename)); + } + return Path.of(l2jHome, getCustomSubpath(filename)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/CustomsConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/CustomsConfiguration.java new file mode 100644 index 00000000..9c92c443 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/CustomsConfiguration.java @@ -0,0 +1,310 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.Map; +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.ColorConverter; +import com.l2jserver.gameserver.config.converter.IPLimitConverter; +import com.l2jserver.gameserver.config.converter.Seconds2MillisecondsConverter; + +/** + * Customs Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/customs.properties", + "file:./config/customs.properties", + "classpath:config/customs.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface CustomsConfiguration extends Reloadable { + + @Key("ChampionEnable") + boolean championEnable(); + + @Key("ChampionPassive") + boolean championPassive(); + + @Key("ChampionFrequency") + int getChampionFrequency(); + + @Key("ChampionTitle") + String getChampionTitle(); + + @Key("ChampionMinLevel") + int getChampionMinLevel(); + + @Key("ChampionMaxLevel") + int getChampionMaxLevel(); + + @Key("ChampionHp") + int getChampionHp(); + + @Key("ChampionHpRegen") + double getChampionHpRegen(); + + @Key("ChampionRewardsExpSp") + double getChampionRewardsExpSp(); + + @Key("ChampionRewardsChance") + double getChampionRewardsChance(); + + @Key("ChampionRewardsAmount") + double getChampionRewardsAmount(); + + @Key("ChampionAdenasRewardsChance") + double getChampionAdenasRewardsChance(); + + @Key("ChampionAdenasRewardsAmount") + double getChampionAdenasRewardsAmount(); + + @Key("ChampionAtk") + float getChampionAtk(); + + @Key("ChampionSpdAtk") + float getChampionSpdAtk(); + + @Key("ChampionRewardItemID") + int getChampionRewardItemID(); + + @Key("ChampionRewardItemQty") + int getChampionRewardItemQty(); + + @Key("ChampionRewardLowerLvlItemChance") + int getChampionRewardLowerLvlItemChance(); + + @Key("ChampionRewardHigherLvlItemChance") + int getChampionRewardHigherLvlItemChance(); + + @Key("ChampionEnableVitality") + boolean championEnableVitality(); + + @Key("ChampionEnableInInstances") + boolean championEnableInInstances(); + + @Key("AllowWedding") + boolean allowWedding(); + + @Key("WeddingPrice") + int getWeddingPrice(); + + @Key("WeddingPunishInfidelity") + boolean weddingPunishInfidelity(); + + // TODO(Zoey76): Implement WeddingTeleport configuration. + @Key("WeddingTeleport") + boolean weddingTeleport(); + + @Key("WeddingTeleportPrice") + int getWeddingTeleportPrice(); + + @Key("WeddingTeleportDuration") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getWeddingTeleportDuration(); + + @Key("WeddingAllowSameSex") + boolean weddingAllowSameSex(); + + @Key("WeddingFormalWear") + boolean weddingFormalWear(); + + @Key("WeddingDivorceCosts") + int getWeddingDivorceCosts(); + + @Key("BankingEnabled") + boolean bankingEnabled(); + + @Key("BankingGoldbarCount") + int getBankingGoldbarCount(); + + @Key("BankingAdenaCount") + int getBankingAdenaCount(); + + @Key("EnableWarehouseSortingClan") + boolean enableWarehouseSortingClan(); + + @Key("EnableWarehouseSortingPrivate") + boolean enableWarehouseSortingPrivate(); + + @Key("OfflineTradeEnable") + boolean offlineTradeEnable(); + + @Key("OfflineCraftEnable") + boolean offlineCraftEnable(); + + @Key("OfflineModeInPeaceZone") + boolean offlineModeInPeaceZone(); + + @Key("OfflineModeNoDamage") + boolean offlineModeNoDamage(); + + @Key("OfflineSetNameColor") + boolean offlineSetNameColor(); + + @Key("OfflineNameColor") + @ConverterClass(ColorConverter.class) + int getOfflineNameColor(); + + @Key("OfflineFame") + boolean offlineFame(); + + @Key("RestoreOffliners") + boolean restoreOffliners(); + + @Key("OfflineMaxDays") + int getOfflineMaxDays(); + + @Key("OfflineDisconnectFinished") + boolean offlineDisconnectFinished(); + + @Key("EnableManaPotionSupport") + boolean enableManaPotionSupport(); + + @Key("DisplayServerTime") + boolean displayServerTime(); + + @Key("ScreenWelcomeMessageEnable") + boolean screenWelcomeMessageEnable(); + + @Key("ScreenWelcomeMessageText") + String getScreenWelcomeMessageText(); + + @Key("ScreenWelcomeMessageTime") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getScreenWelcomeMessageTime(); + + @Key("AntiFeedEnable") + boolean antiFeedEnable(); + + @Key("AntiFeedDualbox") + boolean antiFeedDualbox(); + + @Key("AntiFeedDisconnectedAsDualbox") + boolean antiFeedDisconnectedAsDualbox(); + + @Key("AntiFeedInterval") + int getAntiFeedInterval(); + + @Key("AnnouncePkPvP") + boolean announcePkPvP(); + + @Key("AnnouncePkPvPNormalMessage") + boolean announcePkPvPNormalMessage(); + + @Key("AnnouncePkMsg") + String getAnnouncePkMsg(); + + @Key("AnnouncePvpMsg") + String getAnnouncePvpMsg(); + + @Key("ChatAdmin") + boolean chatAdmin(); + + @Key("HellboundStatus") + boolean hellboundStatus(); + + @Key("AutoLootVoiceEnable") + boolean autoLootVoiceCommand(); + + @Key("AutoLootVoiceRestore") + boolean autoLootVoiceRestore(); + + @Key("AutoLootItemsVoiceRestore") + boolean autoLootItemsVoiceRestore(); + + @Key("AutoLootHerbsVoiceRestore") + boolean autoLootHerbsVoiceRestore(); + + @Key("AutoLootHerbsList") + Set getAutoLootHerbsList(); + + @Key("AutoLootItemsList") + Set getAutoLootItemsList(); + + @Key("MultiLangEnable") + boolean multiLangEnable(); + + @Key("MultiLangDefault") + String getMultiLangDefault(); + + @Key("MultiLangAllowed") + Set getMultiLangAllowed(); + + @Key("MultiLangHandler") + boolean multiLangHandler(); + + @Key("MultiLangSystemMessageEnable") + boolean multiLangSystemMessageEnable(); + + @Key("MultiLangSystemMessageAllowed") + Set getMultiLangSystemMessageAllowed(); + + @Key("MultiLangNpcStringEnable") + boolean multiLangNpcStringEnable(); + + @Key("MultiLangNpcStringAllowed") + Set getMultiLangNpcStringAllowed(); + + @Key("L2WalkerProtection") + boolean l2WalkerProtection(); + + @Key("DebugHandler") + boolean debugHandler(); + + @Key("DualboxCheckMaxPlayersPerIP") + int getDualboxCheckMaxPlayersPerIP(); + + @Key("DualboxCheckMaxOlympiadParticipantsPerIP") + int getDualboxCheckMaxOlympiadParticipantsPerIP(); + + @Key("DualboxCheckMaxL2EventParticipantsPerIP") + int getDualboxCheckMaxL2EventParticipantsPerIP(); + + @Key("DualboxCheckWhitelist") + @ConverterClass(IPLimitConverter.class) + Map getDualboxCheckWhitelist(); + + @Key("AllowChangePassword") + boolean allowChangePassword(); + + @Key("AllowXpHandler") + boolean allowXpHandler(); + + @Key("AllowCastleHandler") + boolean allowCastleHandler(); + + @Key("AllowClanHandler") + boolean allowClanHandler(); + + @Key("AllowStatHandler") + boolean allowStatHandler(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/DatabaseConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/DatabaseConfiguration.java new file mode 100644 index 00000000..57d5308f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/DatabaseConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Database Configuration. + * @author Zoey76 + * @version 2.6.3.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/database.properties", + "file:./config/database.properties", + "classpath:config/database.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface DatabaseConfiguration extends Reloadable { + + @Key("URL") + String getURL(); + + @Key("User") + String getUser(); + + @Key("Password") + String getPassword(); + + @Key("MaxConnections") + int getMaxConnections(); + + @Key("MaxIdleTime") + int getMaxIdleTime(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/DiscordConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/DiscordConfiguration.java new file mode 100644 index 00000000..a4455bb4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/DiscordConfiguration.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Mutable; +import org.aeonbits.owner.Reloadable; + +/** + * Discord Configuration. + * @author Stalitsa + * @version 2.6.2.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/discord.properties", + "file:./config/discord.properties", + "classpath:config/discord.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface DiscordConfiguration extends Mutable, Reloadable { + + @Key("BotEnable") + boolean enableBot(); + + @Key("BotPrefix") + String getPrefix(); + + @Key("BotToken") + String getBotToken(); + + @Key("ServerId") + String getServerId(); + + @Key("GameMasterId") + String getGameMasterId(); + + @Key("ConsoleLogChannelId") + String getConsoleLogChannelId(); + + @Key("GameChatChannelId") + String getGameChatChannelId(); +} diff --git a/src/main/java/com/l2jserver/gameserver/config/FloodProtectorConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/FloodProtectorConfiguration.java new file mode 100644 index 00000000..edbde206 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/FloodProtectorConfiguration.java @@ -0,0 +1,286 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Accessible; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Flood Protector Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/floodprotector.properties", + "file:./config/floodprotector.properties", + "classpath:config/floodprotector.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface FloodProtectorConfiguration extends Accessible, Reloadable { + + // TODO(Zoey76): Implement all these configurations. + + @Key("UseItemInterval") + int getUseItemInterval(); + + @Key("UseItemLogFlooding") + boolean useItemLogFlooding(); + + @Key("UseItemPunishmentLimit") + int getUseItemPunishmentLimit(); + + @Key("UseItemPunishmentType") + String getUseItemPunishmentType(); + + @Key("UseItemPunishmentTime") + int getUseItemPunishmentTime(); + + @Key("RollDiceInterval") + int getRollDiceInterval(); + + @Key("RollDiceLogFlooding") + boolean rollDiceLogFlooding(); + + @Key("RollDicePunishmentLimit") + int getRollDicePunishmentLimit(); + + @Key("RollDicePunishmentType") + String getRollDicePunishmentType(); + + @Key("RollDicePunishmentTime") + int getRollDicePunishmentTime(); + + @Key("FireworkInterval") + int getFireworkInterval(); + + @Key("FireworkLogFlooding") + boolean fireworkLogFlooding(); + + @Key("FireworkPunishmentLimit") + int getFireworkPunishmentLimit(); + + @Key("FireworkPunishmentType") + String getFireworkPunishmentType(); + + @Key("FireworkPunishmentTime") + int getFireworkPunishmentTime(); + + @Key("ItemPetSummonInterval") + int geItemPetSummonInterval(); + + @Key("ItemPetSummonLogFlooding") + boolean itemPetSummonLogFlooding(); + + @Key("ItemPetSummonPunishmentLimit") + int getItemPetSummonPunishmentLimit(); + + @Key("ItemPetSummonPunishmentType") + String getItemPetSummonPunishmentType(); + + @Key("ItemPetSummonPunishmentTime") + int getItemPetSummonPunishmentTime(); + + @Key("HeroVoiceInterval") + int getHeroVoiceInterval(); + + @Key("HeroVoiceLogFlooding") + boolean heroVoiceLogFlooding(); + + @Key("HeroVoicePunishmentLimit") + int getHeroVoicePunishmentLimit(); + + @Key("HeroVoicePunishmentType") + String getHeroVoicePunishmentType(); + + @Key("HeroVoicePunishmentTime") + int getHeroVoicePunishmentTime(); + + @Key("GlobalChatInterval") + int getGlobalChatInterval(); + + @Key("GlobalChatLogFlooding") + boolean globalChatLogFlooding(); + + @Key("GlobalChatPunishmentLimit") + int getGlobalChatPunishmentLimit(); + + @Key("GlobalChatPunishmentType") + String getGlobalChatPunishmentType(); + + @Key("GlobalChatPunishmentTime") + int getGlobalChatPunishmentTime(); + + @Key("SubclassInterval") + int getSubclassInterval(); + + @Key("SubclassLogFlooding") + boolean subclassLogFlooding(); + + @Key("SubclassPunishmentLimit") + int getSubclassPunishmentLimit(); + + @Key("SubclassPunishmentType") + String getSubclassPunishmentType(); + + @Key("SubclassPunishmentTime") + int getSubclassPunishmentTime(); + + @Key("DropItemInterval") + int getDropItemInterval(); + + @Key("DropItemLogFlooding") + boolean dropItemLogFlooding(); + + @Key("DropItemPunishmentLimit") + int getDropItemPunishmentLimit(); + + @Key("DropItemPunishmentType") + String getDropItemPunishmentType(); + + @Key("DropItemPunishmentTime") + int getDropItemPunishmentTime(); + + @Key("ServerBypassInterval") + int getServerBypassInterval(); + + @Key("ServerBypassLogFlooding") + boolean serverBypassLogFlooding(); + + @Key("ServerBypassPunishmentLimit") + int getServerBypassPunishmentLimit(); + + @Key("ServerBypassPunishmentType") + String getServerBypassPunishmentType(); + + @Key("ServerBypassPunishmentTime") + int getServerBypassPunishmentTime(); + + @Key("MultiSellInterval") + int getMultiSellInterval(); + + @Key("MultiSellLogFlooding") + boolean multiSellLogFlooding(); + + @Key("MultiSellPunishmentLimit") + int getMultiSellPunishmentLimit(); + + @Key("MultiSellPunishmentType") + String getMultiSellPunishmentType(); + + @Key("MultiSellPunishmentTime") + int getMultiSellPunishmentTime(); + + @Key("TransactionInterval") + int getTransactionInterval(); + + @Key("TransactionLogFlooding") + boolean transactionLogFlooding(); + + @Key("TransactionPunishmentLimit") + int getTransactionPunishmentLimit(); + + @Key("TransactionPunishmentType") + String getTransactionPunishmentType(); + + @Key("TransactionPunishmentTime") + int getTransactionPunishmentTime(); + + @Key("ManufactureInterval") + int getManufactureInterval(); + + @Key("ManufactureLogFlooding") + boolean manufactureLogFlooding(); + + @Key("ManufacturePunishmentLimit") + int getManufacturePunishmentLimit(); + + @Key("ManufacturePunishmentType") + String getManufacturePunishmentType(); + + @Key("ManufacturePunishmentTime") + int getManufacturePunishmentTime(); + + @Key("ManorInterval") + int getManorInterval(); + + @Key("ManorLogFlooding") + boolean manorLogFlooding(); + + @Key("ManorPunishmentLimit") + int getManorPunishmentLimit(); + + @Key("ManorPunishmentType") + String getManorPunishmentType(); + + @Key("ManorPunishmentTime") + int getManorPunishmentTime(); + + @Key("SendMailInterval") + int getSendMailInterval(); + + @Key("SendMailLogFlooding") + boolean sendMailLogFlooding(); + + @Key("SendMailPunishmentLimit") + int getSendMailPunishmentLimit(); + + @Key("SendMailPunishmentType") + String getSendMailPunishmentType(); + + @Key("SendMailPunishmentTime") + int getSendMailPunishmentTime(); + + @Key("CharacterSelectInterval") + int getCharacterSelectInterval(); + + @Key("CharacterSelectLogFlooding") + boolean characterSelectLogFlooding(); + + @Key("CharacterSelectPunishmentLimit") + int getCharacterSelectPunishmentLimit(); + + @Key("CharacterSelectPunishmentType") + String getCharacterSelectPunishmentType(); + + @Key("CharacterSelectPunishmentTime") + int getCharacterSelectPunishmentTime(); + + @Key("ItemAuctionInterval") + int getItemAuctionInterval(); + + @Key("ItemAuctionLogFlooding") + boolean itemAuctionLogFlooding(); + + @Key("ItemAuctionPunishmentLimit") + int getItemAuctionPunishmentLimit(); + + @Key("ItemAuctionPunishmentType") + String getItemAuctionPunishmentType(); + + @Key("ItemAuctionPunishmentTime") + int getItemAuctionPunishmentTime(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/FortSiegeConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/FortSiegeConfiguration.java new file mode 100644 index 00000000..e67ac10b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/FortSiegeConfiguration.java @@ -0,0 +1,478 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Accessible; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Minutes2MillisecondsConverter; + +/** + * Fort Siege Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/fortsiege.properties", + "file:./config/fortsiege.properties", + "classpath:config/fortsiege.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface FortSiegeConfiguration extends Accessible, Reloadable { + + @Key("SiegeLength") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getSiegeLength(); + + @Key("SuspiciousMerchantRespawnDelay") + int getSuspiciousMerchantRespawnDelay(); + + @Key("CountDownLength") + int getCountDownLength(); + + @Key("MaxFlags") + int getMaxFlags(); + + @Key("SiegeClanMinLevel") + int getSiegeClanMinLevel(); + + @Key("AttackerMaxClans") + int getAttackerMaxClans(); + + @Key("JustToTerritory") + boolean justToTerritory(); + + // TODO(Zoey76): Most, if not all, of this configurations are not used. + + @Key("ShantyCommander1") + String getShantyCommander1(); + + @Key("ShantyCommander2") + String getShantyCommander2(); + + @Key("ShantyCommander3") + String getShantyCommander3(); + + @Key("ShantyFlag1") + String getShantyFlag1(); + + @Key("ShantyFlag2") + String getShantyFlag2(); + + @Key("ShantyFlag3") + String getShantyFlag3(); + + @Key("SouthernCommander1") + String getSouthernCommander1(); + + @Key("SouthernCommander2") + String getSouthernCommander2(); + + @Key("SouthernCommander3") + String getSouthernCommander3(); + + @Key("SouthernCommander4") + String getSouthernCommander4(); + + @Key("SouthernFlag1") + String getSouthernFlag1(); + + @Key("SouthernFlag2") + String getSouthernFlag2(); + + @Key("SouthernFlag3") + String getSouthernFlag3(); + + @Key("HiveCommander1") + String getHiveCommander1(); + + @Key("HiveCommander2") + String getHiveCommander2(); + + @Key("HiveCommander3") + String getHiveCommander3(); + + @Key("HiveFlag1") + String getHiveFlag1(); + + @Key("HiveFlag2") + String getHiveFlag2(); + + @Key("HiveFlag3") + String getHiveFlag3(); + + @Key("ValleyCommander1") + String getValleyCommander1(); + + @Key("ValleyCommander2") + String getValleyCommander2(); + + @Key("ValleyCommander3") + String getValleyCommander3(); + + @Key("ValleyCommander4") + String getValleyCommander4(); + + @Key("ValleyFlag1") + String getValleyFlag1(); + + @Key("ValleyFlag2") + String getValleyFlag2(); + + @Key("ValleyFlag3") + String getValleyFlag3(); + + @Key("IvoryCommander1") + String getIvoryCommander1(); + + @Key("IvoryCommander2") + String getIvoryCommander2(); + + @Key("IvoryCommander3") + String getIvoryCommander3(); + + @Key("IvoryFlag1") + String getIvoryFlag1(); + + @Key("IvoryFlag2") + String getIvoryFlag2(); + + @Key("IvoryFlag3") + String getIvoryFlag3(); + + @Key("NarsellCommander1") + String getNarsellCommander1(); + + @Key("NarsellCommander2") + String getNarsellCommander2(); + + @Key("NarsellCommander3") + String getNarsellCommander3(); + + @Key("NarsellFlag1") + String getNarsellFlag1(); + + @Key("NarsellFlag2") + String getNarsellFlag2(); + + @Key("NarsellFlag3") + String getNarsellFlag3(); + + @Key("BayouCommander1") + String getBayouCommander1(); + + @Key("BayouCommander2") + String getBayouCommander2(); + + @Key("BayouCommander3") + String getBayouCommander3(); + + @Key("BayouCommander4") + String getBayouCommander4(); + + @Key("BayouFlag1") + String getBayouFlag1(); + + @Key("BayouFlag2") + String getBayouFlag2(); + + @Key("BayouFlag3") + String getBayouFlag3(); + + @Key("WhiteSandsCommander1") + String getWhiteSandsCommander1(); + + @Key("WhiteSandsCommander2") + String getWhiteSandsCommander2(); + + @Key("WhiteSandsCommander3") + String getWhiteSandsCommander3(); + + @Key("WhiteSandsFlag1") + String getWhiteSandsFlag1(); + + @Key("WhiteSandsFlag2") + String getWhiteSandsFlag2(); + + @Key("WhiteSandsFlag3") + String getWhiteSandsFlag3(); + + @Key("BorderlandCommander1") + String getBorderlandCommander1(); + + @Key("BorderlandCommander2") + String getBorderlandCommander2(); + + @Key("BorderlandCommander3") + String getBorderlandCommander3(); + + @Key("BorderlandCommander4") + String getBorderlandCommander4(); + + @Key("BorderlandFlag1") + String getBorderlandFlag1(); + + @Key("BorderlandFlag2") + String getBorderlandFlag2(); + + @Key("BorderlandFlag3") + String getBorderlandFlag3(); + + @Key("SwampCommander1") + String getSwampCommander1(); + + @Key("SwampCommander2") + String getSwampCommander2(); + + @Key("SwampCommander3") + String getSwampCommander3(); + + @Key("SwampCommander4") + String getSwampCommander4(); + + @Key("SwampFlag1") + String getSwampFlag1(); + + @Key("SwampFlag2") + String getSwampFlag2(); + + @Key("SwampFlag3") + String getSwampFlag3(); + + @Key("ArchaicCommander1") + String getArchaicCommander1(); + + @Key("ArchaicCommander2") + String getArchaicCommander2(); + + @Key("ArchaicCommander3") + String getArchaicCommander3(); + + @Key("ArchaicFlag1") + String getArchaicFlag1(); + + @Key("ArchaicFlag2") + String getArchaicFlag2(); + + @Key("ArchaicFlag3") + String getArchaicFlag3(); + + @Key("FloranCommander1") + String getFloranCommander1(); + + @Key("FloranCommander2") + String getFloranCommander2(); + + @Key("FloranCommander3") + String getFloranCommander3(); + + @Key("FloranCommander4") + String getFloranCommander4(); + + @Key("FloranFlag1") + String getFloranFlag1(); + + @Key("FloranFlag2") + String getFloranFlag2(); + + @Key("FloranFlag3") + String getFloranFlag3(); + + @Key("CloudMountainCommander1") + String getCloudMountainCommander1(); + + @Key("CloudMountainCommander2") + String getCloudMountainCommander2(); + + @Key("CloudMountainCommander3") + String getCloudMountainCommander3(); + + @Key("CloudMountainCommander4") + String getCloudMountainCommander4(); + + @Key("CloudMountainFlag1") + String getCloudMountainFlag1(); + + @Key("CloudMountainFlag2") + String getCloudMountainFlag2(); + + @Key("CloudMountainFlag3") + String getCloudMountainFlag3(); + + @Key("TanorCommander1") + String getTanorCommander1(); + + @Key("TanorCommander2") + String getTanorCommander2(); + + @Key("TanorCommander3") + String getTanorCommander3(); + + @Key("TanorFlag1") + String getTanorFlag1(); + + @Key("TanorFlag2") + String getTanorFlag2(); + + @Key("TanorFlag3") + String getTanorFlag3(); + + @Key("DragonspineCommander1") + String getDragonspineCommander1(); + + @Key("DragonspineCommander2") + String getDragonspineCommander2(); + + @Key("DragonspineCommander3") + String getDragonspineCommander3(); + + @Key("DragonspineFlag1") + String getDragonspineFlag1(); + + @Key("DragonspineFlag2") + String getDragonspineFlag2(); + + @Key("DragonspineFlag3") + String getDragonspineFlag3(); + + @Key("AntharasCommander1") + String getAntharasCommander1(); + + @Key("AntharasCommander2") + String getAntharasCommander2(); + + @Key("AntharasCommander3") + String getAntharasCommander3(); + + @Key("AntharasCommander4") + String getAntharasCommander4(); + + @Key("AntharasFlag1") + String getAntharasFlag1(); + + @Key("AntharasFlag2") + String getAntharasFlag2(); + + @Key("AntharasFlag3") + String getAntharasFlag3(); + + @Key("WesternCommander1") + String getWesternCommander1(); + + @Key("WesternCommander2") + String getWesternCommander2(); + + @Key("WesternCommander3") + String getWesternCommander3(); + + @Key("WesternCommander4") + String getWesternCommander4(); + + @Key("WesternFlag1") + String getWesternFlag1(); + + @Key("WesternFlag2") + String getWesternFlag2(); + + @Key("WesternFlag3") + String getWesternFlag3(); + + @Key("HuntersCommander1") + String getHuntersCommander1(); + + @Key("HuntersCommander2") + String getHuntersCommander2(); + + @Key("HuntersCommander3") + String getHuntersCommander3(); + + @Key("HuntersCommander4") + String getHuntersCommander4(); + + @Key("HuntersFlag1") + String getHuntersFlag1(); + + @Key("HuntersFlag2") + String getHuntersFlag2(); + + @Key("HuntersFlag3") + String getHuntersFlag3(); + + @Key("AaruCommander1") + String getAaruCommander1(); + + @Key("AaruCommander2") + String getAaruCommander2(); + + @Key("AaruCommander3") + String getAaruCommander3(); + + @Key("AaruFlag1") + String getAaruFlag1(); + + @Key("AaruFlag2") + String getAaruFlag2(); + + @Key("AaruFlag3") + String getAaruFlag3(); + + @Key("DemonCommander1") + String getDemonCommander1(); + + @Key("DemonCommander2") + String getDemonCommander2(); + + @Key("DemonCommander3") + String getDemonCommander3(); + + @Key("DemonFlag1") + String getDemonFlag1(); + + @Key("DemonFlag2") + String getDemonFlag2(); + + @Key("DemonFlag3") + String getDemonFlag3(); + + @Key("MonasticCommander1") + String getMonasticCommander1(); + + @Key("MonasticCommander2") + String getMonasticCommander2(); + + @Key("MonasticCommander3") + String getMonasticCommander3(); + + @Key("MonasticFlag1") + String getMonasticFlag1(); + + @Key("MonasticFlag2") + String getMonasticFlag2(); + + @Key("MonasticFlag3") + String getMonasticFlag3(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/FortressConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/FortressConfiguration.java new file mode 100644 index 00000000..e6d269ab --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/FortressConfiguration.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Minutes2MillisecondsConverter; + +/** + * Fortress Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/fortress.properties", + "file:./config/fortress.properties", + "classpath:config/fortress.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface FortressConfiguration extends Reloadable { + + @Key("FunctionFeeDay7") + long getFunctionFeeDay7(); + + @Key("TeleportFunctionFeeLvl11") + int getTeleportFunctionFeeLvl11(); + + @Key("TeleportFunctionFeeLvl12") + int getTeleportFunctionFeeLvl12(); + + @Key("SupportFeeLvl15") + int getSupportFeeLvl15(); + + @Key("SupportFeeLvl18") + int getSupportFeeLvl18(); + + @Key("MpRegenerationFeeLvl18") + int getMpRegenerationFeeLvl18(); + + @Key("MpRegenerationFeeLvl20") + int getMpRegenerationFeeLvl20(); + + @Key("HpRegenerationFeeLvl25") + int getHpRegenerationFeeLvl25(); + + @Key("HpRegenerationFeeLvl30") + int getHpRegenerationFeeLvl30(); + + @Key("ExpRegenerationFeeLvl19") + int getExpRegenerationFeeLvl19(); + + @Key("ExpRegenerationFeeLvl20") + int getExpRegenerationFeeLvl20(); + + @Key("PeriodicUpdateFrequency") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getPeriodicUpdateFrequency(); + + @Key("BloodOathCount") + int getBloodOathCount(); + + @Key("MaxSupplyLevel") + int getMaxSupplyLevel(); + + @Key("FeeForCastle") + int getFeeForCastle(); + + @Key("MaxKeepTime") + int getMaxKeepTime(); + + @Key("MpBuffFree") + boolean mpBuffFree(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/GeneralConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/GeneralConfiguration.java new file mode 100644 index 00000000..65345be4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/GeneralConfiguration.java @@ -0,0 +1,650 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Mutable; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Minutes2MillisecondsConverter; +import com.l2jserver.gameserver.config.converter.Seconds2MillisecondsConverter; +import com.l2jserver.gameserver.config.converter.ServerListTypeConverter; +import com.l2jserver.gameserver.enums.IllegalActionPunishmentType; + +/** + * General Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/general.properties", + "file:./config/general.properties", + "classpath:config/general.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface GeneralConfiguration extends Mutable, Reloadable { + + @Key("EverybodyHasAdminRights") + boolean everybodyHasAdminRights(); + + @Key("ServerListBrackets") + boolean getServerListBrackets(); + + @Key("ServerListType") + @ConverterClass(ServerListTypeConverter.class) + int getServerListType(); + + @Key("ServerListAge") + int getServerListAge(); + + @Key("ServerGMOnly") + boolean serverGMOnly(); + + @Key("GMHeroAura") + boolean gmHeroAura(); + + @Key("GMStartupInvulnerable") + boolean gmStartupInvulnerable(); + + @Key("GMStartupInvisible") + boolean gmStartupInvisible(); + + @Key("GMStartupSilence") + boolean gmStartupSilence(); + + @Key("GMStartupAutoList") + boolean gmStartupAutoList(); + + @Key("GMStartupDietMode") + boolean gmStartupDietMode(); + + @Key("GMItemRestriction") + boolean gmItemRestriction(); + + @Key("GMSkillRestriction") + boolean gmSkillRestriction(); + + @Key("GMTradeRestrictedItems") + boolean gmTradeRestrictedItems(); + + @Key("GMRestartFighting") + boolean gmRestartFighting(); + + @Key("GMShowAnnouncerName") + boolean gmShowAnnouncerName(); + + // TODO(Zoey76): Implement GMShowCritAnnouncerName configuration. + @Key("GMShowCritAnnouncerName") + boolean gmShowCritAnnouncerName(); + + @Key("GMGiveSpecialSkills") + boolean gmGiveSpecialSkills(); + + @Key("GMGiveSpecialAuraSkills") + boolean gmGiveSpecialAuraSkills(); + + @Key("GameGuardEnforce") + boolean gameGuardEnforce(); + + // TODO(Zoey76): Implement GameGuardProhibitAction configuration. + @Key("GameGuardProhibitAction") + boolean gameGuardProhibitAction(); + + @Key("LogChat") + boolean logChat(); + + // TODO(Zoey76): Implement LogAutoAnnouncements configuration. + @Key("LogAutoAnnouncements") + boolean logAutoAnnouncements(); + + @Key("LogItems") + boolean logItems(); + + @Key("LogItemsSmallLog") + boolean logItemsSmallLog(); + + @Key("LogItemEnchants") + boolean logItemEnchants(); + + @Key("LogSkillEnchants") + boolean logSkillEnchants(); + + @Key("GMAudit") + boolean gmAudit(); + + @Key("SkillCheckEnable") + boolean skillCheckEnable(); + + @Key("SkillCheckRemove") + boolean skillCheckRemove(); + + @Key("SkillCheckGM") + boolean skillCheckGM(); + + @Key("ThreadPoolSizeEffects") + int getThreadPoolSizeEffects(); + + @Key("ThreadPoolSizeGeneral") + int getThreadPoolSizeGeneral(); + + @Key("ThreadPoolSizeEvents") + int getThreadPoolSizeEvents(); + + @Key("UrgentPacketThreadCoreSize") + int getUrgentPacketThreadCoreSize(); + + @Key("GeneralPacketThreadCoreSize") + int getGeneralPacketThreadCoreSize(); + + @Key("GeneralThreadCoreSize") + int getGeneralThreadCoreSize(); + + @Key("AiMaxThread") + int getAiMaxThread(); + + @Key("EventsMaxThread") + int getEventsMaxThread(); + + @Key("DeadLockDetector") + boolean deadLockDetector(); + + @Key("DeadLockCheckInterval") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getDeadLockCheckInterval(); + + @Key("RestartOnDeadlock") + boolean restartOnDeadlock(); + + @Key("ClientPacketQueueSize") + int getClientPacketQueueSize(); + + @Key("ClientPacketQueueMaxBurstSize") + int getClientPacketQueueMaxBurstSize(); + + @Key("ClientPacketQueueMaxPacketsPerSecond") + int getClientPacketQueueMaxPacketsPerSecond(); + + @Key("ClientPacketQueueMeasureInterval") + int getClientPacketQueueMeasureInterval(); + + @Key("ClientPacketQueueMaxAveragePacketsPerSecond") + int getClientPacketQueueMaxAveragePacketsPerSecond(); + + @Key("ClientPacketQueueMaxFloodsPerMin") + int getClientPacketQueueMaxFloodsPerMin(); + + @Key("ClientPacketQueueMaxOverflowsPerMin") + int getClientPacketQueueMaxOverflowsPerMin(); + + @Key("ClientPacketQueueMaxUnderflowsPerMin") + int getClientPacketQueueMaxUnderflowsPerMin(); + + @Key("ClientPacketQueueMaxUnknownPerMin") + int getClientPacketQueueMaxUnknownPerMin(); + + @Key("AllowDiscardItem") + boolean allowDiscardItem(); + + @Key("AutoDestroyDroppedItemAfter") + int getAutoDestroyDroppedItemAfter(); + + @Key("AutoDestroyHerbTime") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getAutoDestroyHerbTime(); + + @Key("ProtectedItems") + Set getProtectedItems(); + + @Key("DatabaseCleanUp") + boolean databaseCleanUp(); + + // TODO(Zoey76): Implement ConnectionCloseTime configuration. + @Key("ConnectionCloseTime") + long getConnectionCloseTime(); + + @Key("CharacterDataStoreInterval") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getCharacterDataStoreInterval(); + + @Key("LazyItemsUpdate") + boolean lazyItemsUpdate(); + + @Key("UpdateItemsOnCharStore") + boolean updateItemsOnCharStore(); + + @Key("DestroyPlayerDroppedItem") + boolean destroyPlayerDroppedItem(); + + @Key("DestroyEquipableItem") + boolean destroyEquipableItem(); + + @Key("SaveDroppedItem") + boolean saveDroppedItem(); + + @Key("EmptyDroppedItemTableAfterLoad") + boolean emptyDroppedItemTableAfterLoad(); + + @Key("SaveDroppedItemInterval") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getSaveDroppedItemInterval(); + + @Key("ClearDroppedItemTable") + boolean clearDroppedItemTable(); + + @Key("AutoDeleteInvalidQuestData") + boolean autoDeleteInvalidQuestData(); + + @Key("PreciseDropCalculation") + boolean preciseDropCalculation(); + + @Key("PreciseDropMultipleGroupRolls") + boolean preciseDropMultipleGroupRolls(); + + @Key("PreciseDropMultipleRollsAggregateDrops") + boolean preciseDropMultipleRollsAggregateDrops(); + + @Key("MultipleItemDrop") + boolean multipleItemDrop(); + + @Key("ForceInventoryUpdate") + boolean forceInventoryUpdate(); + + @Key("LazyCache") + boolean lazyCache(); + + @Key("CacheCharNames") + boolean cacheCharNames(); + + @Key("MinNPCAnimation") + int getMinNPCAnimation(); + + @Key("MaxNPCAnimation") + int getMaxNPCAnimation(); + + @Key("MinMonsterAnimation") + int getMinMonsterAnimation(); + + @Key("MaxMonsterAnimation") + int getMaxMonsterAnimation(); + + @Key("MoveBasedKnownList") + boolean moveBasedKnownList(); + + @Key("KnownListUpdateInterval") + long getKnownListUpdateInterval(); + + @Key("CheckKnownList") + boolean checkKnownList(); + + @Key("GridsAlwaysOn") + boolean gridsAlwaysOn(); + + @Key("GridNeighborTurnOnTime") + int getGridNeighborTurnOnTime(); + + @Key("GridNeighborTurnOffTime") + int getGridNeighborTurnOffTime(); + + @Key("EnableFallingDamage") + boolean enableFallingDamage(); + + @Key("PeaceZoneMode") + int getPeaceZoneMode(); + + @Key("GlobalChat") + String getGlobalChat(); + + @Key("TradeChat") + String getTradeChat(); + + @Key("AllowWarehouse") + boolean allowWarehouse(); + + @Key("WarehouseCache") + boolean warehouseCache(); + + @Key("WarehouseCacheTime") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getWarehouseCacheTime(); + + @Key("AllowRefund") + boolean allowRefund(); + + @Key("AllowMail") + boolean allowMail(); + + @Key("AllowAttachments") + boolean allowAttachments(); + + @Key("AllowWear") + boolean allowWear(); + + @Key("WearDelay") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getWearDelay(); + + @Key("WearPrice") + int getWearPrice(); + + @Key("RestorePlayerInstance") + boolean restorePlayerInstance(); + + @Key("AllowSummonInInstance") + boolean allowSummonInInstance(); + + @Key("EjectDeadPlayerTime") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getEjectDeadPlayerTime(); + + @Key("InstanceFinishTime") + int getInstanceFinishTime(); + + // TODO(Zoey76): Implement AllowRace configuration. + @Key("AllowRace") + boolean allowRace(); + + @Key("AllowWater") + boolean allowWater(); + + @Key("AllowRentPet") + boolean allowRentPet(); + + @Key("AllowFishing") + boolean allowFishing(); + + @Key("AllowBoat") + boolean allowBoat(); + + @Key("BoatBroadcastRadius") + int getBoatBroadcastRadius(); + + @Key("AllowCursedWeapons") + boolean allowCursedWeapons(); + + @Key("AllowPetWalkers") + boolean allowPetWalkers(); + + @Key("ShowServerNews") + boolean showServerNews(); + + @Key("EnableCommunityBoard") + boolean enableCommunityBoard(); + + @Key("BBSDefault") + String getBBSDefault(); + + @Key("UseChatFilter") + boolean useChatFilter(); + + @Key("ChatFilterChars") + String getChatFilterChars(); + + @Key("ChatFilter") + Set getChatFilter(); + + @Key("BanChatChannels") + Set getBanChatChannels(); + + @Key("AllowManor") + boolean allowManor(); + + @Key("ManorRefreshTime") + int getManorRefreshTime(); + + @Key("ManorRefreshMin") + int getManorRefreshMin(); + + @Key("ManorApproveTime") + int getManorApproveTime(); + + @Key("ManorApproveMin") + int getManorApproveMin(); + + @Key("ManorMaintenanceMin") + int getManorMaintenanceMin(); + + @Key("ManorSaveAllActions") + boolean manorSaveAllActions(); + + @Key("ManorSavePeriodRate") + int getManorSavePeriodRate(); + + @Key("AllowLottery") + boolean allowLottery(); + + @Key("LotteryPrize") + long getLotteryPrize(); + + @Key("LotteryTicketPrice") + long getLotteryTicketPrice(); + + @Key("Lottery5NumberRate") + float getLottery5NumberRate(); + + @Key("Lottery4NumberRate") + float getLottery4NumberRate(); + + @Key("Lottery3NumberRate") + float getLottery3NumberRate(); + + @Key("Lottery2and1NumberPrize") + long getLottery2and1NumberPrize(); + + @Key("ItemAuctionEnabled") + boolean itemAuctionEnabled(); + + @Key("ItemAuctionExpiredAfter") + int getItemAuctionExpiredAfter(); + + @Key("ItemAuctionTimeExtendsOnBid") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getItemAuctionTimeExtendsOnBid(); + + // TODO(Zoey76): Implement RiftMinPartySize configuration. + @Key("RiftMinPartySize") + int getRiftMinPartySize(); + + @Key("MaxRiftJumps") + int getMaxRiftJumps(); + + @Key("RiftSpawnDelay") + int getRiftSpawnDelay(); + + @Key("AutoJumpsDelayMin") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getAutoJumpsDelayMin(); + + @Key("AutoJumpsDelayMax") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getAutoJumpsDelayMax(); + + @Key("BossRoomTimeMultiply") + float getBossRoomTimeMultiply(); + + // TODO(Zoey76): Implement RecruitCost configuration. + @Key("RecruitCost") + int getRecruitCost(); + + // TODO(Zoey76): Implement SoldierCost configuration. + @Key("SoldierCost") + int getSoldierCost(); + + // TODO(Zoey76): Implement OfficerCost configuration. + @Key("OfficerCost") + int getOfficerCost(); + + // TODO(Zoey76): Implement CaptainCost configuration. + @Key("CaptainCost") + int getCaptainCost(); + + // TODO(Zoey76): Implement CommanderCost configuration. + @Key("CommanderCost") + int getCommanderCost(); + + // TODO(Zoey76): Implement HeroCost configuration. + @Key("HeroCost") + int getHeroCost(); + + @Key("TimeOfAttack") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getTimeOfAttack(); + + // TODO(Zoey76): Implement TimeOfCoolDown configuration. + @Key("TimeOfCoolDown") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getTimeOfCoolDown(); + + @Key("TimeOfEntry") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getTimeOfEntry(); + + @Key("TimeOfWarmUp") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getTimeOfWarmUp(); + + // TODO(Zoey76): Move this four sepulchers to own configuration file. + @Key("NumberOfNecessaryPartyMembers") + int getNumberOfNecessaryPartyMembers(); + + @Key("DefaultPunish") + IllegalActionPunishmentType getDefaultPunish(); + + @Key("DefaultPunishParam") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getDefaultPunishParam(); + + @Key("OnlyGMItemsFree") + boolean onlyGMItemsFree(); + + @Key("JailIsPvp") + boolean jailIsPvp(); + + @Key("JailDisableChat") + boolean jailDisableChat(); + + @Key("JailDisableTransaction") + boolean jailDisableTransaction(); + + @Key("NormalEnchantCostMultipiler") + int getNormalEnchantCostMultipiler(); + + @Key("SafeEnchantCostMultipiler") + int getSafeEnchantCostMultipiler(); + + @Key("CustomSpawnlistTable") + boolean customSpawnlistTable(); + + @Key("SaveGmSpawnOnCustom") + boolean saveGmSpawnOnCustom(); + + @Key("CustomNpcData") + boolean customNpcData(); + + @Key("CustomTeleportTable") + boolean customTeleportTable(); + + @Key("CustomNpcBufferTables") + boolean customNpcBufferTables(); + + @Key("CustomSkillsLoad") + boolean customSkillsLoad(); + + @Key("CustomItemsLoad") + boolean customItemsLoad(); + + @Key("CustomMultisellLoad") + boolean customMultisellLoad(); + + @Key("CustomBuyListLoad") + boolean customBuyListLoad(); + + @Key("BirthdayGift") + int getBirthdayGift(); + + @Key("BirthdayMailSubject") + String getBirthdayMailSubject(); + + @Key("BirthdayMailText") + String getBirthdayMailText(); + + @Key("EnableBlockCheckerEvent") + boolean enableBlockCheckerEvent(); + + @Key("BlockCheckerMinTeamMembers") + int getBlockCheckerMinTeamMembers(); + + @Key("HBCEFairPlay") + boolean isHBCEFairPlay(); + + @Key("HellboundWithoutQuest") + boolean hellboundWithoutQuest(); + + @Key("EnableBotReportButton") + boolean enableBotReportButton(); + + @Key("BotReportPointsResetHour") + String getBotReportPointsResetHour(); + + @Key("BotReportDelay") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getBotReportDelay(); + + @Key("AllowReportsFromSameClanMembers") + boolean allowReportsFromSameClanMembers(); + + @Key("Debug") + boolean debug(); + + @Key("InstanceDebug") + boolean instanceDebug(); + + @Key("HtmlActionCacheDebug") + boolean htmlActionCacheDebug(); + + @Key("PacketHandlerDebug") + boolean packetHandlerDebug(); + + @Key("Developer") + boolean developer(); + + @Key("NoHandlers") + boolean noHandlers(); + + @Key("NoQuests") + boolean noQuests(); + + @Key("NoSpawns") + boolean noSpawns(); + + @Key("ShowQuestsLoadInLogs") + boolean showQuestsLoadInLogs(); + + // TODO(Zoey76): Implement ShowScriptsLoadInLogs configuration. + @Key("ShowScriptsLoadInLogs") + boolean showScriptsLoadInLogs(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/GeodataConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/GeodataConfiguration.java new file mode 100644 index 00000000..f2c6eca2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/GeodataConfiguration.java @@ -0,0 +1,106 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.io.File; +import java.util.List; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Mutable; +import org.aeonbits.owner.Reloadable; + +/** + * Geodata Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/geodata.properties", + "file:./config/geodata.properties", + "classpath:config/geodata.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface GeodataConfiguration extends Mutable, Reloadable { + + /** + * Pathfinding options: + *
      + *
    • 0 = Disabled
    • + *
    • 1 = Enabled using path node files
    • + *
    • 2 = Enabled using geodata cells at runtime
    • + *
    + * @return + */ + @Key("PathFinding") + int getPathFinding(); + + @Key("PathnodePath") + File getPathnodePath(); + + @Key("PathFindBuffers") + String getPathFindBuffers(); + + @Key("LowWeight") + float getLowWeight(); + + @Key("MediumWeight") + float getMediumWeight(); + + @Key("HighWeight") + float getHighWeight(); + + @Key("AdvancedDiagonalStrategy") + boolean advancedDiagonalStrategy(); + + @Key("DiagonalWeight") + float getDiagonalWeight(); + + @Key("MaxPostfilterPasses") + int getMaxPostfilterPasses(); + + @Key("DebugPath") + boolean debugPath(); + + @Key("ForceGeoData") + boolean forceGeoData(); + + @Key("CoordSynchronize") + int getCoordSynchronize(); + + @Key("GeoDataPath") + File getGeoDataPath(); + + @Key("TryLoadUnspecifiedRegions") + boolean tryLoadUnspecifiedRegions(); + + @Separator(";") + @Key("IncludedRegions") + List getIncludedRegions(); + + @Separator(";") + @Key("ExcludedRegions") + List getExcludedRegions(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/GraciaSeedsConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/GraciaSeedsConfiguration.java new file mode 100644 index 00000000..89062416 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/GraciaSeedsConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Minutes2MillisecondsConverter; + +/** + * Gracia Seeds Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/graciaseeds.properties", + "file:./config/graciaseeds.properties", + "classpath:config/graciaseeds.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface GraciaSeedsConfiguration extends Reloadable { + + @Key("TiatKillCountForNextState") + int getTiatKillCountForNextState(); + + @Key("Stage2Length") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getStage2Length(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/GrandBossConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/GrandBossConfiguration.java new file mode 100644 index 00000000..270ce749 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/GrandBossConfiguration.java @@ -0,0 +1,135 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.Hours2MillisecondsConverter; +import com.l2jserver.gameserver.config.converter.Minutes2MillisecondsConverter; + +/** + * Grand Boss Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/grandboss.properties", + "file:./config/grandboss.properties", + "classpath:config/grandboss.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface GrandBossConfiguration extends Reloadable { + + @Key("AntharasWaitTime") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getAntharasWaitTime(); + + @Key("IntervalOfAntharasSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfAntharasSpawn(); + + @Key("RandomOfAntharasSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfAntharasSpawn(); + + @Key("ValakasWaitTime") + @ConverterClass(Minutes2MillisecondsConverter.class) + long getValakasWaitTime(); + + @Key("IntervalOfValakasSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfValakasSpawn(); + + @Key("RandomOfValakasSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfValakasSpawn(); + + @Key("IntervalOfBaiumSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfBaiumSpawn(); + + @Key("RandomOfBaiumSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfBaiumSpawn(); + + @Key("IntervalOfCoreSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfCoreSpawn(); + + @Key("RandomOfCoreSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfCoreSpawn(); + + @Key("IntervalOfOrfenSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfOrfenSpawn(); + + @Key("RandomOfOrfenSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfOrfenSpawn(); + + @Key("IntervalOfQueenAntSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfQueenAntSpawn(); + + @Key("RandomOfQueenAntSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfQueenAntSpawn(); + + @Key("IntervalOfBelethSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getIntervalOfBelethSpawn(); + + @Key("RandomOfBelethSpawn") + @ConverterClass(Hours2MillisecondsConverter.class) + long getRandomOfBelethSpawn(); + + @Key("BelethMinPlayers") + int getBelethMinPlayers(); + + // TODO(Zoey76): Implement AllowZakenWithoutParty configuration. + @Key("AllowZakenWithoutParty") + boolean getAllowZakenWithoutParty(); + + // TODO(Zoey76): Implement ZakenMinPlayers configuration. + @Key("ZakenMinPlayers") + List getZakenMinPlayers(); + + // TODO(Zoey76): Implement ZakenMaxPlayers configuration. + @Key("ZakenMaxPlayers") + List getZakenMaxPlayers(); + + // TODO(Zoey76): Implement ZakenMinPlayerLevel configuration. + @Key("ZakenMinPlayerLevel") + List getZakenMinPlayerLevel(); + + // TODO(Zoey76): Implement ZakenCanBeRevealedByAOESpells configuration. + @Key("ZakenCanBeRevealedByAOESpells") + boolean getZakenCanBeRevealedByAoeSPells(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/HexIdConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/HexIdConfiguration.java new file mode 100644 index 00000000..853f70cc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/HexIdConfiguration.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.math.BigInteger; + +import org.aeonbits.owner.Accessible; +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Mutable; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.HexIdConverter; + +/** + * Hex Id Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/" + Configuration.CUSTOM_SUBPATH + HexIdConfiguration.FILENAME, + "file:" + Configuration.DEFAULT_PATH + HexIdConfiguration.FILENAME, + "classpath:" + Configuration.DEFAULT_PATH + HexIdConfiguration.FILENAME +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface HexIdConfiguration extends Mutable, Reloadable, Accessible { + String FILENAME = "hexid.txt"; + + String SERVERID_KEY = "ServerID"; + String HEXID_KEY = "HexID"; + + @Key(SERVERID_KEY) + int getServerID(); + + @Key(HEXID_KEY) + @ConverterClass(HexIdConverter.class) + BigInteger getHexID(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/HuntingSystemConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/HuntingSystemConfiguration.java new file mode 100644 index 00000000..5357e5cf --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/HuntingSystemConfiguration.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Hunting System Configuration. + * @author Maneco2 + * @version 2.6.3.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/huntingsystem.properties", + "file:./config/huntingsystem.properties", + "classpath:config/huntingsystem.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface HuntingSystemConfiguration extends Reloadable { + @Key("NevitEnable") + boolean getNevitEnable(); + + @Key("NevitBlessingMaxPoints") + int getNevitBlessingMaxPoints(); + + @Key("NevitBonusMaxTime") + int getHuntingBonusMaxTime(); + + @Key("NevitRegularPoints") + int getNevitRegularPoints(); + + @Key("NevitRegularPoints2") + int getNevitRegularPoints2(); + + @Key("NevitNormalPoints") + int getNevitNormalPoints(); + + @Key("NevitLevelAcquirePoints") + int getNevitLevelAcquirePoints(); + + @Key("NevitLevelAcquirePoints2") + int getNevitLevelAcquirePoints2(); + + @Key("NevitDecreaseVitalityAcquirePoints") + int getNevitDecreaseVitalityAcquirePoints(); + + @Key("NevitBonusEffectTime") + int getNevitBlessingEffetcTime(); + + @Key("HuntingSystemLimit") + boolean getHuntingBonusLimit(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/IPConfigData.java b/src/main/java/com/l2jserver/gameserver/config/IPConfigData.java new file mode 100644 index 00000000..0390964f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/IPConfigData.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static com.l2jserver.gameserver.GameServer.startTimedSection; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Inet6Address; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; + +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * IP Config Data. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class IPConfigData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(IPConfigData.class); + + private static final String IP_CONFIG_FILE = "./config/ipconfig.xml"; + + private final List _subnets = new ArrayList<>(5); + + private final List _hosts = new ArrayList<>(5); + + public IPConfigData() { + load(); + } + + @Override + public void load() { + startTimedSection("Network Configuration"); + final File f = new File(IP_CONFIG_FILE); + if (f.exists()) { + LOG.info("Using existing ipconfig.xml."); + parseFile(new File(IP_CONFIG_FILE)); + } else { + LOG.info("Using automatic network configuration."); + autoIpConfig(); + } + } + + @Override + public void parseDocument(Document doc) { + for (var n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("gameserver".equalsIgnoreCase(n.getNodeName())) { + for (var d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("define".equalsIgnoreCase(d.getNodeName())) { + final var attrs = d.getAttributes(); + _subnets.add(attrs.getNamedItem("subnet").getNodeValue()); + _hosts.add(attrs.getNamedItem("address").getNodeValue()); + + if (_hosts.size() != _subnets.size()) { + LOG.warn("Failed to load {} file - subnets does not match server addresses.", IP_CONFIG_FILE); + } + } + } + + final var att = n.getAttributes().getNamedItem("address"); + if (att == null) { + LOG.warn("Failed to load {} file - default server address is missing.", IP_CONFIG_FILE); + _hosts.add("127.0.0.1"); + } else { + _hosts.add(att.getNodeValue()); + } + _subnets.add("0.0.0.0/0"); + } + } + } + + protected void autoIpConfig() { + String externalIp; + try { + final var autoIp = URI.create("http://ip1.dynupdate.no-ip.com:8245/").toURL(); + try (var in = new BufferedReader(new InputStreamReader(autoIp.openStream()))) { + externalIp = in.readLine(); + } + } catch (IOException ex) { + LOG.warn("Failed to connect to ip1.dynupdate.no-ip.com:8245 please check your internet connection using 127.0.0.1!"); + externalIp = "127.0.0.1"; + } + + try { + final var niList = NetworkInterface.getNetworkInterfaces(); + while (niList.hasMoreElements()) { + final var ni = niList.nextElement(); + if (!ni.isUp() || ni.isVirtual()) { + continue; + } + + if (!ni.isLoopback() && ((ni.getHardwareAddress() == null) || (ni.getHardwareAddress().length != 6))) { + continue; + } + + for (InterfaceAddress ia : ni.getInterfaceAddresses()) { + if (ia.getAddress() instanceof Inet6Address) { + continue; + } + + final String hostAddress = ia.getAddress().getHostAddress(); + final int subnetPrefixLength = ia.getNetworkPrefixLength(); + final int subnetMaskInt = subnetPrefixLength == 0 ? 0 : -1 << (32 - subnetPrefixLength); + final int hostAddressInt = Arrays.stream(hostAddress.split("\\.")).mapToInt(Integer::parseInt).reduce((r, e) -> (r << 8) + e).orElse(0); + final int subnetAddressInt = hostAddressInt & subnetMaskInt; + final String subnetAddress = ((subnetAddressInt >> 24) & 0xFF) + "." + ((subnetAddressInt >> 16) & 0xFF) + "." + ((subnetAddressInt >> 8) & 0xFF) + "." + (subnetAddressInt & 0xFF); + final String subnet = subnetAddress + '/' + subnetPrefixLength; + if (!_subnets.contains(subnet) && !subnet.equals("0.0.0.0/0")) { + _subnets.add(subnet); + _hosts.add(hostAddress); + LOG.info("Adding new subnet: " + subnet + " address: " + hostAddress); + } + } + } + + // External host and subnet + _hosts.add(externalIp); + _subnets.add("0.0.0.0/0"); + LOG.info("Adding new subnet: 0.0.0.0/0 address: {}", externalIp); + } catch (SocketException e) { + LOG.error("Configuration failed please manually configure ipconfig.xml", e); + System.exit(0); + } + } + + public List getSubnets() { + if (_subnets.isEmpty()) { + return List.of("0.0.0.0/0"); + } + return _subnets; + } + + public List getHosts() { + if (_hosts.isEmpty()) { + return List.of("127.0.0.1"); + } + return _hosts; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/MMOConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/MMOConfiguration.java new file mode 100644 index 00000000..7e1972ce --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/MMOConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * MMO Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/mmo.properties", + "file:./config/mmo.properties", + "classpath:config/mmo.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface MMOConfiguration extends Reloadable { + + @Key("SleepTime") + int getSleepTime(); + + @Key("MaxSendPerPass") + int getMaxSendPerPass(); + + @Key("MaxReadPerPass") + int getMaxReadPerPass(); + + @Key("HelperBufferCount") + int getHelperBufferCount(); + + @Key("TcpNoDelay") + boolean isTcpNoDelay(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/NPCConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/NPCConfiguration.java new file mode 100644 index 00000000..051d7bd6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/NPCConfiguration.java @@ -0,0 +1,204 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.MapIntegerIntegerConverter; + +/** + * NPC Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/npc.properties", + "file:./config/npc.properties", + "classpath:config/npc.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface NPCConfiguration extends Reloadable { + + @Key("AnnounceMammonSpawn") + boolean announceMammonSpawn(); + + @Key("MobAggroInPeaceZone") + boolean mobAggroInPeaceZone(); + + @Key("AttackableNpcs") + boolean attackableNpcs(); + + @Key("ViewNpc") + boolean viewNpc(); + + @Key("MaxDriftRange") + int getMaxDriftRange(); + + @Key("ShowNpcLevel") + boolean showNpcLevel(); + + @Key("ShowCrestWithoutQuest") + boolean showCrestWithoutQuest(); + + @Key("RandomEnchantEffect") + boolean randomEnchantEffect(); + + @Key("MinNPCLevelForDmgPenalty") + int getMinNPCLevelForDmgPenalty(); + + @Key("DmgPenaltyForLvLDifferences") + List getDmgPenaltyForLvLDifferences(); + + @Key("CritDmgPenaltyForLvLDifferences") + List getCritDmgPenaltyForLvLDifferences(); + + @Key("SkillDmgPenaltyForLvLDifferences") + List getSkillDmgPenaltyForLvLDifferences(); + + // TODO(Zoey76): Implement MinNPCLevelForMagicPenalty configuration. + @Key("MinNPCLevelForMagicPenalty") + int getMinNPCLevelForMagicPenalty(); + + @Key("SkillChancePenaltyForLvLDifferences") + List getSkillChancePenaltyForLvLDifferences(); + + // Monsters + + // TODO(Zoey76): Implement DecayTimeTask configuration. + @Key("DecayTimeTask") + int getDecayTimeTask(); + + @Key("DefaultCorpseTime") + int getDefaultCorpseTime(); + + @Key("SpoiledCorpseExtendTime") + int getSpoiledCorpseExtendTime(); + + @Key("CorpseConsumeSkillAllowedTimeBeforeDecay") + int getCorpseConsumeSkillAllowedTimeBeforeDecay(); + + // Guards + + @Key("GuardAttackAggroMob") + boolean guardAttackAggroMob(); + + // Pets + + // TODO(Zoey76): Implement AllowWyvernUpgrader configuration. + @Key("AllowWyvernUpgrader") + int allowWyvernUpgrader(); + + @Key("PetRentNPCs") + Set getPetRentNPCs(); + + @Key("MaximumSlotsForPet") + int getMaximumSlotsForPet(); + + @Key("PetHpRegenMultiplier") + double getPetHpRegenMultiplier(); + + @Key("PetMpRegenMultiplier") + double getPetMpRegenMultiplier(); + + // Raid Bosses + + @Key("RaidHpRegenMultiplier") + double getRaidHpRegenMultiplier(); + + @Key("RaidMpRegenMultiplier") + double getRaidMpRegenMultiplier(); + + @Key("RaidPDefenceMultiplier") + double getRaidPDefenceMultiplier(); + + @Key("RaidMDefenceMultiplier") + double getRaidMDefenceMultiplier(); + + @Key("RaidPAttackMultiplier") + double getRaidPAttackMultiplier(); + + @Key("RaidMAttackMultiplier") + double getRaidMAttackMultiplier(); + + @Key("RaidMinRespawnMultiplier") + double getRaidMinRespawnMultiplier(); + + @Key("RaidMaxRespawnMultiplier") + double getRaidMaxRespawnMultiplier(); + + @Key("RaidMinionRespawnTime") + long getRaidMinionRespawnTime(); + + @Key("CustomMinionsRespawnTime") + @ConverterClass(MapIntegerIntegerConverter.class) + Map getCustomMinionsRespawnTime(); + + @Key("RaidCurse") + boolean raidCurse(); + + @Key("RaidChaosTime") + int getRaidChaosTime(); + + @Key("GrandChaosTime") + int getGrandChaosTime(); + + @Key("MinionChaosTime") + int getMinionChaosTime(); + + // Drops + + @Key("UseDeepBlueDropRules") + boolean useDeepBlueDropRules(); + + @Key("UseDeepBlueDropRulesRaid") + boolean useDeepBlueDropRulesRaid(); + + @Key("DropAdenaMinLevelDifference") + int getDropAdenaMinLevelDifference(); + + @Key("DropAdenaMaxLevelDifference") + int getDropAdenaMaxLevelDifference(); + + @Key("DropAdenaMinLevelGapChance") + int getDropAdenaMinLevelGapChance(); + + @Key("DropItemMinLevelDifference") + int getDropItemMinLevelDifference(); + + @Key("DropItemMaxLevelDifference") + int getDropItemMaxLevelDifference(); + + @Key("DropItemMinLevelGapChance") + int getDropItemMinLevelGapChance(); + + @Key("MaxAggroRange") + int getMaxAggroRange(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/OlympiadConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/OlympiadConfiguration.java new file mode 100644 index 00000000..a5c07e76 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/OlympiadConfiguration.java @@ -0,0 +1,189 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.ItemHolderConverter; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * Olympiad Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/olympiad.properties", + "file:./config/olympiad.properties", + "classpath:config/olympiad.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface OlympiadConfiguration extends Reloadable { + + @Key("StartHour") + int getStartHour(); + + @Key("StartMinute") + int getStartMinute(); + + @Key("MaxBuffs") + int getMaxBuffs(); + + // TODO(Zoey76): Check if this should be in minutes or other time unit. + @Key("CompetitionPeriod") + int getCompetitionPeriod(); + + @Key("BattlePeriod") + int getBattlePeriod(); + + // TODO(Zoey76): Check if this should be in minutes or other time unit. + @Key("WeeklyPeriod") + int getWeeklyPeriod(); + + // TODO(Zoey76): Check if this should be in minutes or other time unit. + @Key("ValidationPeriod") + int getValidationPeriod(); + + @Key("StartPoints") + int getStartPoints(); + + @Key("WeeklyPoints") + int getWeeklyPoints(); + + @Key("ClassedParticipants") + int getClassedParticipants(); + + @Key("NonClassedParticipants") + int getNonClassedParticipants(); + + @Key("TeamsParticipants") + int getTeamsParticipants(); + + @Key("RegistrationDisplayNumber") + int getRegistrationDisplayNumber(); + + @Separator(";") + @Key("ClassedReward") + @ConverterClass(ItemHolderConverter.class) + List getClassedReward(); + + @Separator(";") + @Key("NonClassedReward") + @ConverterClass(ItemHolderConverter.class) + List getNonClassedReward(); + + @Separator(";") + @Key("TeamReward") + @ConverterClass(ItemHolderConverter.class) + List getTeamReward(); + + @Key("CompetitionRewardItem") + int getCompetitionRewardItem(); + + @Key("MinMatchesForPoints") + int getMinMatchesForPoints(); + + @Key("GPPerPoint") + int getGPPerPoint(); + + @Key("HeroPoints") + int getHeroPoints(); + + @Key("Rank1Points") + int getRank1Points(); + + @Key("Rank2Points") + int getRank2Points(); + + @Key("Rank3Points") + int getRank3Points(); + + @Key("Rank4Points") + int getRank4Points(); + + @Key("Rank5Points") + int getRank5Points(); + + @Key("MaxPoints") + int getMaxPoints(); + + @Key("ShowMonthlyWinners") + boolean showMonthlyWinners(); + + @Key("AnnounceGames") + boolean announceGames(); + + @Key("RestrictedItems") + Set getRestrictedItems(); + + @Key("EnchantLimit") + int getEnchantLimit(); + + @Key("LogFights") + boolean logFights(); + + @Key("WaitTime") + int getWaitTime(); + + @Key("DividerClassed") + int getDividerClassed(); + + @Key("DividerNonClassed") + int getDividerNonClassed(); + + @Key("MaxWeeklyMatches") + int getMaxWeeklyMatches(); + + @Key("MaxWeeklyMatchesNonClassed") + int getMaxWeeklyMatchesNonClassed(); + + @Key("MaxWeeklyMatchesClassed") + int getMaxWeeklyMatchesClassed(); + + @Key("MaxWeeklyMatchesTeam") + int getMaxWeeklyMatchesTeam(); + + // TODO(Zoey76): Change this so the check doesn't depend on a null value. + @Key("CurrentCycle") + Integer getCurrentCycle(); + + @Key("Period") + int getPeriod(); + + // TODO(Zoey76): Check if this should be in minutes or other time unit. + @Key("OlympiadEnd") + long getOlympiadEnd(); + + @Key("ValidationEnd") + long getValidationEnd(); + + @Key("NextWeeklyChange") + long getNextWeeklyChange(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/PvPConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/PvPConfiguration.java new file mode 100644 index 00000000..4a2adf42 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/PvPConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * PvP Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/pvp.properties", + "file:./config/pvp.properties", + "classpath:config/pvp.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface PvPConfiguration extends Reloadable { + + @Key("CanGMDropEquipment") + boolean canGMDropEquipment(); + + @Key("PetItems") + Set getPetItems(); + + @Key("NonDroppableItems") + Set getNonDroppableItems(); + + @Key("MinimumPKRequiredToDrop") + int getMinimumPKRequiredToDrop(); + + @Key("AwardPKKillPVPPoint") + boolean awardPKKillPVPPoint(); + + @Key("PvPVsNormalTime") + int getPvPVsNormalTime(); + + @Key("PvPVsPvPTime") + int getPvPVsPvPTime(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/RatesConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/RatesConfiguration.java new file mode 100644 index 00000000..8991f67c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/RatesConfiguration.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.Map; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.MapIntegerFloatConverter; + +/** + * Rates Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/rates.properties", + "file:./config/rates.properties", + "classpath:config/rates.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface RatesConfiguration extends Reloadable { + + @Key("DeathDropAmountMultiplier") + double getDeathDropAmountMultiplier(); + + @Key("CorpseDropAmountMultiplier") + double getCorpseDropAmountMultiplier(); + + @Key("HerbDropAmountMultiplier") + double getHerbDropAmountMultiplier(); + + @Key("RaidDropAmountMultiplier") + double getRaidDropAmountMultiplier(); + + @Key("DeathDropChanceMultiplier") + double getDeathDropChanceMultiplier(); + + @Key("CorpseDropChanceMultiplier") + double getCorpseDropChanceMultiplier(); + + @Key("HerbDropChanceMultiplier") + double getHerbDropChanceMultiplier(); + + @Key("RaidDropChanceMultiplier") + double getRaidDropChanceMultiplier(); + + @Key("DropAmountMultiplierByItemId") + @ConverterClass(MapIntegerFloatConverter.class) + Map getDropAmountMultiplierByItemId(); + + @Key("DropChanceMultiplierByItemId") + @ConverterClass(MapIntegerFloatConverter.class) + Map getDropChanceMultiplierByItemId(); + + @Key("RateXp") + float getRateXp(); + + @Key("RateSp") + float getRateSp(); + + @Key("RatePartyXp") + float getRatePartyXp(); + + @Key("RatePartySp") + float getRatePartySp(); + + // TODO(Zoey76): Should this be int instead of float? + @Key("RateDropManor") + int getRateDropManor(); + + @Key("RateKarmaLost") + double getRateKarmaLost(); + + @Key("RateKarmaExpLost") + double getRateKarmaExpLost(); + + @Key("RateSiegeGuardsPrice") + double getRateSiegeGuardsPrice(); + + @Key("RateExtractable") + float getRateExtractable(); + + @Key("RateHellboundTrustIncrease") + float getRateHellboundTrustIncrease(); + + @Key("RateHellboundTrustDecrease") + float getRateHellboundTrustDecrease(); + + @Key("QuestDropChanceMultiplier") + float getQuestDropChanceMultiplier(); + + @Key("QuestDropAmountMultiplier") + float getQuestDropAmountMultiplier(); + + @Key("RateQuestRewardXP") + float getRateQuestRewardXP(); + + @Key("RateQuestRewardSP") + float getRateQuestRewardSP(); + + @Key("RateQuestRewardAdena") + float getRateQuestRewardAdena(); + + @Key("UseQuestRewardMultipliers") + boolean useQuestRewardMultipliers(); + + @Key("RateQuestReward") + float getRateQuestReward(); + + @Key("RateQuestRewardPotion") + float getRateQuestRewardPotion(); + + @Key("RateQuestRewardScroll") + float getRateQuestRewardScroll(); + + @Key("RateQuestRewardRecipe") + float getRateQuestRewardRecipe(); + + @Key("RateQuestRewardMaterial") + float getRateQuestRewardMaterial(); + + @Key("PlayerDropLimit") + int getPlayerDropLimit(); + + @Key("PlayerRateDrop") + int getPlayerRateDrop(); + + @Key("PlayerRateDropItem") + int getPlayerRateDropItem(); + + @Key("PlayerRateDropEquip") + int getPlayerRateDropEquip(); + + @Key("PlayerRateDropEquipWeapon") + int getPlayerRateDropEquipWeapon(); + + @Key("KarmaDropLimit") + int getKarmaDropLimit(); + + @Key("KarmaRateDrop") + int getKarmaRateDrop(); + + @Key("KarmaRateDropItem") + int getKarmaRateDropItem(); + + @Key("KarmaRateDropEquip") + int getKarmaRateDropEquip(); + + @Key("KarmaRateDropEquipWeapon") + int getKarmaRateDropEquipWeapon(); + + @Key("PetXpRate") + double getPetXpRate(); + + // TODO(Zoey76): Should PetFoodRate be float? + @Key("PetFoodRate") + int getPetFoodRate(); + + @Key("SinEaterXpRate") + double getSinEaterXpRate(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/ServerConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/ServerConfiguration.java new file mode 100644 index 00000000..68c5b766 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/ServerConfiguration.java @@ -0,0 +1,98 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.io.File; +import java.util.Set; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Mutable; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.FileConverter; +import com.l2jserver.gameserver.idfactory.IdFactoryType; + +/** + * Server Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/server.properties", + "file:./config/server.properties", + "classpath:config/server.properties", + "system:properties", + "system:env" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface ServerConfiguration extends Mutable, Reloadable { + + @Key("EnableUPnP") + boolean enableUPnP(); + + @Key("LoginHost") + String getLoginHost(); + + @Key("LoginPort") + int getLoginPort(); + + @Key("Host") + String getHost(); + + @Key("Port") + int getPort(); + + @Key("RequestServerId") + int getRequestServerId(); + + @Key("AcceptAlternateId") + boolean acceptAlternateId(); + + @Key("ReserveHostOnLogin") + boolean reserveHostOnLogin(); + + @Key("DatapackRoot") + @ConverterClass(FileConverter.class) + File getDatapackRoot(); + + @Key("ScriptRoot") + File getScriptRoot(); + + @Key("MaxOnlineUsers") + int getMaxOnlineUsers(); + + @Key("AllowedProtocolRevisions") + Set getAllowedProtocolRevisions(); + + @Key("IdFactory") + IdFactoryType getIdFactory(); + + @Key("BadIdChecking") + boolean badIdChecking(); + + @Key("L2J_HOME") + String getL2jHome(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/SevenSignsConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/SevenSignsConfiguration.java new file mode 100644 index 00000000..2d845114 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/SevenSignsConfiguration.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Seven Signs Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/sevensigns.properties", + "file:./config/sevensigns.properties", + "classpath:config/sevensigns.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface SevenSignsConfiguration extends Reloadable { + + // TODO(Zoey76): Implement RequireClanCastle configuration. + @Key("RequireClanCastle") + boolean requireClanCastle(); + + // TODO(Zoey76): Implement CastleForDawn configuration. + @Key("CastleForDawn") + boolean castleForDawn(); + + // TODO(Zoey76): Implement CastleForDusk configuration. + @Key("CastleForDusk") + boolean castleForDusk(); + + @Key("FestivalMinPlayer") + int getFestivalMinPlayer(); + + @Key("MaxPlayerContrib") + int getMaxPlayerContrib(); + + // TODO(Zoey76): Convert to milliseconds. + @Key("FestivalManagerStart") + int getFestivalManagerStart(); + + @Key("FestivalLength") + int getFestivalLength(); + + @Key("FestivalCycleLength") + int getFestivalCycleLength(); + + @Key("FestivalFirstSpawn") + int getFestivalFirstSpawn(); + + @Key("FestivalFirstSwarm") + int getFestivalFirstSwarm(); + + @Key("FestivalSecondSpawn") + int getFestivalSecondSpawn(); + + @Key("FestivalSecondSwarm") + int getFestivalSecondSwarm(); + + @Key("FestivalChestSpawn") + int getFestivalChestSpawn(); + + @Key("DawnGatesPdefMult") + double getDawnGatesPdefMult(); + + @Key("DuskGatesPdefMult") + double getDuskGatesPdefMult(); + + @Key("DawnGatesMdefMult") + double getDawnGatesMdefMult(); + + @Key("DuskGatesMdefMult") + double getDuskGatesMdefMult(); + + @Key("StrictSevenSigns") + boolean strictSevenSigns(); + + @Key("SevenSignsLazyUpdate") + boolean sevenSignsLazyUpdate(); + + @Key("SevenSignsDawnTicketQuantity") + int getSevenSignsDawnTicketQuantity(); + + @Key("SevenSignsDawnTicketPrice") + int getSevenSignsDawnTicketPrice(); + + @Key("SevenSignsDawnTicketBundle") + int getSevenSignsDawnTicketBundle(); + + @Key("SevenSignsManorsAgreementId") + int getSevenSignsManorsAgreementId(); + + @Key("SevenSignsJoinDawnFee") + int getSevenSignsJoinDawnFee(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/SiegeConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/SiegeConfiguration.java new file mode 100644 index 00000000..650e8acb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/SiegeConfiguration.java @@ -0,0 +1,275 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.ControlTowerConverter; +import com.l2jserver.gameserver.config.converter.FlameTowerConverter; +import com.l2jserver.gameserver.model.TowerSpawn; + +/** + * Siege Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/siege.properties", + "file:./config/siege.properties", + "classpath:config/siege.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface SiegeConfiguration extends Reloadable { + + @Key("SiegeLength") + int getSiegeLength(); + + @Key("MaxFlags") + int getMaxFlags(); + + @Key("ClanMinLevel") + int getClanMinLevel(); + + @Key("AttackerMaxClans") + int getAttackerMaxClans(); + + @Key("DefenderMaxClans") + int getDefenderMaxClans(); + + @Key("AttackerRespawn") + int getAttackerRespawn(); + + @Key("BloodAllianceReward") + int getBloodAllianceReward(); + + @Key("GludioFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGludioFlameTower1(); + + @Key("GludioFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGludioFlameTower2(); + + @Key("GludioControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGludioControlTower1(); + + @Key("GludioControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGludioControlTower2(); + + @Key("GludioControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGludioControlTower3(); + + @Key("GludioMaxMercenaries") + int getGludioMaxMercenaries(); + + @Key("GiranFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGiranFlameTower1(); + + @Key("GiranFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGiranFlameTower2(); + + @Key("GiranControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGiranControlTower1(); + + @Key("GiranControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGiranControlTower2(); + + @Key("GiranControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGiranControlTower3(); + + @Key("GiranMaxMercenaries") + int getGiranMaxMercenaries(); + + @Key("DionFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getDionFlameTower1(); + + @Key("DionFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getDionFlameTower2(); + + @Key("DionControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getDionControlTower1(); + + @Key("DionControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getDionControlTower2(); + + @Key("DionControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getDionControlTower3(); + + @Key("DionMaxMercenaries") + int getDionMaxMercenaries(); + + @Key("OrenFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getOrenFlameTower1(); + + @Key("OrenFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getOrenFlameTower2(); + + @Key("OrenControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getOrenControlTower1(); + + @Key("OrenControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getOrenControlTower2(); + + @Key("OrenControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getOrenControlTower3(); + + @Key("OrenMaxMercenaries") + int getOrenMaxMercenaries(); + + @Key("AdenFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getAdenFlameTower1(); + + @Key("AdenFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn AdenFlameTower2(); + + @Key("AdenControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getAdenControlTower1(); + + @Key("AdenControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getAdenControlTower2(); + + @Key("AdenControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getAdenControlTower3(); + + @Key("AdenMaxMercenaries") + int getAdenMaxMercenaries(); + + @Key("InnadrilFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getInnadrilFlameTower1(); + + @Key("InnadrilFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getInnadrilFlameTower2(); + + @Key("InnadrilControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getInnadrilControlTower1(); + + @Key("InnadrilControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getInnadrilControlTower2(); + + @Key("InnadrilControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getInnadrilControlTower3(); + + @Key("InnadrilMaxMercenaries") + int getInnadrilMaxMercenaries(); + + @Key("GoddardFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGoddardFlameTower1(); + + @Key("GoddardFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getGoddardFlameTower2(); + + @Key("GoddardControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGoddardControlTower1(); + + @Key("GoddardControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGoddardControlTower2(); + + @Key("GoddardControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getGoddardControlTower3(); + + @Key("GoddardMaxMercenaries") + int getGoddardMaxMercenaries(); + + @Key("RuneFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getRuneFlameTower1(); + + @Key("RuneFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getRuneFlameTower2(); + + @Key("RuneControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getRuneControlTower1(); + + @Key("RuneControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getRuneControlTower2(); + + @Key("RuneControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getRuneControlTower3(); + + @Key("RuneMaxMercenaries") + int getRuneMaxMercenaries(); + + @Key("SchuttgartFlameTower1") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getSchuttgartFlameTower1(); + + @Key("SchuttgartFlameTower2") + @ConverterClass(FlameTowerConverter.class) + TowerSpawn getSchuttgartFlameTower2(); + + @Key("SchuttgartControlTower1") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getSchuttgartControlTower1(); + + @Key("SchuttgartControlTower2") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getSchuttgartControlTower2(); + + @Key("SchuttgartControlTower3") + @ConverterClass(ControlTowerConverter.class) + TowerSpawn getSchuttgartControlTower3(); + + @Key("SchuttgartMaxMercenaries") + int getSchuttgartMaxMercenaries(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/TeleporterServiceConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/TeleporterServiceConfiguration.java new file mode 100644 index 00000000..037e5d74 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/TeleporterServiceConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.model.zone.ZoneId; + +/** + * Teleporter Service Configuration. + * @author HorridoJoho + * @version 2.6.2.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/teleporterservice.properties", + "file:./config/teleporterservice.properties", + "classpath:config/teleporterservice.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface TeleporterServiceConfiguration extends Reloadable { + + @Key("Enable") + boolean enable(); + + @Key("Debug") + boolean getDebug(); + + @Key("ForbidInZones") + ZoneId[] getForbidInZones(); + + @Key("ForbidInEvents") + boolean getForbidInEvents(); + + @Key("ForbidInDuel") + boolean getForbidInDuel(); + + @Key("ForbidInFight") + boolean getForbidInFight(); + + @Key("ForbidInPvp") + boolean getForbidInPvp(); + + @Key("ForbidForChaoticPlayers") + boolean getForbidForChaoticPlayers(); + + @Key("VoicedEnable") + boolean getVoicedEnable(); + + @Key("VoicedCommand") + String getVoicedCommand(); + + @Key("VoicedName") + String getVoicedName(); + + @Key("VoicedRequiredItem") + int getVoicedRequiredItem(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/TelnetConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/TelnetConfiguration.java new file mode 100644 index 00000000..14ef0861 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/TelnetConfiguration.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Telnet Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/telnet.properties", + "file:./config/telnet.properties", + "classpath:config/telnet.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface TelnetConfiguration extends Reloadable { + + @Key("EnableTelnet") + boolean isEnabled(); + + @Key("Port") + int getPort(); + + @Key("Password") + String getPassword(); + + @Key("Hosts") + List getHosts(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/TerritoryWarConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/TerritoryWarConfiguration.java new file mode 100644 index 00000000..d87c80aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/TerritoryWarConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Territory War Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/territorywar.properties", + "file:./config/territorywar.properties", + "classpath:config/territorywar.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface TerritoryWarConfiguration extends Reloadable { + + // TODO(Zoey76): Convert this to milliseconds. + @Key("WarLength") + long getWarLength(); + + // TODO(Zoey76): Implement ClanMinLevel configuration. + @Key("ClanMinLevel") + int getClanMinLevel(); + + @Key("PlayerMinLevel") + int getPlayerMinLevel(); + + // TODO(Zoey76): Implement DefenderMaxClans configuration. + @Key("DefenderMaxClans") + int getDefenderMaxClans(); + + // TODO(Zoey76): Implement DefenderMaxPlayers configuration. + @Key("DefenderMaxPlayers") + int getDefenderMaxPlayers(); + + @Key("PlayerWithWardCanBeKilledInPeaceZone") + boolean playerWithWardCanBeKilledInPeaceZone(); + + @Key("SpawnWardsWhenTWIsNotInProgress") + boolean spawnWardsWhenTWIsNotInProgress(); + + @Key("ReturnWardsWhenTWStarts") + boolean returnWardsWhenTWStarts(); + + @Key("MinTerritoryBadgeForNobless") + int getMinTerritoryBadgeForNobless(); + + @Key("MinTerritoryBadgeForStriders") + int getMinTerritoryBadgeForStriders(); + + @Key("MinTerritoryBadgeForBigStrider") + int getMinTerritoryBadgeForBigStrider(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/TvTConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/TvTConfiguration.java new file mode 100644 index 00000000..a9758ee6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/TvTConfiguration.java @@ -0,0 +1,164 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import java.util.List; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +import com.l2jserver.gameserver.config.converter.ItemHolderConverter; +import com.l2jserver.gameserver.config.converter.LocationConverter; +import com.l2jserver.gameserver.config.converter.Seconds2MillisecondsConverter; +import com.l2jserver.gameserver.config.converter.SkillHolderConverter; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; + +/** + * TvT Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/tvt.properties", + "file:./config/tvt.properties", + "classpath:config/tvt.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface TvTConfiguration extends Reloadable { + + @Key("Enabled") + boolean enabled(); + + @Key("Instanced") + boolean instanced(); + + @Key("InstanceFile") + String getInstanceFile(); + + @Key("Interval") + List getInterval(); + + // TODO(Zoey76): Convert to milliseconds. + @Key("ParticipationTime") + int getParticipationTime(); + + // TODO(Zoey76): Convert to milliseconds. + @Key("RunningTime") + int getRunningTime(); + + @Key("ParticipationNpcId") + int getParticipationNpcId(); + + @Key("ParticipationFee") + @ConverterClass(ItemHolderConverter.class) + ItemHolder getParticipationFee(); + + @Key("ParticipationNpcLoc") + @ConverterClass(LocationConverter.class) + Location getParticipationNpcLoc(); + + @Key("MinPlayersInTeams") + int getMinPlayersInTeams(); + + @Key("MaxPlayersInTeams") + int getMaxPlayersInTeams(); + + @Key("MinPlayerLevel") + int getMinPlayerLevel(); + + @Key("MaxPlayerLevel") + int getMaxPlayerLevel(); + + @Key("RespawnTeleportDelay") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getRespawnTeleportDelay(); + + @Key("StartLeaveTeleportDelay") + @ConverterClass(Seconds2MillisecondsConverter.class) + long getStartLeaveTeleportDelay(); + + @Key("Team1Name") + String getTeam1Name(); + + @Key("Team1Loc") + @ConverterClass(LocationConverter.class) + Location getTeam1Loc(); + + @Key("Team2Name") + String getTeam2Name(); + + @Key("Team2Loc") + @ConverterClass(LocationConverter.class) + Location getTeam2Loc(); + + @Separator(";") + @Key("Reward") + @ConverterClass(ItemHolderConverter.class) + List getReward(); + + @Key("AllowTargetTeamMember") + boolean allowTargetTeamMember(); + + @Key("AllowScroll") + boolean allowScroll(); + + @Key("AllowPotion") + boolean allowPotion(); + + // TODO(Zoey76): Implement AllowSummonByItem configuration. + @Key("AllowSummonByItem") + boolean allowSummonByItem(); + + @Key("DoorsToOpen") + List getDoorsToOpen(); + + @Key("DoorsToClose") + List getDoorsToClose(); + + @Key("RewardTeamTie") + boolean rewardTeamTie(); + + @Key("EffectsRemoval") + int getEffectsRemoval(); + + @Separator(";") + @Key("FighterBuffs") + @ConverterClass(SkillHolderConverter.class) + List getFighterBuffs(); + + @Separator(";") + @Key("MageBuffs") + @ConverterClass(SkillHolderConverter.class) + List getMageBuffs(); + + @Key("MaxParticipantsPerIP") + int getMaxParticipantsPerIP(); + + @Key("AllowVoicedInfoCommand") + boolean allowVoicedInfoCommand(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/VitalityConfiguration.java b/src/main/java/com/l2jserver/gameserver/config/VitalityConfiguration.java new file mode 100644 index 00000000..e44bcecd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/VitalityConfiguration.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.aeonbits.owner.Config.HotReloadType.ASYNC; +import static org.aeonbits.owner.Config.LoadType.MERGE; + +import org.aeonbits.owner.Config.HotReload; +import org.aeonbits.owner.Config.LoadPolicy; +import org.aeonbits.owner.Config.Sources; +import org.aeonbits.owner.Reloadable; + +/** + * Vitality Configuration. + * @author Zoey76 + * @version 2.6.1.0 + */ +@Sources({ + "file:${L2J_HOME}/custom/game/config/vitality.properties", + "file:./config/vitality.properties", + "classpath:config/vitality.properties" +}) +@LoadPolicy(MERGE) +@HotReload(value = 20, unit = MINUTES, type = ASYNC) +public interface VitalityConfiguration extends Reloadable { + + @Key("Enabled") + boolean enabled(); + + @Key("RecoverVitalityOnReconnect") + boolean recoverVitalityOnReconnect(); + + @Key("StartingVitalityPoints") + int getStartingVitalityPoints(); + + @Key("RateVitalityLevel1") + float getRateVitalityLevel1(); + + @Key("RateVitalityLevel2") + float getRateVitalityLevel2(); + + @Key("RateVitalityLevel3") + float getRateVitalityLevel3(); + + @Key("RateVitalityLevel4") + float getRateVitalityLevel4(); + + @Key("RateVitalityGain") + float getRateVitalityGain(); + + @Key("RateVitalityLost") + float getRateVitalityLost(); + + @Key("RateRecoveryPeaceZone") + float getRateRecoveryPeaceZone(); + + @Key("RateRecoveryOnReconnect") + float getRateRecoveryOnReconnect(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSetting.java b/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSetting.java new file mode 100644 index 00000000..1ee1f6a5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSetting.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * Class Master Setting. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ClassMasterSetting { + + private final Map> claimItems = new HashMap<>(3); + + private final Map> rewardItems = new HashMap<>(3); + + private final Map allowedClassChange = new HashMap<>(3); + + public void addClaimItems(Integer job, List items) { + claimItems.put(job, items); + } + + public void addRewardItems(Integer job, List items) { + rewardItems.put(job, items); + } + + public void addAllowedClassChange(Integer job) { + allowedClassChange.put(job, true); + } + + public boolean isAllowed(int job) { + return allowedClassChange.getOrDefault(job, false); + } + + public List getRewardItems(int job) { + return rewardItems.getOrDefault(job, Collections.emptyList()); + } + + public List getRequireItems(int job) { + return claimItems.getOrDefault(job, Collections.emptyList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSettingConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSettingConverter.java new file mode 100644 index 00000000..fac6c29f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ClassMasterSettingConverter.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.aeonbits.owner.Converter; + +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * Class Master Setting converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ClassMasterSettingConverter implements Converter { + + @Override + public ClassMasterSetting convert(Method method, String input) { + final var classMasterSetting = new ClassMasterSetting(); + input = input.trim(); + if (input.isEmpty()) { + return classMasterSetting; + } + + final StringTokenizer st = new StringTokenizer(input, ";"); + while (st.hasMoreTokens()) { + // get allowed class change + final int job = Integer.parseInt(st.nextToken()); + classMasterSetting.addAllowedClassChange(job); + + final List requiredItems = new ArrayList<>(); + // parse items needed for class change + if (st.hasMoreTokens()) { + final StringTokenizer st2 = new StringTokenizer(st.nextToken(), "[],"); + while (st2.hasMoreTokens()) { + final StringTokenizer st3 = new StringTokenizer(st2.nextToken(), "()"); + final int itemId = Integer.parseInt(st3.nextToken()); + final int quantity = Integer.parseInt(st3.nextToken()); + requiredItems.add(new ItemHolder(itemId, quantity)); + } + } + classMasterSetting.addClaimItems(job, requiredItems); + + final List rewardItems = new ArrayList<>(); + // parse gifts after class change + if (st.hasMoreTokens()) { + final StringTokenizer st2 = new StringTokenizer(st.nextToken(), "[],"); + while (st2.hasMoreTokens()) { + final StringTokenizer st3 = new StringTokenizer(st2.nextToken(), "()"); + final int itemId = Integer.parseInt(st3.nextToken()); + final int quantity = Integer.parseInt(st3.nextToken()); + rewardItems.add(new ItemHolder(itemId, quantity)); + } + } + classMasterSetting.addRewardItems(job, rewardItems); + } + return classMasterSetting; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ColorConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ColorConverter.java new file mode 100644 index 00000000..1c8ee9cd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ColorConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Color Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ColorConverter implements Converter { + + private static final Integer DEFAULT_COLOR = Integer.decode("0x808080"); + + @Override + public Integer convert(Method method, String input) { + if (Strings.isBlank(input)) { + return DEFAULT_COLOR; + } + return Integer.decode("0x" + input); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ControlTowerConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ControlTowerConverter.java new file mode 100644 index 00000000..433cfbb5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ControlTowerConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.StringTokenizer; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TowerSpawn; + +/** + * Control Tower Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ControlTowerConverter implements Converter { + + @Override + public TowerSpawn convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final StringTokenizer st = new StringTokenizer(input, ","); + final int x = Integer.parseInt(st.nextToken()); + final int y = Integer.parseInt(st.nextToken()); + final int z = Integer.parseInt(st.nextToken()); + final int npcId = Integer.parseInt(st.nextToken()); + + return new TowerSpawn(npcId, new Location(x, y, z)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/Days2MillisecondsConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/Days2MillisecondsConverter.java new file mode 100644 index 00000000..18f8732d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/Days2MillisecondsConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import static java.util.concurrent.TimeUnit.DAYS; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Days to Milliseconds converter. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class Days2MillisecondsConverter implements Converter { + + @Override + public Long convert(Method method, String input) { + if (Strings.isBlank(input)) { + return 0L; + } + return DAYS.toMillis(Long.parseLong(input)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/FileConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/FileConverter.java new file mode 100644 index 00000000..f4279712 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/FileConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.io.File; +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * File Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class FileConverter implements Converter { + + private static final Logger LOG = LoggerFactory.getLogger(FileConverter.class); + + @Override + public File convert(Method method, String input) { + try { + return new File(input.replace('\\', '/')).getCanonicalFile(); + } catch (Exception ex) { + LOG.warn("Error setting script root!", ex); + return new File("."); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/FlameTowerConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/FlameTowerConverter.java new file mode 100644 index 00000000..24f34ef9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/FlameTowerConverter.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import java.util.StringTokenizer; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TowerSpawn; + +/** + * Flame Tower Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class FlameTowerConverter implements Converter { + + @Override + public TowerSpawn convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final StringTokenizer st = new StringTokenizer(input, ","); + final int x = Integer.parseInt(st.nextToken()); + final int y = Integer.parseInt(st.nextToken()); + final int z = Integer.parseInt(st.nextToken()); + final int npcId = Integer.parseInt(st.nextToken()); + final List zones = new LinkedList<>(); + while (st.hasMoreTokens()) { + zones.add(Integer.parseInt(st.nextToken())); + } + + return new TowerSpawn(npcId, new Location(x, y, z), zones); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/HexIdConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/HexIdConverter.java new file mode 100644 index 00000000..cd7956d1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/HexIdConverter.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.math.BigInteger; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Hex Id Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class HexIdConverter implements Converter { + + @Override + public BigInteger convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + return new BigInteger(input, 16); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/Hours2MillisecondsConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/Hours2MillisecondsConverter.java new file mode 100644 index 00000000..bb2e8933 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/Hours2MillisecondsConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import static java.util.concurrent.TimeUnit.HOURS; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Hours to Milliseconds converter. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class Hours2MillisecondsConverter implements Converter { + + @Override + public Long convert(Method method, String input) { + if (Strings.isBlank(input)) { + return 0L; + } + return HOURS.toMillis(Long.parseLong(input)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/IPLimitConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/IPLimitConverter.java new file mode 100644 index 00000000..66d85e35 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/IPLimitConverter.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * IP-Limit Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class IPLimitConverter implements Converter> { + + private static final Logger LOG = LoggerFactory.getLogger(IPLimitConverter.class); + + @Override + public Map convert(Method method, String input) { + final var map = new HashMap(); + if (Strings.isBlank(input)) { + return map; + } + + final var tokens = input.replace(" ", "").split(";"); + for (var token : tokens) { + final var splitToken = token.split(","); + try { + map.put(InetAddress.getByName(splitToken[0]).hashCode(), Integer.valueOf(splitToken[1])); + } catch (Exception ex) { + LOG.warn("Invalid DualboxCheckWhitelist IP {} or limit {}!", splitToken[0], splitToken[1], ex); + } + } + return map; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ItemChanceHolderConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ItemChanceHolderConverter.java new file mode 100644 index 00000000..d32f18a1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ItemChanceHolderConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.holders.ItemChanceHolder; + +/** + * Item Chance Holder Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ItemChanceHolderConverter implements Converter { + + @Override + public ItemChanceHolder convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final var tokens = input.replace(" ", "").split(","); + return new ItemChanceHolder(Integer.parseInt(tokens[0]), Float.parseFloat(tokens[1])); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ItemHolderConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ItemHolderConverter.java new file mode 100644 index 00000000..43f59c78 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ItemHolderConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * Item Holder Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ItemHolderConverter implements Converter { + + @Override + public ItemHolder convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final var tokens = input.replace(" ", "").split(","); + return new ItemHolder(Integer.parseInt(tokens[0]), Long.parseLong(tokens[1])); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/LocationConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/LocationConverter.java new file mode 100644 index 00000000..6b58a370 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/LocationConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.Location; + +/** + * Location Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class LocationConverter implements Converter { + + @Override + public Location convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final var tokens = input.replace(" ", "").split(","); + final var location = new Location(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1]), Integer.parseInt(tokens[2])); + if (tokens.length >= 4) { + location.setHeading(Integer.parseInt(tokens[3])); + } + if (tokens.length == 5) { + location.setInstanceId(Integer.parseInt(tokens[4])); + } + return location; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerFloatConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerFloatConverter.java new file mode 100644 index 00000000..bda1de9e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerFloatConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Map Integer-Float Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class MapIntegerFloatConverter implements Converter> { + + @Override + public Map convert(Method method, String input) { + final var map = new HashMap(); + if (Strings.isBlank(input)) { + return map; + } + + final var tokens = input.replace(" ", "").split(";"); + for (var token : tokens) { + final var splitToken = token.split(","); + map.put(Integer.valueOf(splitToken[0]), Float.valueOf(splitToken[1])); + } + return map; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerIntegerConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerIntegerConverter.java new file mode 100644 index 00000000..1e3cafa3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/MapIntegerIntegerConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Map Integer-Integer Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class MapIntegerIntegerConverter implements Converter> { + + @Override + public Map convert(Method method, String input) { + final var map = new HashMap(); + if (Strings.isBlank(input)) { + return map; + } + + final var tokens = input.replace(" ", "").split(";"); + for (var token : tokens) { + final var splitToken = token.split(","); + map.put(Integer.valueOf(splitToken[0]), Integer.valueOf(splitToken[1])); + } + return map; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/Minutes2MillisecondsConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/Minutes2MillisecondsConverter.java new file mode 100644 index 00000000..0aa69430 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/Minutes2MillisecondsConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import static java.util.concurrent.TimeUnit.MINUTES; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Minutes to Milliseconds Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class Minutes2MillisecondsConverter implements Converter { + + @Override + public Long convert(Method method, String input) { + if (Strings.isBlank(input)) { + return 0L; + } + return MINUTES.toMillis(Long.parseLong(input)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/PatternConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/PatternConverter.java new file mode 100644 index 00000000..ba3bf5b4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/PatternConverter.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; +import java.util.regex.Pattern; + +import org.aeonbits.owner.Converter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Pattern Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class PatternConverter implements Converter { + + private static final Logger LOG = LoggerFactory.getLogger(PatternConverter.class); + + private static final Pattern DEFAULT_PATTERN = Pattern.compile("[a-zA-Z0-9]*"); + + @Override + public Pattern convert(Method method, String input) { + try { + return Pattern.compile(input); + } catch (Exception ex) { + LOG.error("Error creating pattern {}!", input, ex); + return DEFAULT_PATTERN; + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/Seconds2MillisecondsConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/Seconds2MillisecondsConverter.java new file mode 100644 index 00000000..f481b349 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/Seconds2MillisecondsConverter.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Seconds to Milliseconds Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class Seconds2MillisecondsConverter implements Converter { + + @Override + public Long convert(Method method, String input) { + if (Strings.isBlank(input)) { + return 0L; + } + return SECONDS.toMillis(Long.parseLong(input)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/ServerListTypeConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/ServerListTypeConverter.java new file mode 100644 index 00000000..5779a58f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/ServerListTypeConverter.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +/** + * Server List Type Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class ServerListTypeConverter implements Converter { + + @Override + public Integer convert(Method method, String input) { + if (Strings.isBlank(input)) { + return 1; + } + return getServerTypeId(input.split(",")); + } + + public static int getServerTypeId(String[] serverTypes) { + int typeId = 0; + for (String serverType : serverTypes) { + switch (serverType.trim().toLowerCase()) { + case "normal" -> typeId |= 0x01; + case "relax" -> typeId |= 0x02; + case "test" -> typeId |= 0x04; + case "nolabel" -> typeId |= 0x08; + case "restricted" -> typeId |= 0x10; + case "event" -> typeId |= 0x20; + case "free" -> typeId |= 0x40; + } + } + return typeId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/config/converter/SkillHolderConverter.java b/src/main/java/com/l2jserver/gameserver/config/converter/SkillHolderConverter.java new file mode 100644 index 00000000..8257eada --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/config/converter/SkillHolderConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.config.converter; + +import java.lang.reflect.Method; + +import org.aeonbits.owner.Converter; +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.holders.SkillHolder; + +/** + * Skill Holder Converter. + * @author Zoey76 + * @version 2.6.1.0 + */ +public class SkillHolderConverter implements Converter { + + @Override + public SkillHolder convert(Method method, String input) { + if (Strings.isBlank(input)) { + return null; + } + + final var tokens = input.replace(" ", "").split(","); + return new SkillHolder(Integer.parseInt(tokens[0]), Integer.parseInt(tokens[1])); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/custom/AutoLootExtension.java b/src/main/java/com/l2jserver/gameserver/custom/AutoLootExtension.java new file mode 100644 index 00000000..e2ea815a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/custom/AutoLootExtension.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.custom; + +import static com.l2jserver.gameserver.config.Configuration.customs; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Auto Loot Extension. + * @author Maneco2 + * @version 2.6.2.0 + */ +public class AutoLootExtension { + public static void onLogin(L2PcInstance player) { + if (customs().autoLootVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLoot")) { + player.setAutoLoot(true); + } + } else if (!customs().autoLootVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLoot")) { + player.getVariables().remove("AutoLoot"); + } + } + + if (customs().autoLootItemsVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLootItems")) { + player.setAutoLootItem(true); + } + } else if (!customs().autoLootItemsVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLootItems")) { + player.getVariables().remove("AutoLootItems"); + } + } + + if (customs().autoLootHerbsVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLootHerbs")) { + player.setAutoLootHerbs(true); + } + } else if (!customs().autoLootHerbsVoiceRestore()) { + if (player.getVariables().hasVariable("AutoLootHerbs")) { + player.getVariables().remove("AutoLootHerbs"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/dao/ClanDAO.java b/src/main/java/com/l2jserver/gameserver/dao/ClanDAO.java new file mode 100644 index 00000000..8c4ffc92 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/ClanDAO.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import java.util.Map; + +/** + * Clan DAO interface. + * @author Zoey76 + */ +public interface ClanDAO { + + /** + * Gets the clan privileges. + * @param clanId the clan Id + * @return the ranks and privileges + */ + Map getPrivileges(int clanId); + + /** + * Stores the clan privileges. + * @param clanId the clan Id + * @param rank the rank + * @param privileges the privileges + */ + void storePrivileges(int clanId, int rank, int privileges); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/FriendDAO.java b/src/main/java/com/l2jserver/gameserver/dao/FriendDAO.java new file mode 100644 index 00000000..e6e19931 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/FriendDAO.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Friend DAO interface. + * @author Zoey76 + */ +public interface FriendDAO { + void load(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/HennaDAO.java b/src/main/java/com/l2jserver/gameserver/dao/HennaDAO.java new file mode 100644 index 00000000..4c223e53 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/HennaDAO.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Henna; + +/** + * Henna DAO interface. + * @author Zoey76 + */ +public interface HennaDAO { + /** + * Restores all hennas for the player. + * @param player the player + */ + void load(L2PcInstance player); + + void insert(L2PcInstance player, L2Henna henna, int index); + + void delete(L2PcInstance player, int slot); + + void deleteAll(L2PcInstance player, int classIndex); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/ItemDAO.java b/src/main/java/com/l2jserver/gameserver/dao/ItemDAO.java new file mode 100644 index 00000000..6807cc46 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/ItemDAO.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Item DAO interface. + * @author Zoey76 + */ +public interface ItemDAO { + /** + * Loads pet's inventory from database. + * @param player the player + */ + void loadPetInventory(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/ItemReuseDAO.java b/src/main/java/com/l2jserver/gameserver/dao/ItemReuseDAO.java new file mode 100644 index 00000000..b6d1fb1b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/ItemReuseDAO.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Item Reuse DAO interface. + * @author Zoey76 + */ +public interface ItemReuseDAO { + /** + * Restores the item reuse for the given player. + * @param player the player + */ + void load(L2PcInstance player); + + /** + * Inserts the item reuse for the given player. + * @param player the player + */ + void insert(L2PcInstance player); + + /** + * Deletes the item reuse from the player. + * @param player the player + */ + void delete(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/PetDAO.java b/src/main/java/com/l2jserver/gameserver/dao/PetDAO.java new file mode 100644 index 00000000..ea037f40 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/PetDAO.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * Pet DAO interface. + * @author Zoey76 + */ +public interface PetDAO { + void updateFood(L2PcInstance player, int petId); + + /** + * Deletes a pet by item object ID. + * @param pet the pet + */ + void delete(L2PetInstance pet); + + /** + * Restores a pet from the database. + * @param control the summoning item + * @param template the NPC template + * @param owner the owner + * @return the pet + */ + L2PetInstance load(L2ItemInstance control, L2NpcTemplate template, L2PcInstance owner); + + /** + * Stores a pet into the database. + * @param pet the pet + */ + void insert(L2PetInstance pet); + + /** + * Updates a pet in the database. + * @param pet the pet + */ + void update(L2PetInstance pet); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/PetSkillSaveDAO.java b/src/main/java/com/l2jserver/gameserver/dao/PetSkillSaveDAO.java new file mode 100644 index 00000000..1347c6e1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/PetSkillSaveDAO.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; + +/** + * Pet Skill Save interface. + * @author Zoey76 + */ +public interface PetSkillSaveDAO { + /** + * Stores the pet skills in the database. + * @param pet the pets + * @param storeEffects if {@code true} effects will be stored + */ + void insert(L2PetInstance pet, boolean storeEffects); + + /** + * Restores the pet skills from the database. + * @param pet the pet + */ + void load(L2PetInstance pet); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/PlayerDAO.java b/src/main/java/com/l2jserver/gameserver/dao/PlayerDAO.java new file mode 100644 index 00000000..5f8ee65a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/PlayerDAO.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Player DAO interface. + * @author Zoey76 + */ +public interface PlayerDAO { + void storeCharBase(L2PcInstance player); + + /** + * Create a new player in the characters table of the database. + * @param player the player + * @return {@code true} if the player was inserted into the database + */ + boolean insert(L2PcInstance player); + + /** + * Updates the database with online status and last access of the player (called when login and logout). + * @param player the player + */ + void updateOnlineStatus(L2PcInstance player); + + /** + * Restores a player from the database. + * @param objectId the player's object ID + * @return the player + */ + L2PcInstance load(int objectId); + + /** + * Retrieve the name and ID of the other characters assigned to this account. + * @param player the player + */ + void loadCharacters(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/PlayerSkillSaveDAO.java b/src/main/java/com/l2jserver/gameserver/dao/PlayerSkillSaveDAO.java new file mode 100644 index 00000000..3c7872eb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/PlayerSkillSaveDAO.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Player Skill Save DAO interface. + * @author Zoey76 + */ +public interface PlayerSkillSaveDAO { + /** + * Deletes the player skills from the database. + * @param player the player + */ + void delete(L2PcInstance player); + + /** + * Deletes the player skills from the database for a given class index. + * @param player the player + * @param classIndex the class index + */ + void delete(L2PcInstance player, int classIndex); + + /** + * Stores the player skills in the database. + * @param player the player + * @param storeEffects if {@code true} effects will be stored + */ + void insert(L2PcInstance player, boolean storeEffects); + + /** + * Restores the player skills from the database. + * @param player the player + */ + void load(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/PremiumItemDAO.java b/src/main/java/com/l2jserver/gameserver/dao/PremiumItemDAO.java new file mode 100644 index 00000000..4fa7d63e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/PremiumItemDAO.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Premium Item DAO interface. + * @author Zoey76 + */ +public interface PremiumItemDAO { + void load(L2PcInstance player); + + void update(L2PcInstance player, int itemNum, long newCount); + + void delete(L2PcInstance player, int itemNum); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/RecipeBookDAO.java b/src/main/java/com/l2jserver/gameserver/dao/RecipeBookDAO.java new file mode 100644 index 00000000..a0658198 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/RecipeBookDAO.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recipe Book DAO interface. + * @author Zoey76 + */ +public interface RecipeBookDAO { + void insert(L2PcInstance player, int recipeId, boolean isDwarf); + + /** + * Restore recipe book data for this L2PcInstance. + * @param player the player + * @param loadCommon if {@code true} the common recipes boom is loaded + */ + void load(L2PcInstance player, boolean loadCommon); + + void delete(L2PcInstance player, int recipeId, boolean isDwarf); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/RecipeShopListDAO.java b/src/main/java/com/l2jserver/gameserver/dao/RecipeShopListDAO.java new file mode 100644 index 00000000..79a1023d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/RecipeShopListDAO.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recipe Shop List DAO interface. + * @author Zoey76 + */ +public interface RecipeShopListDAO { + void load(L2PcInstance player); + + void delete(L2PcInstance player); + + void insert(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/RecommendationBonusDAO.java b/src/main/java/com/l2jserver/gameserver/dao/RecommendationBonusDAO.java new file mode 100644 index 00000000..197e89aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/RecommendationBonusDAO.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recommendation Bonus DAO interface. + * @author Zoey76 + */ +public interface RecommendationBonusDAO { + void load(L2PcInstance player); + + void insert(L2PcInstance player, int recomTime); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/ServitorSkillSaveDAO.java b/src/main/java/com/l2jserver/gameserver/dao/ServitorSkillSaveDAO.java new file mode 100644 index 00000000..fa99e1d7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/ServitorSkillSaveDAO.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; + +/** + * Servitor Skill Save DAO interface. + * @author Zoey76 + */ +public interface ServitorSkillSaveDAO { + /** + * Stores the servitor skills in the database. + * @param servitor the servitor + * @param storeEffects if {@code true} effects will be stored + */ + void insert(L2ServitorInstance servitor, boolean storeEffects); + + /** + * Restores the servitor skills from the database. + * @param servitor the servitor. + */ + void load(L2ServitorInstance servitor); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/ShortcutDAO.java b/src/main/java/com/l2jserver/gameserver/dao/ShortcutDAO.java new file mode 100644 index 00000000..81497495 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/ShortcutDAO.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Shortcut DAO interface. + * @author Zoey76 + */ +public interface ShortcutDAO { + boolean delete(L2PcInstance player, int classIndex); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/SkillDAO.java b/src/main/java/com/l2jserver/gameserver/dao/SkillDAO.java new file mode 100644 index 00000000..8ba37d60 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/SkillDAO.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Skill DAO interface. + * @author Zoey76 + */ +public interface SkillDAO { + void insert(L2PcInstance player, int classIndex, Skill skill); + + void update(L2PcInstance player, int classIndex, Skill newSkill, Skill oldSkill); + + void delete(L2PcInstance player, Skill skill); + + /** + * Adds or updates player's skills in the database. + * @param player the player + * @param newClassIndex if newClassIndex > -1, the skills will be stored for that class index, not the current one + * @param newSkills the list of skills to store + */ + void insert(L2PcInstance player, int newClassIndex, List newSkills); + + /** + * Retrieves all skills from the database. + * @param player the player + */ + void load(L2PcInstance player); + + void deleteAll(L2PcInstance player, int classIndex); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/SubclassDAO.java b/src/main/java/com/l2jserver/gameserver/dao/SubclassDAO.java new file mode 100644 index 00000000..4e5bde03 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/SubclassDAO.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.SubClass; + +/** + * Subclass DAO interface. + * @author Zoey76 + */ +public interface SubclassDAO { + void update(L2PcInstance player); + + /** + * Store the basic info about this new subclass. + * @param player the player + * @param newClass the new subclass + * @return {@code true} if the subclass is added to the database + */ + boolean insert(L2PcInstance player, SubClass newClass); + + void delete(L2PcInstance player, int classIndex); + + /** + * Restores the player subclass data. + * @param player the player + */ + void load(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/TeleportBookmarkDAO.java b/src/main/java/com/l2jserver/gameserver/dao/TeleportBookmarkDAO.java new file mode 100644 index 00000000..9fee7bb5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/TeleportBookmarkDAO.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Teleport Bookmark DAO interface. + * @author Zoey76 + */ +public interface TeleportBookmarkDAO { + void delete(L2PcInstance player, int id); + + void insert(L2PcInstance player, int id, int x, int y, int z, int icon, String tag, String name); + + void update(L2PcInstance player, int id, int icon, String tag, String name); + + void load(L2PcInstance player); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/factory/IDAOFactory.java b/src/main/java/com/l2jserver/gameserver/dao/factory/IDAOFactory.java new file mode 100644 index 00000000..edacedd9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/factory/IDAOFactory.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.factory; + +import com.l2jserver.gameserver.bbs.repository.ForumRepository; +import com.l2jserver.gameserver.bbs.repository.PostRepository; +import com.l2jserver.gameserver.bbs.repository.TopicRepository; +import com.l2jserver.gameserver.dao.ClanDAO; +import com.l2jserver.gameserver.dao.FriendDAO; +import com.l2jserver.gameserver.dao.HennaDAO; +import com.l2jserver.gameserver.dao.ItemDAO; +import com.l2jserver.gameserver.dao.ItemReuseDAO; +import com.l2jserver.gameserver.dao.PetDAO; +import com.l2jserver.gameserver.dao.PetSkillSaveDAO; +import com.l2jserver.gameserver.dao.PlayerDAO; +import com.l2jserver.gameserver.dao.PlayerSkillSaveDAO; +import com.l2jserver.gameserver.dao.PremiumItemDAO; +import com.l2jserver.gameserver.dao.RecipeBookDAO; +import com.l2jserver.gameserver.dao.RecipeShopListDAO; +import com.l2jserver.gameserver.dao.RecommendationBonusDAO; +import com.l2jserver.gameserver.dao.ServitorSkillSaveDAO; +import com.l2jserver.gameserver.dao.ShortcutDAO; +import com.l2jserver.gameserver.dao.SkillDAO; +import com.l2jserver.gameserver.dao.SubclassDAO; +import com.l2jserver.gameserver.dao.TeleportBookmarkDAO; + +/** + * DAO Factory interface. + * @author Zoey76 + */ +public interface IDAOFactory { + FriendDAO getFriendDAO(); + + HennaDAO getHennaDAO(); + + ItemDAO getItemDAO(); + + ItemReuseDAO getItemReuseDAO(); + + PetDAO getPetDAO(); + + PetSkillSaveDAO getPetSkillSaveDAO(); + + PlayerDAO getPlayerDAO(); + + PlayerSkillSaveDAO getPlayerSkillSaveDAO(); + + PremiumItemDAO getPremiumItemDAO(); + + RecipeBookDAO getRecipeBookDAO(); + + RecipeShopListDAO getRecipeShopListDAO(); + + RecommendationBonusDAO getRecommendationBonusDAO(); + + ServitorSkillSaveDAO getServitorSkillSaveDAO(); + + ShortcutDAO getShortcutDAO(); + + SkillDAO getSkillDAO(); + + SubclassDAO getSubclassDAO(); + + TeleportBookmarkDAO getTeleportBookmarkDAO(); + + ClanDAO getClanDAO(); + + ForumRepository getForumRepository(); + + TopicRepository getTopicRepository(); + + PostRepository getPostRepository(); +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/factory/impl/DAOFactory.java b/src/main/java/com/l2jserver/gameserver/dao/factory/impl/DAOFactory.java new file mode 100644 index 00000000..d4cef254 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/factory/impl/DAOFactory.java @@ -0,0 +1,202 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.factory.impl; + +import com.l2jserver.gameserver.bbs.repository.ForumRepository; +import com.l2jserver.gameserver.bbs.repository.PostRepository; +import com.l2jserver.gameserver.bbs.repository.TopicRepository; +import com.l2jserver.gameserver.bbs.repository.impl.ForumRepositoryMySQLImpl; +import com.l2jserver.gameserver.bbs.repository.impl.PostRepositoryMySQLImpl; +import com.l2jserver.gameserver.bbs.repository.impl.TopicRepositoryMySQLImpl; +import com.l2jserver.gameserver.dao.ClanDAO; +import com.l2jserver.gameserver.dao.FriendDAO; +import com.l2jserver.gameserver.dao.HennaDAO; +import com.l2jserver.gameserver.dao.ItemDAO; +import com.l2jserver.gameserver.dao.ItemReuseDAO; +import com.l2jserver.gameserver.dao.PetDAO; +import com.l2jserver.gameserver.dao.PetSkillSaveDAO; +import com.l2jserver.gameserver.dao.PlayerDAO; +import com.l2jserver.gameserver.dao.PlayerSkillSaveDAO; +import com.l2jserver.gameserver.dao.PremiumItemDAO; +import com.l2jserver.gameserver.dao.RecipeBookDAO; +import com.l2jserver.gameserver.dao.RecipeShopListDAO; +import com.l2jserver.gameserver.dao.RecommendationBonusDAO; +import com.l2jserver.gameserver.dao.ServitorSkillSaveDAO; +import com.l2jserver.gameserver.dao.ShortcutDAO; +import com.l2jserver.gameserver.dao.SkillDAO; +import com.l2jserver.gameserver.dao.SubclassDAO; +import com.l2jserver.gameserver.dao.TeleportBookmarkDAO; +import com.l2jserver.gameserver.dao.factory.IDAOFactory; +import com.l2jserver.gameserver.dao.impl.mysql.ClanDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.FriendDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.HennaDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.ItemDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.ItemReuseDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.PetDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.PetSkillSaveDAOMySQL; +import com.l2jserver.gameserver.dao.impl.mysql.PlayerDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.PlayerSkillSaveDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.PremiumItemDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.RecipeBookDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.RecipeShopListDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.RecommendationBonusDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.ServitorSkillSaveDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.ShortcutDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.SkillDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.SubclassDAOMySQLImpl; +import com.l2jserver.gameserver.dao.impl.mysql.TeleportBookmarkDAOMySQLImpl; + +/** + * MySQL DAO Factory implementation. + * @author Zoey76 + */ +public enum DAOFactory implements IDAOFactory { + INSTANCE; + + private final FriendDAO friendDAO = new FriendDAOMySQLImpl(); + private final HennaDAO hennaDAO = new HennaDAOMySQLImpl(); + private final ItemDAO itemDAO = new ItemDAOMySQLImpl(); + private final ItemReuseDAO itemReuseDAO = new ItemReuseDAOMySQLImpl(); + private final PetDAO petDAO = new PetDAOMySQLImpl(); + private final PetSkillSaveDAO petSkillSaveDAO = new PetSkillSaveDAOMySQL(); + private final PlayerDAO playerDAO = new PlayerDAOMySQLImpl(); + private final PlayerSkillSaveDAO playerSkillSaveDAO = new PlayerSkillSaveDAOMySQLImpl(); + private final PremiumItemDAO premiumItemDAO = new PremiumItemDAOMySQLImpl(); + private final RecipeBookDAO recipeBookDAO = new RecipeBookDAOMySQLImpl(); + private final RecipeShopListDAO recipeShopListDAO = new RecipeShopListDAOMySQLImpl(); + private final RecommendationBonusDAO recommendationBonusDAO = new RecommendationBonusDAOMySQLImpl(); + private final ServitorSkillSaveDAO servitorSkillSaveDAO = new ServitorSkillSaveDAOMySQLImpl(); + private final ShortcutDAO shortcutDAO = new ShortcutDAOMySQLImpl(); + private final SkillDAO skillDAO = new SkillDAOMySQLImpl(); + private final SubclassDAO subclassDAO = new SubclassDAOMySQLImpl(); + private final TeleportBookmarkDAO teleportBookmarkDAO = new TeleportBookmarkDAOMySQLImpl(); + private final ClanDAO clanDAO = new ClanDAOMySQLImpl(); + private final ForumRepository forumRepository = new ForumRepositoryMySQLImpl(); + private final TopicRepository topicRepository = new TopicRepositoryMySQLImpl(); + private final PostRepository postRepository = new PostRepositoryMySQLImpl(); + + public static DAOFactory getInstance() { + return INSTANCE; + } + + @Override + public FriendDAO getFriendDAO() { + return friendDAO; + } + + @Override + public HennaDAO getHennaDAO() { + return hennaDAO; + } + + @Override + public ItemDAO getItemDAO() { + return itemDAO; + } + + @Override + public ItemReuseDAO getItemReuseDAO() { + return itemReuseDAO; + } + + @Override + public PetDAO getPetDAO() { + return petDAO; + } + + @Override + public PetSkillSaveDAO getPetSkillSaveDAO() { + return petSkillSaveDAO; + } + + @Override + public PlayerDAO getPlayerDAO() { + return playerDAO; + } + + @Override + public PlayerSkillSaveDAO getPlayerSkillSaveDAO() { + return playerSkillSaveDAO; + } + + @Override + public PremiumItemDAO getPremiumItemDAO() { + return premiumItemDAO; + } + + @Override + public RecipeBookDAO getRecipeBookDAO() { + return recipeBookDAO; + } + + @Override + public RecipeShopListDAO getRecipeShopListDAO() { + return recipeShopListDAO; + } + + @Override + public RecommendationBonusDAO getRecommendationBonusDAO() { + return recommendationBonusDAO; + } + + @Override + public ServitorSkillSaveDAO getServitorSkillSaveDAO() { + return servitorSkillSaveDAO; + } + + @Override + public ShortcutDAO getShortcutDAO() { + return shortcutDAO; + } + + @Override + public SkillDAO getSkillDAO() { + return skillDAO; + } + + @Override + public SubclassDAO getSubclassDAO() { + return subclassDAO; + } + + @Override + public TeleportBookmarkDAO getTeleportBookmarkDAO() { + return teleportBookmarkDAO; + } + + @Override + public ClanDAO getClanDAO() { + return clanDAO; + } + + @Override + public ForumRepository getForumRepository() { + return forumRepository; + } + + @Override + public TopicRepository getTopicRepository() { + return topicRepository; + } + + @Override + public PostRepository getPostRepository() { + return postRepository; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ClanDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ClanDAOMySQLImpl.java new file mode 100644 index 00000000..64157265 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ClanDAOMySQLImpl.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.ClanDAO; + +/** + * Clan DAO MySQL implementation. + * @author Zoey76 + */ +public class ClanDAOMySQLImpl implements ClanDAO { + + private static final Logger LOG = LoggerFactory.getLogger(ClanDAOMySQLImpl.class); + + private static final String SELECT_CLAN_PRIVILEGES = "SELECT `privs`, `rank`, `party` FROM `clan_privs` WHERE clan_id=?"; + + private static final String INSERT_CLAN_PRIVILEGES = "INSERT INTO `clan_privs` (`clan_id`, `rank`, `party`, `privs`) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE `privs`=?"; + + @Override + public Map getPrivileges(int clanId) { + final var result = new HashMap(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_CLAN_PRIVILEGES)) { + ps.setInt(1, clanId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final int rank = rs.getInt("rank"); + if (rank == -1) { + continue; + } + result.put(rank, rs.getInt("privs")); + } + } + } catch (Exception ex) { + LOG.error("Unable to restore clan privileges for clan Id {}!", clanId, ex); + } + return result; + } + + @Override + public void storePrivileges(int clanId, int rank, int privileges) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_CLAN_PRIVILEGES)) { + ps.setInt(1, clanId); + ps.setInt(2, rank); + ps.setInt(3, 0); + ps.setInt(4, privileges); + ps.setInt(5, privileges); + ps.execute(); + } catch (Exception ex) { + LOG.error("Unable to store clan privileges for clan Id {}!", clanId, ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/FriendDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/FriendDAOMySQLImpl.java new file mode 100644 index 00000000..b8716571 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/FriendDAOMySQLImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import java.sql.ResultSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.FriendDAO; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Friend DAO MySQL implementation. + * @author Zoey76 + */ +public class FriendDAOMySQLImpl implements FriendDAO { + + private static final Logger LOG = LoggerFactory.getLogger(FriendDAOMySQLImpl.class); + + private static final String SELECT = "SELECT friendId FROM character_friends WHERE charId=? AND relation=0 AND friendId<>charId"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + player.addFriend(rs.getInt("friendId")); + } + } + } catch (Exception e) { + LOG.error("Error found in {} FriendList: ", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/HennaDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/HennaDAOMySQLImpl.java new file mode 100644 index 00000000..dcc60955 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/HennaDAOMySQLImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.HennaDAO; +import com.l2jserver.gameserver.data.xml.impl.HennaData; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Henna; + +/** + * Henna DAO MySQL implementation. + * @author Zoey76 + */ +public class HennaDAOMySQLImpl implements HennaDAO { + + private static final Logger LOG = LoggerFactory.getLogger(HennaDAOMySQLImpl.class); + + private static final String SELECT = "SELECT slot,symbol_id FROM character_hennas WHERE charId=? AND class_index=?"; + + private static final String INSERT = "INSERT INTO character_hennas (charId,symbol_id,slot,class_index) VALUES (?,?,?,?)"; + + private static final String DELETE_ONE = "DELETE FROM character_hennas WHERE charId=? AND slot=? AND class_index=?"; + + private static final String DELETE_ALL = "DELETE FROM character_hennas WHERE charId=? AND class_index=?"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, player.getClassIndex()); + try (var rs = ps.executeQuery()) { + final L2Henna[] henna = new L2Henna[3]; + while (rs.next()) { + int slot = rs.getInt("slot"); + if ((slot < 1) || (slot > 3)) { + continue; + } + + int symbolId = rs.getInt("symbol_id"); + if (symbolId == 0) { + continue; + } + henna[slot - 1] = HennaData.getInstance().getHenna(symbolId); + } + player.setHenna(henna); + } + } catch (Exception ex) { + LOG.error("Failed restoring character {} hennas!", player, ex); + } + } + + @Override + public void insert(L2PcInstance player, L2Henna henna, int slot) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, henna.getDyeId()); + ps.setInt(3, slot); + ps.setInt(4, player.getClassIndex()); + ps.execute(); + } catch (Exception ex) { + LOG.error("Failed saving character henna!", ex); + } + } + + @Override + public void delete(L2PcInstance player, int slot) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_ONE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, slot); + ps.setInt(3, player.getClassIndex()); + ps.execute(); + } catch (Exception ex) { + LOG.error("Failed removing character henna!", ex); + } + } + + @Override + public void deleteAll(L2PcInstance player, int classIndex) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_ALL)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, classIndex); + ps.execute(); + } catch (Exception ex) { + LOG.error("Failed removing character henna!", ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemDAOMySQLImpl.java new file mode 100644 index 00000000..b38741fd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemDAOMySQLImpl.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.ItemDAO; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Item DAO MySQL implementation. + * @author Zoey76 + */ +public class ItemDAOMySQLImpl implements ItemDAO { + + private static final Logger LOG = LoggerFactory.getLogger(ItemDAOMySQLImpl.class); + + private static final String SELECT = "SELECT object_id FROM `items` WHERE `owner_id`=? AND (`loc`='PET' OR `loc`='PET_EQUIP') LIMIT 1;"; + + @Override + public void loadPetInventory(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + player.setPetInvItems(rs.next() && (rs.getInt("object_id") > 0)); + } + } catch (Exception e) { + LOG.error("Could not check Items in Pet Inventory for {}, {}", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemReuseDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemReuseDAOMySQLImpl.java new file mode 100644 index 00000000..d719b138 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ItemReuseDAOMySQLImpl.java @@ -0,0 +1,123 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.ItemReuseDAO; +import com.l2jserver.gameserver.model.TimeStamp; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.serverpackets.ExUseSharedGroupItem; + +/** + * Item Reuse DAO MySQL implementation. + * @author Zoey76 + */ +public class ItemReuseDAOMySQLImpl implements ItemReuseDAO { + + private static final Logger LOG = LoggerFactory.getLogger(ItemReuseDAOMySQLImpl.class); + + private static final String INSERT = "INSERT INTO character_item_reuse_save (charId,itemId,itemObjId,reuseDelay,systime) VALUES (?,?,?,?,?)"; + + private static final String SELECT = "SELECT charId,itemId,itemObjId,reuseDelay,systime FROM character_item_reuse_save WHERE charId=?"; + + private static final String DELETE = "DELETE FROM character_item_reuse_save WHERE charId=?"; + + @Override + public void delete(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not delete player's {} item reuse data!", player, e); + } + } + + @Override + public void insert(L2PcInstance player) { + delete(player); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + final var itemReuseTimeStamps = player.getItemReuseTimeStamps(); + if (itemReuseTimeStamps != null) { + for (TimeStamp ts : itemReuseTimeStamps.values()) { + if ((ts != null) && ts.hasNotPassed()) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, ts.getItemId()); + ps.setInt(3, ts.getItemObjectId()); + ps.setLong(4, ts.getReuse()); + ps.setLong(5, ts.getStamp()); + ps.addBatch(); + } + } + ps.executeBatch(); + } + } catch (Exception e) { + LOG.warn("Could not store player's {} item reuse data!", player, e); + } + } + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final int itemId = rs.getInt("itemId"); + final long reuseDelay = rs.getLong("reuseDelay"); + final long systime = rs.getLong("systime"); + boolean isInInventory = true; + + // Using item Id + L2ItemInstance item = player.getInventory().getItemByItemId(itemId); + if (item == null) { + item = player.getWarehouse().getItemByItemId(itemId); + isInInventory = false; + } + + if ((item != null) && (item.getId() == itemId) && (item.getReuseDelay() > 0)) { + long remainingTime = systime - System.currentTimeMillis(); + // Hardcoded to 10 seconds. + if (remainingTime > 10) { + player.addTimeStampItem(item, reuseDelay, systime); + + if (isInInventory && item.isEtcItem()) { + final int group = item.getSharedReuseGroup(); + if (group > 0) { + player.sendPacket(new ExUseSharedGroupItem(itemId, group, (int) remainingTime, (int) reuseDelay)); + } + } + } + } + } + } + + // Deletes item reuse. + delete(player); + } catch (Exception e) { + LOG.error("Could not restore {} Item Reuse data: {}", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetDAOMySQLImpl.java new file mode 100644 index 00000000..f2e2e234 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetDAOMySQLImpl.java @@ -0,0 +1,148 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.PetDAO; +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * Pet DAO MySQL implementation. + * @author Zoey76 + */ +public class PetDAOMySQLImpl implements PetDAO { + + private static final Logger LOG = LoggerFactory.getLogger(PetDAOMySQLImpl.class); + + private static final String SELECT = "SELECT item_obj_id, name, level, curHp, curMp, exp, sp, fed FROM pets WHERE item_obj_id=?"; + + private static final String UPDATE_FOOD = "UPDATE pets SET fed=? WHERE item_obj_id=?"; + + private static final String DELETE = "DELETE FROM pets WHERE item_obj_id=?"; + + private static final String INSERT = "INSERT INTO pets (name,level,curHp,curMp,exp,sp,fed,ownerId,restore,item_obj_id) VALUES (?,?,?,?,?,?,?,?,?,?)"; + + private static final String UPDATE = "UPDATE pets SET name=?,level=?,curHp=?,curMp=?,exp=?,sp=?,fed=?,ownerId=?,restore=? WHERE item_obj_id=?"; + + @Override + public void updateFood(L2PcInstance player, int petId) { + if ((player.getControlItemId() != 0) && (petId != 0)) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_FOOD)) { + ps.setInt(1, player.getCurrentFeed()); + ps.setInt(2, player.getControlItemId()); + ps.executeUpdate(); + player.setControlItemId(0); + } catch (Exception e) { + LOG.error("Failed to store Pet [NpcId: {}] data {}", petId, e); + } + } + } + + @Override + public void delete(L2PetInstance pet) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, pet.getControlObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.error("Failed to delete pet {}!", pet, e); + } + } + + @Override + public L2PetInstance load(L2ItemInstance control, L2NpcTemplate template, L2PcInstance owner) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, control.getObjectId()); + try (var rs = ps.executeQuery()) { + final var objectId = IdFactory.getInstance().getNextId(); + if (!rs.next()) { + return new L2PetInstance(objectId, template, owner, control); + } + + final var pet = new L2PetInstance(objectId, template, owner, control, rs.getByte("level")); + pet.setRespawned(true); + pet.setName(rs.getString("name")); + + long exp = rs.getLong("exp"); + final var info = PetDataTable.getInstance().getPetLevelData(pet.getId(), pet.getLevel()); + // DS: update experience based by level + // Avoiding pet delevels due to exp per level values changed. + if ((info != null) && (exp < info.getPetMaxExp())) { + exp = info.getPetMaxExp(); + } + + pet.setExp(exp); + pet.setSp(rs.getInt("sp")); + + pet.getStatus().setCurrentHp(rs.getInt("curHp")); + pet.getStatus().setCurrentMp(rs.getInt("curMp")); + pet.getStatus().setCurrentCp(pet.getMaxCp()); + if (rs.getDouble("curHp") < 1) { + pet.setIsDead(true); + pet.stopHpMpRegeneration(); + } + + pet.setCurrentFed(rs.getInt("fed")); + return pet; + } + } catch (Exception e) { + LOG.error("Could not restore pet data for owner: {}, {}", owner, e); + } + return null; + } + + @Override + public void insert(L2PetInstance pet) { + insertOrUpdate(pet, INSERT); + } + + @Override + public void update(L2PetInstance pet) { + insertOrUpdate(pet, UPDATE); + } + + private static void insertOrUpdate(L2PetInstance pet, String query) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(query)) { + ps.setString(1, pet.getName()); + ps.setInt(2, pet.getLevel()); + ps.setDouble(3, pet.getStatus().getCurrentHp()); + ps.setDouble(4, pet.getStatus().getCurrentMp()); + ps.setLong(5, pet.getExp()); + ps.setInt(6, pet.getSp()); + ps.setInt(7, pet.getCurrentFed()); + ps.setInt(8, pet.getOwner().getObjectId()); + ps.setBoolean(9, pet.isRestoreSummon()); // True restores pet on login + ps.setInt(10, pet.getControlObjectId()); + ps.executeUpdate(); + } catch (Exception e) { + LOG.error("Failed to store pet {} data!", pet, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetSkillSaveDAOMySQL.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetSkillSaveDAOMySQL.java new file mode 100644 index 00000000..ecb63bb7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PetSkillSaveDAOMySQL.java @@ -0,0 +1,139 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.LinkedList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.PetSkillSaveDAO; +import com.l2jserver.gameserver.data.sql.impl.SummonEffectsTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Pet Skill Save MySQL implementation. + * @author Zoey76 + */ +public class PetSkillSaveDAOMySQL implements PetSkillSaveDAO { + + private static final Logger LOG = LoggerFactory.getLogger(PetSkillSaveDAOMySQL.class); + + private static final String ADD_SKILL_SAVE = "INSERT INTO character_pet_skills_save (petObjItemId,skill_id,skill_level,remaining_time,buff_index) VALUES (?,?,?,?,?)"; + + private static final String RESTORE_SKILL_SAVE = "SELECT petObjItemId,skill_id,skill_level,remaining_time,buff_index FROM character_pet_skills_save WHERE petObjItemId=? ORDER BY buff_index"; + + private static final String DELETE_SKILL_SAVE = "DELETE FROM character_pet_skills_save WHERE petObjItemId=?"; + + @Override + public void insert(L2PetInstance pet, boolean storeEffects) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement(DELETE_SKILL_SAVE); + var ps2 = con.prepareStatement(ADD_SKILL_SAVE)) { + // Delete all current stored effects for summon to avoid dupe + ps1.setInt(1, pet.getControlObjectId()); + ps1.execute(); + + int buff_index = 0; + + final List storedSkills = new LinkedList<>(); + + // Store all effect data along with calculated remaining + if (storeEffects) { + for (BuffInfo info : pet.getEffectList().getEffects()) { + if (info == null) { + continue; + } + + final Skill skill = info.getSkill(); + // Do not save heals. + if (skill.getAbnormalType() == AbnormalType.LIFE_FORCE_OTHERS) { + continue; + } + + if (skill.isToggle()) { + continue; + } + + // Dances and songs are not kept in retail. + if (skill.isDance() && !character().storeDances()) { + continue; + } + + if (storedSkills.contains(skill.getReuseHashCode())) { + continue; + } + + storedSkills.add(skill.getReuseHashCode()); + + ps2.setInt(1, pet.getControlObjectId()); + ps2.setInt(2, skill.getId()); + ps2.setInt(3, skill.getLevel()); + ps2.setInt(4, info.getTime()); + ps2.setInt(5, ++buff_index); + ps2.execute(); + + SummonEffectsTable.getInstance().addPetEffect(pet.getControlObjectId(), skill, info.getTime()); + } + } + } catch (Exception e) { + LOG.warn("Could not store pet effect data!", e); + } + } + + @Override + public void load(L2PetInstance pet) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement(RESTORE_SKILL_SAVE); + var ps2 = con.prepareStatement(DELETE_SKILL_SAVE)) { + if (!SummonEffectsTable.getInstance().containsPetId(pet.getControlObjectId())) { + ps1.setInt(1, pet.getControlObjectId()); + try (var rs = ps1.executeQuery()) { + while (rs.next()) { + int effectCurTime = rs.getInt("remaining_time"); + + final Skill skill = SkillData.getInstance().getSkill(rs.getInt("skill_id"), rs.getInt("skill_level")); + if (skill == null) { + continue; + } + + if (skill.hasEffects(EffectScope.GENERAL)) { + SummonEffectsTable.getInstance().addPetEffect(pet.getControlObjectId(), skill, effectCurTime); + } + } + } + } + + ps2.setInt(1, pet.getControlObjectId()); + ps2.executeUpdate(); + } catch (Exception e) { + LOG.warn("Could not restore {} active effect data!", pet, e); + } + } + +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerDAOMySQLImpl.java new file mode 100644 index 00000000..d3c67346 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerDAOMySQLImpl.java @@ -0,0 +1,345 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.sql.Timestamp; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.PlayerDAO; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.enums.Sex; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.actor.appearance.PcAppearance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.SubClass; +import com.l2jserver.gameserver.model.entity.Hero; + +/** + * Player DAO MySQL implementation. + * @author Zoey76 + */ +public class PlayerDAOMySQLImpl implements PlayerDAO { + + private static final Logger LOG = LoggerFactory.getLogger(PlayerDAOMySQLImpl.class); + + private static final String INSERT = "INSERT INTO characters (account_name,charId,char_name,level,maxHp,curHp,maxCp,curCp,maxMp,curMp,face,hairStyle,hairColor,sex,exp,sp,karma,fame,pvpkills,pkkills,clanid,race,classid,deletetime,cancraft,title,title_color,accesslevel,online,isin7sdungeon,clan_privs,wantspeace,base_class,newbie,nobless,power_grade,createDate) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + + private static final String SELECT = "SELECT * FROM characters WHERE charId=?"; + + private static final String UPDATE = "UPDATE characters SET level=?,maxHp=?,curHp=?,maxCp=?,curCp=?,maxMp=?,curMp=?,face=?,hairStyle=?,hairColor=?,sex=?,heading=?,x=?,y=?,z=?,exp=?,expBeforeDeath=?,sp=?,karma=?,fame=?,pvpkills=?,pkkills=?,clanid=?,race=?,classid=?,deletetime=?,title=?,title_color=?,accesslevel=?,online=?,isin7sdungeon=?,clan_privs=?,wantspeace=?,base_class=?,onlinetime=?,newbie=?,nobless=?,power_grade=?,subpledge=?,lvl_joined_academy=?,apprentice=?,sponsor=?,clan_join_expiry_time=?,clan_create_expiry_time=?,char_name=?,death_penalty_level=?,bookmarkslot=?,vitality_points=?,hunting_bonus=?,nevit_blessing_points=?,nevit_blessing_time=?,language=? WHERE charId=?"; + + private static final String UPDATE_ONLINE = "UPDATE characters SET online=?, lastAccess=? WHERE charId=?"; + + private static final String SELECT_CHARACTERS = "SELECT charId, char_name FROM characters WHERE account_name=? AND charId<>?"; + + @Override + public L2PcInstance load(int objectId) { + L2PcInstance player = null; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + // Retrieve the L2PcInstance from the characters table of the database + ps.setInt(1, objectId); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + final int activeClassId = rs.getInt("classid"); + final boolean female = rs.getInt("sex") != Sex.MALE.ordinal(); + PcAppearance app = new PcAppearance(rs.getByte("face"), rs.getByte("hairColor"), rs.getByte("hairStyle"), female); + + player = new L2PcInstance(objectId, activeClassId, rs.getString("account_name"), app); + player.setName(rs.getString("char_name")); + player.setLastAccess(rs.getLong("lastAccess")); + player.setExp(rs.getLong("exp")); + player.setExpBeforeDeath(rs.getLong("expBeforeDeath")); + player.setLevel(rs.getInt("level")); + player.setSp(rs.getInt("sp")); + player.setWantsPeace(rs.getInt("wantspeace")); + player.setHeading(rs.getInt("heading")); + player.setKarma(rs.getInt("karma")); + player.setFame(rs.getInt("fame")); + player.setPvpKills(rs.getInt("pvpkills")); + player.setPkKills(rs.getInt("pkkills")); + player.setOnlineTime(rs.getLong("onlinetime")); + player.setNewbie(rs.getInt("newbie")); + player.setNoble(rs.getInt("nobless") == 1); + + player.setClanJoinExpiryTime(rs.getLong("clan_join_expiry_time")); + if (player.getClanJoinExpiryTime() < System.currentTimeMillis()) { + player.setClanJoinExpiryTime(0); + } + player.setClanCreateExpiryTime(rs.getLong("clan_create_expiry_time")); + if (player.getClanCreateExpiryTime() < System.currentTimeMillis()) { + player.setClanCreateExpiryTime(0); + } + + player.setPowerGrade(rs.getInt("power_grade")); + player.setPledgeType(rs.getInt("subpledge")); + // player.setApprentice(rs.getInt("apprentice")); + + player.setDeleteTimer(rs.getLong("deletetime")); + player.setTitle(rs.getString("title")); + player.setAccessLevel(rs.getInt("accesslevel")); + player.getAppearance().setTitleColor(rs.getInt("title_color")); + player.setFistsWeaponItem(player.findFistsWeaponItem(activeClassId)); + player.setUptime(System.currentTimeMillis()); + + player.setCurrentCp(rs.getDouble("curCp")); + player.setCurrentHp(rs.getDouble("curHp")); + player.setCurrentMp(rs.getDouble("curMp")); + player.setClassIndex(0); + player.setBaseClass(rs.getInt("base_class")); + + // Restore Subclass Data (cannot be done earlier in function) + DAOFactory.getInstance().getSubclassDAO().load(player); + + if (activeClassId != player.getBaseClass()) { + for (SubClass subClass : player.getSubClasses().values()) { + if (subClass.getClassId() == activeClassId) { + player.setClassIndex(subClass.getClassIndex()); + } + } + } + + if ((player.getClassIndex() == 0) && (activeClassId != player.getBaseClass())) { + // Subclass in use but doesn't exist in DB - + // a possible restart-while-modify-subclass cheat has been attempted. + // Switching to use base class + player.setClassId(player.getBaseClass()); + LOG.warn("{} reverted to base class. Possibly has tried a relogin exploit while subclassing.", player); + } else { + player.setActiveClass(activeClassId); + } + + player.setApprentice(rs.getInt("apprentice")); + player.setSponsor(rs.getInt("sponsor")); + player.setLvlJoinedAcademy(rs.getInt("lvl_joined_academy")); + player.setIsIn7sDungeon(rs.getInt("isin7sdungeon") == 1); + + CursedWeaponsManager.getInstance().checkPlayer(player); + + player.setDeathPenaltyBuffLevel(rs.getInt("death_penalty_level")); + + player.setVitalityPoints(rs.getInt("vitality_points"), true); + + player.getHuntingSystem().setHuntingBonusTime(rs.getInt("hunting_bonus")); + + player.getHuntingSystem().setNevitBlessingPoints(rs.getInt("nevit_blessing_points")); + + player.getHuntingSystem().setNevitBlessingTime(rs.getInt("nevit_blessing_time")); + + // Set the x,y,z position of the L2PcInstance and make it invisible + player.setXYZInvisible(rs.getInt("x"), rs.getInt("y"), rs.getInt("z")); + + // Set Teleport Bookmark Slot + player.setBookMarkSlot(rs.getInt("BookmarkSlot")); + + // character creation Time + player.getCreateDate().setTimeInMillis(rs.getTimestamp("createDate").getTime()); + + // Language + player.setLang(rs.getString("language")); + + // Set Hero status if it applies + player.setHero(Hero.getInstance().isHero(objectId)); + + player.setClan(ClanTable.getInstance().getClan(rs.getInt("clanid"))); + + if (player.getClan() != null) { + if (player.getClan().getLeaderId() != player.getObjectId()) { + if (player.getPowerGrade() == 0) { + player.setPowerGrade(5); + } + player.setClanPrivileges(player.getClan().getRankPrivs(player.getPowerGrade())); + } else { + player.getClanPrivileges().setAll(); + player.setPowerGrade(1); + } + player.setPledgeClass(L2ClanMember.calculatePledgeClass(player)); + } else { + if (player.isNoble()) { + player.setPledgeClass(5); + } + + if (player.isHero()) { + player.setPledgeClass(8); + } + + player.getClanPrivileges().clear(); + } + } + } + } catch (Exception ex) { + LOG.error("Failed loading character!", ex); + } + return player; + } + + @Override + public void loadCharacters(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var stmt = con.prepareStatement(SELECT_CHARACTERS)) { + stmt.setString(1, player.getAccountName()); + stmt.setInt(2, player.getObjectId()); + try (var rs = stmt.executeQuery()) { + while (rs.next()) { + player.getAccountChars().put(rs.getInt("charId"), rs.getString("char_name")); + } + } + } catch (Exception e) { + LOG.error("Failed to load {} characters.", player, e); + } + } + + @Override + public boolean insert(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setString(1, player.getAccountName()); + ps.setInt(2, player.getObjectId()); + ps.setString(3, player.getName()); + ps.setInt(4, player.getBaseLevel()); + ps.setInt(5, player.getMaxHp()); + ps.setDouble(6, player.getCurrentHp()); + ps.setInt(7, player.getMaxCp()); + ps.setDouble(8, player.getCurrentCp()); + ps.setInt(9, player.getMaxMp()); + ps.setDouble(10, player.getCurrentMp()); + ps.setInt(11, player.getAppearance().getFace()); + ps.setInt(12, player.getAppearance().getHairStyle()); + ps.setInt(13, player.getAppearance().getHairColor()); + ps.setInt(14, player.getAppearance().getSex() ? 1 : 0); + ps.setLong(15, player.getBaseExp()); + ps.setInt(16, player.getBaseSp()); + ps.setInt(17, player.getKarma()); + ps.setInt(18, player.getFame()); + ps.setInt(19, player.getPvpKills()); + ps.setInt(20, player.getPkKills()); + ps.setInt(21, player.getClanId()); + ps.setInt(22, player.getRace().ordinal()); + ps.setInt(23, player.getClassId().getId()); + ps.setLong(24, player.getDeleteTimer()); + ps.setInt(25, player.hasDwarvenCraft() ? 1 : 0); + ps.setString(26, player.getTitle()); + ps.setInt(27, player.getAppearance().getTitleColor()); + ps.setInt(28, player.getAccessLevel().getLevel()); + ps.setInt(29, player.isOnlineInt()); + ps.setInt(30, player.isIn7sDungeon() ? 1 : 0); + ps.setInt(31, player.getClanPrivileges().getBitmask()); + ps.setInt(32, player.getWantsPeace()); + ps.setInt(33, player.getBaseClass()); + ps.setInt(34, player.getNewbie()); + ps.setInt(35, player.isNoble() ? 1 : 0); + ps.setLong(36, 0); + ps.setTimestamp(37, new Timestamp(player.getCreateDate().getTimeInMillis())); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.error("Could not insert player data!", ex); + return false; + } + return true; + } + + @Override + public void storeCharBase(L2PcInstance player) { + long totalOnlineTime = player.getOnlineTime(); + if (player.getOnlineBeginTime() > 0) { + totalOnlineTime += MILLISECONDS.toSeconds(System.currentTimeMillis() - player.getOnlineBeginTime()); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE)) { + ps.setInt(1, player.getBaseLevel()); + ps.setInt(2, player.getMaxHp()); + ps.setDouble(3, player.getCurrentHp()); + ps.setInt(4, player.getMaxCp()); + ps.setDouble(5, player.getCurrentCp()); + ps.setInt(6, player.getMaxMp()); + ps.setDouble(7, player.getCurrentMp()); + ps.setInt(8, player.getAppearance().getFace()); + ps.setInt(9, player.getAppearance().getHairStyle()); + ps.setInt(10, player.getAppearance().getHairColor()); + ps.setInt(11, player.getAppearance().getSex() ? 1 : 0); + ps.setInt(12, player.getHeading()); + ps.setInt(13, player.inObserverMode() ? player.getLastLocation().getX() : player.getX()); + ps.setInt(14, player.inObserverMode() ? player.getLastLocation().getY() : player.getY()); + ps.setInt(15, player.inObserverMode() ? player.getLastLocation().getZ() : player.getZ()); + ps.setLong(16, player.getBaseExp()); + ps.setLong(17, player.getExpBeforeDeath()); + ps.setInt(18, player.getBaseSp()); + ps.setInt(19, player.getKarma()); + ps.setInt(20, player.getFame()); + ps.setInt(21, player.getPvpKills()); + ps.setInt(22, player.getPkKills()); + ps.setInt(23, player.getClanId()); + ps.setInt(24, player.getRace().ordinal()); + ps.setInt(25, player.getClassId().getId()); + ps.setLong(26, player.getDeleteTimer()); + ps.setString(27, player.getTitle()); + ps.setInt(28, player.getAppearance().getTitleColor()); + ps.setInt(29, player.getAccessLevel().getLevel()); + ps.setInt(30, player.isOnlineInt()); + ps.setInt(31, player.isIn7sDungeon() ? 1 : 0); + ps.setInt(32, player.getClanPrivileges().getBitmask()); + ps.setInt(33, player.getWantsPeace()); + ps.setInt(34, player.getBaseClass()); + ps.setLong(35, totalOnlineTime); + ps.setInt(36, player.getNewbie()); + ps.setInt(37, player.isNoble() ? 1 : 0); + ps.setInt(38, player.getPowerGrade()); + ps.setInt(39, player.getPledgeType()); + ps.setInt(40, player.getLvlJoinedAcademy()); + ps.setLong(41, player.getApprentice()); + ps.setLong(42, player.getSponsor()); + ps.setLong(43, player.getClanJoinExpiryTime()); + ps.setLong(44, player.getClanCreateExpiryTime()); + ps.setString(45, player.getName()); + ps.setLong(46, player.getDeathPenaltyBuffLevel()); + ps.setInt(47, player.getBookMarkSlot()); + ps.setInt(48, player.getVitalityPoints()); + ps.setInt(49, player.getHuntingSystem().getHuntingBonusTime()); + ps.setInt(50, player.getHuntingSystem().getNevitBlessingPoints()); + ps.setInt(51, player.getHuntingSystem().getNevitBlessingTime()); + ps.setString(52, player.getLang()); + ps.setInt(53, player.getObjectId()); + + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not store {} base data!", player, ex); + } + } + + @Override + public void updateOnlineStatus(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_ONLINE)) { + ps.setInt(1, player.isOnlineInt()); + ps.setLong(2, System.currentTimeMillis()); + ps.setInt(3, player.getObjectId()); + ps.execute(); + } catch (Exception ex) { + LOG.error("Failed updating player online status!", ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerSkillSaveDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerSkillSaveDAOMySQLImpl.java new file mode 100644 index 00000000..bc6fd62c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PlayerSkillSaveDAOMySQLImpl.java @@ -0,0 +1,192 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.PlayerSkillSaveDAO; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.TimeStamp; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Player Skill Save DAO MySQL implementation. + * @author Zoey76 + */ +public class PlayerSkillSaveDAOMySQLImpl implements PlayerSkillSaveDAO { + + private static final Logger LOG = LoggerFactory.getLogger(PlayerSkillSaveDAOMySQLImpl.class); + + private static final String INSERT = "INSERT INTO character_skills_save (charId,skill_id,skill_level,remaining_time,reuse_delay,systime,restore_type,class_index,buff_index) VALUES (?,?,?,?,?,?,?,?,?)"; + + private static final String SELECT = "SELECT skill_id,skill_level,remaining_time, reuse_delay, systime, restore_type FROM character_skills_save WHERE charId=? AND class_index=? ORDER BY buff_index"; + + private static final String DELETE = "DELETE FROM character_skills_save WHERE charId=? AND class_index=?"; + + @Override + public void delete(L2PcInstance player, int classIndex) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, classIndex); + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not delete all effect data for player {}!", player, ex); + } + } + + @Override + public void delete(L2PcInstance player) { + delete(player, player.getClassIndex()); + } + + @Override + public void insert(L2PcInstance player, boolean storeEffects) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + int buff_index = 0; + final List storedSkills = new ArrayList<>(); + + // Store all effect data along with calculated remaining + // reuse delays for matching skills. 'restore_type'= 0. + if (storeEffects) { + for (BuffInfo info : player.getEffectList().getEffects()) { + if (info == null) { + continue; + } + + final Skill skill = info.getSkill(); + // Do not save heals. + if (skill.getAbnormalType() == AbnormalType.LIFE_FORCE_OTHERS) { + continue; + } + + if (skill.isToggle()) { + continue; + } + + // Dances and songs are not kept in retail. + if (skill.isDance() && !character().storeDances()) { + continue; + } + + if (storedSkills.contains(skill.getReuseHashCode())) { + continue; + } + + storedSkills.add(skill.getReuseHashCode()); + + ps.setInt(1, player.getObjectId()); + ps.setInt(2, skill.getId()); + ps.setInt(3, skill.getLevel()); + ps.setInt(4, info.getTime()); + + final TimeStamp t = player.getSkillReuseTimeStamp(skill.getReuseHashCode()); + ps.setLong(5, (t != null) && t.hasNotPassed() ? t.getReuse() : 0); + ps.setLong(6, (t != null) && t.hasNotPassed() ? t.getStamp() : 0); + + ps.setInt(7, 0); // Store type 0, active buffs/debuffs. + ps.setInt(8, player.getClassIndex()); + ps.setInt(9, ++buff_index); + ps.execute(); + } + } + + // Skills under reuse. + final Map reuseTimeStamps = player.getSkillReuseTimeStamps(); + if (reuseTimeStamps != null) { + for (Entry ts : reuseTimeStamps.entrySet()) { + final int hash = ts.getKey(); + if (storedSkills.contains(hash)) { + continue; + } + + final TimeStamp t = ts.getValue(); + if ((t != null) && t.hasNotPassed()) { + storedSkills.add(hash); + + ps.setInt(1, player.getObjectId()); + ps.setInt(2, t.getSkillId()); + ps.setInt(3, t.getSkillLvl()); + ps.setInt(4, -1); + ps.setLong(5, t.getReuse()); + ps.setLong(6, t.getStamp()); + ps.setInt(7, 1); // Restore type 1, skill reuse. + ps.setInt(8, player.getClassIndex()); + ps.setInt(9, ++buff_index); + ps.execute(); + } + } + } + } catch (Exception e) { + LOG.error("Could not store {} effect data!", player, e); + } + } + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, player.getClassIndex()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int remainingTime = rs.getInt("remaining_time"); + long reuseDelay = rs.getLong("reuse_delay"); + long systime = rs.getLong("systime"); + int restoreType = rs.getInt("restore_type"); + + final Skill skill = SkillData.getInstance().getSkill(rs.getInt("skill_id"), rs.getInt("skill_level")); + if (skill == null) { + continue; + } + + final long time = systime - System.currentTimeMillis(); + if (time > 10) { + player.disableSkill(skill, time); + player.addTimeStamp(skill, reuseDelay, systime); + } + + // Restore Type 1 The remaining skills lost effect upon logout but were still under a high reuse delay. + if (restoreType > 0) { + continue; + } + + // Restore Type 0 These skill were still in effect on the character upon logout. + // Some of which were self casted and might still have had a long reuse delay which also is restored. + skill.applyEffects(player, player, false, remainingTime); + } + } + } catch (Exception e) { + LOG.error("Could not restore {} active effect data!", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PremiumItemDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PremiumItemDAOMySQLImpl.java new file mode 100644 index 00000000..ce85ea41 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/PremiumItemDAOMySQLImpl.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.PremiumItemDAO; +import com.l2jserver.gameserver.model.L2PremiumItem; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Premium Item DAO MySQL implementation. + * @author Zoey76 + */ +public class PremiumItemDAOMySQLImpl implements PremiumItemDAO { + + private static final Logger LOG = LoggerFactory.getLogger(PremiumItemDAOMySQLImpl.class); + + private static final String GET_PREMIUM_ITEMS = "SELECT itemNum, itemId, itemCount, itemSender FROM character_premium_items WHERE charId=?"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(GET_PREMIUM_ITEMS)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int itemNum = rs.getInt("itemNum"); + int itemId = rs.getInt("itemId"); + long itemCount = rs.getLong("itemCount"); + String itemSender = rs.getString("itemSender"); + player.getPremiumItemList().put(itemNum, new L2PremiumItem(itemId, itemCount, itemSender)); + } + } + } catch (Exception ex) { + LOG.error("Could not restore premium items!", ex); + } + } + + @Override + public void update(L2PcInstance player, int itemNum, long newCount) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE character_premium_items SET itemCount=? WHERE charId=? AND itemNum=? ")) { + ps.setLong(1, newCount); + ps.setInt(2, player.getObjectId()); + ps.setInt(3, itemNum); + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not update premium items!", ex); + } + } + + @Override + public void delete(L2PcInstance player, int itemNum) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM character_premium_items WHERE charId=? AND itemNum=? ")) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, itemNum); + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not delete premium item!" + ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeBookDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeBookDAOMySQLImpl.java new file mode 100644 index 00000000..8d8a6e2a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeBookDAOMySQLImpl.java @@ -0,0 +1,108 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.RecipeBookDAO; +import com.l2jserver.gameserver.data.xml.impl.RecipeData; +import com.l2jserver.gameserver.model.L2RecipeList; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recipe Book DAO MySQL implementation. + * @author Zoey76 + */ +public class RecipeBookDAOMySQLImpl implements RecipeBookDAO { + + private static final Logger LOG = LoggerFactory.getLogger(RecipeBookDAOMySQLImpl.class); + + private static final String INSERT = "INSERT INTO character_recipebook (charId, id, classIndex, type) values(?,?,?,?)"; + + private static final String DELETE = "DELETE FROM character_recipebook WHERE charId=? AND id=? AND classIndex=?"; + + private static final String SELECT_COMMON = "SELECT id, type, classIndex FROM character_recipebook WHERE charId=?"; + + private static final String SELECT = "SELECT id FROM character_recipebook WHERE charId=? AND classIndex=? AND type = 1"; + + @Override + public void insert(L2PcInstance player, int recipeId, boolean isDwarf) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, recipeId); + ps.setInt(3, isDwarf ? player.getClassIndex() : 0); + ps.setInt(4, isDwarf ? 1 : 0); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error inserting recipe Id {} from player {}!", recipeId, player, ex); + } + } + + @Override + public void delete(L2PcInstance player, int recipeId, boolean isDwarf) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, recipeId); + ps.setInt(3, isDwarf ? player.getClassIndex() : 0); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error deleting recipe Id {} from player {}!", recipeId, player, ex); + } + } + + @Override + public void load(L2PcInstance player, boolean loadCommon) { + // TODO(Zoey76): Split into two methods. + final String sql = loadCommon ? SELECT_COMMON : SELECT; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(sql)) { + ps.setInt(1, player.getObjectId()); + if (!loadCommon) { + ps.setInt(2, player.getClassIndex()); + } + + try (var rs = ps.executeQuery()) { + // TODO(Zoey76): Why only dwarven is cleared? + player.getDwarvenRecipeBookClear(); + + final RecipeData rd = RecipeData.getInstance(); + while (rs.next()) { + final L2RecipeList recipe = rd.getRecipeList(rs.getInt("id")); + if (loadCommon) { + if (rs.getInt(2) == 1) { + if (rs.getInt(3) == player.getClassIndex()) { + player.registerDwarvenRecipeList(recipe, false); + } + } else { + player.registerCommonRecipeList(recipe, false); + } + } else { + player.registerDwarvenRecipeList(recipe, false); + } + } + } + } catch (Exception ex) { + LOG.error("Could not restore recipe book data!", ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeShopListDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeShopListDAOMySQLImpl.java new file mode 100644 index 00000000..4e99eb84 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecipeShopListDAOMySQLImpl.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.RecipeShopListDAO; +import com.l2jserver.gameserver.model.L2ManufactureItem; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recipe Shop List DAO MySQL implementation. + * @author Zoey76 + */ +public class RecipeShopListDAOMySQLImpl implements RecipeShopListDAO { + + private static final Logger LOG = LoggerFactory.getLogger(RecipeShopListDAOMySQLImpl.class); + + private static final String DELETE = "DELETE FROM character_recipeshoplist WHERE charId=?"; + + private static final String INSERT = "REPLACE INTO character_recipeshoplist (`charId`, `recipeId`, `price`, `index`) VALUES (?, ?, ?, ?)"; + + private static final String SELECT = "SELECT * FROM character_recipeshoplist WHERE charId=? ORDER BY `index`"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + player.getManufactureItems().clear(); + + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + player.getManufactureItems().put(rs.getInt("recipeId"), new L2ManufactureItem(rs.getInt("recipeId"), rs.getLong("price"))); + } + } + } catch (Exception e) { + LOG.error("Could not restore recipe shop list data for {}, {}", player, e); + } + } + + @Override + public void delete(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not store recipe shop for {}, {}", player, e); + } + } + + @Override + public void insert(L2PcInstance player) { + if (!player.hasManufactureShop()) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + int i = 1; + for (L2ManufactureItem item : player.getManufactureItems().values()) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, item.getRecipeId()); + ps.setLong(3, item.getCost()); + ps.setInt(4, i++); + ps.addBatch(); + } + ps.executeBatch(); + } catch (Exception e) { + LOG.error("Could not store recipe shop for {}, {}", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecommendationBonusDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecommendationBonusDAOMySQLImpl.java new file mode 100644 index 00000000..758e9e41 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/RecommendationBonusDAOMySQLImpl.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.RecommendationBonusDAO; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Recommendation Bonus DAO MySQL implementation. + * @author Zoey76 + */ +public class RecommendationBonusDAOMySQLImpl implements RecommendationBonusDAO { + + private static final Logger LOG = LoggerFactory.getLogger(RecommendationBonusDAOMySQLImpl.class); + + private static final String SELECT = "SELECT rec_have,rec_left,time_left FROM character_reco_bonus WHERE charId=? LIMIT 1"; + + private static final String INSERT = "INSERT INTO character_reco_bonus (charId,rec_have,rec_left,time_left) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE rec_have=?, rec_left=?, time_left=?"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + player.getRecSystem().setHave(rs.getInt("rec_have")); + player.getRecSystem().setLeft(rs.getInt("rec_left")); + player.getRecSystem().setBonusTime(rs.getInt("time_left")); + } else { + player.getRecSystem().setBonusTime(3600); + } + } + } catch (Exception e) { + LOG.error("Could not restore Recommendations for {}, {}", player, e); + } + } + + @Override + public void insert(L2PcInstance player, int recomTime) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, player.getRecSystem().getHave()); + ps.setInt(3, player.getRecSystem().getLeft()); + ps.setLong(4, recomTime); + // Update part + ps.setInt(5, player.getRecSystem().getHave()); + ps.setInt(6, player.getRecSystem().getLeft()); + ps.setLong(7, recomTime); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not update Recommendations for player: {}", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ServitorSkillSaveDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ServitorSkillSaveDAOMySQLImpl.java new file mode 100644 index 00000000..0925e057 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ServitorSkillSaveDAOMySQLImpl.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.LinkedList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.ServitorSkillSaveDAO; +import com.l2jserver.gameserver.data.sql.impl.SummonEffectsTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Servitor Skill Save DAO MySQL implementation. + * @author Zoey76 + */ +public class ServitorSkillSaveDAOMySQLImpl implements ServitorSkillSaveDAO { + + private static final Logger LOG = LoggerFactory.getLogger(ServitorSkillSaveDAOMySQLImpl.class); + + private static final String ADD_SKILL_SAVE = "INSERT INTO character_summon_skills_save (ownerId,ownerClassIndex,summonSkillId,skill_id,skill_level,remaining_time,buff_index) VALUES (?,?,?,?,?,?,?)"; + + private static final String RESTORE_SKILL_SAVE = "SELECT skill_id,skill_level,remaining_time,buff_index FROM character_summon_skills_save WHERE ownerId=? AND ownerClassIndex=? AND summonSkillId=? ORDER BY buff_index"; + + private static final String DELETE_SKILL_SAVE = "DELETE FROM character_summon_skills_save WHERE ownerId=? AND ownerClassIndex=? AND summonSkillId=?"; + + @Override + public void insert(L2ServitorInstance servitor, boolean storeEffects) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_SKILL_SAVE)) { + con.setAutoCommit(false); + // Delete all current stored effects for summon to avoid dupe + ps.setInt(1, servitor.getOwner().getObjectId()); + ps.setInt(2, servitor.getOwner().getClassIndex()); + ps.setInt(3, servitor.getReferenceSkill()); + ps.execute(); + + int buff_index = 0; + + final List storedSkills = new LinkedList<>(); + + // Store all effect data along with calculated remaining + if (storeEffects) { + try (var ps2 = con.prepareStatement(ADD_SKILL_SAVE)) { + for (BuffInfo info : servitor.getEffectList().getEffects()) { + if (info == null) { + continue; + } + + final Skill skill = info.getSkill(); + // Do not save heals. + if (skill.getAbnormalType() == AbnormalType.LIFE_FORCE_OTHERS) { + continue; + } + + if (skill.isToggle()) { + continue; + } + + // Dances and songs are not kept in retail. + if (skill.isDance() && !character().storeDances()) { + continue; + } + + if (storedSkills.contains(skill.getReuseHashCode())) { + continue; + } + + storedSkills.add(skill.getReuseHashCode()); + + ps2.setInt(1, servitor.getOwner().getObjectId()); + ps2.setInt(2, servitor.getOwner().getClassIndex()); + ps2.setInt(3, servitor.getReferenceSkill()); + ps2.setInt(4, skill.getId()); + ps2.setInt(5, skill.getLevel()); + ps2.setInt(6, info.getTime()); + ps2.setInt(7, ++buff_index); + ps2.addBatch(); + + SummonEffectsTable.getInstance().addServitorEffect(servitor.getOwner(), servitor.getReferenceSkill(), skill, info.getTime()); + } + ps2.executeBatch(); + } + } + con.commit(); + } catch (Exception e) { + LOG.error("Could not store summon effect data for owner {}, class index {}, skill {}!", servitor.getOwner().getObjectId(), servitor.getOwner().getClassIndex(), servitor.getReferenceSkill(), e); + } + } + + @Override + public void load(L2ServitorInstance servitor) { + try (var con = ConnectionFactory.getInstance().getConnection()) { + if (!SummonEffectsTable.getInstance().containsSkill(servitor.getOwner(), servitor.getReferenceSkill())) { + try (var ps = con.prepareStatement(RESTORE_SKILL_SAVE)) { + ps.setInt(1, servitor.getOwner().getObjectId()); + ps.setInt(2, servitor.getOwner().getClassIndex()); + ps.setInt(3, servitor.getReferenceSkill()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int effectCurTime = rs.getInt("remaining_time"); + + final Skill skill = SkillData.getInstance().getSkill(rs.getInt("skill_id"), rs.getInt("skill_level")); + if (skill == null) { + continue; + } + + if (skill.hasEffects(EffectScope.GENERAL)) { + SummonEffectsTable.getInstance().addServitorEffect(servitor.getOwner(), servitor.getReferenceSkill(), skill, effectCurTime); + } + } + } + } + } + + try (var ps = con.prepareStatement(DELETE_SKILL_SAVE)) { + ps.setInt(1, servitor.getOwner().getObjectId()); + ps.setInt(2, servitor.getOwner().getClassIndex()); + ps.setInt(3, servitor.getReferenceSkill()); + ps.executeUpdate(); + } + } catch (Exception e) { + LOG.error("Could not restore {} active effect data!", servitor, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ShortcutDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ShortcutDAOMySQLImpl.java new file mode 100644 index 00000000..380c42f9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/ShortcutDAOMySQLImpl.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.ShortcutDAO; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Shortcut DAO MySQL implementation. + * @author Zoey76 + */ +public class ShortcutDAOMySQLImpl implements ShortcutDAO { + + private static final Logger LOG = LoggerFactory.getLogger(ShortcutDAOMySQLImpl.class); + + private static final String DELETE = "DELETE FROM character_shortcuts WHERE charId=? AND class_index=?"; + + @Override + public boolean delete(L2PcInstance player, int classIndex) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, classIndex); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not modify sub class for {} to class index {}, {}", player, classIndex, e); + return false; + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SkillDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SkillDAOMySQLImpl.java new file mode 100644 index 00000000..75e64eee --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SkillDAOMySQLImpl.java @@ -0,0 +1,174 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.SkillDAO; +import com.l2jserver.gameserver.data.xml.impl.ClassListData; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.IllegalActionPunishmentType; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.Util; + +/** + * Skill DAO MySQL implementation. + * @author Zoey76 + */ +public class SkillDAOMySQLImpl implements SkillDAO { + + private static final Logger LOG = LoggerFactory.getLogger(SkillDAOMySQLImpl.class); + + private static final String SELECT = "SELECT skill_id,skill_level FROM character_skills WHERE charId=? AND class_index=?"; + + private static final String INSERT = "INSERT INTO character_skills (charId,skill_id,skill_level,class_index) VALUES (?,?,?,?)"; + + private static final String UPDATE = "UPDATE character_skills SET skill_level=? WHERE skill_id=? AND charId=? AND class_index=?"; + + private static final String REPLACE = "REPLACE INTO character_skills (charId,skill_id,skill_level,class_index) VALUES (?,?,?,?)"; + + private static final String DELETE_ONE = "DELETE FROM character_skills WHERE skill_id=? AND charId=? AND class_index=?"; + + private static final String DELETE_ALL = "DELETE FROM character_skills WHERE charId=? AND class_index=?"; + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, player.getClassIndex()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final int id = rs.getInt("skill_id"); + final int level = rs.getInt("skill_level"); + + // Create a L2Skill object for each record + final Skill skill = SkillData.getInstance().getSkill(id, level); + + if (skill == null) { + LOG.warn("Skipped null skill Id: {}, Level: {} while restoring player skills for {}", id, level, this); + continue; + } + + // Add the L2Skill object to the L2Character _skills and its Func objects to the calculator set of the L2Character + player.addSkill(skill); + + if (general().skillCheckEnable() && (!player.canOverrideCond(PcCondOverride.SKILL_CONDITIONS) || general().skillCheckGM())) { + if (!SkillTreesData.getInstance().isSkillAllowed(player, skill)) { + Util.handleIllegalPlayerAction(player, "Player " + player.getName() + " has invalid skill " + skill.getName() + " (" + skill.getId() + "/" + skill.getLevel() + "), class:" + + ClassListData.getInstance().getClass(player.getClassId()).getClassName(), IllegalActionPunishmentType.BROADCAST); + if (general().skillCheckRemove()) { + player.removeSkill(skill); + } + } + } + } + } + } catch (Exception ex) { + LOG.error("Could not restore {} skills!", player, ex); + } + } + + @Override + public void insert(L2PcInstance player, int classIndex, Skill skill) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, skill.getId()); + ps.setInt(3, skill.getLevel()); + ps.setInt(4, classIndex); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error could not store char skills!", ex); + } + } + + @Override + public void insert(L2PcInstance player, int newClassIndex, List newSkills) { + if (newSkills.isEmpty()) { + return; + } + + final int classIndex = (newClassIndex > -1) ? newClassIndex : player.getClassIndex(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(REPLACE)) { + con.setAutoCommit(false); + for (final Skill addSkill : newSkills) { + + ps.setInt(1, player.getObjectId()); + ps.setInt(2, addSkill.getId()); + ps.setInt(3, addSkill.getLevel()); + ps.setInt(4, classIndex); + ps.addBatch(); + } + ps.executeBatch(); + con.commit(); + } catch (Exception ex) { + LOG.error("Error could not store {} skills!", player, ex); + } + } + + @Override + public void update(L2PcInstance player, int classIndex, Skill newSkill, Skill oldSkill) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE)) { + ps.setInt(1, newSkill.getLevel()); + ps.setInt(2, oldSkill.getId()); + ps.setInt(3, player.getObjectId()); + ps.setInt(4, classIndex); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error could not store char skills!", ex); + } + } + + @Override + public void delete(L2PcInstance player, Skill skill) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_ONE)) { + ps.setInt(1, skill.getId()); + ps.setInt(2, player.getObjectId()); + ps.setInt(3, player.getClassIndex()); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error could not delete skill!", ex); + } + } + + @Override + public void deleteAll(L2PcInstance player, int classIndex) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_ALL)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, classIndex); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Error could not delete skill!", ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SubclassDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SubclassDAOMySQLImpl.java new file mode 100644 index 00000000..0f781110 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/SubclassDAOMySQLImpl.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.SubclassDAO; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.SubClass; + +/** + * Subclass DAO MySQL implementation. + * @author Zoey76 + */ +public class SubclassDAOMySQLImpl implements SubclassDAO { + + private static final Logger LOG = LoggerFactory.getLogger(SubclassDAOMySQLImpl.class); + + private static final String SELECT = "SELECT class_id,exp,sp,level,class_index FROM character_subclasses WHERE charId=? ORDER BY class_index"; + + private static final String INSERT = "INSERT INTO character_subclasses (charId,class_id,exp,sp,level,class_index) VALUES (?,?,?,?,?,?)"; + + private static final String UPDATE = "UPDATE character_subclasses SET exp=?,sp=?,level=?,class_id=? WHERE charId=? AND class_index =?"; + + private static final String DELETE = "DELETE FROM character_subclasses WHERE charId=? AND class_index=?"; + + @Override + public void update(L2PcInstance player) { + if (player.getTotalSubClasses() <= 0) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE)) { + for (SubClass subClass : player.getSubClasses().values()) { + ps.setLong(1, subClass.getExp()); + ps.setInt(2, subClass.getSp()); + ps.setInt(3, subClass.getLevel()); + ps.setInt(4, subClass.getClassId()); + ps.setInt(5, player.getObjectId()); + ps.setInt(6, subClass.getClassIndex()); + ps.addBatch(); + } + ps.executeBatch(); + } catch (Exception e) { + LOG.error("Could not store sub class data for {} : {}", player, e); + } + } + + @Override + public boolean insert(L2PcInstance player, SubClass newClass) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, newClass.getClassId()); + ps.setLong(3, newClass.getExp()); + ps.setInt(4, newClass.getSp()); + ps.setInt(5, newClass.getLevel()); + ps.setInt(6, newClass.getClassIndex()); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not add character sub class for {}, {}", player, e); + return false; + } + return true; + } + + @Override + public void delete(L2PcInstance player, int classIndex) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, classIndex); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not delete subclass for {} to class index {}, {}", player, classIndex, e); + } + } + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final SubClass subClass = new SubClass(player); + subClass.setClassId(rs.getInt("class_id")); + subClass.setExp(rs.getLong("exp")); + subClass.setLevel(rs.getInt("level")); + subClass.setSp(rs.getInt("sp")); + subClass.setClassIndex(rs.getInt("class_index")); + + // Enforce the correct indexing of _subClasses against their class indexes. + player.getSubClasses().put(subClass.getClassIndex(), subClass); + } + } + } catch (Exception e) { + LOG.error("Could not restore classes for {}, {}", player, e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/TeleportBookmarkDAOMySQLImpl.java b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/TeleportBookmarkDAOMySQLImpl.java new file mode 100644 index 00000000..3ac44ec0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/dao/impl/mysql/TeleportBookmarkDAOMySQLImpl.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.dao.impl.mysql; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.dao.TeleportBookmarkDAO; +import com.l2jserver.gameserver.model.TeleportBookmark; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Teleport Bookmark DAO MySQL implementation. + * @author Zoey76 + */ +public class TeleportBookmarkDAOMySQLImpl implements TeleportBookmarkDAO { + + private static final Logger LOG = LoggerFactory.getLogger(TeleportBookmarkDAOMySQLImpl.class); + + private static final String INSERT = "INSERT INTO character_tpbookmark (charId,Id,x,y,z,icon,tag,name) values (?,?,?,?,?,?,?,?)"; + + private static final String UPDATE = "UPDATE character_tpbookmark SET icon=?,tag=?,name=? where charId=? AND Id=?"; + + private static final String SELECT = "SELECT Id,x,y,z,icon,tag,name FROM character_tpbookmark WHERE charId=?"; + + private static final String DELETE = "DELETE FROM character_tpbookmark WHERE charId=? AND Id=?"; + + @Override + public void delete(L2PcInstance player, int id) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, id); + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not delete character teleport bookmark data!", ex); + } + } + + @Override + public void insert(L2PcInstance player, int id, int x, int y, int z, int icon, String tag, String name) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT)) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, id); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ps.setInt(6, icon); + ps.setString(7, tag); + ps.setString(8, name); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Could not insert character teleport bookmark data!", ex); + } + } + + @Override + public void update(L2PcInstance player, int id, int icon, String tag, String name) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE)) { + ps.setInt(1, icon); + ps.setString(2, tag); + ps.setString(3, name); + ps.setInt(4, player.getObjectId()); + ps.setInt(5, id); + ps.execute(); + } catch (Exception ex) { + LOG.error("Could not update character teleport bookmark data!", ex); + } + } + + @Override + public void load(L2PcInstance player) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT)) { + ps.setInt(1, player.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + player.getTpbookmarks().put(rs.getInt("Id"), new TeleportBookmark(rs.getInt("Id"), rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("icon"), rs.getString("tag"), rs.getString("name"))); + } + } + } catch (Exception ex) { + LOG.error("Failed restoring character teleport bookmark!", ex); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/json/ExperienceData.java b/src/main/java/com/l2jserver/gameserver/data/json/ExperienceData.java new file mode 100644 index 00000000..5881833e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/json/ExperienceData.java @@ -0,0 +1,91 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.json; + +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; + +/** + * @author Zealar + * @since 2.6.0.0 + */ +public final class ExperienceData { + private static final Logger LOG = LoggerFactory.getLogger(ExperienceData.class); + + private static final Gson GSON = new Gson(); + private static final Type TYPE_MAP_INTEGER_LONG = new TypeToken>() { + }.getType(); + private final Map _expTable = new HashMap<>(); + + ExperienceData() { + load(); + } + + public void load() { + _expTable.clear(); + try (JsonReader reader = new JsonReader(new FileReader(new File(server().getDatapackRoot(), "data/stats/expData.json")))) { + _expTable.putAll(GSON.fromJson(reader, TYPE_MAP_INTEGER_LONG)); + } catch (FileNotFoundException ex) { + LOG.warn("data/stats/expData.json not found!", ex); + } catch (IOException ex) { + LOG.warn("Failed to load expData.json for: ", ex); + } + } + + /** + * Gets the exp for level. + * @param level the level required. + * @return the experience points required to reach the given level. + */ + public long getExpForLevel(int level) { + return _expTable.get(level); + } + + public float getPercentFromCurrentLevel(long exp, int level) { + long expPerLevel = getExpForLevel(level); + long expPerLevel2 = getExpForLevel(level + 1); + return (float) (exp - expPerLevel) / (expPerLevel2 - expPerLevel); + } + + /** + * Gets the single instance of ExperienceTable. + * @return single instance of ExperienceTable + */ + public static ExperienceData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + static final ExperienceData _instance = new ExperienceData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/AnnouncementsTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/AnnouncementsTable.java new file mode 100644 index 00000000..f1c2fc92 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/AnnouncementsTable.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentSkipListMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.announce.Announcement; +import com.l2jserver.gameserver.model.announce.AnnouncementType; +import com.l2jserver.gameserver.model.announce.AutoAnnouncement; +import com.l2jserver.gameserver.model.announce.IAnnouncement; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; + +/** + * Loads announcements from database. + * @author UnAfraid + */ +public final class AnnouncementsTable { + + private static final Logger LOG = LoggerFactory.getLogger(AnnouncementsTable.class); + + private final Map _announcements = new ConcurrentSkipListMap<>(); + + protected AnnouncementsTable() { + load(); + } + + private void load() { + _announcements.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery("SELECT `id`, `type`, `initial`, `delay`, `repeat`, `author`, `content` FROM announcements")) { + while (rs.next()) { + final AnnouncementType type = AnnouncementType.findById(rs.getInt("type")); + final var author = rs.getString("author"); + final var content = rs.getString("content"); + final Announcement announce; + switch (type) { + case NORMAL, CRITICAL -> announce = new Announcement(rs.getInt("id"), type, content, author); + case AUTO_NORMAL, AUTO_CRITICAL -> announce = new AutoAnnouncement(type, content, author, rs.getLong("initial"), rs.getLong("delay"), rs.getInt("repeat")); + default -> { + continue; + } + } + _announcements.put(announce.getId(), announce); + } + } catch (Exception e) { + LOG.warn("Failed loading announcements:", e); + } + } + + /** + * Sending all announcements to the player + * @param player + */ + public void showAnnouncements(L2PcInstance player) { + sendAnnouncements(player, AnnouncementType.NORMAL); + sendAnnouncements(player, AnnouncementType.CRITICAL); + sendAnnouncements(player, AnnouncementType.EVENT); + } + + /** + * Sends all announcements to the player by the specified type + * @param player + * @param type + */ + public void sendAnnouncements(L2PcInstance player, AnnouncementType type) { + for (IAnnouncement announce : _announcements.values()) { + if (announce.isValid() && (announce.getType() == type)) { + player.sendPacket(new CreatureSay(0, // + type == AnnouncementType.CRITICAL ? Say2.CRITICAL_ANNOUNCE : Say2.ANNOUNCEMENT, // + player.getName(), announce.getContent())); + } + } + } + + /** + * Adds announcement + * @param announce + */ + public void addAnnouncement(IAnnouncement announce) { + if (announce.storeMe()) { + _announcements.put(announce.getId(), announce); + } + } + + /** + * Removes announcement by id + * @param id + * @return {@code true} if announcement exists and was deleted successfully, {@code false} otherwise. + */ + public boolean deleteAnnouncement(int id) { + final IAnnouncement announce = _announcements.remove(id); + return (announce != null) && announce.deleteMe(); + } + + /** + * @param id + * @return {@link IAnnouncement} by id + */ + public IAnnouncement getAnnounce(int id) { + return _announcements.get(id); + } + + /** + * @return {@link Collection} containing all announcements + */ + public Collection getAllAnnouncements() { + return _announcements.values(); + } + + /** + * @return Single instance of {@link AnnouncementsTable} + */ + public static AnnouncementsTable getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final AnnouncementsTable _instance = new AnnouncementsTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharNameTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharNameTable.java new file mode 100644 index 00000000..985a7bb4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharNameTable.java @@ -0,0 +1,201 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Loads name and access level for all players. + * @since 2005/03/27 + */ +public class CharNameTable { + + private static final Logger LOG = LoggerFactory.getLogger(CharNameTable.class); + + private final Map _chars = new ConcurrentHashMap<>(); + + private final Map _accessLevels = new ConcurrentHashMap<>(); + + protected CharNameTable() { + if (general().cacheCharNames()) { + loadAll(); + } + } + + public final void addName(L2PcInstance player) { + if (player != null) { + addName(player.getObjectId(), player.getName()); + _accessLevels.put(player.getObjectId(), player.getAccessLevel().getLevel()); + } + } + + private void addName(int objectId, String name) { + if (name != null) { + if (!name.equals(_chars.get(objectId))) { + _chars.put(objectId, name); + } + } + } + + public final void removeName(int objId) { + _chars.remove(objId); + _accessLevels.remove(objId); + } + + public final int getIdByName(String name) { + if ((name == null) || name.isEmpty()) { + return -1; + } + + for (Entry entry : _chars.entrySet()) { + if (entry.getValue().equalsIgnoreCase(name)) { + return entry.getKey(); + } + } + + if (general().cacheCharNames()) { + return -1; + } + + int id = -1; + int accessLevel = 0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT charId,accesslevel FROM characters WHERE char_name=?")) { + ps.setString(1, name); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + id = rs.getInt(1); + accessLevel = rs.getInt(2); + } + } + } catch (Exception ex) { + LOG.warn("Could not check existing char name!", ex); + } + + if (id > 0) { + _chars.put(id, name); + _accessLevels.put(id, accessLevel); + return id; + } + + return -1; // not found + } + + public final String getNameById(int id) { + if (id <= 0) { + return null; + } + + String name = _chars.get(id); + if (name != null) { + return name; + } + + if (general().cacheCharNames()) { + return null; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT char_name,accesslevel FROM characters WHERE charId=?")) { + ps.setInt(1, id); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + name = rs.getString(1); + _chars.put(id, name); + _accessLevels.put(id, rs.getInt(2)); + return name; + } + } + } catch (Exception ex) { + LOG.warn("Could not check existing char id!", ex); + } + + return null; // not found + } + + public final int getAccessLevelById(int objectId) { + if (getNameById(objectId) != null) { + return _accessLevels.get(objectId); + } + + return 0; + } + + public boolean doesCharNameExist(String name) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT account_name FROM characters WHERE char_name=?")) { + ps.setString(1, name); + try (var rs = ps.executeQuery()) { + return rs.next(); + } + } catch (Exception ex) { + LOG.warn("Could not check existing player name!", ex); + } + return false; + } + + public int getAccountCharacterCount(String account) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT COUNT(char_name) FROM characters WHERE account_name=?")) { + ps.setString(1, account); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } + } + } catch (Exception ex) { + LOG.warn("Could not check existing char count!", ex); + } + return 0; + } + + private void loadAll() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT charId, char_name, accesslevel FROM characters")) { + while (rs.next()) { + final int id = rs.getInt(1); + _chars.put(id, rs.getString(2)); + _accessLevels.put(id, rs.getInt(3)); + } + } catch (Exception ex) { + LOG.warn("Could not load char name!", ex); + } + LOG.info("Loaded {} char names.", _chars.size()); + } + + public static CharNameTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final CharNameTable INSTANCE = new CharNameTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharSummonTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharSummonTable.java new file mode 100644 index 00000000..d2da1841 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CharSummonTable.java @@ -0,0 +1,218 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.L2PetData; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.PetItemList; + +/** + * @author Nyaran + */ +public class CharSummonTable { + + private static final Logger LOG = LoggerFactory.getLogger(CharSummonTable.class); + + private static final Map _pets = new ConcurrentHashMap<>(); + + private static final Map _servitors = new ConcurrentHashMap<>(); + + // SQL + private static final String INIT_PET = "SELECT ownerId, item_obj_id FROM pets WHERE restore=TRUE"; + + private static final String INIT_SUMMONS = "SELECT ownerId, summonSkillId FROM character_summons"; + + private static final String LOAD_SUMMON = "SELECT curHp, curMp, time FROM character_summons WHERE ownerId = ? AND summonSkillId = ?"; + + private static final String REMOVE_SUMMON = "DELETE FROM character_summons WHERE ownerId = ?"; + + private static final String SAVE_SUMMON = "REPLACE INTO character_summons (ownerId,summonSkillId,curHp,curMp,time) VALUES (?,?,?,?,?)"; + + public Map getPets() { + return _pets; + } + + public Map getServitors() { + return _servitors; + } + + public void init() { + if (character().restoreServitorOnReconnect()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(INIT_SUMMONS)) { + while (rs.next()) { + _servitors.put(rs.getInt("ownerId"), rs.getInt("summonSkillId")); + } + } catch (Exception e) { + LOG.warn("Error while loading saved servitor!", e); + } + } + + if (character().restorePetOnReconnect()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(INIT_PET)) { + while (rs.next()) { + _pets.put(rs.getInt("ownerId"), rs.getInt("item_obj_id")); + } + } catch (Exception e) { + LOG.warn("Error while loading saved pet!", e); + } + } + } + + public void removeServitor(L2PcInstance activeChar) { + _servitors.remove(activeChar.getObjectId()); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(REMOVE_SUMMON)) { + ps.setInt(1, activeChar.getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Summon cannot be removed!", e); + } + } + + public void restorePet(L2PcInstance activeChar) { + final L2ItemInstance item = activeChar.getInventory().getItemByObjectId(_pets.get(activeChar.getObjectId())); + if (item == null) { + LOG.warn("Null pet summoning item for player {}!", activeChar); + return; + } + final L2PetData petData = PetDataTable.getInstance().getPetDataByItemId(item.getId()); + if (petData == null) { + LOG.warn("Null pet data for: {} and summoning item {}!", activeChar, item); + return; + } + final L2NpcTemplate npcTemplate = NpcData.getInstance().getTemplate(petData.getNpcId()); + if (npcTemplate == null) { + LOG.warn("Null pet NPC template for player {} and pet ID {}!", activeChar, petData.getNpcId()); + return; + } + + final L2PetInstance pet = L2PetInstance.spawnPet(npcTemplate, activeChar, item); + if (pet == null) { + LOG.warn("Null pet instance for player {} and pet NPC template {}!", activeChar, npcTemplate); + return; + } + + pet.setShowSummonAnimation(true); + pet.setTitle(activeChar.getName()); + + if (!pet.isRespawned()) { + pet.setCurrentHp(pet.getMaxHp()); + pet.setCurrentMp(pet.getMaxMp()); + pet.setExp(pet.getExpForThisLevel()); + pet.setCurrentFed(pet.getMaxFed()); + } + + pet.setRunning(); + + if (!pet.isRespawned()) { + pet.storeMe(); + } + + item.setEnchantLevel(pet.getLevel()); + activeChar.setPet(pet); + pet.spawnMe(activeChar.getX() + 50, activeChar.getY() + 100, activeChar.getZ()); + pet.startFeed(); + pet.setFollowStatus(true); + pet.getOwner().sendPacket(new PetItemList(pet.getInventory().getItems())); + pet.broadcastStatusUpdate(); + } + + public void restoreServitor(L2PcInstance activeChar) { + int skillId = _servitors.get(activeChar.getObjectId()); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(LOAD_SUMMON)) { + ps.setInt(1, activeChar.getObjectId()); + ps.setInt(2, skillId); + try (var rs = ps.executeQuery()) { + Skill skill; + + while (rs.next()) { + int curHp = rs.getInt("curHp"); + int curMp = rs.getInt("curMp"); + int time = rs.getInt("time"); + + skill = SkillData.getInstance().getSkill(skillId, activeChar.getSkillLevel(skillId)); + if (skill == null) { + removeServitor(activeChar); + return; + } + + skill.applyEffects(activeChar, activeChar); + if (activeChar.hasServitor()) { + final L2ServitorInstance summon = (L2ServitorInstance) activeChar.getSummon(); + summon.setCurrentHp(curHp); + summon.setCurrentMp(curMp); + summon.setLifeTimeRemaining(time); + } + } + } + } catch (Exception e) { + LOG.warn("Servitor cannot be restored!", e); + } + } + + public void saveSummon(L2ServitorInstance summon) { + if ((summon == null) || (summon.getLifeTimeRemaining() <= 0)) { + return; + } + + _servitors.put(summon.getOwner().getObjectId(), summon.getReferenceSkill()); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SAVE_SUMMON)) { + ps.setInt(1, summon.getOwner().getObjectId()); + ps.setInt(2, summon.getReferenceSkill()); + ps.setInt(3, (int) Math.round(summon.getCurrentHp())); + ps.setInt(4, (int) Math.round(summon.getCurrentMp())); + ps.setInt(5, summon.getLifeTimeRemaining()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Failed to store summon {} from {}!", summon, summon.getOwner(), e); + } + } + + public static CharSummonTable getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final CharSummonTable _instance = new CharSummonTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/ClanTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/ClanTable.java new file mode 100644 index 00000000..084cfe18 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/ClanTable.java @@ -0,0 +1,461 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.AuctionManager; +import com.l2jserver.gameserver.instancemanager.ClanHallSiegeManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.model.ClanPrivilege; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Auction; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.FortSiege; +import com.l2jserver.gameserver.model.entity.Siege; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanCreate; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanDestroy; +import com.l2jserver.gameserver.model.events.impl.clan.ClanWarFinish; +import com.l2jserver.gameserver.model.events.impl.clan.ClanWarStart; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowInfoUpdate; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListAll; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.EnumIntBitmask; + +/** + * Clan Table. + */ +public class ClanTable { + + private static final Logger LOG = LoggerFactory.getLogger(ClanTable.class); + + public static final int CLAN_NAME_MAX_LENGTH = 16; + + private final Map _clans = new ConcurrentHashMap<>(); + + protected ClanTable() { + L2Clan clan; + // Count the clans + int clanCount = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT clan_id FROM clan_data")) { + while (rs.next()) { + int clanId = rs.getInt("clan_id"); + _clans.put(clanId, new L2Clan(clanId)); + clan = getClan(clanId); + if (clan.getDissolvingExpiryTime() != 0) { + scheduleRemoveClan(clan.getId()); + } + clanCount++; + } + } catch (Exception ex) { + LOG.error("There has been an error restoring clans from database!", ex); + } + LOG.info("Restored {} clans from the database.", clanCount); + allianceCheck(); + restorewars(); + } + + /** + * Gets the clans. + * @return the clans + */ + public Collection getClans() { + return _clans.values(); + } + + /** + * Gets the clan count. + * @return the clan count + */ + public int getClanCount() { + return _clans.size(); + } + + public L2Clan getClan(int clanId) { + return _clans.get(clanId); + } + + public L2Clan getClanByName(String clanName) { + return getClans().stream().filter(c -> c.getName().equalsIgnoreCase(clanName)).findFirst().orElse(null); + } + + /** + * Creates a new clan and store clan info to database + * @param player + * @param clanName + * @return NULL if clan with same name already exists + */ + public L2Clan createClan(L2PcInstance player, String clanName) { + if (null == player) { + return null; + } + + if (general().debug()) { + LOG.info("{} requested a clan creation.", player); + } + + if (10 > player.getLevel()) { + player.sendPacket(SystemMessageId.YOU_DO_NOT_MEET_CRITERIA_IN_ORDER_TO_CREATE_A_CLAN); + return null; + } + if (0 != player.getClanId()) { + player.sendPacket(SystemMessageId.FAILED_TO_CREATE_CLAN); + return null; + } + if (System.currentTimeMillis() < player.getClanCreateExpiryTime()) { + player.sendPacket(SystemMessageId.YOU_MUST_WAIT_XX_DAYS_BEFORE_CREATING_A_NEW_CLAN); + return null; + } + if (clanName.length() < 2) { + player.sendPacket(SystemMessageId.CLAN_NAME_INCORRECT); + return null; + } + if (clanName.length() > CLAN_NAME_MAX_LENGTH) { + player.sendPacket(SystemMessageId.CLAN_NAME_TOO_LONG); + return null; + } + + if (null != getClanByName(clanName)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_ALREADY_EXISTS); + sm.addString(clanName); + player.sendPacket(sm); + return null; + } + + final var objectId = IdFactory.getInstance().getNextId(); + final var clan = new L2Clan(objectId, clanName); + final var leader = new L2ClanMember(clan, player); + clan.setLeader(leader); + leader.setPlayerInstance(player); + clan.store(); + player.setClan(clan); + player.setPledgeClass(L2ClanMember.calculatePledgeClass(player)); + player.setClanPrivileges(new EnumIntBitmask<>(ClanPrivilege.class, true)); + + _clans.put(clan.getId(), clan); + + // should be update packet only + player.sendPacket(new PledgeShowInfoUpdate(clan)); + player.sendPacket(new PledgeShowMemberListAll(clan, player)); + player.sendPacket(new UserInfo(player)); + player.sendPacket(new ExBrExtraUserInfo(player)); + player.sendPacket(new PledgeShowMemberListUpdate(player)); + player.sendPacket(SystemMessageId.CLAN_CREATED); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanCreate(player, clan)); + return clan; + } + + public synchronized void destroyClan(int clanId) { + L2Clan clan = getClan(clanId); + if (clan == null) { + return; + } + + clan.broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.CLAN_HAS_DISPERSED)); + int castleId = clan.getCastleId(); + if (castleId == 0) { + for (Siege siege : SiegeManager.getInstance().getSieges()) { + siege.removeSiegeClan(clan); + } + } + + int fortId = clan.getFortId(); + if (fortId == 0) { + for (FortSiege siege : FortSiegeManager.getInstance().getSieges()) { + siege.removeAttacker(clan); + } + } + + int hallId = clan.getHideoutId(); + if (hallId == 0) { + for (SiegableHall hall : ClanHallSiegeManager.getInstance().getConquerableHalls().values()) { + hall.removeAttacker(clan); + } + } + + Auction auction = AuctionManager.getInstance().getAuction(clan.getAuctionBidAt()); + if (auction != null) { + auction.cancelBid(clan.getId()); + } + + L2ClanMember leaderMember = clan.getLeader(); + if (leaderMember == null) { + clan.getWarehouse().destroyAllItems("ClanRemove", null, null); + } else { + clan.getWarehouse().destroyAllItems("ClanRemove", clan.getLeader().getPlayerInstance(), null); + } + + for (L2ClanMember member : clan.getMembers()) { + clan.removeClanMember(member.getObjectId(), 0); + } + + _clans.remove(clanId); + IdFactory.getInstance().releaseId(clanId); + + try (var con = ConnectionFactory.getInstance().getConnection()) { + try (var ps = con.prepareStatement("DELETE FROM clan_data WHERE clan_id=?")) { + ps.setInt(1, clanId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM clan_privs WHERE clan_id=?")) { + ps.setInt(1, clanId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM clan_skills WHERE clan_id=?")) { + ps.setInt(1, clanId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM clan_subpledges WHERE clan_id=?")) { + ps.setInt(1, clanId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM clan_wars WHERE clan1=? OR clan2=?")) { + ps.setInt(1, clanId); + ps.setInt(2, clanId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM clan_notices WHERE clan_id=?")) { + ps.setInt(1, clanId); + ps.execute(); + } + + if (castleId != 0) { + try (var ps = con.prepareStatement("UPDATE castle SET taxPercent = 0 WHERE id = ?")) { + ps.setInt(1, castleId); + ps.execute(); + } + } + + if (fortId != 0) { + Fort fort = FortManager.getInstance().getFortById(fortId); + if (fort != null) { + L2Clan owner = fort.getOwnerClan(); + if (clan == owner) { + fort.removeOwner(true); + } + } + } + + if (hallId != 0) { + SiegableHall hall = ClanHallSiegeManager.getInstance().getSiegableHall(hallId); + if ((hall != null) && (hall.getOwnerId() == clanId)) { + hall.free(); + } + } + } catch (Exception ex) { + LOG.error("There has been an error deleting a clan from database!", ex); + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanDestroy(leaderMember, clan)); + } + + public void scheduleRemoveClan(final int clanId) { + ThreadPoolManager.getInstance().scheduleGeneral(() -> { + if (getClan(clanId) == null) { + return; + } + if (getClan(clanId).getDissolvingExpiryTime() != 0) { + destroyClan(clanId); + } + }, Math.max(getClan(clanId).getDissolvingExpiryTime() - System.currentTimeMillis(), 300000)); + } + + public boolean isAllyExists(String allyName) { + for (L2Clan clan : getClans()) { + if ((clan.getAllyName() != null) && clan.getAllyName().equalsIgnoreCase(allyName)) { + return true; + } + } + return false; + } + + public void storeclanswars(int clanId1, int clanId2) { + final L2Clan clan1 = getClan(clanId1); + final L2Clan clan2 = getClan(clanId2); + + EventDispatcher.getInstance().notifyEventAsync(new ClanWarStart(clan1, clan2)); + + clan1.setEnemyClan(clan2); + clan2.setAttackerClan(clan1); + clan1.broadcastClanStatus(); + clan2.broadcastClanStatus(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO clan_wars (clan1, clan2, wantspeace1, wantspeace2) VALUES(?,?,?,?)")) { + ps.setInt(1, clanId1); + ps.setInt(2, clanId2); + ps.setInt(3, 0); + ps.setInt(4, 0); + ps.execute(); + } catch (Exception ex) { + LOG.error("There has been an error saving clan wars data!", ex); + } + + // SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.WAR_WITH_THE_S1_CLAN_HAS_BEGUN); + // + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.CLAN_WAR_DECLARED_AGAINST_S1_IF_KILLED_LOSE_LOW_EXP); + msg.addString(clan2.getName()); + clan1.broadcastToOnlineMembers(msg); + // msg = SystemMessage.getSystemMessage(SystemMessageId.WAR_WITH_THE_S1_CLAN_HAS_BEGUN); + // msg.addString(clan1.getName()); + // clan2.broadcastToOnlineMembers(msg); + // clan1 declared clan war. + msg = SystemMessage.getSystemMessage(SystemMessageId.CLAN_S1_DECLARED_WAR); + msg.addString(clan1.getName()); + clan2.broadcastToOnlineMembers(msg); + } + + public void deleteclanswars(int clanId1, int clanId2) { + L2Clan clan1 = getClan(clanId1); + L2Clan clan2 = getClan(clanId2); + + EventDispatcher.getInstance().notifyEventAsync(new ClanWarFinish(clan1, clan2)); + + clan1.deleteEnemyClan(clan2); + clan2.deleteAttackerClan(clan1); + clan1.broadcastClanStatus(); + clan2.broadcastClanStatus(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM clan_wars WHERE clan1=? AND clan2=?")) { + ps.setInt(1, clanId1); + ps.setInt(2, clanId2); + ps.execute(); + } catch (Exception ex) { + LOG.error("There has been an error removing clan wars data!", ex); + } + + // SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.WAR_WITH_THE_S1_CLAN_HAS_ENDED); + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.WAR_AGAINST_S1_HAS_STOPPED); + msg.addString(clan2.getName()); + clan1.broadcastToOnlineMembers(msg); + msg = SystemMessage.getSystemMessage(SystemMessageId.CLAN_S1_HAS_DECIDED_TO_STOP); + msg.addString(clan1.getName()); + clan2.broadcastToOnlineMembers(msg); + } + + public void checkSurrender(L2Clan clan1, L2Clan clan2) { + int count = 0; + for (L2ClanMember player : clan1.getMembers()) { + if ((player != null) && (player.getPlayerInstance().getWantsPeace() == 1)) { + count++; + } + } + if (count == (clan1.getMembers().length - 1)) { + clan1.deleteEnemyClan(clan2); + clan2.deleteEnemyClan(clan1); + deleteclanswars(clan1.getId(), clan2.getId()); + } + } + + private void restorewars() { + L2Clan clan1, clan2; + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.createStatement(); + var rs = statement.executeQuery("SELECT clan1, clan2 FROM clan_wars")) { + while (rs.next()) { + clan1 = getClan(rs.getInt("clan1")); + clan2 = getClan(rs.getInt("clan2")); + if ((clan1 != null) && (clan2 != null)) { + clan1.setEnemyClan(rs.getInt("clan2")); + clan2.setAttackerClan(rs.getInt("clan1")); + } else { + LOG.warn("While restoring clan wars on of the clans [{}, {}] is null!", clan1, clan2); + } + } + } catch (Exception ex) { + LOG.error("There has been an error restoring clan wars data!", ex); + } + } + + /** + * Check for nonexistent alliances + */ + private void allianceCheck() { + for (L2Clan clan : _clans.values()) { + int allyId = clan.getAllyId(); + if ((allyId != 0) && (clan.getId() != allyId)) { + if (!_clans.containsKey(allyId)) { + clan.setAllyId(0); + clan.setAllyName(null); + clan.changeAllyCrest(0, true); + clan.updateClanInDB(); + LOG.info("Removed alliance from clan {}.", clan); + } + } + } + } + + public List getClanAllies(int allianceId) { + final List clanAllies = new ArrayList<>(); + if (allianceId != 0) { + for (L2Clan clan : _clans.values()) { + if ((clan != null) && (clan.getAllyId() == allianceId)) { + clanAllies.add(clan); + } + } + } + return clanAllies; + } + + public void storeClanScore() { + for (L2Clan clan : _clans.values()) { + clan.updateClanScoreInDB(); + } + } + + public static ClanTable getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final ClanTable _instance = new ClanTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/CrestTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CrestTable.java new file mode 100644 index 00000000..d7d6eb89 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/CrestTable.java @@ -0,0 +1,193 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static java.sql.ResultSet.CONCUR_UPDATABLE; +import static java.sql.ResultSet.TYPE_FORWARD_ONLY; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Crest; +import com.l2jserver.gameserver.model.L2Crest.CrestType; + +/** + * Loads and saves crests from database. + * @author NosBit + */ +public final class CrestTable { + + private static final Logger LOG = LoggerFactory.getLogger(CrestTable.class); + + private final Map _crests = new ConcurrentHashMap<>(); + + private final AtomicInteger _nextId = new AtomicInteger(1); + + protected CrestTable() { + load(); + } + + public synchronized void load() { + _crests.clear(); + Set crestsInUse = new HashSet<>(); + for (L2Clan clan : ClanTable.getInstance().getClans()) { + if (clan.getCrestId() != 0) { + crestsInUse.add(clan.getCrestId()); + } + + if (clan.getCrestLargeId() != 0) { + crestsInUse.add(clan.getCrestLargeId()); + } + + if (clan.getAllyCrestId() != 0) { + crestsInUse.add(clan.getAllyCrestId()); + } + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.createStatement(TYPE_FORWARD_ONLY, CONCUR_UPDATABLE); + var rs = statement.executeQuery("SELECT `crest_id`, `data`, `type` FROM `crests` ORDER BY `crest_id` DESC")) { + while (rs.next()) { + int id = rs.getInt("crest_id"); + + if (_nextId.get() <= id) { + _nextId.set(id + 1); + } + + // delete all unused crests except the last one we don't want to reuse + // a crest id because client will display wrong crest if its reused + if (!crestsInUse.contains(id) && (id != (_nextId.get() - 1))) { + rs.deleteRow(); + continue; + } + + byte[] data = rs.getBytes("data"); + CrestType crestType = CrestType.getById(rs.getInt("type")); + if (crestType != null) { + _crests.put(id, new L2Crest(id, data, crestType)); + } else { + LOG.warn("Unknown crest type {} found in database!", rs.getInt("type")); + } + } + + } catch (Exception ex) { + LOG.warn("There was an error while loading crests from database!", ex); + } + + LOG.info("Loaded {} crests.", _crests.size()); + + for (L2Clan clan : ClanTable.getInstance().getClans()) { + if (clan.getCrestId() != 0) { + if (getCrest(clan.getCrestId()) == null) { + LOG.info("Removing non-existent crest Id {} for clan {}.", clan.getCrestId(), clan.getName()); + clan.setCrestId(0); + clan.changeClanCrest(0); + } + } + + if (clan.getCrestLargeId() != 0) { + if (getCrest(clan.getCrestLargeId()) == null) { + LOG.info("Removing non-existent large crest Id {} for clan {}.", clan.getCrestId(), clan.getName()); + clan.setCrestLargeId(0); + clan.changeLargeCrest(0); + } + } + + if (clan.getAllyCrestId() != 0) { + if (getCrest(clan.getAllyCrestId()) == null) { + LOG.info("Removing non-existent ally crest Id {} for clan {}.", clan.getCrestId(), clan.getName()); + clan.setAllyCrestId(0); + clan.changeAllyCrest(0, true); + } + } + } + } + + /** + * @param crestId The crest id + * @return {@code L2Crest} if crest is found, {@code null} if crest was not found. + */ + public L2Crest getCrest(int crestId) { + return _crests.get(crestId); + } + + /** + * Creates a {@code L2Crest} object and inserts it in database and cache. + * @param data + * @param crestType + * @return {@code L2Crest} on success, {@code null} on failure. + */ + public L2Crest createCrest(byte[] data, CrestType crestType) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement("INSERT INTO `crests`(`crest_id`, `data`, `type`) VALUES(?, ?, ?)")) { + final L2Crest crest = new L2Crest(getNextId(), data, crestType); + statement.setInt(1, crest.getId()); + statement.setBytes(2, crest.getData()); + statement.setInt(3, crest.getType().getId()); + statement.executeUpdate(); + _crests.put(crest.getId(), crest); + return crest; + } catch (Exception ex) { + LOG.warn("There has been an error while saving crest in database!", ex); + } + return null; + } + + /** + * Removes crest from database and cache. + * @param crestId the id of crest to be removed. + */ + public void removeCrest(int crestId) { + _crests.remove(crestId); + + // avoid removing last crest id we don't want to lose index... + // because client will display wrong crest if its reused + if (crestId == (_nextId.get() - 1)) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement("DELETE FROM `crests` WHERE `crest_id` = ?")) { + statement.setInt(1, crestId); + statement.executeUpdate(); + } catch (Exception ex) { + LOG.warn("There has been an error while deleting crest from database!", ex); + } + } + + public int getNextId() { + return _nextId.getAndIncrement(); + } + + public static CrestTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final CrestTable INSTANCE = new CrestTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/NpcBufferTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/NpcBufferTable.java new file mode 100644 index 00000000..e6469c8f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/NpcBufferTable.java @@ -0,0 +1,160 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; + +public class NpcBufferTable { + + private static final Logger LOG = LoggerFactory.getLogger(NpcBufferTable.class); + + private final Map _buffers = new HashMap<>(); + + public static class NpcBufferData { + private final SkillHolder _skill; + private final ItemHolder _fee; + + protected NpcBufferData(int skillId, int skillLevel, int feeId, int feeAmount) { + _skill = new SkillHolder(skillId, skillLevel); + _fee = new ItemHolder(feeId, feeAmount); + } + + public SkillHolder getSkill() { + return _skill; + } + + public ItemHolder getFee() { + return _fee; + } + } + + private static class NpcBufferSkills { + private final Map _skills = new HashMap<>(); + + public void addSkill(int skillId, int skillLevel, int skillFeeId, int skillFeeAmount, int buffGroup) { + _skills.put(buffGroup, new NpcBufferData(skillId, skillLevel, skillFeeId, skillFeeAmount)); + } + + public NpcBufferData getSkillGroupInfo(int buffGroup) { + return _skills.get(buffGroup); + } + } + + protected NpcBufferTable() { + int skillCount = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery("SELECT `npc_id`,`skill_id`,`skill_level`,`skill_fee_id`,`skill_fee_amount`,`buff_group` FROM `npc_buffer` ORDER BY `npc_id`")) { + int lastNpcId = 0; + NpcBufferSkills skills = null; + + while (rs.next()) { + int npcId = rs.getInt("npc_id"); + int skillId = rs.getInt("skill_id"); + int skillLevel = rs.getInt("skill_level"); + int skillFeeId = rs.getInt("skill_fee_id"); + int skillFeeAmount = rs.getInt("skill_fee_amount"); + int buffGroup = rs.getInt("buff_group"); + + if (npcId != lastNpcId) { + if (lastNpcId != 0) { + _buffers.put(lastNpcId, skills); + } + + skills = new NpcBufferSkills(); + skills.addSkill(skillId, skillLevel, skillFeeId, skillFeeAmount, buffGroup); + } else if (skills != null) { + skills.addSkill(skillId, skillLevel, skillFeeId, skillFeeAmount, buffGroup); + } + + lastNpcId = npcId; + skillCount++; + } + + if (lastNpcId != 0) { + _buffers.put(lastNpcId, skills); + } + } catch (Exception ex) { + LOG.warn("There has been an error reading npc_buffer table!", ex); + } + + if (general().customNpcBufferTables()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery("SELECT `npc_id`,`skill_id`,`skill_level`,`skill_fee_id`,`skill_fee_amount`,`buff_group` FROM `custom_npc_buffer` ORDER BY `npc_id`")) { + int lastNpcId = 0; + NpcBufferSkills skills = null; + while (rs.next()) { + int npcId = rs.getInt("npc_id"); + int skillId = rs.getInt("skill_id"); + int skillLevel = rs.getInt("skill_level"); + int skillFeeId = rs.getInt("skill_fee_id"); + int skillFeeAmount = rs.getInt("skill_fee_amount"); + int buffGroup = rs.getInt("buff_group"); + + if (npcId != lastNpcId) { + if (lastNpcId != 0) { + _buffers.put(lastNpcId, skills); + } + + skills = new NpcBufferSkills(); + skills.addSkill(skillId, skillLevel, skillFeeId, skillFeeAmount, buffGroup); + } else if (skills != null) { + skills.addSkill(skillId, skillLevel, skillFeeId, skillFeeAmount, buffGroup); + } + lastNpcId = npcId; + skillCount++; + } + + if (lastNpcId != 0) { + _buffers.put(lastNpcId, skills); + } + } catch (Exception ex) { + LOG.warn("There has been an error reading custom_npc_buffer table!", ex); + } + } + LOG.info("Loaded {} buffers and {} skills.", _buffers.size(), skillCount); + } + + public NpcBufferData getSkillInfo(int npcId, int buffGroup) { + final NpcBufferSkills skills = _buffers.get(npcId); + if (skills != null) { + return skills.getSkillGroupInfo(buffGroup); + } + return null; + } + + public static NpcBufferTable getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final NpcBufferTable _instance = new NpcBufferTable(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/OfflineTradersTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/OfflineTradersTable.java new file mode 100644 index 00000000..9d8c205b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/OfflineTradersTable.java @@ -0,0 +1,239 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.enums.PrivateStoreType.NONE; + +import java.util.Calendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.LoginServerThread; +import com.l2jserver.gameserver.enums.PrivateStoreType; +import com.l2jserver.gameserver.model.L2ManufactureItem; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.TradeItem; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.L2GameClient.GameClientState; + +public class OfflineTradersTable { + + private static final Logger LOG = LoggerFactory.getLogger(OfflineTradersTable.class); + + private static final String SAVE_OFFLINE_STATUS = "INSERT INTO character_offline_trade (`charId`,`time`,`type`,`title`) VALUES (?,?,?,?)"; + + private static final String SAVE_ITEMS = "INSERT INTO character_offline_trade_items (`charId`,`item`,`count`,`price`) VALUES (?,?,?,?)"; + + private static final String CLEAR_OFFLINE_TABLE = "DELETE FROM character_offline_trade"; + + private static final String CLEAR_OFFLINE_TABLE_ITEMS = "DELETE FROM character_offline_trade_items"; + + private static final String LOAD_OFFLINE_STATUS = "SELECT * FROM character_offline_trade"; + + private static final String LOAD_OFFLINE_ITEMS = "SELECT * FROM character_offline_trade_items WHERE charId = ?"; + + public void storeOffliners() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var stm1 = con.prepareStatement(CLEAR_OFFLINE_TABLE); + var stm2 = con.prepareStatement(CLEAR_OFFLINE_TABLE_ITEMS); + var stm3 = con.prepareStatement(SAVE_OFFLINE_STATUS); + var stm_items = con.prepareStatement(SAVE_ITEMS)) { + stm1.execute(); + stm2.execute(); + con.setAutoCommit(false); // avoid halfway done + + for (L2PcInstance pc : L2World.getInstance().getPlayers()) { + try { + if ((pc.getPrivateStoreType() != NONE) && pc.isInOfflineMode()) { + stm3.setInt(1, pc.getObjectId()); + stm3.setLong(2, pc.getOfflineStartTime()); + stm3.setInt(3, pc.getPrivateStoreType().getId()); + String title = null; + + switch (pc.getPrivateStoreType()) { + case BUY -> { + if (!customs().offlineTradeEnable()) { + continue; + } + title = pc.getBuyList().getTitle(); + for (TradeItem i : pc.getBuyList().getItems()) { + stm_items.setInt(1, pc.getObjectId()); + stm_items.setInt(2, i.getItem().getId()); + stm_items.setLong(3, i.getCount()); + stm_items.setLong(4, i.getPrice()); + stm_items.executeUpdate(); + stm_items.clearParameters(); + } + } + case SELL, PACKAGE_SELL -> { + if (!customs().offlineTradeEnable()) { + continue; + } + title = pc.getSellList().getTitle(); + for (TradeItem i : pc.getSellList().getItems()) { + stm_items.setInt(1, pc.getObjectId()); + stm_items.setInt(2, i.getObjectId()); + stm_items.setLong(3, i.getCount()); + stm_items.setLong(4, i.getPrice()); + stm_items.executeUpdate(); + stm_items.clearParameters(); + } + } + case MANUFACTURE -> { + if (!customs().offlineCraftEnable()) { + continue; + } + title = pc.getStoreName(); + for (L2ManufactureItem i : pc.getManufactureItems().values()) { + stm_items.setInt(1, pc.getObjectId()); + stm_items.setInt(2, i.getRecipeId()); + stm_items.setLong(3, 0); + stm_items.setLong(4, i.getCost()); + stm_items.executeUpdate(); + stm_items.clearParameters(); + } + } + } + stm3.setString(4, title); + stm3.executeUpdate(); + stm3.clearParameters(); + con.commit(); // flush + } + } catch (Exception ex) { + LOG.warn("There has been an error while saving offline trader {}!", pc, ex); + } + } + LOG.info("Offline traders stored."); + } catch (Exception ex) { + LOG.warn("There has been an error while saving offline traders!", ex); + } + } + + public void restoreOfflineTraders() { + LOG.info("Loading offline traders..."); + int nTraders = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var stm = con.createStatement(); + var rs = stm.executeQuery(LOAD_OFFLINE_STATUS)) { + while (rs.next()) { + long time = rs.getLong("time"); + if (customs().getOfflineMaxDays() > 0) { + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(time); + cal.add(Calendar.DAY_OF_YEAR, customs().getOfflineMaxDays()); + if (cal.getTimeInMillis() <= System.currentTimeMillis()) { + continue; + } + } + + final var type = PrivateStoreType.findById(rs.getInt("type")); + if (type == null) { + LOG.warn("PrivateStoreType with Id {} could not be found!", rs.getInt("type")); + continue; + } + + if (type == NONE) { + continue; + } + + L2PcInstance player = null; + + try { + L2GameClient client = new L2GameClient(null); + client.setDetached(true); + player = L2PcInstance.load(rs.getInt("charId")); + client.setActiveChar(player); + player.setOnlineStatus(true, false); + client.setAccountName(player.getAccountNamePlayer()); + L2World.getInstance().addPlayerToWorld(player); + client.setState(GameClientState.IN_GAME); + player.setClient(client); + player.setOfflineStartTime(time); + player.spawnMe(player.getX(), player.getY(), player.getZ()); + LoginServerThread.getInstance().addGameServerLogin(player.getAccountName(), client); + try (var stm_items = con.prepareStatement(LOAD_OFFLINE_ITEMS)) { + stm_items.setInt(1, player.getObjectId()); + try (var items = stm_items.executeQuery()) { + switch (type) { + case BUY -> { + while (items.next()) { + if (player.getBuyList().addItemByItemId(items.getInt(2), items.getLong(3), items.getLong(4)) == null) { + throw new NullPointerException(); + } + } + player.getBuyList().setTitle(rs.getString("title")); + } + case SELL, PACKAGE_SELL -> { + while (items.next()) { + if (player.getSellList().addItem(items.getInt(2), items.getLong(3), items.getLong(4)) == null) { + throw new NullPointerException(); + } + } + player.getSellList().setTitle(rs.getString("title")); + player.getSellList().setPackaged(type == PrivateStoreType.PACKAGE_SELL); + } + case MANUFACTURE -> { + while (items.next()) { + player.getManufactureItems().put(items.getInt(2), new L2ManufactureItem(items.getInt(2), items.getLong(4))); + } + player.setStoreName(rs.getString("title")); + } + } + } + } + player.sitDown(); + if (customs().offlineSetNameColor()) { + player.getAppearance().setNameColor(customs().getOfflineNameColor()); + } + player.setPrivateStoreType(type); + player.setOnlineStatus(true, true); + player.restoreEffects(); + player.broadcastUserInfo(); + nTraders++; + } catch (Exception ex) { + LOG.warn("There has been an error loading trader {}!", player, ex); + if (player != null) { + player.deleteMe(); + } + } + } + + LOG.info("Loaded {} offline trader(s).", nTraders); + + try (var stm1 = con.createStatement()) { + stm1.execute(CLEAR_OFFLINE_TABLE); + stm1.execute(CLEAR_OFFLINE_TABLE_ITEMS); + } + } catch (Exception ex) { + LOG.warn("There has been an error while loading offline traders!", ex); + } + } + + public static OfflineTradersTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final OfflineTradersTable INSTANCE = new OfflineTradersTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/PetNameTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/PetNameTable.java new file mode 100644 index 00000000..3f68ba7c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/PetNameTable.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +public class PetNameTable { + + private static final Logger LOG = LoggerFactory.getLogger(PetNameTable.class); + + public boolean doesPetNameExist(String name) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT name FROM pets WHERE name=?")) { + ps.setString(1, name); + try (var rs = ps.executeQuery()) { + return rs.next(); + } + } catch (Exception ex) { + LOG.warn("Could not check existing pet name!", ex); + } + return false; + } + + public boolean isValidPetName(String name) { + return character().getPetNameTemplate().matcher(name).matches(); + } + + public static PetNameTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PetNameTable INSTANCE = new PetNameTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonEffectsTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonEffectsTable.java new file mode 100644 index 00000000..af0c40d2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonEffectsTable.java @@ -0,0 +1,146 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * @author Nyaran + */ +public class SummonEffectsTable { + /** Servitors **/ + // Map tree + // -> key: charObjectId, value: classIndex Map + // --> key: classIndex, value: servitors Map + // ---> key: servitorSkillId, value: Effects list + private final Map>>> _servitorEffects = new ConcurrentHashMap<>(); + /** Pets **/ + // key: petItemObjectId, value: Effects list + private final Map> _petEffects = new ConcurrentHashMap<>(); + + private Map> getServitorEffects(L2PcInstance owner) { + final var servitorMap = _servitorEffects.get(owner.getObjectId()); + return servitorMap == null ? null : servitorMap.get(owner.getClassIndex()); + } + + private Map getServitorEffects(L2PcInstance owner, int referenceSkill) { + final var servitorEffects = getServitorEffects(owner); + return servitorEffects == null ? null : servitorEffects.get(referenceSkill); + } + + private void removeEffects(Map map, int skillId) { + if (map != null) { + map.remove(skillId); + } + } + + private void applyEffects(L2Summon summon, Map map) { + if (map == null) { + return; + } + for (var se : map.values()) { + if (se != null) { + se.getSkill().applyEffects(summon, summon, false, se.getEffectCurTime()); + } + } + } + + public boolean containsSkill(L2PcInstance owner, int referenceSkill) { + final var servitorEffects = getServitorEffects(owner); + return (servitorEffects != null) && servitorEffects.containsKey(referenceSkill); + } + + public void clearServitorEffects(L2PcInstance owner, int referenceSkill) { + final var servitorEffects = getServitorEffects(owner, referenceSkill); + if (servitorEffects != null) { + servitorEffects.clear(); + } + } + + public void addServitorEffect(L2PcInstance owner, int referenceSkill, Skill skill, int effectCurTime) { + _servitorEffects.computeIfAbsent(owner.getObjectId(), _ -> new ConcurrentHashMap<>()) // + .computeIfAbsent(owner.getClassIndex(), _ -> new ConcurrentHashMap<>()) // + .computeIfAbsent(referenceSkill, _ -> new ConcurrentHashMap<>()) // + .put(skill.getId(), new SummonEffect(skill, effectCurTime)); + } + + public void removeServitorEffects(L2PcInstance owner, int referenceSkill, int skillId) { + removeEffects(getServitorEffects(owner, referenceSkill), skillId); + } + + public void applyServitorEffects(L2ServitorInstance servitor, L2PcInstance owner, int referenceSkill) { + applyEffects(servitor, getServitorEffects(owner, referenceSkill)); + } + + public void addPetEffect(int controlObjectId, Skill skill, int effectCurTime) { + _petEffects.computeIfAbsent(controlObjectId, _ -> new ConcurrentHashMap<>()).put(skill.getId(), new SummonEffect(skill, effectCurTime)); + } + + public boolean containsPetId(int controlObjectId) { + return _petEffects.containsKey(controlObjectId); + } + + public void applyPetEffects(L2PetInstance l2PetInstance, int controlObjectId) { + applyEffects(l2PetInstance, _petEffects.get(controlObjectId)); + } + + public void clearPetEffects(int controlObjectId) { + final var effects = _petEffects.get(controlObjectId); + if (effects != null) { + effects.clear(); + } + } + + public void removePetEffects(int controlObjectId, int skillId) { + removeEffects(_petEffects.get(controlObjectId), skillId); + } + + private static class SummonEffect { + private final Skill _skill; + private final int _effectCurTime; + + public SummonEffect(Skill skill, int effectCurTime) { + _skill = skill; + _effectCurTime = effectCurTime; + } + + public Skill getSkill() { + return _skill; + } + + public int getEffectCurTime() { + return _effectCurTime; + } + } + + public static SummonEffectsTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SummonEffectsTable INSTANCE = new SummonEffectsTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonSkillsTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonSkillsTable.java new file mode 100644 index 00000000..84df1618 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/SummonSkillsTable.java @@ -0,0 +1,148 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.actor.L2Summon; + +public class SummonSkillsTable { + + private static final Logger LOG = LoggerFactory.getLogger(SummonSkillsTable.class); + + private final Map> _skillTrees = new HashMap<>(); + + protected SummonSkillsTable() { + load(); + } + + public void load() { + _skillTrees.clear(); + int count = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT templateId, minLvl, skillId, skillLvl FROM pets_skills")) { + while (rs.next()) { + final int npcId = rs.getInt("templateId"); + final var skillTree = _skillTrees.computeIfAbsent(npcId, _ -> new HashMap<>()); + + int id = rs.getInt("skillId"); + int lvl = rs.getInt("skillLvl"); + skillTree.put(SkillData.getSkillHashCode(id, lvl + 1), new L2PetSkillLearn(id, lvl, rs.getInt("minLvl"))); + count++; + } + } catch (Exception ex) { + LOG.error("Error while loading pet skill tree!", ex); + } + LOG.info("Loaded {} summon skills.", count); + } + + public int getAvailableLevel(L2Summon cha, int skillId) { + int lvl = 0; + if (!_skillTrees.containsKey(cha.getId())) { + LOG.warn("Pet Id {} does not have any skills assigned!", cha.getId()); + return lvl; + } + Collection skills = _skillTrees.get(cha.getId()).values(); + for (L2PetSkillLearn temp : skills) { + if (temp.getId() != skillId) { + continue; + } + if (temp.getLevel() == 0) { + if (cha.getLevel() < 70) { + lvl = (cha.getLevel() / 10); + if (lvl <= 0) { + lvl = 1; + } + } else { + lvl = (7 + ((cha.getLevel() - 70) / 5)); + } + + // formula usable for skill that have 10 or more skill levels + int maxLvl = SkillData.getInstance().getMaxLevel(temp.getId()); + if (lvl > maxLvl) { + lvl = maxLvl; + } + break; + } else if (temp.getMinLevel() <= cha.getLevel()) { + if (temp.getLevel() > lvl) { + lvl = temp.getLevel(); + } + } + } + return lvl; + } + + public List getAvailableSkills(L2Summon cha) { + List skillIds = new ArrayList<>(); + if (!_skillTrees.containsKey(cha.getId())) { + LOG.warn("Pet Id {} does not have any skills assigned!", cha.getId()); + return skillIds; + } + Collection skills = _skillTrees.get(cha.getId()).values(); + for (L2PetSkillLearn temp : skills) { + if (skillIds.contains(temp.getId())) { + continue; + } + skillIds.add(temp.getId()); + } + return skillIds; + } + + public static final class L2PetSkillLearn { + private final int _id; + private final int _level; + private final int _minLevel; + + public L2PetSkillLearn(int id, int lvl, int minLvl) { + _id = id; + _level = lvl; + _minLevel = minLvl; + } + + public int getId() { + return _id; + } + + public int getLevel() { + return _level; + } + + public int getMinLevel() { + return _minLevel; + } + } + + public static SummonSkillsTable getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final SummonSkillsTable _instance = new SummonSkillsTable(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/sql/impl/TerritoryTable.java b/src/main/java/com/l2jserver/gameserver/data/sql/impl/TerritoryTable.java new file mode 100644 index 00000000..79c3c1f5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/sql/impl/TerritoryTable.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.sql.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2Territory; +import com.l2jserver.gameserver.model.Location; + +/** + * @author MrBalancer + */ +public class TerritoryTable { + + private static final Logger LOG = LoggerFactory.getLogger(TerritoryTable.class); + + private static final Map _territory = new HashMap<>(); + + protected TerritoryTable() { + load(); + } + + /** + * Gets the random point. + * @param terr the territory Id? + * @return the random point + */ + public Location getRandomPoint(int terr) { + return _territory.get(terr).getRandomPoint(); + } + + /** + * Gets the proc max. + * @param terr the territory Id? + * @return the proc max + */ + public int getProcMax(int terr) { + return _territory.get(terr).getProcMax(); + } + + /** + * Load the data from database. + */ + public void load() { + _territory.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var stmt = con.createStatement(); + var rs = stmt.executeQuery("SELECT * FROM locations WHERE loc_id>0")) { + while (rs.next()) { + int terrId = rs.getInt("loc_id"); + L2Territory terr = _territory.get(terrId); + if (terr == null) { + terr = new L2Territory(terrId); + _territory.put(terrId, terr); + } + terr.add(rs.getInt("loc_x"), rs.getInt("loc_y"), rs.getInt("loc_zmin"), rs.getInt("loc_zmax"), rs.getInt("proc")); + } + LOG.info("Loaded {} territories from database.", _territory.size()); + } catch (Exception ex) { + LOG.error("Failed to load territories from database!", ex); + } + } + + public static TerritoryTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final TerritoryTable INSTANCE = new TerritoryTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/AdminData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/AdminData.java new file mode 100644 index 00000000..42947bcd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/AdminData.java @@ -0,0 +1,303 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.L2AccessLevel; +import com.l2jserver.gameserver.model.L2AdminCommandAccessRight; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads administrator access levels and commands. + * @author UnAfraid + */ +public final class AdminData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(AdminData.class); + + private final Map _accessLevels = new HashMap<>(); + + private final Map _adminCommandAccessRights = new HashMap<>(); + + private final Map _gmList = new ConcurrentHashMap<>(); + + private int _highestLevel = 0; + + protected AdminData() { + load(); + } + + @Override + public synchronized void load() { + _accessLevels.clear(); + _adminCommandAccessRights.clear(); + parseDatapackFile("config/accessLevels.xml"); + LOG.info("Loaded {} access levels.", _accessLevels.size()); + parseDatapackFile("config/adminCommands.xml"); + LOG.info("Loaded {} access commands.", _adminCommandAccessRights.size()); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + Node attr; + StatsSet set; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("access".equalsIgnoreCase(d.getNodeName())) { + set = new StatsSet(); + attrs = d.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + attr = attrs.item(i); + set.set(attr.getNodeName(), attr.getNodeValue()); + } + final L2AccessLevel level = new L2AccessLevel(set); + if (level.getLevel() > _highestLevel) { + _highestLevel = level.getLevel(); + } + _accessLevels.put(level.getLevel(), level); + } else if ("admin".equalsIgnoreCase(d.getNodeName())) { + set = new StatsSet(); + attrs = d.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + attr = attrs.item(i); + set.set(attr.getNodeName(), attr.getNodeValue()); + } + final L2AdminCommandAccessRight command = new L2AdminCommandAccessRight(set); + _adminCommandAccessRights.put(command.getAdminCommand(), command); + } + } + } + } + } + + /** + * Returns the access level by characterAccessLevel. + * @param accessLevelNum as int + * @return the access level instance by char access level + */ + public L2AccessLevel getAccessLevel(int accessLevelNum) { + if (accessLevelNum < 0) { + return _accessLevels.get(-1); + } else if (!_accessLevels.containsKey(accessLevelNum)) { + _accessLevels.put(accessLevelNum, new L2AccessLevel()); + } + return _accessLevels.get(accessLevelNum); + } + + /** + * Gets the master access level. + * @return the master access level + */ + public L2AccessLevel getMasterAccessLevel() { + return _accessLevels.get(_highestLevel); + } + + /** + * Checks for access level. + * @param id the id + * @return {@code true}, if successful, {@code false} otherwise + */ + public boolean hasAccessLevel(int id) { + return _accessLevels.containsKey(id); + } + + /** + * Checks for access. + * @param adminCommand the admin command + * @param accessLevel the access level + * @return {@code true}, if successful, {@code false} otherwise + */ + public boolean hasAccess(String adminCommand, L2AccessLevel accessLevel) { + L2AdminCommandAccessRight acar = _adminCommandAccessRights.get(adminCommand); + if (acar == null) { + // Trying to avoid the spam for next time when the gm would try to use the same command + if ((accessLevel.getLevel() > 0) && (accessLevel.getLevel() == _highestLevel)) { + acar = new L2AdminCommandAccessRight(adminCommand, true, accessLevel.getLevel()); + _adminCommandAccessRights.put(adminCommand, acar); + LOG.info("No rights defined for admin command {} auto setting access level {}!", adminCommand, accessLevel.getLevel()); + } else { + LOG.info("No rights defined for admin command {}!", adminCommand); + return false; + } + } + return acar.hasAccess(accessLevel); + } + + /** + * Require confirm. + * @param command the command + * @return {@code true}, if the command require confirmation, {@code false} otherwise + */ + public boolean requireConfirm(String command) { + final L2AdminCommandAccessRight acar = _adminCommandAccessRights.get(command); + if (acar == null) { + LOG.info("No rights defined for admin command {}.", command); + return false; + } + return acar.getRequireConfirm(); + } + + /** + * Gets the all GMs. + * @param includeHidden the include hidden + * @return the all GMs + */ + public List getAllGms(boolean includeHidden) { + final List tmpGmList = new ArrayList<>(); + for (Entry entry : _gmList.entrySet()) { + if (includeHidden || !entry.getValue()) { + tmpGmList.add(entry.getKey()); + } + } + return tmpGmList; + } + + /** + * Gets the all GM names. + * @param includeHidden the include hidden + * @return the all GM names + */ + public List getAllGmNames(boolean includeHidden) { + final List tmpGmList = new ArrayList<>(); + for (Entry entry : _gmList.entrySet()) { + if (!entry.getValue()) { + tmpGmList.add(entry.getKey().getName()); + } else if (includeHidden) { + tmpGmList.add(entry.getKey().getName() + " (invis)"); + } + } + return tmpGmList; + } + + /** + * Add a L2PcInstance player to the Set _gmList. + * @param player the player + * @param hidden the hidden + */ + public void addGm(L2PcInstance player, boolean hidden) { + _gmList.put(player, hidden); + } + + /** + * Delete a GM. + * @param player the player + */ + public void deleteGm(L2PcInstance player) { + _gmList.remove(player); + } + + /** + * GM will be displayed on clients GM list. + * @param player the player + */ + public void showGm(L2PcInstance player) { + if (_gmList.containsKey(player)) { + _gmList.put(player, false); + } + } + + /** + * GM will no longer be displayed on clients GM list. + * @param player the player + */ + public void hideGm(L2PcInstance player) { + if (_gmList.containsKey(player)) { + _gmList.put(player, true); + } + } + + /** + * Checks if is GM online. + * @param includeHidden the include hidden + * @return true, if is GM online + */ + public boolean isGmOnline(boolean includeHidden) { + for (Entry entry : _gmList.entrySet()) { + if (includeHidden || !entry.getValue()) { + return true; + } + } + return false; + } + + /** + * Send list to player. + * @param player the player + */ + public void sendListToPlayer(L2PcInstance player) { + if (isGmOnline(player.isGM())) { + player.sendPacket(SystemMessageId.GM_LIST); + + for (String name : getAllGmNames(player.isGM())) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.GM_C1); + sm.addString(name); + player.sendPacket(sm); + } + } else { + player.sendPacket(SystemMessageId.NO_GM_PROVIDING_SERVICE_NOW); + } + } + + /** + * Broadcast to GMs. + * @param packet the packet + */ + public void broadcastToGMs(L2GameServerPacket packet) { + for (L2PcInstance gm : getAllGms(true)) { + gm.sendPacket(packet); + } + } + + /** + * Broadcast message to GMs. + * @param message the message + */ + public void broadcastMessageToGMs(String message) { + for (L2PcInstance gm : getAllGms(true)) { + gm.sendMessage(message); + } + } + + public static AdminData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final AdminData INSTANCE = new AdminData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/ArmorSetsData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/ArmorSetsData.java new file mode 100644 index 00000000..1e95558b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/ArmorSetsData.java @@ -0,0 +1,116 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.L2ArmorSet; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads armor set bonuses. + * @author godson + * @author Luno + * @author UnAfraid + */ +public final class ArmorSetsData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(ArmorSetsData.class); + + private final Map _armorSets = new HashMap<>(); + + protected ArmorSetsData() { + load(); + } + + @Override + public void load() { + _armorSets.clear(); + parseDatapackDirectory("data/stats/armorsets", false); + LOG.info("Loaded {} armor sets.", _armorSets.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("set".equalsIgnoreCase(d.getNodeName())) { + final L2ArmorSet set = new L2ArmorSet(); + for (Node a = d.getFirstChild(); a != null; a = a.getNextSibling()) { + final NamedNodeMap attrs = a.getAttributes(); + switch (a.getNodeName()) { + case "chest" -> set.addChest(parseInteger(attrs, "id")); + case "feet" -> set.addFeet(parseInteger(attrs, "id")); + case "gloves" -> set.addGloves(parseInteger(attrs, "id")); + case "head" -> set.addHead(parseInteger(attrs, "id")); + case "legs" -> set.addLegs(parseInteger(attrs, "id")); + case "shield" -> set.addShield(parseInteger(attrs, "id")); + case "skill" -> set.addSkill(new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "level"))); + case "shield_skill" -> set.addShieldSkill(new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "level"))); + case "enchant6skill" -> set.addEnchant6Skill(new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "level"))); + case "con" -> set.addCon(parseInteger(attrs, "val")); + case "dex" -> set.addDex(parseInteger(attrs, "val")); + case "str" -> set.addStr(parseInteger(attrs, "val")); + case "men" -> set.addMen(parseInteger(attrs, "val")); + case "wit" -> set.addWit(parseInteger(attrs, "val")); + case "int" -> set.addInt(parseInteger(attrs, "val")); + } + } + _armorSets.put(set.getChestId(), set); + } + } + } + } + } + + /** + * Checks if is armor set. + * @param chestId the chest Id to verify. + * @return {@code true} if the chest Id belongs to a registered armor set, {@code false} otherwise. + */ + public boolean isArmorSet(int chestId) { + return _armorSets.containsKey(chestId); + } + + /** + * Gets the sets the. + * @param chestId the chest Id identifying the armor set. + * @return the armor set associated to the give chest Id. + */ + public L2ArmorSet getSet(int chestId) { + return _armorSets.get(chestId); + } + + public static ArmorSetsData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ArmorSetsData INSTANCE = new ArmorSetsData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/BuyListData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/BuyListData.java new file mode 100644 index 00000000..6d5d7683 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/BuyListData.java @@ -0,0 +1,162 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.io.File; +import java.io.FileFilter; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.buylist.L2BuyList; +import com.l2jserver.gameserver.model.buylist.Product; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.file.filter.NumericNameFilter; + +/** + * Loads buy lists for NPCs. + * @author NosBit + */ +public final class BuyListData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(BuyListData.class); + + private static final FileFilter NUMERIC_FILTER = new NumericNameFilter(); + + private final Map _buyLists = new HashMap<>(); + + @Override + public synchronized void load() { + parseDatapackDirectory("data/buylists", false); + if (general().customBuyListLoad()) { + parseDatapackDirectory("data/buylists/custom", false); + } + + LOG.info("Loaded {} buy lists.", _buyLists.size()); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.createStatement(); + var rs = statement.executeQuery("SELECT * FROM `buylists`")) { + while (rs.next()) { + int buyListId = rs.getInt("buylist_id"); + int itemId = rs.getInt("item_id"); + long count = rs.getLong("count"); + long nextRestockTime = rs.getLong("next_restock_time"); + final L2BuyList buyList = getBuyList(buyListId); + if (buyList == null) { + LOG.warn("Buy list {} found in database but not loaded from XML!", buyListId); + continue; + } + final Product product = buyList.getProductByItemId(itemId); + if (product == null) { + LOG.warn("Item Id {} found in database but not loaded from XML {}!", itemId, buyListId); + continue; + } + if (count < product.getMaxCount()) { + product.setCount(count); + product.restartRestockTask(nextRestockTime); + } + } + } catch (Exception ex) { + LOG.warn("Failed to load buy list data from database.", ex); + } + } + + @Override + public void parseDocument(Document doc, File f) { + try { + final int buyListId = Integer.parseInt(f.getName().replaceAll(".xml", "")); + + for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equalsIgnoreCase(node.getNodeName())) { + final L2BuyList buyList = new L2BuyList(buyListId); + for (Node list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { + if ("item".equalsIgnoreCase(list_node.getNodeName())) { + long price = -1; + long restockDelay = -1; + long count = -1; + NamedNodeMap attrs = list_node.getAttributes(); + Node attr = attrs.getNamedItem("id"); + int itemId = Integer.parseInt(attr.getNodeValue()); + attr = attrs.getNamedItem("price"); + if (attr != null) { + price = Long.parseLong(attr.getNodeValue()); + } + attr = attrs.getNamedItem("restock_delay"); + if (attr != null) { + restockDelay = Long.parseLong(attr.getNodeValue()); + } + attr = attrs.getNamedItem("count"); + if (attr != null) { + count = Long.parseLong(attr.getNodeValue()); + } + final L2Item item = ItemTable.getInstance().getTemplate(itemId); + if (item != null) { + buyList.addProduct(new Product(buyList.getListId(), item, price, 0, restockDelay, count)); + } else { + LOG.warn("Item not found. BuyList: {} Item ID: {} File: {}", buyList.getListId(), itemId, f.getName()); + } + } else if ("npcs".equalsIgnoreCase(list_node.getNodeName())) { + for (Node npcs_node = list_node.getFirstChild(); npcs_node != null; npcs_node = npcs_node.getNextSibling()) { + if ("npc".equalsIgnoreCase(npcs_node.getNodeName())) { + int npcId = Integer.parseInt(npcs_node.getTextContent()); + buyList.addAllowedNpc(npcId); + } + } + } + } + _buyLists.put(buyList.getListId(), buyList); + } + } + } catch (Exception ex) { + LOG.warn("Failed to load buy list data from XML {}!", f.getName(), ex); + } + } + + @Override + public FileFilter getCurrentFileFilter() { + return NUMERIC_FILTER; + } + + public L2BuyList getBuyList(int listId) { + return _buyLists.get(listId); + } + + public void addBuyList(L2BuyList buyList) { + _buyLists.put(buyList.getListId(), buyList); + } + + public static BuyListData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final BuyListData INSTANCE = new BuyListData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/CategoryData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/CategoryData.java new file mode 100644 index 00000000..c633b4f2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/CategoryData.java @@ -0,0 +1,125 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.enums.CategoryType; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads the category data with Class or NPC IDs. + * @author NosBit + * @author xban1x + */ +public final class CategoryData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(CategoryData.class); + + private final Map> _categories = new HashMap<>(); + + protected CategoryData() { + load(); + } + + @Override + public void load() { + _categories.clear(); + parseDatapackFile("data/categoryData.xml"); + LOG.info("Loaded {} Categories.", _categories.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equalsIgnoreCase(node.getNodeName())) { + for (Node list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { + if ("category".equalsIgnoreCase(list_node.getNodeName())) { + final NamedNodeMap attrs = list_node.getAttributes(); + final CategoryType categoryType = CategoryType.findByName(attrs.getNamedItem("name").getNodeValue()); + if (categoryType == null) { + LOG.warn("Can't find category by name :{}", attrs.getNamedItem("name").getNodeValue()); + continue; + } + + final Set ids = new HashSet<>(); + for (Node category_node = list_node.getFirstChild(); category_node != null; category_node = category_node.getNextSibling()) { + if ("id".equalsIgnoreCase(category_node.getNodeName())) { + ids.add(Integer.parseInt(category_node.getTextContent())); + } + } + _categories.put(categoryType, ids); + } + } + } + } + } + + /** + * Checks if ID is in category. + * @param type the category type + * @param classId the id to be checked + * @return {@code true} if id is in category, {@code false} if id is not in category or category was not found + */ + public boolean isInCategory(CategoryType type, ClassId classId) { + return isInCategory(type, classId.getId()); + } + + /** + * Checks if ID is in category. + * @param type The category type + * @param id The id to be checked + * @return {@code true} if id is in category, {@code false} if id is not in category or category was not found + */ + public boolean isInCategory(CategoryType type, int id) { + final Set category = getCategoryByType(type); + if (category == null) { + LOG.warn("Can not find category type {}!", type); + return false; + } + return category.contains(id); + } + + /** + * Gets the category by category type. + * @param type The category type + * @return A {@code Set} containing all the IDs in category if category is found, {@code null} if category was not found + */ + public Set getCategoryByType(CategoryType type) { + return _categories.get(type); + } + + public static CategoryData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final CategoryData INSTANCE = new CategoryData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/ClassListData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/ClassListData.java new file mode 100644 index 00000000..9c8cfcfc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/ClassListData.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.base.ClassInfo; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads the the list of classes and it's info. + * @author Zoey76 + */ +public final class ClassListData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(ClassListData.class); + + private final Map _classData = new HashMap<>(); + + protected ClassListData() { + load(); + } + + @Override + public void load() { + _classData.clear(); + parseDatapackFile("data/stats/chars/classList.xml"); + LOG.info("Loaded {} class data.", _classData.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equals(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + final NamedNodeMap attrs = d.getAttributes(); + if ("class".equals(d.getNodeName())) { + Node attr = attrs.getNamedItem("classId"); + final ClassId classId = ClassId.getClassId(parseInteger(attr)); + attr = attrs.getNamedItem("name"); + final String className = attr.getNodeValue(); + attr = attrs.getNamedItem("parentClassId"); + final ClassId parentClassId = (attr != null) ? ClassId.getClassId(parseInteger(attr)) : null; + _classData.put(classId, new ClassInfo(classId, className, parentClassId)); + } + } + } + } + } + + /** + * Gets the class list. + * @return the complete class list + */ + public Map getClassList() { + return _classData; + } + + /** + * Gets the class info. + * @param classId the class ID + * @return the class info related to the given {@code classId} + */ + public ClassInfo getClass(ClassId classId) { + return _classData.get(classId); + } + + /** + * Gets the class info. + * @param classId the class Id as integer + * @return the class info related to the given {@code classId} + */ + public ClassInfo getClass(int classId) { + final ClassId id = ClassId.getClassId(classId); + return (id != null) ? _classData.get(id) : null; + } + + public static ClassListData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ClassListData INSTANCE = new ClassListData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/DoorData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/DoorData.java new file mode 100644 index 00000000..aacfad1a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/DoorData.java @@ -0,0 +1,230 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.templates.L2DoorTemplate; +import com.l2jserver.gameserver.pathfinding.AbstractNodeLoc; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads doors. + * @author JIV + * @author GodKratos + * @author UnAfraid + */ +public class DoorData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(DoorData.class); + + private static final Map> GROUPS = new HashMap<>(); + + private final Map _doors = new HashMap<>(); + + private final Map _doorsName = new HashMap<>(); + + private final Map _templates = new HashMap<>(); + + private final Map> _regions = new HashMap<>(); + + protected DoorData() { + load(); + } + + @Override + public void load() { + _doors.clear(); + _doorsName.clear(); + GROUPS.clear(); + _regions.clear(); + parseDatapackFile("data/doors.xml"); + LOG.info("Loaded {} door templates for {} regions.", _doors.size(), _regions.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node a = doc.getFirstChild(); a != null; a = a.getNextSibling()) { + if ("list".equalsIgnoreCase(a.getNodeName())) { + for (Node b = a.getFirstChild(); b != null; b = b.getNextSibling()) { + if ("door".equalsIgnoreCase(b.getNodeName())) { + final NamedNodeMap attrs = b.getAttributes(); + final StatsSet set = new StatsSet(); + set.set("baseHpMax", 1); // Avoid doors without HP value created dead due to default value 0 in L2CharTemplate + for (int i = 0; i < attrs.getLength(); i++) { + final Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + makeDoor(set); + _templates.put(set.getInt("id"), set); + } + } + } + } + } + + public void insertCollisionData(StatsSet set) { + int posX, posY, nodeX, nodeY, height; + height = set.getInt("height"); + String[] pos = set.getString("node1").split(","); + nodeX = Integer.parseInt(pos[0]); + nodeY = Integer.parseInt(pos[1]); + pos = set.getString("node2").split(","); + posX = Integer.parseInt(pos[0]); + posY = Integer.parseInt(pos[1]); + int collisionRadius; // (max) radius for movement checks + collisionRadius = Math.min(Math.abs(nodeX - posX), Math.abs(nodeY - posY)); + if (collisionRadius < 20) { + collisionRadius = 20; + } + + set.set("collisionRadius", collisionRadius); + set.set("collisionHeight", height); + } + + private void makeDoor(StatsSet set) { + insertCollisionData(set); + final var objectId = IdFactory.getInstance().getNextId(); + final var template = new L2DoorTemplate(set); + final var door = new L2DoorInstance(objectId, template); + door.setCurrentHp(door.getMaxHp()); + door.spawnMe(template.getX(), template.getY(), template.getZ()); + putDoor(door, MapRegionManager.getInstance().getMapRegionLocId(door)); + } + + public StatsSet getDoorTemplate(int doorId) { + return _templates.get(doorId); + } + + public L2DoorInstance getDoor(int doorId) { + return _doors.get(doorId); + } + + public L2DoorInstance getDoorByName(String name) { + return _doorsName.get(name); + } + + public void putDoor(L2DoorInstance door, int region) { + _doors.put(door.getId(), door); + _doorsName.put(door.getDoorName(), door); + _regions.computeIfAbsent(region, _ -> new ArrayList<>()).add(door); + } + + public static void addDoorGroup(String groupName, int doorId) { + GROUPS.computeIfAbsent(groupName, _ -> new HashSet<>()).add(doorId); + } + + public static Set getDoorsByGroup(String groupName) { + return GROUPS.get(groupName); + } + + public Collection getDoors() { + return _doors.values(); + } + + public boolean checkIfDoorsBetween(AbstractNodeLoc start, AbstractNodeLoc end, int instanceId) { + return checkIfDoorsBetween(start.getX(), start.getY(), start.getZ(), end.getX(), end.getY(), end.getZ(), instanceId); + } + + public boolean checkIfDoorsBetween(int x, int y, int z, int tx, int ty, int tz, int instanceId) { + return checkIfDoorsBetween(x, y, z, tx, ty, tz, instanceId, false); + } + + /** + * GodKratos: TODO: remove GeoData checks from door table and convert door nodes to Geo zones + * @param x + * @param y + * @param z + * @param tx + * @param ty + * @param tz + * @param instanceId + * @param doubleFaceCheck + * @return {@code boolean} + */ + public boolean checkIfDoorsBetween(int x, int y, int z, int tx, int ty, int tz, int instanceId, boolean doubleFaceCheck) { + Collection allDoors; + if ((instanceId > 0) && (InstanceManager.getInstance().getInstance(instanceId) != null)) { + allDoors = InstanceManager.getInstance().getInstance(instanceId).getDoors(); + } else { + allDoors = _regions.get(MapRegionManager.getInstance().getMapRegionLocId(x, y)); + } + + if (allDoors == null) { + return false; + } + + for (L2DoorInstance doorInst : allDoors) { + // check dead and open + if (doorInst.isDead() || doorInst.getOpen() || !doorInst.checkCollision() || (doorInst.getX(0) == 0)) { + continue; + } + + boolean intersectFace = false; + for (int i = 0; i < 4; i++) { + int j = (i + 1) < 4 ? i + 1 : 0; + // lower part of the multiplier fraction, if it is 0 we avoid an error and also know that the lines are parallel + int denominator = ((ty - y) * (doorInst.getX(i) - doorInst.getX(j))) - ((tx - x) * (doorInst.getY(i) - doorInst.getY(j))); + if (denominator == 0) { + continue; + } + + // multipliers to the equations of the lines. If they are lower than 0 or bigger than 1, we know that segments don't intersect + float multiplier1 = (float) (((doorInst.getX(j) - doorInst.getX(i)) * (y - doorInst.getY(i))) - ((doorInst.getY(j) - doorInst.getY(i)) * (x - doorInst.getX(i)))) / denominator; + float multiplier2 = (float) (((tx - x) * (y - doorInst.getY(i))) - ((ty - y) * (x - doorInst.getX(i)))) / denominator; + if ((multiplier1 >= 0) && (multiplier1 <= 1) && (multiplier2 >= 0) && (multiplier2 <= 1)) { + int intersectZ = Math.round(z + (multiplier1 * (tz - z))); + // now checking if the resulting point is between door's min and max z + if ((intersectZ > doorInst.getZMin()) && (intersectZ < doorInst.getZMax())) { + if (!doubleFaceCheck || intersectFace) { + return true; + } + intersectFace = true; + } + } + } + } + return false; + } + + public static DoorData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final DoorData INSTANCE = new DoorData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemData.java new file mode 100644 index 00000000..01c1dfaf --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemData.java @@ -0,0 +1,141 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.items.enchant.EnchantScroll; +import com.l2jserver.gameserver.model.items.enchant.EnchantSupportItem; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads item enchant data. + * @author UnAfraid + */ +public class EnchantItemData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(EnchantItemData.class); + + private final Map _scrolls = new HashMap<>(); + + private final Map _supports = new HashMap<>(); + + public EnchantItemData() { + load(); + } + + @Override + public synchronized void load() { + _scrolls.clear(); + _supports.clear(); + parseDatapackFile("data/enchantItemData.xml"); + LOG.info("Loaded {} enchant scrolls.", _scrolls.size()); + LOG.info("Loaded {} support items.", _supports.size()); + } + + @Override + public void parseDocument(Document doc) { + StatsSet set; + Node att; + NamedNodeMap attrs; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("enchant".equalsIgnoreCase(d.getNodeName())) { + attrs = d.getAttributes(); + set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + + try { + final EnchantScroll item = new EnchantScroll(set); + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("item".equalsIgnoreCase(cd.getNodeName())) { + item.addItem(parseInteger(cd.getAttributes(), "id")); + } + } + _scrolls.put(item.getId(), item); + } catch (NullPointerException e) { + LOG.warn("Nonexistent enchant scroll: {} defined in enchant data!", set.getString("id")); + } catch (IllegalAccessError e) { + LOG.warn("Wrong enchant scroll item type: {} defined in enchant data!", set.getString("id")); + } + } else if ("support".equalsIgnoreCase(d.getNodeName())) { + attrs = d.getAttributes(); + set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + + try { + final EnchantSupportItem item = new EnchantSupportItem(set); + _supports.put(item.getId(), item); + } catch (NullPointerException e) { + LOG.warn("Nonexistent enchant support item: {} defined in enchant data!", set.getString("id")); + } catch (IllegalAccessError e) { + LOG.warn("Wrong enchant support item type: {} defined in enchant data!", set.getString("id")); + } + } + } + } + } + } + + /** + * Gets the enchant scroll. + * @param scroll the scroll + * @return enchant template for scroll + */ + public final EnchantScroll getEnchantScroll(L2ItemInstance scroll) { + return _scrolls.get(scroll.getId()); + } + + /** + * Gets the support item. + * @param item the item + * @return enchant template for support item + */ + public final EnchantSupportItem getSupportItem(L2ItemInstance item) { + return _supports.get(item.getId()); + } + + /** + * Gets the single instance of EnchantItemData. + * @return single instance of EnchantItemData + */ + public static EnchantItemData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final EnchantItemData _instance = new EnchantItemData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemGroupsData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemGroupsData.java new file mode 100644 index 00000000..9d947f53 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemGroupsData.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.holders.RangeChanceHolder; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.enchant.EnchantItemGroup; +import com.l2jserver.gameserver.model.items.enchant.EnchantRateItem; +import com.l2jserver.gameserver.model.items.enchant.EnchantScrollGroup; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.Util; + +/** + * Enchant Item Group data. + * @author UnAfraid + */ +public final class EnchantItemGroupsData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(EnchantItemGroupsData.class); + + private final Map _itemGroups = new HashMap<>(); + + private final Map _scrollGroups = new HashMap<>(); + + protected EnchantItemGroupsData() { + load(); + } + + @Override + public synchronized void load() { + _itemGroups.clear(); + _scrollGroups.clear(); + parseDatapackFile("data/enchantItemGroups.xml"); + LOG.info("Loaded {} item group templates.", _itemGroups.size()); + LOG.info("Loaded {} scroll group templates.", _scrollGroups.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("enchantRateGroup".equalsIgnoreCase(d.getNodeName())) { + final String name = parseString(d.getAttributes(), "name"); + final EnchantItemGroup group = new EnchantItemGroup(name); + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("current".equalsIgnoreCase(cd.getNodeName())) { + String range = parseString(cd.getAttributes(), "enchant"); + double chance = parseDouble(cd.getAttributes(), "chance"); + int min = -1; + int max = 0; + if (range.contains("-")) { + String[] split = range.split("-"); + if ((split.length == 2) && Util.isDigit(split[0]) && Util.isDigit(split[1])) { + min = Integer.parseInt(split[0]); + max = Integer.parseInt(split[1]); + } + } else if (Util.isDigit(range)) { + min = Integer.parseInt(range); + max = min; + } + if ((min > -1) && (max > 0)) { + group.addChance(new RangeChanceHolder(min, max, chance)); + } + } + } + _itemGroups.put(name, group); + } else if ("enchantScrollGroup".equals(d.getNodeName())) { + int id = parseInteger(d.getAttributes(), "id"); + final EnchantScrollGroup group = new EnchantScrollGroup(id); + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("enchantRate".equalsIgnoreCase(cd.getNodeName())) { + final EnchantRateItem rateGroup = new EnchantRateItem(parseString(cd.getAttributes(), "group")); + for (Node z = cd.getFirstChild(); z != null; z = z.getNextSibling()) { + if ("item".equals(z.getNodeName())) { + final NamedNodeMap attrs = z.getAttributes(); + if (attrs.getNamedItem("slot") != null) { + rateGroup.addSlot(ItemTable.SLOTS.get(parseString(attrs, "slot"))); + } + if (attrs.getNamedItem("magicWeapon") != null) { + rateGroup.setMagicWeapon(parseBoolean(attrs, "magicWeapon")); + } + if (attrs.getNamedItem("id") != null) { + rateGroup.setItemId(parseInteger(attrs, "id")); + } + } + } + group.addRateGroup(rateGroup); + } + } + _scrollGroups.put(id, group); + } + } + } + } + } + + public EnchantItemGroup getItemGroup(L2Item item, int scrollGroup) { + final EnchantScrollGroup group = _scrollGroups.get(scrollGroup); + final EnchantRateItem rateGroup = group.getRateGroup(item); + return rateGroup != null ? _itemGroups.get(rateGroup.getName()) : null; + } + + public EnchantItemGroup getItemGroup(String name) { + return _itemGroups.get(name); + } + + public EnchantScrollGroup getScrollGroup(int id) { + return _scrollGroups.get(id); + } + + public static EnchantItemGroupsData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final EnchantItemGroupsData _instance = new EnchantItemGroupsData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemHPBonusData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemHPBonusData.java new file mode 100644 index 00000000..547c42aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemHPBonusData.java @@ -0,0 +1,136 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.StatFunction; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.CrystalType; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.functions.FuncTemplate; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Enchant HP Bonus data parser. + * @author MrPoke + * @author Zoey76 + */ +public class EnchantItemHPBonusData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(EnchantItemHPBonusData.class); + + private static final float FULL_ARMOR_MODIFIER = 1.5f; // TODO: Move it to config! + + private final Map> _armorHPBonuses = new EnumMap<>(CrystalType.class); + + protected EnchantItemHPBonusData() { + load(); + } + + @Override + public void load() { + _armorHPBonuses.clear(); + parseDatapackFile("data/stats/enchantHPBonus.xml"); + LOG.info("Loaded {} Enchant HP Bonuses.", _armorHPBonuses.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("enchantHP".equalsIgnoreCase(d.getNodeName())) { + final List bonuses = new ArrayList<>(12); + for (Node e = d.getFirstChild(); e != null; e = e.getNextSibling()) { + if ("bonus".equalsIgnoreCase(e.getNodeName())) { + bonuses.add(Integer.parseInt(e.getTextContent())); + } + } + _armorHPBonuses.put(parseEnum(d.getAttributes(), CrystalType.class, "grade"), bonuses); + } + } + } + } + + if (!_armorHPBonuses.isEmpty()) { + final ItemTable it = ItemTable.getInstance(); + // Armors + final Collection armorIds = it.getAllArmorsId(); + for (Integer itemId : armorIds) { + L2Item item = it.getTemplate(itemId); + if ((item != null) && (item.getCrystalType() != CrystalType.NONE)) { + switch (item.getBodyPart()) { + case L2Item.SLOT_CHEST: + case L2Item.SLOT_FEET: + case L2Item.SLOT_GLOVES: + case L2Item.SLOT_HEAD: + case L2Item.SLOT_LEGS: + case L2Item.SLOT_BACK: + case L2Item.SLOT_FULL_ARMOR: + case L2Item.SLOT_UNDERWEAR: + case L2Item.SLOT_L_HAND: + case L2Item.SLOT_BELT: + item.attach(new FuncTemplate(null, null, StatFunction.ENCHANTHP.getName(), -1, Stats.MAX_HP, 0)); + break; + default: + break; + } + } + } + } + } + + /** + * Gets the HP bonus. + * @param item the item + * @return the HP bonus + */ + public final int getHPBonus(L2ItemInstance item) { + final List values = _armorHPBonuses.get(item.getItem().getItemGradeSPlus()); + if ((values == null) || values.isEmpty() || (item.getOlyEnchantLevel() <= 0)) { + return 0; + } + + final int bonus = values.get(Math.min(item.getOlyEnchantLevel(), values.size()) - 1); + if (item.getItem().getBodyPart() == L2Item.SLOT_FULL_ARMOR) { + return (int) (bonus * FULL_ARMOR_MODIFIER); + } + return bonus; + } + + public static EnchantItemHPBonusData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final EnchantItemHPBonusData INSTANCE = new EnchantItemHPBonusData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemOptionsData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemOptionsData.java new file mode 100644 index 00000000..381c0727 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantItemOptionsData.java @@ -0,0 +1,103 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.options.EnchantOptions; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.Util; + +/** + * @author UnAfraid + */ +public class EnchantItemOptionsData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(EnchantItemOptionsData.class); + + private final Map> _data = new HashMap<>(); + + protected EnchantItemOptionsData() { + load(); + } + + @Override + public synchronized void load() { + _data.clear(); + parseDatapackFile("data/enchantItemOptions.xml"); + } + + @Override + public void parseDocument(Document doc) { + int counter = 0; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("item".equalsIgnoreCase(d.getNodeName())) { + int itemId = parseInteger(d.getAttributes(), "id"); + if (!_data.containsKey(itemId)) { + _data.put(itemId, new HashMap<>()); + } + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("options".equalsIgnoreCase(cd.getNodeName())) { + final EnchantOptions op = new EnchantOptions(parseInteger(cd.getAttributes(), "level")); + _data.get(itemId).put(op.getLevel(), op); + + for (byte i = 0; i < 3; i++) { + final Node att = cd.getAttributes().getNamedItem("option" + (i + 1)); + if ((att != null) && Util.isDigit(att.getNodeValue())) { + op.setOption(i, parseInteger(att)); + } + } + counter++; + } + } + } + } + } + } + LOG.info("Loaded {} items and {} options.", _data.size(), counter); + } + + public EnchantOptions getOptions(int itemId, int enchantLevel) { + if (!_data.containsKey(itemId) || !_data.get(itemId).containsKey(enchantLevel)) { + return null; + } + return _data.get(itemId).get(enchantLevel); + } + + public EnchantOptions getOptions(L2ItemInstance item) { + return item != null ? getOptions(item.getId(), item.getEnchantLevel()) : null; + } + + public static EnchantItemOptionsData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final EnchantItemOptionsData _instance = new EnchantItemOptionsData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantSkillGroupsData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantSkillGroupsData.java new file mode 100644 index 00000000..8dc66b32 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/EnchantSkillGroupsData.java @@ -0,0 +1,217 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.L2EnchantSkillGroup; +import com.l2jserver.gameserver.model.L2EnchantSkillGroup.EnchantSkillHolder; +import com.l2jserver.gameserver.model.L2EnchantSkillLearn; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Enchant skill group data parser. + * @author Micr0 + */ +public class EnchantSkillGroupsData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(EnchantSkillGroupsData.class); + + public static final int NORMAL_ENCHANT_BOOK = 6622; + + public static final int SAFE_ENCHANT_BOOK = 9627; + + public static final int CHANGE_ENCHANT_BOOK = 9626; + + public static final int UNTRAIN_ENCHANT_BOOK = 9625; + + private final Map _enchantSkillGroups = new HashMap<>(); + + private final Map _enchantSkillTrees = new HashMap<>(); + + protected EnchantSkillGroupsData() { + load(); + } + + @Override + public void load() { + _enchantSkillGroups.clear(); + _enchantSkillTrees.clear(); + parseDatapackFile("data/enchantSkillGroups.xml"); + int routes = 0; + for (L2EnchantSkillGroup group : _enchantSkillGroups.values()) { + routes += group.getEnchantGroupDetails().size(); + } + LOG.info("Loaded {} groups and {} routes.", _enchantSkillGroups.size(), routes); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("group".equalsIgnoreCase(d.getNodeName())) { + NamedNodeMap attrs = d.getAttributes(); + final int id = parseInteger(attrs, "id"); + L2EnchantSkillGroup group = _enchantSkillGroups.get(id); + if (group == null) { + group = new L2EnchantSkillGroup(id); + _enchantSkillGroups.put(id, group); + } + + for (Node b = d.getFirstChild(); b != null; b = b.getNextSibling()) { + if ("enchant".equalsIgnoreCase(b.getNodeName())) { + attrs = b.getAttributes(); + StatsSet set = new StatsSet(); + + for (int i = 0; i < attrs.getLength(); i++) { + Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + group.addEnchantDetail(new EnchantSkillHolder(set)); + } + } + } + } + } + } + } + + /** + * Adds the new route for skill. + * @param skillId the skill id + * @param maxLvL the max lvl + * @param route the route + * @param group the group + * @return the int + */ + public int addNewRouteForSkill(int skillId, int maxLvL, int route, int group) { + L2EnchantSkillLearn enchantableSkill = _enchantSkillTrees.get(skillId); + if (enchantableSkill == null) { + enchantableSkill = new L2EnchantSkillLearn(skillId, maxLvL); + _enchantSkillTrees.put(skillId, enchantableSkill); + } + if (_enchantSkillGroups.containsKey(group)) { + enchantableSkill.addNewEnchantRoute(route, group); + + return _enchantSkillGroups.get(group).getEnchantGroupDetails().size(); + } + LOG.error("There has been an error while loading generating enchant skill Id {}, route {} and group {}!", skillId, route, group); + return 0; + } + + /** + * Gets the skill enchantment for skill. + * @param skill the skill + * @return the skill enchantment for skill + */ + public L2EnchantSkillLearn getSkillEnchantmentForSkill(Skill skill) { + // there is enchantment for this skill and we have the required level of it + final L2EnchantSkillLearn esl = getSkillEnchantmentBySkillId(skill.getId()); + if ((esl != null) && (skill.getLevel() >= esl.getBaseLevel())) { + return esl; + } + return null; + } + + /** + * Gets the skill enchantment by skill id. + * @param skillId the skill id + * @return the skill enchantment by skill id + */ + public L2EnchantSkillLearn getSkillEnchantmentBySkillId(int skillId) { + return _enchantSkillTrees.get(skillId); + } + + /** + * Gets the enchant skill group by id. + * @param id the id + * @return the enchant skill group by id + */ + public L2EnchantSkillGroup getEnchantSkillGroupById(int id) { + return _enchantSkillGroups.get(id); + } + + /** + * Gets the enchant skill sp cost. + * @param skill the skill + * @return the enchant skill sp cost + */ + public int getEnchantSkillSpCost(Skill skill) { + final L2EnchantSkillLearn enchantSkillLearn = _enchantSkillTrees.get(skill.getId()); + if (enchantSkillLearn != null) { + final EnchantSkillHolder esh = enchantSkillLearn.getEnchantSkillHolder(skill.getLevel()); + if (esh != null) { + return esh.getSpCost(); + } + } + return Integer.MAX_VALUE; + } + + /** + * Gets the enchant skill Adena cost. + * @param skill the skill + * @return the enchant skill Adena cost + */ + public int getEnchantSkillAdenaCost(Skill skill) { + final L2EnchantSkillLearn enchantSkillLearn = _enchantSkillTrees.get(skill.getId()); + if (enchantSkillLearn != null) { + final EnchantSkillHolder esh = enchantSkillLearn.getEnchantSkillHolder(skill.getLevel()); + if (esh != null) { + return esh.getAdenaCost(); + } + } + return Integer.MAX_VALUE; + } + + /** + * Gets the enchant skill rate. + * @param player the player + * @param skill the skill + * @return the enchant skill rate + */ + public byte getEnchantSkillRate(L2PcInstance player, Skill skill) { + final L2EnchantSkillLearn enchantSkillLearn = _enchantSkillTrees.get(skill.getId()); + if (enchantSkillLearn != null) { + final EnchantSkillHolder esh = enchantSkillLearn.getEnchantSkillHolder(skill.getLevel()); + if (esh != null) { + return esh.getRate(player); + } + } + return 0; + } + + public static EnchantSkillGroupsData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final EnchantSkillGroupsData INSTANCE = new EnchantSkillGroupsData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishData.java new file mode 100644 index 00000000..9321baff --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishData.java @@ -0,0 +1,129 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.fishing.L2Fish; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Fish data parser. + * @author nonom + */ +public final class FishData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(FishData.class); + + private final Map _fishNormal = new HashMap<>(); + + private final Map _fishEasy = new HashMap<>(); + + private final Map _fishHard = new HashMap<>(); + + protected FishData() { + load(); + } + + @Override + public void load() { + _fishEasy.clear(); + _fishNormal.clear(); + _fishHard.clear(); + parseDatapackFile("data/stats/fishing/fishes.xml"); + LOG.info("Loaded {} fish.", (_fishEasy.size() + _fishNormal.size() + _fishHard.size())); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("fish".equalsIgnoreCase(d.getNodeName())) { + final NamedNodeMap attrs = d.getAttributes(); + + final StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + final Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + + final L2Fish fish = new L2Fish(set); + switch (fish.getFishGrade()) { + case 0 -> _fishEasy.put(fish.getFishId(), fish); + case 1 -> _fishNormal.put(fish.getFishId(), fish); + case 2 -> _fishHard.put(fish.getFishId(), fish); + } + } + } + } + } + } + + /** + * Gets the fish. + * @param level the fish Level + * @param group the fish Group + * @param grade the fish Grade + * @return List of Fish that can be fished + */ + public List getFish(int level, int group, int grade) { + final ArrayList result = new ArrayList<>(); + Map fish; + switch (grade) { + case 0 -> fish = _fishEasy; + case 1 -> fish = _fishNormal; + case 2 -> fish = _fishHard; + default -> { + LOG.warn("Unmanaged fish grade!"); + return result; + } + } + + for (L2Fish f : fish.values()) { + if ((f.getFishLevel() != level) || (f.getFishGroup() != group)) { + continue; + } + result.add(f); + } + + if (result.isEmpty()) { + LOG.warn("Cannot find any fish for level: {} group: {} and grade: {}!", level, group, grade); + } + return result; + } + + public static FishData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final FishData INSTANCE = new FishData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingMonstersData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingMonstersData.java new file mode 100644 index 00000000..3f1fd25e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingMonstersData.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.fishing.L2FishingMonster; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Fishing monsters data parser. + * @author nonom + */ +public final class FishingMonstersData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(FishingMonstersData.class); + + private final Map _fishingMonstersData = new HashMap<>(); + + protected FishingMonstersData() { + load(); + } + + @Override + public void load() { + _fishingMonstersData.clear(); + parseDatapackFile("data/stats/fishing/fishingMonsters.xml"); + LOG.info("Loaded {} fishing monsters.", _fishingMonstersData.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("fishingMonster".equalsIgnoreCase(d.getNodeName())) { + + final NamedNodeMap attrs = d.getAttributes(); + final StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + final Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + + final L2FishingMonster fishingMonster = new L2FishingMonster(set); + _fishingMonstersData.put(fishingMonster.getFishingMonsterId(), fishingMonster); + } + } + } + } + } + + /** + * Gets the fishing monster. + * @param lvl the fisherman level + * @return a fishing monster given the fisherman level + */ + public L2FishingMonster getFishingMonster(int lvl) { + for (L2FishingMonster fishingMonster : _fishingMonstersData.values()) { + if ((lvl >= fishingMonster.getUserMinLevel()) && (lvl <= fishingMonster.getUserMaxLevel())) { + return fishingMonster; + } + } + return null; + } + + /** + * Gets the fishing monster by Id. + * @param id the fishing monster Id + * @return the fishing monster by Id + */ + public L2FishingMonster getFishingMonsterById(int id) { + if (_fishingMonstersData.containsKey(id)) { + return _fishingMonstersData.get(id); + } + return null; + } + + public static FishingMonstersData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final FishingMonstersData INSTANCE = new FishingMonstersData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingRodsData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingRodsData.java new file mode 100644 index 00000000..fedfba41 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/FishingRodsData.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.fishing.L2FishingRod; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Fishing rods data parser. + * @author nonom + */ +public final class FishingRodsData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(FishingRodsData.class); + + private final Map _fishingRods = new HashMap<>(); + + protected FishingRodsData() { + load(); + } + + @Override + public void load() { + _fishingRods.clear(); + parseDatapackFile("data/stats/fishing/fishingRods.xml"); + LOG.info("Loaded {} fishing rods.", _fishingRods.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("fishingRod".equalsIgnoreCase(d.getNodeName())) { + final NamedNodeMap attrs = d.getAttributes(); + final StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + final Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + + final L2FishingRod fishingRod = new L2FishingRod(set); + _fishingRods.put(fishingRod.getFishingRodItemId(), fishingRod); + } + } + } + } + } + + /** + * Gets the fishing rod. + * @param itemId the item id + * @return A fishing Rod by Item Id + */ + public L2FishingRod getFishingRod(int itemId) { + return _fishingRods.get(itemId); + } + + public static FishingRodsData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final FishingRodsData INSTANCE = new FishingRodsData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/HennaData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/HennaData.java new file mode 100644 index 00000000..d03dd60f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/HennaData.java @@ -0,0 +1,149 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.items.L2Henna; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Henna data parser.
    + * Cost and required amount to add the henna to the player.
    + * Cost and retrieved amount for removing the henna from the player.
    + * Allowed classes to wear each henna. + * @author Zoey76 + */ +public final class HennaData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(HennaData.class); + + private final Map _hennaList = new HashMap<>(); + + protected HennaData() { + load(); + } + + @Override + public void load() { + _hennaList.clear(); + parseDatapackFile("data/stats/hennaList.xml"); + LOG.info("Loaded {} Henna data.", _hennaList.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equals(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("henna".equals(d.getNodeName())) { + parseHenna(d); + } + } + } + } + } + + /** + * Parses the henna. + * @param d the d + */ + private void parseHenna(Node d) { + final StatsSet set = new StatsSet(); + final List wearClassIds = new ArrayList<>(); + NamedNodeMap attrs = d.getAttributes(); + Node attr; + for (int i = 0; i < attrs.getLength(); i++) { + attr = attrs.item(i); + set.set(attr.getNodeName(), attr.getNodeValue()); + } + + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + final String name = c.getNodeName(); + attrs = c.getAttributes(); + switch (name) { + case "stats" -> { + for (int i = 0; i < attrs.getLength(); i++) { + attr = attrs.item(i); + set.set(attr.getNodeName(), attr.getNodeValue()); + } + } + case "wear" -> { + attr = attrs.getNamedItem("count"); + set.set("wear_count", attr.getNodeValue()); + attr = attrs.getNamedItem("fee"); + set.set("wear_fee", attr.getNodeValue()); + } + case "cancel" -> { + attr = attrs.getNamedItem("count"); + set.set("cancel_count", attr.getNodeValue()); + attr = attrs.getNamedItem("fee"); + set.set("cancel_fee", attr.getNodeValue()); + } + case "classId" -> wearClassIds.add(ClassId.getClassId(Integer.parseInt(c.getTextContent()))); + } + } + final L2Henna henna = new L2Henna(set); + henna.setWearClassIds(wearClassIds); + _hennaList.put(henna.getDyeId(), henna); + } + + /** + * Gets the henna. + * @param id of the dye. + * @return the dye with that id. + */ + public L2Henna getHenna(int id) { + return _hennaList.get(id); + } + + /** + * Gets the henna list. + * @param classId the player's class Id. + * @return the list with all the allowed dyes. + */ + public List getHennaList(ClassId classId) { + final List list = new ArrayList<>(); + for (L2Henna henna : _hennaList.values()) { + if (henna.isAllowedClass(classId)) { + list.add(henna); + } + } + return list; + } + + public static HennaData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final HennaData INSTANCE = new HennaData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/HitConditionBonusData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/HitConditionBonusData.java new file mode 100644 index 00000000..dbe68aa4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/HitConditionBonusData.java @@ -0,0 +1,128 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Hit condition bonuses parser. + * @author Nik + */ +public final class HitConditionBonusData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(HitConditionBonusData.class); + + private int frontBonus = 0; + + private int sideBonus = 0; + + private int backBonus = 0; + + private int highBonus = 0; + + private int lowBonus = 0; + + private int darkBonus = 0; + + private int rainBonus = 0; + + protected HitConditionBonusData() { + load(); + } + + @Override + public void load() { + parseDatapackFile("data/stats/hitConditionBonus.xml"); + LOG.info("Loaded Hit Condition bonuses."); + LOG.debug("Front bonus {}.", frontBonus); + LOG.debug("Side bonus {}.", sideBonus); + LOG.debug("Back bonus {}.", backBonus); + LOG.debug("High bonus {}.", highBonus); + LOG.debug("Low bonus {}.", lowBonus); + LOG.debug("Dark bonus {}.", darkBonus); + LOG.debug("Rain bonus {}.", rainBonus); + } + + @Override + public void parseDocument(Document doc) { + for (Node d = doc.getFirstChild().getFirstChild(); d != null; d = d.getNextSibling()) { + NamedNodeMap attrs = d.getAttributes(); + switch (d.getNodeName()) { + case "front" -> frontBonus = parseInteger(attrs, "val"); + case "side" -> sideBonus = parseInteger(attrs, "val"); + case "back" -> backBonus = parseInteger(attrs, "val"); + case "high" -> highBonus = parseInteger(attrs, "val"); + case "low" -> lowBonus = parseInteger(attrs, "val"); + case "dark" -> darkBonus = parseInteger(attrs, "val"); + case "rain" -> rainBonus = parseInteger(attrs, "val"); + } + } + } + + /** + * Gets the condition bonus. + * @param attacker the attacking character. + * @param target the attacked character. + * @return the bonus of the attacker against the target. + */ + public double getConditionBonus(L2Character attacker, L2Character target) { + double mod = 100; + // Get high or low bonus + if ((attacker.getZ() - target.getZ()) > 50) { + mod += highBonus; + } else if ((attacker.getZ() - target.getZ()) < -50) { + mod += lowBonus; + } + + // Get weather bonus + if (GameTimeController.getInstance().isNight()) { + mod += darkBonus; + // else if () No rain support yet. + // chance += hitConditionBonus.rainBonus; + } + + // Get side bonus + if (attacker.isBehindTarget()) { + mod += backBonus; + } else if (attacker.isInFrontOfTarget()) { + mod += frontBonus; + } else { + mod += sideBonus; + } + + // If (mod / 100) is less than 0, return 0, because we can't lower more than 100%. + return Math.max(mod / 100, 0); + } + + public static HitConditionBonusData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final HitConditionBonusData INSTANCE = new HitConditionBonusData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialEquipmentData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialEquipmentData.java new file mode 100644 index 00000000..ceae7ea3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialEquipmentData.java @@ -0,0 +1,125 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.items.PcItemTemplate; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class holds the Initial Equipment information.
    + * What items get each newly created character and if this item is equipped upon creation (Requires the item to be equippable). + * @author Zoey76 + */ +public final class InitialEquipmentData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(InitialEquipmentData.class); + + private static final String NORMAL = "data/stats/initialEquipment.xml"; + + private static final String EVENT = "data/stats/initialEquipmentEvent.xml"; + + private final Map> _initialEquipmentList = new HashMap<>(); + + protected InitialEquipmentData() { + load(); + } + + @Override + public void load() { + _initialEquipmentList.clear(); + parseDatapackFile(character().initialEquipmentEvent() ? EVENT : NORMAL); + LOG.info("Loaded {} initial equipment data.", _initialEquipmentList.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("equipment".equalsIgnoreCase(d.getNodeName())) { + parseEquipment(d); + } + } + } + } + } + + /** + * Parses the equipment. + * @param d parse an initial equipment and add it to {@link #_initialEquipmentList} + */ + private void parseEquipment(Node d) { + NamedNodeMap attrs = d.getAttributes(); + final ClassId classId = ClassId.getClassId(Integer.parseInt(attrs.getNamedItem("classId").getNodeValue())); + final List equipList = new ArrayList<>(); + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("item".equalsIgnoreCase(c.getNodeName())) { + final StatsSet set = new StatsSet(); + attrs = c.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node attr = attrs.item(i); + set.set(attr.getNodeName(), attr.getNodeValue()); + } + equipList.add(new PcItemTemplate(set)); + } + } + _initialEquipmentList.put(classId, equipList); + } + + /** + * Gets the equipment list. + * @param cId the class Id for the required initial equipment. + * @return the initial equipment for the given class Id. + */ + public List getEquipmentList(ClassId cId) { + return _initialEquipmentList.get(cId); + } + + /** + * Gets the equipment list. + * @param cId the class Id for the required initial equipment. + * @return the initial equipment for the given class Id. + */ + public List getEquipmentList(int cId) { + return _initialEquipmentList.get(ClassId.getClassId(cId)); + } + + public static InitialEquipmentData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final InitialEquipmentData INSTANCE = new InitialEquipmentData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialShortcutData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialShortcutData.java new file mode 100644 index 00000000..5d5077ea --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/InitialShortcutData.java @@ -0,0 +1,287 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.enums.MacroType; +import com.l2jserver.gameserver.enums.ShortcutType; +import com.l2jserver.gameserver.model.Macro; +import com.l2jserver.gameserver.model.MacroCmd; +import com.l2jserver.gameserver.model.Shortcut; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.serverpackets.ShortCutRegister; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class holds the Initial Shortcuts information.
    + * What shortcuts get each newly created character. + * @author Zoey76 + */ +public final class InitialShortcutData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(InitialShortcutData.class); + + private final Map> _initialShortcutData = new HashMap<>(); + + private final List _initialGlobalShortcutList = new ArrayList<>(); + + private final Map _macroPresets = new HashMap<>(); + + protected InitialShortcutData() { + load(); + } + + @Override + public void load() { + _initialShortcutData.clear(); + _initialGlobalShortcutList.clear(); + + parseDatapackFile("data/stats/initialShortcuts.xml"); + + LOG.info("Loaded {} initial dlobal shortcuts data.", _initialGlobalShortcutList.size()); + LOG.info("Loaded {} initial shortcuts data.", _initialShortcutData.size()); + LOG.info("Loaded {} macros presets.", _macroPresets.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equals(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + switch (d.getNodeName()) { + case "shortcuts" -> parseShortcuts(d); + case "macros" -> parseMacros(d); + } + } + } + } + } + + /** + * Parses a shortcut. + * @param d the node + */ + private void parseShortcuts(Node d) { + NamedNodeMap attrs = d.getAttributes(); + final Node classIdNode = attrs.getNamedItem("classId"); + final List list = new ArrayList<>(); + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("page".equals(c.getNodeName())) { + attrs = c.getAttributes(); + final int pageId = parseInteger(attrs, "pageId"); + for (Node b = c.getFirstChild(); b != null; b = b.getNextSibling()) { + if ("slot".equals(b.getNodeName())) { + list.add(createShortcut(pageId, b)); + } + } + } + } + + if (classIdNode != null) { + _initialShortcutData.put(ClassId.getClassId(Integer.parseInt(classIdNode.getNodeValue())), list); + } else { + _initialGlobalShortcutList.addAll(list); + } + } + + /** + * Parses a macro. + * @param d the node + */ + private void parseMacros(Node d) { + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("macro".equals(c.getNodeName())) { + NamedNodeMap attrs = c.getAttributes(); + if (!parseBoolean(attrs, "enabled", true)) { + continue; + } + + final int macroId = parseInteger(attrs, "macroId"); + final int icon = parseInteger(attrs, "icon"); + final String name = parseString(attrs, "name"); + final String description = parseString(attrs, "description"); + final String acronym = parseString(attrs, "acronym"); + final List commands = new ArrayList<>(1); + int entry = 0; + for (Node b = c.getFirstChild(); b != null; b = b.getNextSibling()) { + if ("command".equals(b.getNodeName())) { + attrs = b.getAttributes(); + final MacroType type = parseEnum(attrs, MacroType.class, "type"); + int d1 = 0; + int d2 = 0; + final String cmd = b.getTextContent(); + switch (type) { + case SKILL -> { + d1 = parseInteger(attrs, "skillId"); // Skill ID + d2 = parseInteger(attrs, "skillLvl", 0); // Skill level + } + case ACTION -> d1 = parseInteger(attrs, "actionId"); // Not handled by client. + case TEXT -> { + // Doesn't have numeric parameters. + } + case SHORTCUT -> { + d1 = parseInteger(attrs, "page"); // Page + d2 = parseInteger(attrs, "slot", 0); // Slot + } + case ITEM -> d1 = parseInteger(attrs, "itemId"); // Not handled by client. + case DELAY -> d1 = parseInteger(attrs, "delay"); // Delay in seconds + } + commands.add(new MacroCmd(entry++, type, d1, d2, cmd)); + } + } + _macroPresets.put(macroId, new Macro(macroId, icon, name, description, acronym, commands)); + } + } + } + + /** + * Parses a node an create a shortcut from it. + * @param pageId the page ID + * @param b the node to parse + * @return the new shortcut + */ + private Shortcut createShortcut(int pageId, Node b) { + final NamedNodeMap attrs = b.getAttributes(); + final int slotId = parseInteger(attrs, "slotId"); + final ShortcutType shortcutType = parseEnum(attrs, ShortcutType.class, "shortcutType"); + final int shortcutId = parseInteger(attrs, "shortcutId"); + final int shortcutLevel = parseInteger(attrs, "shortcutLevel", 0); + final int characterType = parseInteger(attrs, "characterType", 0); + return new Shortcut(slotId, pageId, shortcutType, shortcutId, shortcutLevel, characterType); + } + + /** + * Gets the shortcut list. + * @param cId the class ID for the shortcut list + * @return the shortcut list for the give class ID + */ + public List getShortcutList(ClassId cId) { + return _initialShortcutData.get(cId); + } + + /** + * Gets the shortcut list. + * @param cId the class ID for the shortcut list + * @return the shortcut list for the give class ID + */ + public List getShortcutList(int cId) { + return _initialShortcutData.get(ClassId.getClassId(cId)); + } + + /** + * Gets the global shortcut list. + * @return the global shortcut list + */ + public List getGlobalMacroList() { + return _initialGlobalShortcutList; + } + + /** + * Register all the available shortcuts for the given player. + * @param player the player + */ + public void registerAllShortcuts(L2PcInstance player) { + if (player == null) { + return; + } + + // Register global shortcuts. + for (Shortcut shortcut : _initialGlobalShortcutList) { + int shortcutId = shortcut.getId(); + switch (shortcut.getType()) { + case ITEM -> { + final L2ItemInstance item = player.getInventory().getItemByItemId(shortcutId); + if (item == null) { + continue; + } + shortcutId = item.getObjectId(); + } + case SKILL -> { + if (!player.getSkills().containsKey(shortcutId)) { + continue; + } + } + case MACRO -> { + final Macro macro = _macroPresets.get(shortcutId); + if (macro == null) { + continue; + } + player.registerMacro(macro); + } + } + + // Register shortcut + final Shortcut newShortcut = new Shortcut(shortcut.getSlot(), shortcut.getPage(), shortcut.getType(), shortcutId, shortcut.getLevel(), shortcut.getCharacterType()); + player.sendPacket(new ShortCutRegister(newShortcut)); + player.registerShortCut(newShortcut); + } + + // Register class specific shortcuts. + if (_initialShortcutData.containsKey(player.getClassId())) { + for (Shortcut shortcut : _initialShortcutData.get(player.getClassId())) { + int shortcutId = shortcut.getId(); + switch (shortcut.getType()) { + case ITEM -> { + final L2ItemInstance item = player.getInventory().getItemByItemId(shortcutId); + if (item == null) { + continue; + } + shortcutId = item.getObjectId(); + } + case SKILL -> { + if (!player.getSkills().containsKey(shortcut.getId())) { + continue; + } + } + case MACRO -> { + final Macro macro = _macroPresets.get(shortcutId); + if (macro == null) { + continue; + } + player.registerMacro(macro); + } + } + // Register shortcut + final Shortcut newShortcut = new Shortcut(shortcut.getSlot(), shortcut.getPage(), shortcut.getType(), shortcutId, shortcut.getLevel(), shortcut.getCharacterType()); + player.sendPacket(new ShortCutRegister(newShortcut)); + player.registerShortCut(newShortcut); + } + } + } + + public static InitialShortcutData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final InitialShortcutData INSTANCE = new InitialShortcutData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/KarmaData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/KarmaData.java new file mode 100644 index 00000000..7aa189f9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/KarmaData.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Karma data. + * @author UnAfraid + */ +public class KarmaData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(KarmaData.class); + + private final Map _karmaTable = new HashMap<>(); + + public KarmaData() { + load(); + } + + @Override + public synchronized void load() { + _karmaTable.clear(); + parseDatapackFile("data/stats/chars/pcKarmaIncrease.xml"); + LOG.info("Loaded {} karma modifiers.", _karmaTable.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("pcKarmaIncrease".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("increase".equalsIgnoreCase(d.getNodeName())) { + final NamedNodeMap attrs = d.getAttributes(); + _karmaTable.put(parseInteger(attrs, "lvl"), parseDouble(attrs, "val")); + } + } + } + } + } + + /** + * @param level + * @return {@code double} modifier used to calculate karma lost upon death. + */ + public double getMultiplier(int level) { + return _karmaTable.get(level); + } + + public static KarmaData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final KarmaData INSTANCE = new KarmaData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/MultisellData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/MultisellData.java new file mode 100644 index 00000000..fb1398b6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/MultisellData.java @@ -0,0 +1,328 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.io.File; +import java.io.FileFilter; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.multisell.Entry; +import com.l2jserver.gameserver.model.multisell.Ingredient; +import com.l2jserver.gameserver.model.multisell.ListContainer; +import com.l2jserver.gameserver.model.multisell.PreparedListContainer; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.MultiSellList; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.Util; +import com.l2jserver.gameserver.util.file.filter.NumericNameFilter; + +public final class MultisellData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(MultisellData.class); + + public static final int PAGE_SIZE = 40; + // Special IDs. + public static final int PC_BANG_POINTS = -100; + public static final int CLAN_REPUTATION = -200; + public static final int FAME = -300; + // Misc + private static final FileFilter NUMERIC_FILTER = new NumericNameFilter(); + + private final Map _entries = new HashMap<>(); + + protected MultisellData() { + load(); + } + + @Override + public void load() { + _entries.clear(); + parseDatapackDirectory("data/multisell", false); + if (general().customMultisellLoad()) { + parseDatapackDirectory("data/multisell/custom", false); + } + + verify(); + LOG.info("Loaded {} multisell lists.", _entries.size()); + } + + @Override + public void parseDocument(Document doc, File f) { + try { + int id = Integer.parseInt(f.getName().replaceAll(".xml", "")); + int entryId = 1; + Node att; + final ListContainer list = new ListContainer(id); + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + att = n.getAttributes().getNamedItem("applyTaxes"); + list.setApplyTaxes((att != null) && Boolean.parseBoolean(att.getNodeValue())); + + att = n.getAttributes().getNamedItem("useRate"); + if (att != null) { + try { + + list.setUseRate(Double.parseDouble(att.getNodeValue())); + if (list.getUseRate() <= 1e-6) { + throw new NumberFormatException("The value cannot be 0"); // threat 0 as invalid value + } + } catch (NumberFormatException ex) { + LOG.warn("Unable to parse {}!", doc.getLocalName(), ex); + list.setUseRate(1.0); + } catch (DOMException ex) { + LOG.warn("Unable to parse {}!", doc.getLocalName(), ex); + } + } + + att = n.getAttributes().getNamedItem("maintainEnchantment"); + list.setMaintainEnchantment((att != null) && Boolean.parseBoolean(att.getNodeValue())); + + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("item".equalsIgnoreCase(d.getNodeName())) { + Entry e = parseEntry(d, entryId++, list); + list.getEntries().add(e); + } else if ("npcs".equalsIgnoreCase(d.getNodeName())) { + for (Node b = d.getFirstChild(); b != null; b = b.getNextSibling()) { + if ("npc".equalsIgnoreCase(b.getNodeName())) { + if (Util.isDigit(b.getTextContent())) { + list.allowNpc(Integer.parseInt(b.getTextContent())); + } + } + } + } + } + } + } + _entries.put(id, list); + } catch (Exception ex) { + LOG.error("There has been an error in file {}!", f, ex); + } + } + + @Override + public FileFilter getCurrentFileFilter() { + return NUMERIC_FILTER; + } + + private Entry parseEntry(Node n, int entryId, ListContainer list) { + Node first = n.getFirstChild(); + final Entry entry = new Entry(entryId); + + NamedNodeMap attrs; + Node att; + StatsSet set; + + for (n = first; n != null; n = n.getNextSibling()) { + if ("ingredient".equalsIgnoreCase(n.getNodeName())) { + attrs = n.getAttributes(); + set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + entry.addIngredient(new Ingredient(set)); + } else if ("production".equalsIgnoreCase(n.getNodeName())) { + attrs = n.getAttributes(); + set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + entry.addProduct(new Ingredient(set)); + } + } + + return entry; + } + + /** + * This will generate the multisell list for the items.
    + * There exist various parameters in multisells that affect the way they will appear: + *
      + *
    1. Inventory only: + *
        + *
      • If true, only show items of the multisell for which the "primary" ingredients are already in the player's inventory. By "primary" ingredients we mean weapon and armor.
      • + *
      • If false, show the entire list.
      • + *
      + *
    2. + *
    3. Maintain enchantment: presumably, only lists with "inventory only" set to true should sometimes have this as true. This makes no sense otherwise... + *
        + *
      • If true, then the product will match the enchantment level of the ingredient.
        + * If the player has multiple items that match the ingredient list but the enchantment levels differ, then the entries need to be duplicated to show the products and ingredients for each enchantment level.
        + * For example: If the player has a crystal staff +1 and a crystal staff +3 and goes to exchange it at the mammon, the list should have all exchange possibilities for the +1 staff, followed by all possibilities for the +3 staff.
      • + *
      • If false, then any level ingredient will be considered equal and product will always be at +0
      • + *
      + *
    4. + *
    5. Apply taxes: Uses the "taxIngredient" entry in order to add a certain amount of adena to the ingredients. + *
    6. + *
    7. Additional product and ingredient multipliers.
    8. + *
    + * @param listId + * @param player + * @param npc + * @param inventoryOnly + * @param productMultiplier + * @param ingredientMultiplier + */ + public void separateAndSend(int listId, L2PcInstance player, L2Npc npc, boolean inventoryOnly, double productMultiplier, double ingredientMultiplier) { + ListContainer template = _entries.get(listId); + if (template == null) { + LOG.warn("Cannot find list ID: {} requested by player: {}, NPC ID: {}!", listId, player, (npc != null ? npc.getId() : 0)); + return; + } + + if (((npc != null) && !template.isNpcAllowed(npc.getId())) || ((npc == null) && template.isNpcOnly())) { + LOG.warn("Player {} attempted to open multisell {} from npc {} which is not allowed!", player, listId, npc); + return; + } + + final PreparedListContainer list = new PreparedListContainer(template, inventoryOnly, player, npc); + + // Pass through this only when multipliers are different from 1 + if ((productMultiplier != 1) || (ingredientMultiplier != 1)) { + list.getEntries().forEach(entry -> { + // Math.max used here to avoid dropping count to 0 + entry.getProducts().forEach(product -> product.setItemCount((long) Math.max(product.getItemCount() * productMultiplier, 1))); + + // Math.max used here to avoid dropping count to 0 + entry.getIngredients().forEach(ingredient -> ingredient.setItemCount((long) Math.max(ingredient.getItemCount() * ingredientMultiplier, 1))); + }); + } + int index = 0; + do { + // send list at least once even if size = 0 + player.sendPacket(new MultiSellList(list, index)); + index += PAGE_SIZE; + } + while (index < list.getEntries().size()); + + player.setMultiSell(list); + } + + public void separateAndSend(int listId, L2PcInstance player, L2Npc npc, boolean inventoryOnly) { + separateAndSend(listId, player, npc, inventoryOnly, 1, 1); + } + + public static boolean hasSpecialIngredient(int id, long amount, L2PcInstance player) { + switch (id) { + case CLAN_REPUTATION -> { + if (player.getClan() == null) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_A_CLAN_MEMBER); + break; + } + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.ONLY_THE_CLAN_LEADER_IS_ENABLED); + break; + } + if (player.getClan().getReputationScore() < amount) { + player.sendPacket(SystemMessageId.THE_CLAN_REPUTATION_SCORE_IS_TOO_LOW); + break; + } + return true; + } + case FAME -> { + if (player.getFame() < amount) { + player.sendPacket(SystemMessageId.NOT_ENOUGH_FAME_POINTS); + break; + } + return true; + } + } + return false; + } + + public static boolean takeSpecialIngredient(int id, long amount, L2PcInstance player) { + switch (id) { + case CLAN_REPUTATION -> { + player.getClan().takeReputationScore((int) amount, true); + SystemMessage smsg = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + smsg.addLong(amount); + player.sendPacket(smsg); + return true; + } + case FAME -> { + player.setFame(player.getFame() - (int) amount); + player.sendPacket(new UserInfo(player)); + player.sendPacket(new ExBrExtraUserInfo(player)); + return true; + } + } + return false; + } + + public static void giveSpecialProduct(int id, long amount, L2PcInstance player) { + switch (id) { + case CLAN_REPUTATION -> player.getClan().addReputationScore((int) amount, true); + case FAME -> { + player.setFame((int) (player.getFame() + amount)); + player.sendPacket(new UserInfo(player)); + player.sendPacket(new ExBrExtraUserInfo(player)); + } + } + } + + private void verify() { + for (ListContainer listContainer : _entries.values()) { + for (Entry ent : listContainer.getEntries()) { + for (Ingredient ing : ent.getIngredients()) { + if (!verifyIngredient(ing)) { + LOG.warn("Cannot find ingredient with item ID: {} in list: {}!", ing.getItemId(), listContainer.getListId()); + } + } + for (Ingredient ing : ent.getProducts()) { + if (!verifyIngredient(ing)) { + LOG.warn("Cannot find product with item ID: {} in list: {}!", ing.getItemId(), listContainer.getListId()); + } + } + } + } + } + + private boolean verifyIngredient(Ingredient ing) { + return switch (ing.getItemId()) { + case CLAN_REPUTATION, FAME -> true; + default -> ing.getTemplate() != null; + }; + } + + public static MultisellData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final MultisellData _instance = new MultisellData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/NpcData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/NpcData.java new file mode 100644 index 00000000..84bb2e88 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/NpcData.java @@ -0,0 +1,636 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.AISkillScope; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.drops.DropListScope; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.drops.GroupedGeneralDropItem; +import com.l2jserver.gameserver.model.drops.IDropItem; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.holders.MinionHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.Util; + +/** + * NPC data parser. + * @author NosBit + */ +public class NpcData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(NpcData.class); + + private final Map _npcs = new ConcurrentHashMap<>(); + + private final Map _clans = new ConcurrentHashMap<>(); + + private MinionData _minionData; + + protected NpcData() { + load(); + } + + @Override + public synchronized void load() { + _minionData = new MinionData(); + + parseDatapackDirectory("data/stats/npcs", false); + LOG.info("Loaded {} NPCs.", _npcs.size()); + + if (general().customNpcData()) { + final int npcCount = _npcs.size(); + parseDatapackDirectory("data/stats/npcs/custom", true); + LOG.info("Loaded {} custom NPCs.", (_npcs.size() - npcCount)); + } + + _minionData = null; + } + + @Override + public void parseDocument(Document doc, File f) { + for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equalsIgnoreCase(node.getNodeName())) { + for (Node listNode = node.getFirstChild(); listNode != null; listNode = listNode.getNextSibling()) { + if ("npc".equalsIgnoreCase(listNode.getNodeName())) { + NamedNodeMap attrs = listNode.getAttributes(); + final StatsSet set = new StatsSet(); + final int npcId = parseInteger(attrs, "id"); + Map parameters = null; + Map skills = null; + Set clans = null; + Set ignoreClanNpcIds = null; + Map> dropLists = null; + set.set("id", npcId); + set.set("displayId", parseInteger(attrs, "displayId")); + set.set("level", parseByte(attrs, "level")); + set.set("type", parseString(attrs, "type")); + set.set("name", parseString(attrs, "name")); + set.set("usingServerSideName", parseBoolean(attrs, "usingServerSideName")); + set.set("title", parseString(attrs, "title")); + set.set("usingServerSideTitle", parseBoolean(attrs, "usingServerSideTitle")); + for (Node npcNode = listNode.getFirstChild(); npcNode != null; npcNode = npcNode.getNextSibling()) { + attrs = npcNode.getAttributes(); + switch (npcNode.getNodeName().toLowerCase()) { + case "parameters" -> { + if (parameters == null) { + parameters = new HashMap<>(); + } + + for (Node parametersNode = npcNode.getFirstChild(); parametersNode != null; parametersNode = parametersNode.getNextSibling()) { + attrs = parametersNode.getAttributes(); + switch (parametersNode.getNodeName().toLowerCase()) { + case "param" -> parameters.put(parseString(attrs, "name"), parseString(attrs, "value")); + case "skill" -> parameters.put(parseString(attrs, "name"), new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "level"))); + case "minions" -> { + final List minions = new ArrayList<>(1); + for (Node minionsNode = parametersNode.getFirstChild(); minionsNode != null; minionsNode = minionsNode.getNextSibling()) { + if (minionsNode.getNodeName().equalsIgnoreCase("npc")) { + attrs = minionsNode.getAttributes(); + minions.add(new MinionHolder(parseInteger(attrs, "id"), parseInteger(attrs, "count"), parseInteger(attrs, "respawnTime"), parseInteger(attrs, "weightPoint"))); + } + } + + if (!minions.isEmpty()) { + parameters.put(parseString(parametersNode.getAttributes(), "name"), minions); + } + } + } + } + } + case "race", "sex" -> set.set(npcNode.getNodeName(), npcNode.getTextContent().toUpperCase()); + case "equipment" -> { + set.set("chestId", parseInteger(attrs, "chest")); + set.set("rhandId", parseInteger(attrs, "rhand")); + set.set("lhandId", parseInteger(attrs, "lhand")); + set.set("weaponEnchant", parseInteger(attrs, "weaponEnchant")); + } + case "acquire" -> { + set.set("expRate", parseDouble(attrs, "expRate")); + set.set("sp", parseDouble(attrs, "sp")); + set.set("raidPoints", parseDouble(attrs, "raidPoints")); + } + case "stats" -> { + set.set("baseSTR", parseInteger(attrs, "str")); + set.set("baseINT", parseInteger(attrs, "int")); + set.set("baseDEX", parseInteger(attrs, "dex")); + set.set("baseWIT", parseInteger(attrs, "wit")); + set.set("baseCON", parseInteger(attrs, "con")); + set.set("baseMEN", parseInteger(attrs, "men")); + for (Node statsNode = npcNode.getFirstChild(); statsNode != null; statsNode = statsNode.getNextSibling()) { + attrs = statsNode.getAttributes(); + switch (statsNode.getNodeName().toLowerCase()) { + case "vitals" -> { + set.set("baseHpMax", parseDouble(attrs, "hp")); + set.set("baseHpReg", parseDouble(attrs, "hpRegen")); + set.set("baseMpMax", parseDouble(attrs, "mp")); + set.set("baseMpReg", parseDouble(attrs, "mpRegen")); + } + case "attack" -> { + set.set("basePAtk", parseDouble(attrs, "physical")); + set.set("baseMAtk", parseDouble(attrs, "magical")); + set.set("baseRndDam", parseInteger(attrs, "random")); + set.set("baseCritRate", parseInteger(attrs, "critical")); + set.set("accuracy", parseDouble(attrs, "accuracy"));// TODO: Implement me + set.set("basePAtkSpd", parseInteger(attrs, "attackSpeed")); + set.set("reuseDelay", parseInteger(attrs, "reuseDelay")); + set.set("baseAtkType", parseString(attrs, "type")); + set.set("baseAtkRange", parseInteger(attrs, "range")); + set.set("distance", parseInteger(attrs, "distance"));// TODO: Implement me + set.set("width", parseInteger(attrs, "width"));// TODO: Implement me + } + case "defence" -> { + set.set("basePDef", parseDouble(attrs, "physical")); + set.set("baseMDef", parseDouble(attrs, "magical")); + set.set("evasion", parseInteger(attrs, "evasion"));// TODO: Implement me + set.set("baseShldDef", parseInteger(attrs, "shield")); + set.set("baseShldRate", parseInteger(attrs, "shieldRate")); + } + case "attribute" -> { + for (Node attributeNode = statsNode.getFirstChild(); attributeNode != null; attributeNode = attributeNode.getNextSibling()) { + attrs = attributeNode.getAttributes(); + switch (attributeNode.getNodeName().toLowerCase()) { + case "attack" -> { + String attackAttributeType = parseString(attrs, "type"); + switch (attackAttributeType.toUpperCase()) { + case "FIRE" -> set.set("baseFire", parseInteger(attrs, "value")); + case "WATER" -> set.set("baseWater", parseInteger(attrs, "value")); + case "WIND" -> set.set("baseWind", parseInteger(attrs, "value")); + case "EARTH" -> set.set("baseEarth", parseInteger(attrs, "value")); + case "DARK" -> set.set("baseDark", parseInteger(attrs, "value")); + case "HOLY" -> set.set("baseHoly", parseInteger(attrs, "value")); + } + } + case "defence" -> { + set.set("baseFireRes", parseInteger(attrs, "fire")); + set.set("baseWaterRes", parseInteger(attrs, "water")); + set.set("baseWindRes", parseInteger(attrs, "wind")); + set.set("baseEarthRes", parseInteger(attrs, "earth")); + set.set("baseHolyRes", parseInteger(attrs, "holy")); + set.set("baseDarkRes", parseInteger(attrs, "dark")); + set.set("baseElementRes", parseInteger(attrs, "default")); + } + } + } + } + case "speed" -> { + for (Node speedNode = statsNode.getFirstChild(); speedNode != null; speedNode = speedNode.getNextSibling()) { + attrs = speedNode.getAttributes(); + switch (speedNode.getNodeName().toLowerCase()) { + case "walk" -> { + set.set("baseWalkSpd", parseDouble(attrs, "ground")); + set.set("baseSwimWalkSpd", parseDouble(attrs, "swim")); + set.set("baseFlyWalkSpd", parseDouble(attrs, "fly")); + } + case "run" -> { + set.set("baseRunSpd", parseDouble(attrs, "ground")); + set.set("baseSwimRunSpd", parseDouble(attrs, "swim")); + set.set("baseFlyRunSpd", parseDouble(attrs, "fly")); + } + } + } + } + case "hittime" -> set.set("hitTime", npcNode.getTextContent());// TODO: Implement me default 600 (value in ms) + } + } + } + case "status" -> { + set.set("unique", parseBoolean(attrs, "unique")); + set.set("attackable", parseBoolean(attrs, "attackable")); + set.set("targetable", parseBoolean(attrs, "targetable")); + set.set("undying", parseBoolean(attrs, "undying")); + set.set("showName", parseBoolean(attrs, "showName")); + set.set("flying", parseBoolean(attrs, "flying")); + set.set("canMove", parseBoolean(attrs, "canMove")); + set.set("noSleepMode", parseBoolean(attrs, "noSleepMode")); + set.set("passableDoor", parseBoolean(attrs, "passableDoor")); + set.set("hasSummoner", parseBoolean(attrs, "hasSummoner")); + set.set("canBeSown", parseBoolean(attrs, "canBeSown")); + } + case "skilllist" -> { + skills = new HashMap<>(); + for (Node skillListNode = npcNode.getFirstChild(); skillListNode != null; skillListNode = skillListNode.getNextSibling()) { + if ("skill".equalsIgnoreCase(skillListNode.getNodeName())) { + attrs = skillListNode.getAttributes(); + final int skillId = parseInteger(attrs, "id"); + final int skillLevel = parseInteger(attrs, "level"); + final Skill skill = SkillData.getInstance().getSkill(skillId, skillLevel); + if (skill != null) { + skills.put(skill.getId(), skill); + } else { + LOG.warn("{} skill not found. NPC Id {}, skill Id {}, skill level: {}!", f, npcId, skillId, skillLevel); + } + } + } + } + case "shots" -> { + set.set("soulShot", parseInteger(attrs, "soul")); + set.set("spiritShot", parseInteger(attrs, "spirit")); + set.set("shotShotChance", parseInteger(attrs, "shotChance")); + set.set("spiritShotChance", parseInteger(attrs, "spiritChance")); + } + case "corpsetime" -> set.set("corpseTime", npcNode.getTextContent()); + case "excrteffect" -> set.set("exCrtEffect", npcNode.getTextContent()); // TODO: Implement me default ? type boolean + case "snpcprophprate" -> set.set("sNpcPropHpRate", npcNode.getTextContent()); // TODO: Implement me default 1 type double + case "ai" -> { + set.set("aiType", parseString(attrs, "type")); + set.set("aggroRange", parseInteger(attrs, "aggroRange")); + set.set("clanHelpRange", parseInteger(attrs, "clanHelpRange")); + set.set("isChaos", parseBoolean(attrs, "isChaos")); + set.set("isAggressive", parseBoolean(attrs, "isAggressive")); + for (Node aiNode = npcNode.getFirstChild(); aiNode != null; aiNode = aiNode.getNextSibling()) { + attrs = aiNode.getAttributes(); + switch (aiNode.getNodeName().toLowerCase()) { + case "skill" -> { + set.set("minSkillChance", parseInteger(attrs, "minChance")); + set.set("maxSkillChance", parseInteger(attrs, "maxChance")); + set.set("primarySkillId", parseInteger(attrs, "primaryId")); + set.set("shortRangeSkillId", parseInteger(attrs, "shortRangeId")); + set.set("shortRangeSkillChance", parseInteger(attrs, "shortRangeChance")); + set.set("longRangeSkillId", parseInteger(attrs, "longRangeId")); + set.set("longRangeSkillChance", parseInteger(attrs, "longRangeChance")); + } + case "clanlist" -> { + for (Node clanListNode = aiNode.getFirstChild(); clanListNode != null; clanListNode = clanListNode.getNextSibling()) { + attrs = clanListNode.getAttributes(); + switch (clanListNode.getNodeName().toLowerCase()) { + case "clan" -> { + if (clans == null) { + clans = new HashSet<>(1); + } + clans.add(getOrCreateClanId(clanListNode.getTextContent())); + } + case "ignorenpcid" -> { + if (ignoreClanNpcIds == null) { + ignoreClanNpcIds = new HashSet<>(1); + } + ignoreClanNpcIds.add(Integer.parseInt(clanListNode.getTextContent())); + } + } + } + } + } + } + } + case "droplists" -> { + for (Node dropListsNode = npcNode.getFirstChild(); dropListsNode != null; dropListsNode = dropListsNode.getNextSibling()) { + DropListScope dropListScope = null; + + try { + dropListScope = Enum.valueOf(DropListScope.class, dropListsNode.getNodeName().toUpperCase()); + } catch (Exception e) { + } + + if (dropListScope != null) { + if (dropLists == null) { + dropLists = new EnumMap<>(DropListScope.class); + } + + List dropList = new ArrayList<>(); + parseDropList(f, dropListsNode, dropListScope, dropList); + dropLists.put(dropListScope, Collections.unmodifiableList(dropList)); + } + } + } + case "collision" -> { + for (Node collisionNode = npcNode.getFirstChild(); collisionNode != null; collisionNode = collisionNode.getNextSibling()) { + attrs = collisionNode.getAttributes(); + switch (collisionNode.getNodeName().toLowerCase()) { + case "radius" -> { + set.set("collisionRadius", parseDouble(attrs, "normal")); + set.set("collisionRadiusGrown", parseDouble(attrs, "grown")); + } + case "height" -> { + set.set("collisionHeight", parseDouble(attrs, "normal")); + set.set("collisionHeightGrown", parseDouble(attrs, "grown")); + } + } + } + } + } + } + + L2NpcTemplate template = _npcs.get(npcId); + if (template == null) { + template = new L2NpcTemplate(set); + _npcs.put(template.getId(), template); + } else { + template.set(set); + } + + if (_minionData._tempMinions.containsKey(npcId)) { + if (parameters == null) { + parameters = new HashMap<>(); + } + parameters.putIfAbsent("Privates", _minionData._tempMinions.get(npcId)); + } + + if (parameters != null) { + // Using unmodifiable map parameters of template are not meant to be changed at runtime. + template.setParameters(new StatsSet(Collections.unmodifiableMap(parameters))); + } else { + template.setParameters(StatsSet.EMPTY_STATSET); + } + + if (skills != null) { + Map> aiSkillLists = null; + for (Skill skill : skills.values()) { + if (skill.isPassive()) { + continue; + } + + if (aiSkillLists == null) { + aiSkillLists = new EnumMap<>(AISkillScope.class); + } + + final List aiSkillScopes = new ArrayList<>(); + final AISkillScope shortOrLongRangeScope = skill.getCastRange() <= 150 ? AISkillScope.SHORT_RANGE : AISkillScope.LONG_RANGE; + if (skill.isSuicideAttack()) { + aiSkillScopes.add(AISkillScope.SUICIDE); + } else { + aiSkillScopes.add(AISkillScope.GENERAL); + + if (skill.isContinuous()) { + if (!skill.isDebuff()) { + aiSkillScopes.add(AISkillScope.BUFF); + } else { + aiSkillScopes.add(AISkillScope.DEBUFF); + aiSkillScopes.add(AISkillScope.COT); + aiSkillScopes.add(shortOrLongRangeScope); + } + } else { + if (skill.hasEffectType(L2EffectType.DISPEL)) { + aiSkillScopes.add(AISkillScope.NEGATIVE); + aiSkillScopes.add(shortOrLongRangeScope); + } else if (skill.hasEffectType(L2EffectType.HP)) { + aiSkillScopes.add(AISkillScope.HEAL); + } else if (skill.hasEffectType(L2EffectType.PHYSICAL_ATTACK, L2EffectType.MAGICAL_ATTACK, L2EffectType.HP_DRAIN)) { + aiSkillScopes.add(AISkillScope.ATTACK); + aiSkillScopes.add(AISkillScope.UNIVERSAL); + aiSkillScopes.add(shortOrLongRangeScope); + } else if (skill.hasEffectType(L2EffectType.SLEEP)) { + aiSkillScopes.add(AISkillScope.IMMOBILIZE); + } else if (skill.hasEffectType(L2EffectType.STUN, L2EffectType.ROOT)) { + aiSkillScopes.add(AISkillScope.IMMOBILIZE); + aiSkillScopes.add(shortOrLongRangeScope); + } else if (skill.hasEffectType(L2EffectType.MUTE, L2EffectType.FEAR)) { + aiSkillScopes.add(AISkillScope.COT); + aiSkillScopes.add(shortOrLongRangeScope); + } else if (skill.hasEffectType(L2EffectType.DMG_OVER_TIME)) { + aiSkillScopes.add(shortOrLongRangeScope); + } else if (skill.hasEffectType(L2EffectType.RESURRECTION)) { + aiSkillScopes.add(AISkillScope.RES); + } else { + aiSkillScopes.add(AISkillScope.UNIVERSAL); + } + } + } + + for (AISkillScope aiSkillScope : aiSkillScopes) { + List aiSkills = aiSkillLists.computeIfAbsent(aiSkillScope, _ -> new ArrayList<>()); + aiSkills.add(skill); + } + } + + template.setSkills(skills); + template.setAISkillLists(aiSkillLists); + } else { + template.setSkills(null); + template.setAISkillLists(null); + } + + template.setClans(clans); + template.setIgnoreClanNpcIds(ignoreClanNpcIds); + + template.setDropLists(dropLists); + } + } + } + } + } + + private void parseDropList(File f, Node dropListNode, DropListScope dropListScope, List drops) { + for (Node dropNode = dropListNode.getFirstChild(); dropNode != null; dropNode = dropNode.getNextSibling()) { + NamedNodeMap attrs = dropNode.getAttributes(); + if ("group".equals(dropNode.getNodeName().toLowerCase())) { + GroupedGeneralDropItem dropItem = dropListScope.newGroupedDropItem(parseDouble(attrs, "chance")); + List groupedDropList = new ArrayList<>(2); + for (Node groupNode = dropNode.getFirstChild(); groupNode != null; groupNode = groupNode.getNextSibling()) { + parseDropListItem(groupNode, dropListScope, groupedDropList); + } + + List items = new ArrayList<>(groupedDropList.size()); + for (IDropItem item : groupedDropList) { + if (item instanceof GeneralDropItem) { + items.add((GeneralDropItem) item); + } else { + LOG.warn("{} grouped general drop item supports only general drop item.", f); + } + } + dropItem.setItems(items); + + drops.add(dropItem); + } else { + parseDropListItem(dropNode, dropListScope, drops); + } + } + } + + private void parseDropListItem(Node dropListItem, DropListScope dropListScope, List drops) { + NamedNodeMap attrs = dropListItem.getAttributes(); + if ("item".equals(dropListItem.getNodeName().toLowerCase())) { + final IDropItem dropItem = dropListScope.newDropItem(parseInteger(attrs, "id"), parseLong(attrs, "min"), parseLong(attrs, "max"), parseDouble(attrs, "chance")); + if (dropItem != null) { + drops.add(dropItem); + } + } + } + + /** + * Gets or creates a clan id if it doesnt exists. + * @param clanName the clan name to get or create its id + * @return the clan id for the given clan name + */ + private int getOrCreateClanId(String clanName) { + Integer id = _clans.get(clanName.toUpperCase()); + if (id == null) { + id = _clans.size(); + _clans.put(clanName.toUpperCase(), id); + } + return id; + } + + /** + * Gets the clan id + * @param clanName the clan name to get its id + * @return the clan id for the given clan name if it exists, -1 otherwise + */ + public int getClanId(String clanName) { + Integer id = _clans.get(clanName.toUpperCase()); + return id != null ? id : -1; + } + + /** + * Gets the template. + * @param id the template Id to get. + * @return the template for the given id. + */ + public L2NpcTemplate getTemplate(int id) { + return _npcs.get(id); + } + + /** + * Gets the template by name. + * @param name of the template to get. + * @return the template for the given name. + */ + public L2NpcTemplate getTemplateByName(String name) { + for (L2NpcTemplate npcTemplate : _npcs.values()) { + if (npcTemplate.getName().equalsIgnoreCase(name)) { + return npcTemplate; + } + } + return null; + } + + /** + * Gets all templates matching the filter. + * @param filter + * @return the template list for the given filter + */ + public List getTemplates(Predicate filter) { + return _npcs.values() + .stream() + .filter(filter) + .toList(); + } + + /** + * Gets the all of level. + * @param lvls of all the templates to get. + * @return the template list for the given level. + */ + public List getAllOfLevel(int... lvls) { + return getTemplates(template -> Util.contains(lvls, template.getLevel())); + } + + /** + * Gets the all monsters of level. + * @param lvls of all the monster templates to get. + * @return the template list for the given level. + */ + public List getAllMonstersOfLevel(int... lvls) { + return getTemplates(template -> Util.contains(lvls, template.getLevel()) && template.isType("L2Monster")); + } + + /** + * Gets the all npc starting with. + * @param text of all the NPC templates which its name start with. + * @return the template list for the given letter. + */ + public List getAllNpcStartingWith(String text) { + return getTemplates(template -> template.isType("L2Npc") && template.getName().startsWith(text)); + } + + /** + * Gets the all npc of class type. + * @param classTypes of all the templates to get. + * @return the template list for the given class type. + */ + public List getAllNpcOfClassType(String... classTypes) { + return getTemplates(template -> Util.contains(classTypes, template.getType(), true)); + } + + /** + * This class handles minions from Spawn System
    + * Once Spawn System gets reworked delete this class
    + * @author Zealar + */ + private static class MinionData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(MinionData.class); + + public final Map> _tempMinions = new HashMap<>(); + + protected MinionData() { + load(); + } + + @Override + public void load() { + _tempMinions.clear(); + parseDatapackFile("data/minionData.xml"); + LOG.info("Loaded {} minions data.", _tempMinions.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equals(node.getNodeName())) { + for (Node listNode = node.getFirstChild(); listNode != null; listNode = listNode.getNextSibling()) { + if ("npc".equals(listNode.getNodeName())) { + final List minions = new ArrayList<>(1); + NamedNodeMap attrs = listNode.getAttributes(); + int id = parseInteger(attrs, "id"); + for (Node npcNode = listNode.getFirstChild(); npcNode != null; npcNode = npcNode.getNextSibling()) { + if ("minion".equals(npcNode.getNodeName())) { + attrs = npcNode.getAttributes(); + minions.add(new MinionHolder(parseInteger(attrs, "id"), parseInteger(attrs, "count"), parseInteger(attrs, "respawnTime"), 0)); + } + } + _tempMinions.put(id, minions); + } + } + } + } + } + } + + public static NpcData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final NpcData INSTANCE = new NpcData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/OptionData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/OptionData.java new file mode 100644 index 00000000..0bf4b6f2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/OptionData.java @@ -0,0 +1,113 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.options.Options; +import com.l2jserver.gameserver.model.options.OptionsSkillHolder; +import com.l2jserver.gameserver.model.options.OptionsSkillType; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.functions.FuncTemplate; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Item Option data. + * @author UnAfraid + */ +public class OptionData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(OptionData.class); + + private final Map _optionData = new HashMap<>(); + + protected OptionData() { + load(); + } + + @Override + public synchronized void load() { + _optionData.clear(); + parseDatapackDirectory("data/stats/options", false); + LOG.info("Loaded {} item options.", _optionData.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("option".equalsIgnoreCase(d.getNodeName())) { + final int id = parseInteger(d.getAttributes(), "id"); + final Options op = new Options(id); + + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + switch (cd.getNodeName()) { + case "for" -> { + for (Node fd = cd.getFirstChild(); fd != null; fd = fd.getNextSibling()) { + switch (fd.getNodeName()) { + case "add", "sub", "mul", "div", "set", "share", "enchant", "enchanthp" -> parseFuncs(fd.getAttributes(), fd.getNodeName(), op); + } + } + } + case "active_skill" -> op.setActiveSkill(new SkillHolder(parseInteger(cd.getAttributes(), "id"), parseInteger(cd.getAttributes(), "level"))); + case "passive_skill" -> op.setPassiveSkill(new SkillHolder(parseInteger(cd.getAttributes(), "id"), parseInteger(cd.getAttributes(), "level"))); + case "attack_skill" -> op.addActivationSkill(new OptionsSkillHolder(parseInteger(cd.getAttributes(), "id"), parseInteger(cd.getAttributes(), "level"), parseDouble(cd.getAttributes(), "chance"), OptionsSkillType.ATTACK)); + case "magic_skill" -> op.addActivationSkill(new OptionsSkillHolder(parseInteger(cd.getAttributes(), "id"), parseInteger(cd.getAttributes(), "level"), parseDouble(cd.getAttributes(), "chance"), OptionsSkillType.MAGIC)); + case "critical_skill" -> op.addActivationSkill(new OptionsSkillHolder(parseInteger(cd.getAttributes(), "id"), parseInteger(cd.getAttributes(), "level"), parseDouble(cd.getAttributes(), "chance"), OptionsSkillType.CRITICAL)); + } + } + _optionData.put(op.getId(), op); + } + } + } + } + } + + private void parseFuncs(NamedNodeMap attrs, String functionName, Options op) { + Stats stat = Stats.valueOfXml(parseString(attrs, "stat")); + double val = parseDouble(attrs, "val"); + int order = -1; + final Node orderNode = attrs.getNamedItem("order"); + if (orderNode != null) { + order = Integer.parseInt(orderNode.getNodeValue()); + } + op.addFunc(new FuncTemplate(null, null, functionName, order, stat, val)); + } + + public Options getOptions(int id) { + return _optionData.get(id); + } + + public static OptionData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final OptionData INSTANCE = new OptionData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/PetDataTable.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PetDataTable.java new file mode 100644 index 00000000..051e22e3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PetDataTable.java @@ -0,0 +1,192 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.enums.MountType; +import com.l2jserver.gameserver.model.L2PetData; +import com.l2jserver.gameserver.model.L2PetLevelData; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class parse and hold all pet parameters.
    + * TODO: load and use all pet parameters. + * @author Zoey76 + */ +public final class PetDataTable implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(PetDataTable.class); + + private final Map _pets = new HashMap<>(); + + protected PetDataTable() { + load(); + } + + @Override + public void load() { + _pets.clear(); + parseDatapackDirectory("data/stats/pets", false); + LOG.info("Loaded {} Pets.", _pets.size()); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + Node n = doc.getFirstChild(); + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if (d.getNodeName().equals("pet")) { + int npcId = parseInteger(d.getAttributes(), "id"); + int itemId = parseInteger(d.getAttributes(), "itemId"); + // index ignored for now + L2PetData data = new L2PetData(npcId, itemId); + for (Node p = d.getFirstChild(); p != null; p = p.getNextSibling()) { + if (p.getNodeName().equals("set")) { + attrs = p.getAttributes(); + String type = attrs.getNamedItem("name").getNodeValue(); + if ("food".equals(type)) { + for (String foodId : attrs.getNamedItem("val").getNodeValue().split(";")) { + data.addFood(Integer.valueOf(foodId)); + } + } else if ("hungry_limit".equals(type)) { + data.setHungryLimit(parseInteger(attrs, "val")); + } else if ("sync_level".equals(type)) { + data.setSyncLevel(parseInteger(attrs, "val") == 1); + } + // evolve ignored + } else if (p.getNodeName().equals("skills")) { + for (Node s = p.getFirstChild(); s != null; s = s.getNextSibling()) { + if (s.getNodeName().equals("skill")) { + attrs = s.getAttributes(); + data.addNewSkill(parseInteger(attrs, "skillId"), parseInteger(attrs, "skillLvl"), parseInteger(attrs, "minLvl")); + } + } + } else if (p.getNodeName().equals("stats")) { + for (Node s = p.getFirstChild(); s != null; s = s.getNextSibling()) { + if (s.getNodeName().equals("stat")) { + final int level = Integer.parseInt(s.getAttributes().getNamedItem("level").getNodeValue()); + final StatsSet set = new StatsSet(); + for (Node bean = s.getFirstChild(); bean != null; bean = bean.getNextSibling()) { + if (bean.getNodeName().equals("set")) { + attrs = bean.getAttributes(); + if (attrs.getNamedItem("name").getNodeValue().equals("speed_on_ride")) { + set.set("walkSpeedOnRide", attrs.getNamedItem("walk").getNodeValue()); + set.set("runSpeedOnRide", attrs.getNamedItem("run").getNodeValue()); + set.set("slowSwimSpeedOnRide", attrs.getNamedItem("slowSwim").getNodeValue()); + set.set("fastSwimSpeedOnRide", attrs.getNamedItem("fastSwim").getNodeValue()); + if (attrs.getNamedItem("slowFly") != null) { + set.set("slowFlySpeedOnRide", attrs.getNamedItem("slowFly").getNodeValue()); + } + if (attrs.getNamedItem("fastFly") != null) { + set.set("fastFlySpeedOnRide", attrs.getNamedItem("fastFly").getNodeValue()); + } + } else { + set.set(attrs.getNamedItem("name").getNodeValue(), attrs.getNamedItem("val").getNodeValue()); + } + } + } + data.addNewStat(level, new L2PetLevelData(set)); + } + } + } + } + _pets.put(npcId, data); + } + } + } + + public L2PetData getPetDataByItemId(int itemId) { + for (L2PetData data : _pets.values()) { + if (data.getItemId() == itemId) { + return data; + } + } + return null; + } + + /** + * Gets the pet level data. + * @param petId the pet Id. + * @param petLevel the pet level. + * @return the pet's parameters for the given Id and level. + */ + public L2PetLevelData getPetLevelData(int petId, int petLevel) { + final L2PetData pd = getPetData(petId); + if (pd != null) { + return pd.getPetLevelData(petLevel); + } + return null; + } + + /** + * Gets the pet data. + * @param petId the pet Id. + * @return the pet data + */ + public L2PetData getPetData(int petId) { + if (!_pets.containsKey(petId)) { + LOG.warn("Missing pet data for NPC Id {}!", petId); + } + return _pets.get(petId); + } + + /** + * Gets the pet min level. + * @param petId the pet Id. + * @return the pet min level + */ + public int getPetMinLevel(int petId) { + return _pets.get(petId).getMinLevel(); + } + + /** + * Gets the pet items by npc. + * @param npcId the NPC ID to get its summoning item + * @return summoning item for the given NPC ID + */ + public int getPetItemsByNpc(int npcId) { + return _pets.get(npcId).getItemId(); + } + + /** + * Checks if is mountable. + * @param npcId the NPC Id to verify. + * @return {@code true} if the given Id is from a mountable pet, {@code false} otherwise. + */ + public static boolean isMountable(int npcId) { + return MountType.findByNpcId(npcId) != MountType.NONE; + } + + public static PetDataTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PetDataTable INSTANCE = new PetDataTable(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerCreationPointData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerCreationPointData.java new file mode 100644 index 00000000..a10ef79c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerCreationPointData.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * @author Michael + */ +public class PlayerCreationPointData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(PlayerCreationPointData.class); + + private final Map _creationPointData = new HashMap<>(); + + protected PlayerCreationPointData() { + load(); + } + + @Override + public void load() { + _creationPointData.clear(); + parseDatapackFile("data/stats/chars/pcCreationPoints.xml"); + LOG.info("Loaded {} character creation points.", _creationPointData.values().stream().mapToInt(array -> array.length).sum()); + } + + /** + * @return random Location of created character spawn. + */ + public Location getCreationPoint(ClassId classId) { + return Rnd.randomElement(_creationPointData.get(classId)); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("startpoints".equalsIgnoreCase(d.getNodeName())) { + List creationPoints = new ArrayList<>(); + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("spawn".equalsIgnoreCase(c.getNodeName())) { + attrs = c.getAttributes(); + creationPoints.add(new Location(parseInteger(attrs, "x"), parseInteger(attrs, "y"), parseInteger(attrs, "z"))); + } else if ("classid".equalsIgnoreCase(c.getNodeName())) { + _creationPointData.put(ClassId.getClassId(Integer.parseInt(c.getTextContent())), creationPoints.toArray(new Location[0])); + } + } + } + } + } + } + } + + public static PlayerCreationPointData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PlayerCreationPointData INSTANCE = new PlayerCreationPointData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerTemplateData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerTemplateData.java new file mode 100644 index 00000000..f60316ed --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerTemplateData.java @@ -0,0 +1,148 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.templates.L2PcTemplate; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Loads player's base stats. + * @author Forsaiken + * @author Zoey76 + * @author GKR + */ +public final class PlayerTemplateData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(PlayerTemplateData.class); + + private final Map _playerTemplates = new HashMap<>(); + + private int _dataCount = 0; + + protected PlayerTemplateData() { + load(); + } + + @Override + public void load() { + _playerTemplates.clear(); + parseDatapackDirectory("data/stats/chars/baseStats", false); + LOG.info("Loaded {} character templates.", _playerTemplates.size()); + LOG.info("Loaded {} level up gain records.", _dataCount); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + int classId = 0; + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("classId".equalsIgnoreCase(d.getNodeName())) { + classId = Integer.parseInt(d.getTextContent()); + } else if ("staticData".equalsIgnoreCase(d.getNodeName())) { + StatsSet set = new StatsSet(); + set.set("classId", classId); + for (Node nd = d.getFirstChild(); nd != null; nd = nd.getNextSibling()) { + // Skip odd nodes + if (nd.getNodeName().equals("#text")) { + continue; + } + + if (nd.getChildNodes().getLength() > 1) { + for (Node cnd = nd.getFirstChild(); cnd != null; cnd = cnd.getNextSibling()) { + // use L2CharTemplate(superclass) fields for male collision height and collision radius + if (nd.getNodeName().equalsIgnoreCase("collisionMale")) { + if (cnd.getNodeName().equalsIgnoreCase("radius")) { + set.set("collisionRadius", cnd.getTextContent()); + } else if (cnd.getNodeName().equalsIgnoreCase("height")) { + set.set("collisionHeight", cnd.getTextContent()); + } + } + if ("walk".equalsIgnoreCase(cnd.getNodeName())) { + set.set("baseWalkSpd", cnd.getTextContent()); + } else if ("run".equalsIgnoreCase(cnd.getNodeName())) { + set.set("baseRunSpd", cnd.getTextContent()); + } else if ("slowSwim".equals(cnd.getNodeName())) { + set.set("baseSwimWalkSpd", cnd.getTextContent()); + } else if ("fastSwim".equals(cnd.getNodeName())) { + set.set("baseSwimRunSpd", cnd.getTextContent()); + } else if (!cnd.getNodeName().equals("#text")) { + set.set((nd.getNodeName() + cnd.getNodeName()), cnd.getTextContent()); + } + } + } else { + set.set(nd.getNodeName(), nd.getTextContent()); + } + } + // calculate total pdef and mdef from parts + set.set("basePDef", (set.getInt("basePDefchest", 0) + set.getInt("basePDeflegs", 0) + set.getInt("basePDefhead", 0) + set.getInt("basePDeffeet", 0) + set.getInt("basePDefgloves", 0) + set.getInt("basePDefunderwear", 0) + set.getInt("basePDefcloak", 0))); + set.set("baseMDef", (set.getInt("baseMDefrear", 0) + set.getInt("baseMDeflear", 0) + set.getInt("baseMDefrfinger", 0) + set.getInt("baseMDefrfinger", 0) + set.getInt("baseMDefneck", 0))); + + _playerTemplates.put(ClassId.getClassId(classId), new L2PcTemplate(set)); + } else if ("lvlUpgainData".equalsIgnoreCase(d.getNodeName())) { + for (Node lvlNode = d.getFirstChild(); lvlNode != null; lvlNode = lvlNode.getNextSibling()) { + if ("level".equalsIgnoreCase(lvlNode.getNodeName())) { + attrs = lvlNode.getAttributes(); + int level = parseInteger(attrs, "val"); + + for (Node valNode = lvlNode.getFirstChild(); valNode != null; valNode = valNode.getNextSibling()) { + String nodeName = valNode.getNodeName(); + + if ((nodeName.startsWith("hp") || nodeName.startsWith("mp") || nodeName.startsWith("cp")) && _playerTemplates.containsKey(ClassId.getClassId(classId))) { + _playerTemplates.get(ClassId.getClassId(classId)).setUpgainValue(nodeName, level, Double.parseDouble(valNode.getTextContent())); + _dataCount++; + } + } + } + } + } + } + } + } + } + + public L2PcTemplate getTemplate(ClassId classId) { + return _playerTemplates.get(classId); + } + + public L2PcTemplate getTemplate(int classId) { + return _playerTemplates.get(ClassId.getClassId(classId)); + } + + public static PlayerTemplateData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PlayerTemplateData INSTANCE = new PlayerTemplateData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerXpPercentLostData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerXpPercentLostData.java new file mode 100644 index 00000000..8f9e6f21 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/PlayerXpPercentLostData.java @@ -0,0 +1,82 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.Arrays; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class holds the Player Xp Percent Lost Data for each level for players. + * @author Zealar + */ +public final class PlayerXpPercentLostData implements IXmlReader { + + private final double[] _playerXpPercentLost = new double[character().getMaxPlayerLevel() + 1]; + + protected PlayerXpPercentLostData() { + Arrays.fill(_playerXpPercentLost, 1.); + load(); + } + + @Override + public void load() { + parseDatapackFile("data/stats/chars/playerXpPercentLost.xml"); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("xpLost".equalsIgnoreCase(d.getNodeName())) { + NamedNodeMap attrs = d.getAttributes(); + _playerXpPercentLost[parseInteger(attrs, "level")] = parseDouble(attrs, "val"); + } + } + } + } + } + + public double getXpPercent(final int level) { + if (level > character().getMaxPlayerLevel()) { + LOG.warn("Require to high level inside PlayerXpPercentLostData ({})", level); + return _playerXpPercentLost[character().getMaxPlayerLevel()]; + } + return _playerXpPercentLost[level]; + } + + /** + * Gets the single instance of PlayerXpPercentLostData. + * @return single instance of PlayerXpPercentLostData. + */ + public static PlayerXpPercentLostData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final PlayerXpPercentLostData _instance = new PlayerXpPercentLostData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/RecipeData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/RecipeData.java new file mode 100644 index 00000000..45d8f842 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/RecipeData.java @@ -0,0 +1,232 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.L2RecipeInstance; +import com.l2jserver.gameserver.model.L2RecipeList; +import com.l2jserver.gameserver.model.L2RecipeStatInstance; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Recipe data. + * @author Zoey76 + */ +public class RecipeData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(RecipeData.class); + + private final Map _recipes = new HashMap<>(); + + protected RecipeData() { + load(); + } + + @Override + public void load() { + _recipes.clear(); + parseDatapackFile("data/recipes.xml"); + LOG.info("Loaded {} recipes.", _recipes.size()); + } + + @Override + public void parseDocument(Document doc) { + // TODO: Cleanup checks enforced by XSD. + final List recipePartList = new ArrayList<>(); + final List recipeStatUseList = new ArrayList<>(); + final List recipeAltStatChangeList = new ArrayList<>(); + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + RECIPES_FILE: + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("item".equalsIgnoreCase(d.getNodeName())) { + recipePartList.clear(); + recipeStatUseList.clear(); + recipeAltStatChangeList.clear(); + NamedNodeMap attrs = d.getAttributes(); + boolean haveRare = false; + StatsSet set = new StatsSet(); + + Node att = attrs.getNamedItem("id"); + if (att == null) { + LOG.error("Missing id for recipe item, skipping!"); + continue; + } + int id = Integer.parseInt(att.getNodeValue()); + set.set("id", id); + + att = attrs.getNamedItem("recipeId"); + if (att == null) { + LOG.error("Missing recipeId for recipe item ID: {}, skipping!", id); + continue; + } + set.set("recipeId", Integer.parseInt(att.getNodeValue())); + + att = attrs.getNamedItem("name"); + if (att == null) { + LOG.error("Missing name for recipe item ID: {}, skipping!", id); + continue; + } + set.set("recipeName", att.getNodeValue()); + + att = attrs.getNamedItem("craftLevel"); + if (att == null) { + LOG.error("Missing level for recipe item ID: {}, skipping!", id); + continue; + } + set.set("craftLevel", Integer.parseInt(att.getNodeValue())); + + att = attrs.getNamedItem("type"); + if (att == null) { + LOG.error("Missing type for recipe item ID: {}, skipping!", id); + continue; + } + set.set("isDwarvenRecipe", att.getNodeValue().equalsIgnoreCase("dwarven")); + + att = attrs.getNamedItem("successRate"); + if (att == null) { + LOG.error("Missing successRate for recipe item ID: {}, skipping!", id); + continue; + } + set.set("successRate", Integer.parseInt(att.getNodeValue())); + + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("statUse".equalsIgnoreCase(c.getNodeName())) { + String statName = c.getAttributes().getNamedItem("name").getNodeValue(); + int value = Integer.parseInt(c.getAttributes().getNamedItem("value").getNodeValue()); + try { + recipeStatUseList.add(new L2RecipeStatInstance(statName, value)); + } catch (Exception e) { + LOG.error("Error in StatUse parameter for recipe item ID: {}, skipping!", id); + continue RECIPES_FILE; + } + } else if ("altStatChange".equalsIgnoreCase(c.getNodeName())) { + String statName = c.getAttributes().getNamedItem("name").getNodeValue(); + int value = Integer.parseInt(c.getAttributes().getNamedItem("value").getNodeValue()); + try { + recipeAltStatChangeList.add(new L2RecipeStatInstance(statName, value)); + } catch (Exception e) { + LOG.error("Error in AltStatChange parameter for recipe item ID: {}, skipping!", id); + continue RECIPES_FILE; + } + } else if ("ingredient".equalsIgnoreCase(c.getNodeName())) { + int ingId = Integer.parseInt(c.getAttributes().getNamedItem("id").getNodeValue()); + int ingCount = Integer.parseInt(c.getAttributes().getNamedItem("count").getNodeValue()); + recipePartList.add(new L2RecipeInstance(ingId, ingCount)); + } else if ("production".equalsIgnoreCase(c.getNodeName())) { + set.set("itemId", Integer.parseInt(c.getAttributes().getNamedItem("id").getNodeValue())); + set.set("count", Integer.parseInt(c.getAttributes().getNamedItem("count").getNodeValue())); + } else if ("productionRare".equalsIgnoreCase(c.getNodeName())) { + set.set("rareItemId", Integer.parseInt(c.getAttributes().getNamedItem("id").getNodeValue())); + set.set("rareCount", Integer.parseInt(c.getAttributes().getNamedItem("count").getNodeValue())); + set.set("rarity", Integer.parseInt(c.getAttributes().getNamedItem("rarity").getNodeValue())); + haveRare = true; + } + } + + L2RecipeList recipeList = new L2RecipeList(set, haveRare); + for (L2RecipeInstance recipePart : recipePartList) { + recipeList.addRecipe(recipePart); + } + for (L2RecipeStatInstance recipeStatUse : recipeStatUseList) { + recipeList.addStatUse(recipeStatUse); + } + for (L2RecipeStatInstance recipeAltStatChange : recipeAltStatChangeList) { + recipeList.addAltStatChange(recipeAltStatChange); + } + + _recipes.put(id, recipeList); + } + } + } + } + } + + /** + * Gets the recipe list. + * @param listId the list id + * @return the recipe list + */ + public L2RecipeList getRecipeList(int listId) { + return _recipes.get(listId); + } + + /** + * Gets the recipe by item id. + * @param itemId the item id + * @return the recipe by item id + */ + public L2RecipeList getRecipeByItemId(int itemId) { + for (L2RecipeList find : _recipes.values()) { + if (find.getRecipeId() == itemId) { + return find; + } + } + return null; + } + + /** + * Gets the all item ids. + * @return the all item ids + */ + public int[] getAllItemIds() { + int[] idList = new int[_recipes.size()]; + int i = 0; + for (L2RecipeList rec : _recipes.values()) { + idList[i++] = rec.getRecipeId(); + } + return idList; + } + + /** + * Gets the valid recipe list. + * @param player the player + * @param id the recipe list id + * @return the valid recipe list + */ + public L2RecipeList getValidRecipeList(L2PcInstance player, int id) { + L2RecipeList recipeList = _recipes.get(id); + if ((recipeList == null) || (recipeList.getRecipes().length == 0)) { + player.sendMessage(getClass().getSimpleName() + ": No recipe for: " + id); + player.isInCraftMode(false); + return null; + } + return recipeList; + } + + public static RecipeData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final RecipeData INSTANCE = new RecipeData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/SecondaryAuthData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SecondaryAuthData.java new file mode 100644 index 00000000..5fb6c7fc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SecondaryAuthData.java @@ -0,0 +1,121 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Secondary Auth data. + * @author NosBit + */ +public class SecondaryAuthData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(SecondaryAuthData.class); + + private final Set _forbiddenPasswords = new HashSet<>(); + + private boolean _enabled = false; + + private int _maxAttempts = 5; + + private int _banTime = 480; + + private String _recoveryLink = ""; + + protected SecondaryAuthData() { + load(); + } + + @Override + public synchronized void load() { + _forbiddenPasswords.clear(); + parseFile(new File("config/SecondaryAuth.xml")); + LOG.info("Loaded {} forbidden passwords.", _forbiddenPasswords.size()); + } + + @Override + public void parseDocument(Document doc) { + try { + for (Node node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equalsIgnoreCase(node.getNodeName())) { + for (Node list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { + if ("enabled".equalsIgnoreCase(list_node.getNodeName())) { + _enabled = Boolean.parseBoolean(list_node.getTextContent()); + } else if ("maxAttempts".equalsIgnoreCase(list_node.getNodeName())) { + _maxAttempts = Integer.parseInt(list_node.getTextContent()); + } else if ("banTime".equalsIgnoreCase(list_node.getNodeName())) { + _banTime = Integer.parseInt(list_node.getTextContent()); + } else if ("recoveryLink".equalsIgnoreCase(list_node.getNodeName())) { + _recoveryLink = list_node.getTextContent(); + } else if ("forbiddenPasswords".equalsIgnoreCase(list_node.getNodeName())) { + for (Node forbiddenPasswords_node = list_node.getFirstChild(); forbiddenPasswords_node != null; forbiddenPasswords_node = forbiddenPasswords_node.getNextSibling()) { + if ("password".equalsIgnoreCase(forbiddenPasswords_node.getNodeName())) { + _forbiddenPasswords.add(forbiddenPasswords_node.getTextContent()); + } + } + } + } + } + } + } catch (Exception ex) { + LOG.warn("Failed to load secondary auth data from xml.", ex); + } + } + + public boolean isEnabled() { + return _enabled; + } + + public int getMaxAttempts() { + return _maxAttempts; + } + + public int getBanTime() { + return _banTime; + } + + public String getRecoveryLink() { + return _recoveryLink; + } + + public Set getForbiddenPasswords() { + return _forbiddenPasswords; + } + + public boolean isForbiddenPassword(String password) { + return _forbiddenPasswords.contains(password); + } + + public static SecondaryAuthData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SecondaryAuthData INSTANCE = new SecondaryAuthData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/SiegeScheduleData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SiegeScheduleData.java new file mode 100644 index 00000000..aa2cac5d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SiegeScheduleData.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.io.File; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.SiegeScheduleDate; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.util.IXmlReader; +import com.l2jserver.gameserver.util.Util; + +/** + * Siege Schedule data. + * @author UnAfraid + */ +public class SiegeScheduleData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(SiegeScheduleData.class); + + private final List _scheduleData = new ArrayList<>(); + + protected SiegeScheduleData() { + load(); + } + + @Override + public synchronized void load() { + _scheduleData.clear(); + parseFile(new File("config/SiegeSchedule.xml")); + LOG.info("Loaded {} siege schedulers.", _scheduleData.size()); + + if (_scheduleData.isEmpty()) { + _scheduleData.add(new SiegeScheduleDate(new StatsSet())); + LOG.info("Emergency Loaded {} default siege schedulers.", _scheduleData.size()); + } + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node cd = n.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("schedule".equals(cd.getNodeName())) { + final StatsSet set = new StatsSet(); + final NamedNodeMap attrs = cd.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node node = attrs.item(i); + String key = node.getNodeName(); + String val = node.getNodeValue(); + if ("day".equals(key)) { + if (!Util.isDigit(val)) { + val = Integer.toString(getValueForField(val)); + } + } + set.set(key, val); + } + _scheduleData.add(new SiegeScheduleDate(set)); + } + } + } + } + } + + private int getValueForField(String field) { + try { + return Calendar.class.getField(field).getInt(Calendar.class); + } catch (Exception ex) { + LOG.warn("Unable to get value!", ex); + return -1; + } + } + + public List getScheduleDates() { + return _scheduleData; + } + + public static SiegeScheduleData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SiegeScheduleData INSTANCE = new SiegeScheduleData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillLearnData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillLearnData.java new file mode 100644 index 00000000..cf4c8322 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillLearnData.java @@ -0,0 +1,88 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; + +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Holds all skill learn data for all NPCs. + * @author xban1x + */ +public final class SkillLearnData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(SkillLearnData.class); + + private final Map> _skillLearn = new HashMap<>(); + + protected SkillLearnData() { + load(); + } + + @Override + public synchronized void load() { + _skillLearn.clear(); + parseDatapackFile("data/skillLearn.xml"); + LOG.info("Loaded {} skill learn data.", _skillLearn.size()); + } + + @Override + public void parseDocument(Document doc) { + for (var node = doc.getFirstChild(); node != null; node = node.getNextSibling()) { + if ("list".equalsIgnoreCase(node.getNodeName())) { + for (var list_node = node.getFirstChild(); list_node != null; list_node = list_node.getNextSibling()) { + if ("npc".equalsIgnoreCase(list_node.getNodeName())) { + final var classIds = new LinkedList(); + for (var c = list_node.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("classId".equalsIgnoreCase(c.getNodeName())) { + classIds.add(ClassId.getClassId(Integer.parseInt(c.getTextContent()))); + } + } + _skillLearn.put(parseInteger(list_node.getAttributes(), "id"), classIds); + } + } + } + } + } + + /** + * @param npcId + * @return {@link List} of {@link ClassId}'s that this npcId can teach. + */ + public List getSkillLearnData(int npcId) { + return _skillLearn.get(npcId); + } + + public static SkillLearnData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SkillLearnData INSTANCE = new SkillLearnData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillTreesData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillTreesData.java new file mode 100644 index 00000000..5a741f47 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/SkillTreesData.java @@ -0,0 +1,1063 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2SkillLearn; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.AcquireSkillType; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.base.SocialClass; +import com.l2jserver.gameserver.model.base.SubClass; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.holders.PlayerSkillHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.interfaces.ISkillsHolder; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class loads and manage the characters and pledges skills trees.
    + * Here can be found the following skill trees:
    + *
      + *
    • Class skill trees: player skill trees for each class.
    • + *
    • Transfer skill trees: player skill trees for each healer class.
    • + *
    • Collect skill tree: player skill tree for Gracia related skills.
    • + *
    • Fishing skill tree: player skill tree for fishing related skills.
    • + *
    • Transform skill tree: player skill tree for transformation related skills.
    • + *
    • Sub-Class skill tree: player skill tree for sub-class related skills.
    • + *
    • Noble skill tree: player skill tree for noblesse related skills.
    • + *
    • Hero skill tree: player skill tree for heroes related skills.
    • + *
    • GM skill tree: player skill tree for Game Master related skills.
    • + *
    • Common skill tree: custom skill tree for players, skills in this skill tree will be available for all players.
    • + *
    • Pledge skill tree: clan skill tree for main clan.
    • + *
    • Sub-Pledge skill tree: clan skill tree for sub-clans.
    • + *
    + * For easy customization of player class skill trees, the parent Id of each class is taken from the XML data, this means you can use a different class parent Id than in the normal game play, for example all 3rd class dagger users will have Treasure Hunter skills as 1st and 2nd class skills.
    + * For XML schema please refer to skillTrees.xsd in datapack in xsd folder and for parameters documentation refer to documentation.txt in skillTrees folder.
    + * @author Zoey76 + */ +public final class SkillTreesData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(SkillTreesData.class); + + // ClassId, Map of Skill Hash Code, L2SkillLearn + private final Map> _classSkillTrees = new LinkedHashMap<>(); + private final Map> _transferSkillTrees = new LinkedHashMap<>(); + // Skill Hash Code, L2SkillLearn + private final Map _collectSkillTree = new LinkedHashMap<>(); + private final Map _fishingSkillTree = new LinkedHashMap<>(); + private final Map _pledgeSkillTree = new LinkedHashMap<>(); + private final Map _subClassSkillTree = new LinkedHashMap<>(); + private final Map _subPledgeSkillTree = new LinkedHashMap<>(); + private final Map _transformSkillTree = new LinkedHashMap<>(); + private final Map _commonSkillTree = new LinkedHashMap<>(); + // Other skill trees + private final Map _nobleSkillTree = new LinkedHashMap<>(); + private final Map _heroSkillTree = new LinkedHashMap<>(); + private final Map _gameMasterSkillTree = new LinkedHashMap<>(); + private final Map _gameMasterAuraSkillTree = new LinkedHashMap<>(); + + // Checker, sorted arrays of hash codes + private Map _skillsByClassIdHashCodes; // Occupation skills + private Map _skillsByRaceHashCodes; // Race-specific Transformations + private int[] _allSkillsHashCodes; // Fishing, Collection, Transformations, Common Skills. + + private boolean _loading = true; + + /** Parent class IDs are read from XML and stored in this map, to allow easy customization. */ + private final Map _parentClassMap = new LinkedHashMap<>(); + + /** + * Instantiates a new skill trees data. + */ + protected SkillTreesData() { + load(); + } + + @Override + public void load() { + _loading = true; + _classSkillTrees.clear(); + _collectSkillTree.clear(); + _fishingSkillTree.clear(); + _pledgeSkillTree.clear(); + _subClassSkillTree.clear(); + _subPledgeSkillTree.clear(); + _transferSkillTrees.clear(); + _transformSkillTree.clear(); + _nobleSkillTree.clear(); + _heroSkillTree.clear(); + _gameMasterSkillTree.clear(); + _gameMasterAuraSkillTree.clear(); + + // Load files. + parseDatapackDirectory("data/skillTrees/", false); + + // Generate check arrays. + generateCheckArrays(); + + _loading = false; + + // Logs a report with skill trees info. + report(); + } + + /** + * Parse a skill tree file and store it into the correct skill tree. + */ + @Override + public void parseDocument(Document doc) { + int cId; + ClassId classId = null; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("skillTree".equalsIgnoreCase(d.getNodeName())) { + final Map classSkillTree = new HashMap<>(); + final Map trasferSkillTree = new HashMap<>(); + final String type = d.getAttributes().getNamedItem("type").getNodeValue(); + Node attr = d.getAttributes().getNamedItem("classId"); + if (attr != null) { + cId = Integer.parseInt(attr.getNodeValue()); + classId = ClassId.values()[cId]; + } else { + cId = -1; + } + + attr = d.getAttributes().getNamedItem("parentClassId"); + if (attr != null) { + final int parentClassId = Integer.parseInt(attr.getNodeValue()); + if ((cId > -1) && (cId != parentClassId) && (parentClassId > -1) && !_parentClassMap.containsKey(classId)) { + _parentClassMap.put(classId, ClassId.values()[parentClassId]); + } + } + + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("skill".equalsIgnoreCase(c.getNodeName())) { + final StatsSet learnSkillSet = new StatsSet(); + NamedNodeMap attrs = c.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + attr = attrs.item(i); + learnSkillSet.set(attr.getNodeName(), attr.getNodeValue()); + } + + final L2SkillLearn skillLearn = new L2SkillLearn(learnSkillSet); + for (Node b = c.getFirstChild(); b != null; b = b.getNextSibling()) { + attrs = b.getAttributes(); + switch (b.getNodeName()) { + case "item" -> skillLearn.addRequiredItem(new ItemHolder(parseInteger(attrs, "id"), parseInteger(attrs, "count"))); + case "preRequisiteSkill" -> skillLearn.addPreReqSkill(new SkillHolder(parseInteger(attrs, "id"), parseInteger(attrs, "lvl"))); + case "race" -> skillLearn.addRace(Race.valueOf(b.getTextContent())); + case "residenceId" -> skillLearn.addResidenceId(Integer.valueOf(b.getTextContent())); + case "socialClass" -> skillLearn.setSocialClass(Enum.valueOf(SocialClass.class, b.getTextContent())); + case "subClassConditions" -> skillLearn.addSubclassConditions(parseInteger(attrs, "slot"), parseInteger(attrs, "lvl")); + } + } + + final int skillHashCode = SkillData.getSkillHashCode(skillLearn.getSkillId(), skillLearn.getSkillLevel()); + switch (type) { + case "classSkillTree" -> { + if (cId != -1) { + classSkillTree.put(skillHashCode, skillLearn); + } else { + _commonSkillTree.put(skillHashCode, skillLearn); + } + } + case "transferSkillTree" -> trasferSkillTree.put(skillHashCode, skillLearn); + case "collectSkillTree" -> _collectSkillTree.put(skillHashCode, skillLearn); + case "fishingSkillTree" -> _fishingSkillTree.put(skillHashCode, skillLearn); + case "pledgeSkillTree" -> _pledgeSkillTree.put(skillHashCode, skillLearn); + case "subClassSkillTree" -> _subClassSkillTree.put(skillHashCode, skillLearn); + case "subPledgeSkillTree" -> _subPledgeSkillTree.put(skillHashCode, skillLearn); + case "transformSkillTree" -> _transformSkillTree.put(skillHashCode, skillLearn); + case "nobleSkillTree" -> _nobleSkillTree.put(skillHashCode, skillLearn); + case "heroSkillTree" -> _heroSkillTree.put(skillHashCode, skillLearn); + case "gameMasterSkillTree" -> _gameMasterSkillTree.put(skillHashCode, skillLearn); + case "gameMasterAuraSkillTree" -> _gameMasterAuraSkillTree.put(skillHashCode, skillLearn); + default -> LOG.warn("Unknown Skill Tree type: {}!", type); + } + } + } + + if (type.equals("transferSkillTree")) { + _transferSkillTrees.put(classId, trasferSkillTree); + } else if (type.equals("classSkillTree") && (cId > -1)) { + if (!_classSkillTrees.containsKey(classId)) { + _classSkillTrees.put(classId, classSkillTree); + } else { + _classSkillTrees.get(classId).putAll(classSkillTree); + } + } + } + } + } + } + } + + /** + * Method to get the complete skill tree for a given class id.
    + * Include all skills common to all classes.
    + * Includes all parent skill trees. + * @param classId the class skill tree Id + * @return the complete Class Skill Tree including skill trees from parent class for a given {@code classId} + */ + public Map getCompleteClassSkillTree(ClassId classId) { + // Add all skills that belong to all classes. + final Map skillTree = new LinkedHashMap<>(_commonSkillTree); + final LinkedList classSequence = new LinkedList<>(); + while (classId != null) { + classSequence.addFirst(classId); + classId = _parentClassMap.get(classId); + } + + for (ClassId cid : classSequence) { + final Map classSkillTree = _classSkillTrees.get(cid); + if (classSkillTree != null) { + skillTree.putAll(classSkillTree); + } + } + return skillTree; + } + + /** + * Gets the transfer skill tree.
    + * If new classes are implemented over 3rd class, we use a recursive call. + * @param classId the transfer skill tree Id + * @return the complete Transfer Skill Tree for a given {@code classId} + */ + public Map getTransferSkillTree(ClassId classId) { + if (classId.level() >= 3) { + return getTransferSkillTree(classId.getParent()); + } + return _transferSkillTrees.get(classId); + } + + /** + * Gets the common skill tree. + * @return the complete Common Skill Tree + */ + public Map getCommonSkillTree() { + return _commonSkillTree; + } + + /** + * Gets the collect skill tree. + * @return the complete Collect Skill Tree + */ + public Map getCollectSkillTree() { + return _collectSkillTree; + } + + /** + * Gets the fishing skill tree. + * @return the complete Fishing Skill Tree + */ + public Map getFishingSkillTree() { + return _fishingSkillTree; + } + + /** + * Gets the pledge skill tree. + * @return the complete Pledge Skill Tree + */ + public Map getPledgeSkillTree() { + return _pledgeSkillTree; + } + + /** + * Gets the sub class skill tree. + * @return the complete Sub-Class Skill Tree + */ + public Map getSubClassSkillTree() { + return _subClassSkillTree; + } + + /** + * Gets the sub pledge skill tree. + * @return the complete Sub-Pledge Skill Tree + */ + public Map getSubPledgeSkillTree() { + return _subPledgeSkillTree; + } + + /** + * Gets the transform skill tree. + * @return the complete Transform Skill Tree + */ + public Map getTransformSkillTree() { + return _transformSkillTree; + } + + /** + * Gets the noble skill tree. + * @return the complete Noble Skill Tree + */ + public Map getNobleSkillTree() { + final Map tree = new HashMap<>(); + final SkillData st = SkillData.getInstance(); + for (Entry e : _nobleSkillTree.entrySet()) { + tree.put(e.getKey(), st.getSkill(e.getValue().getSkillId(), e.getValue().getSkillLevel())); + } + return tree; + } + + /** + * Gets the hero skill tree. + * @return the complete Hero Skill Tree + */ + public Map getHeroSkillTree() { + final Map tree = new HashMap<>(); + final SkillData st = SkillData.getInstance(); + for (Entry e : _heroSkillTree.entrySet()) { + tree.put(e.getKey(), st.getSkill(e.getValue().getSkillId(), e.getValue().getSkillLevel())); + } + return tree; + } + + /** + * Gets the Game Master skill tree. + * @return the complete Game Master Skill Tree + */ + public Map getGMSkillTree() { + final Map tree = new HashMap<>(); + final SkillData st = SkillData.getInstance(); + for (Entry e : _gameMasterSkillTree.entrySet()) { + tree.put(e.getKey(), st.getSkill(e.getValue().getSkillId(), e.getValue().getSkillLevel())); + } + return tree; + } + + /** + * Gets the Game Master Aura skill tree. + * @return the complete Game Master Aura Skill Tree + */ + public Map getGMAuraSkillTree() { + final Map tree = new HashMap<>(); + final SkillData st = SkillData.getInstance(); + for (Entry e : _gameMasterAuraSkillTree.entrySet()) { + tree.put(e.getKey(), st.getSkill(e.getValue().getSkillId(), e.getValue().getSkillLevel())); + } + return tree; + } + + /** + * Gets the available skills. + * @param player the learning skill player + * @param classId the learning skill class Id + * @param includeByFs if {@code true} skills from Forgotten Scroll will be included + * @param includeAutoGet if {@code true} Auto-Get skills will be included + * @return all available skills for a given {@code player}, {@code classId}, {@code includeByFs} and {@code includeAutoGet} + */ + public List getAvailableSkills(L2PcInstance player, ClassId classId, boolean includeByFs, boolean includeAutoGet) { + return getAvailableSkills(player, classId, includeByFs, includeAutoGet, player); + } + + /** + * Gets the available skills. + * @param player the learning skill player + * @param classId the learning skill class Id + * @param includeByFs if {@code true} skills from Forgotten Scroll will be included + * @param includeAutoGet if {@code true} Auto-Get skills will be included + * @param holder + * @return all available skills for a given {@code player}, {@code classId}, {@code includeByFs} and {@code includeAutoGet} + */ + private List getAvailableSkills(L2PcInstance player, ClassId classId, boolean includeByFs, boolean includeAutoGet, ISkillsHolder holder) { + final List result = new ArrayList<>(); + final Map skills = getCompleteClassSkillTree(classId); + + if (skills.isEmpty()) { + // The Skill Tree for this class is undefined. + LOG.warn("{}: Skilltree for class {} is not defined!", getClass().getSimpleName(), classId); + return result; + } + + for (L2SkillLearn skill : skills.values()) { + if (((skill.getSkillId() == CommonSkill.DIVINE_INSPIRATION.getId()) && (!character().autoLearnDivineInspiration() && includeAutoGet) && !player.isGM())) { + continue; + } + + if (((includeAutoGet && skill.isAutoGet()) || skill.isLearnedByNpc() || (includeByFs && skill.isLearnedByFS())) && (player.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = holder.getKnownSkill(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() == (skill.getSkillLevel() - 1)) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + } + return result; + } + + public Collection getAllAvailableSkills(L2PcInstance player, ClassId classId, boolean includeByFs, boolean includeAutoGet) { + // Get available skills + PlayerSkillHolder holder = new PlayerSkillHolder(player); + List learnable = getAvailableSkills(player, classId, includeByFs, includeAutoGet, holder); + while (learnable.size() > 0) { + for (L2SkillLearn s : learnable) { + Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + holder.addSkill(sk); + } + + // Get new available skills, some skills depend of previous skills to be available. + learnable = getAvailableSkills(player, classId, includeByFs, includeAutoGet, holder); + } + return holder.getSkills().values(); + } + + /** + * Gets the available auto get skills. + * @param player the player requesting the Auto-Get skills + * @return all the available Auto-Get skills for a given {@code player} + */ + public List getAvailableAutoGetSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + final Map skills = getCompleteClassSkillTree(player.getClassId()); + if (skills.isEmpty()) { + // The Skill Tree for this class is undefined, so we return an empty list. + LOG.warn("{}: Skill Tree for class ID {} is not defined!", getClass().getSimpleName(), player.getClassId()); + return result; + } + + final Race race = player.getRace(); + for (L2SkillLearn skill : skills.values()) { + if (!skill.getRaces().isEmpty() && !skill.getRaces().contains(race)) { + continue; + } + + if (skill.isAutoGet() && (player.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = player.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() < skill.getSkillLevel()) { + result.add(skill); + } + } else { + result.add(skill); + } + } + } + return result; + } + + /** + * Dwarvens will get additional dwarven only fishing skills. + * @param player the player + * @return all the available Fishing skills for a given {@code player} + */ + public List getAvailableFishingSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + final Race playerRace = player.getRace(); + for (L2SkillLearn skill : _fishingSkillTree.values()) { + // If skill is Race specific and the player's race isn't allowed, skip it. + if (!skill.getRaces().isEmpty() && !skill.getRaces().contains(playerRace)) { + continue; + } + + if (skill.isLearnedByNpc() && (player.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = player.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() == (skill.getSkillLevel() - 1)) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + } + return result; + } + + /** + * Used in Gracia continent. + * @param player the collecting skill learning player + * @return all the available Collecting skills for a given {@code player} + */ + public List getAvailableCollectSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + for (L2SkillLearn skill : _collectSkillTree.values()) { + final Skill oldSkill = player.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() == (skill.getSkillLevel() - 1)) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + return result; + } + + /** + * Gets the available transfer skills. + * @param player the transfer skill learning player + * @return all the available Transfer skills for a given {@code player} + */ + public List getAvailableTransferSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + ClassId classId = player.getClassId(); + // If new classes are implemented over 3rd class, a different way should be implemented. + if (classId.level() == 3) { + classId = classId.getParent(); + } + + if (!_transferSkillTrees.containsKey(classId)) { + return result; + } + + for (L2SkillLearn skill : _transferSkillTrees.get(classId).values()) { + // If player doesn't know this transfer skill: + if (player.getKnownSkill(skill.getSkillId()) == null) { + result.add(skill); + } + } + return result; + } + + /** + * Some transformations are not available for some races. + * @param player the transformation skill learning player + * @return all the available Transformation skills for a given {@code player} + */ + public List getAvailableTransformSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + final Race race = player.getRace(); + for (L2SkillLearn skill : _transformSkillTree.values()) { + if ((player.getLevel() >= skill.getGetLevel()) && (skill.getRaces().isEmpty() || skill.getRaces().contains(race))) { + final Skill oldSkill = player.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() == (skill.getSkillLevel() - 1)) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + } + return result; + } + + /** + * Gets the available pledge skills. + * @param clan the pledge skill learning clan + * @return all the available Pledge skills for a given {@code clan} + */ + public List getAvailablePledgeSkills(L2Clan clan) { + final List result = new ArrayList<>(); + + for (L2SkillLearn skill : _pledgeSkillTree.values()) { + if (!skill.isResidentialSkill() && (clan.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = clan.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if ((oldSkill.getLevel() + 1) == skill.getSkillLevel()) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + } + return result; + } + + /** + * Gets the available pledge skills. + * @param clan the pledge skill learning clan + * @param includeSquad if squad skill will be added too + * @return all the available pledge skills for a given {@code clan} + */ + public Map getMaxPledgeSkills(L2Clan clan, boolean includeSquad) { + final Map result = new HashMap<>(); + for (L2SkillLearn skill : _pledgeSkillTree.values()) { + if (!skill.isResidentialSkill() && (clan.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = clan.getSkills().get(skill.getSkillId()); + if ((oldSkill == null) || (oldSkill.getLevel() < skill.getSkillLevel())) { + result.put(skill.getSkillId(), skill); + } + } + } + + if (includeSquad) { + for (L2SkillLearn skill : _subPledgeSkillTree.values()) { + if ((clan.getLevel() >= skill.getGetLevel())) { + final Skill oldSkill = clan.getSkills().get(skill.getSkillId()); + if ((oldSkill == null) || (oldSkill.getLevel() < skill.getSkillLevel())) { + result.put(skill.getSkillId(), skill); + } + } + } + } + return result; + } + + /** + * Gets the available sub pledge skills. + * @param clan the sub-pledge skill learning clan + * @return all the available Sub-Pledge skills for a given {@code clan} + */ + public List getAvailableSubPledgeSkills(L2Clan clan) { + final List result = new ArrayList<>(); + for (L2SkillLearn skill : _subPledgeSkillTree.values()) { + if ((clan.getLevel() >= skill.getGetLevel()) && clan.isLearnableSubSkill(skill.getSkillId(), skill.getSkillLevel())) { + result.add(skill); + } + } + return result; + } + + /** + * Gets the available sub class skills. + * @param player the sub-class skill learning player + * @return all the available Sub-Class skills for a given {@code player} + */ + public List getAvailableSubClassSkills(L2PcInstance player) { + final List result = new ArrayList<>(); + for (L2SkillLearn skill : _subClassSkillTree.values()) { + if (player.getLevel() >= skill.getGetLevel()) { + for (SubClass subClass : player.getSubClasses().values()) { + final var subClassConds = skill.getSubClassConditions(); + if (!subClassConds.isEmpty() && (subClass.getClassIndex() <= subClassConds.size()) && (subClass.getClassIndex() == subClassConds.get(subClass.getClassIndex() - 1).getSlot()) && (subClassConds.get(subClass.getClassIndex() - 1).getLvl() <= subClass.getLevel())) { + final Skill oldSkill = player.getSkills().get(skill.getSkillId()); + if (oldSkill != null) { + if (oldSkill.getLevel() == (skill.getSkillLevel() - 1)) { + result.add(skill); + } + } else if (skill.getSkillLevel() == 1) { + result.add(skill); + } + } + } + } + } + return result; + } + + /** + * Gets the available residential skills. + * @param residenceId the id of the Castle, Fort, Territory + * @return all the available Residential skills for a given {@code residenceId} + */ + public List getAvailableResidentialSkills(int residenceId) { + final List result = new ArrayList<>(); + for (L2SkillLearn skill : _pledgeSkillTree.values()) { + if (skill.isResidentialSkill() && skill.getResidenceIds().contains(residenceId)) { + result.add(skill); + } + } + return result; + } + + /** + * Just a wrapper for all skill trees. + * @param skillType the skill type + * @param id the skill Id + * @param lvl the skill level + * @param player the player learning the skill + * @return the skill learn for the specified parameters + */ + public L2SkillLearn getSkillLearn(AcquireSkillType skillType, int id, int lvl, L2PcInstance player) { + return switch (skillType) { + case CLASS -> getClassSkill(id, lvl, player.getLearningClass()); + case TRANSFORM -> getTransformSkill(id, lvl); + case FISHING -> getFishingSkill(id, lvl); + case PLEDGE -> getPledgeSkill(id, lvl); + case SUBPLEDGE -> getSubPledgeSkill(id, lvl); + case TRANSFER -> getTransferSkill(id, lvl, player.getClassId()); + case SUBCLASS -> getSubClassSkill(id, lvl); + case COLLECT -> getCollectSkill(id, lvl); + default -> null; + }; + } + + /** + * Gets the transform skill. + * @param id the transformation skill Id + * @param lvl the transformation skill level + * @return the transform skill from the Transform Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getTransformSkill(int id, int lvl) { + return _transformSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the class skill. + * @param id the class skill Id + * @param lvl the class skill level. + * @param classId the class skill tree Id + * @return the class skill from the Class Skill Trees for a given {@code classId}, {@code id} and {@code lvl} + */ + public L2SkillLearn getClassSkill(int id, int lvl, ClassId classId) { + return getCompleteClassSkillTree(classId).get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the fishing skill. + * @param id the fishing skill Id + * @param lvl the fishing skill level + * @return Fishing skill from the Fishing Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getFishingSkill(int id, int lvl) { + return _fishingSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the pledge skill. + * @param id the pledge skill Id + * @param lvl the pledge skill level + * @return the pledge skill from the Pledge Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getPledgeSkill(int id, int lvl) { + return _pledgeSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the sub pledge skill. + * @param id the sub-pledge skill Id + * @param lvl the sub-pledge skill level + * @return the sub-pledge skill from the Sub-Pledge Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getSubPledgeSkill(int id, int lvl) { + return _subPledgeSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the transfer skill. + * @param id the transfer skill Id + * @param lvl the transfer skill level. + * @param classId the transfer skill tree Id + * @return the transfer skill from the Transfer Skill Trees for a given {@code classId}, {@code id} and {@code lvl} + */ + public L2SkillLearn getTransferSkill(int id, int lvl, ClassId classId) { + if (classId.getParent() != null) { + final ClassId parentId = classId.getParent(); + if (_transferSkillTrees.get(parentId) != null) { + return _transferSkillTrees.get(parentId).get(SkillData.getSkillHashCode(id, lvl)); + } + } + return null; + } + + /** + * Gets the sub class skill. + * @param id the sub-class skill Id + * @param lvl the sub-class skill level + * @return the sub-class skill from the Sub-Class Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getSubClassSkill(int id, int lvl) { + return _subClassSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the common skill. + * @param id the common skill Id. + * @param lvl the common skill level + * @return the common skill from the Common Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getCommonSkill(int id, int lvl) { + return _commonSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the collect skill. + * @param id the collect skill Id + * @param lvl the collect skill level + * @return the collect skill from the Collect Skill Tree for a given {@code id} and {@code lvl} + */ + public L2SkillLearn getCollectSkill(int id, int lvl) { + return _collectSkillTree.get(SkillData.getSkillHashCode(id, lvl)); + } + + /** + * Gets the minimum level for new skill. + * @param player the player that requires the minimum level + * @param skillTree the skill tree to search the minimum get level + * @return the minimum level for a new skill for a given {@code player} and {@code skillTree} + */ + public int getMinLevelForNewSkill(L2PcInstance player, Map skillTree) { + int minLevel = 0; + if (skillTree.isEmpty()) { + LOG.warn("{}: SkillTree is not defined for getMinLevelForNewSkill!", getClass().getSimpleName()); + } else { + for (L2SkillLearn s : skillTree.values()) { + if (s.isLearnedByNpc() && (player.getLevel() < s.getGetLevel())) { + if ((minLevel == 0) || (minLevel > s.getGetLevel())) { + minLevel = s.getGetLevel(); + } + } + } + } + return minLevel; + } + + /** + * Checks if is hero skill. + * @param skillId the Id of the skill to check + * @param skillLevel the level of the skill to check, if it's -1 only Id will be checked + * @return {@code true} if the skill is present in the Hero Skill Tree, {@code false} otherwise + */ + public boolean isHeroSkill(int skillId, int skillLevel) { + if (_heroSkillTree.containsKey(SkillData.getSkillHashCode(skillId, skillLevel))) { + return true; + } + + for (L2SkillLearn skill : _heroSkillTree.values()) { + if ((skill.getSkillId() == skillId) && (skillLevel == -1)) { + return true; + } + } + return false; + } + + /** + * Checks if is GM skill. + * @param skillId the Id of the skill to check + * @param skillLevel the level of the skill to check, if it's -1 only Id will be checked + * @return {@code true} if the skill is present in the Game Master Skill Trees, {@code false} otherwise + */ + public boolean isGMSkill(int skillId, int skillLevel) { + if (skillLevel <= 0) { + return _gameMasterSkillTree.values().stream().anyMatch(s -> s.getSkillId() == skillId) // + || _gameMasterAuraSkillTree.values().stream().anyMatch(s -> s.getSkillId() == skillId); + } + final int hashCode = SkillData.getSkillHashCode(skillId, skillLevel); + return _gameMasterSkillTree.containsKey(hashCode) || _gameMasterAuraSkillTree.containsKey(hashCode); + } + + /** + * Checks if a skill is a Clan skill. + * @param skillId the Id of the skill to check + * @param skillLevel the level of the skill to check + * @return {@code true} if the skill is present in the Pledge or Subpledge Skill Trees, {@code false} otherwise + */ + public boolean isClanSkill(int skillId, int skillLevel) { + final int hashCode = SkillData.getSkillHashCode(skillId, skillLevel); + return _pledgeSkillTree.containsKey(hashCode) || _subPledgeSkillTree.containsKey(hashCode); + } + + /** + * Adds the skills. + * @param gmchar the player to add the Game Master skills + * @param auraSkills if {@code true} it will add "GM Aura" skills, else will add the "GM regular" skills + */ + public void addSkills(L2PcInstance gmchar, boolean auraSkills) { + final Collection skills = auraSkills ? _gameMasterAuraSkillTree.values() : _gameMasterSkillTree.values(); + final SkillData st = SkillData.getInstance(); + for (L2SkillLearn sl : skills) { + gmchar.addSkill(st.getSkill(sl.getSkillId(), sl.getSkillLevel()), false); // Don't Save GM skills to database + } + } + + /** + * Create and store hash values for skills for easy and fast checks. + */ + private void generateCheckArrays() { + int i; + int[] array; + + // Class specific skills: + Map tempMap; + final Set keySet = _classSkillTrees.keySet(); + _skillsByClassIdHashCodes = new HashMap<>(keySet.size()); + for (ClassId cls : keySet) { + i = 0; + tempMap = getCompleteClassSkillTree(cls); + array = new int[tempMap.size()]; + for (int h : tempMap.keySet()) { + array[i++] = h; + } + tempMap.clear(); + Arrays.sort(array); + _skillsByClassIdHashCodes.put(cls.ordinal(), array); + } + + // Race specific skills from Fishing and Transformation skill trees. + final List list = new ArrayList<>(); + _skillsByRaceHashCodes = new HashMap<>(Race.values().length); + for (Race r : Race.values()) { + for (L2SkillLearn s : _fishingSkillTree.values()) { + if (s.getRaces().contains(r)) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + } + + for (L2SkillLearn s : _transformSkillTree.values()) { + if (s.getRaces().contains(r)) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + } + + i = 0; + array = new int[list.size()]; + for (int s : list) { + array[i++] = s; + } + Arrays.sort(array); + _skillsByRaceHashCodes.put(r.ordinal(), array); + list.clear(); + } + + // Skills available for all classes and races + for (L2SkillLearn s : _commonSkillTree.values()) { + if (s.getRaces().isEmpty()) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + } + + for (L2SkillLearn s : _fishingSkillTree.values()) { + if (s.getRaces().isEmpty()) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + } + + for (L2SkillLearn s : _transformSkillTree.values()) { + if (s.getRaces().isEmpty()) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + } + + for (L2SkillLearn s : _collectSkillTree.values()) { + list.add(SkillData.getSkillHashCode(s.getSkillId(), s.getSkillLevel())); + } + + _allSkillsHashCodes = new int[list.size()]; + int j = 0; + for (int hashcode : list) { + _allSkillsHashCodes[j++] = hashcode; + } + Arrays.sort(_allSkillsHashCodes); + } + + /** + * Verify if the give skill is valid for the given player.
    + * GM's skills are excluded for GM players + * @param player the player to verify the skill + * @param skill the skill to be verified + * @return {@code true} if the skill is allowed to the given player + */ + public boolean isSkillAllowed(L2PcInstance player, Skill skill) { + if (skill.isExcludedFromCheck()) { + return true; + } + + if (player.isGM() && skill.isGMSkill()) { + return true; + } + + // Prevent accidental skill remove during reload + if (_loading) { + return true; + } + + final int maxLvl = SkillData.getInstance().getMaxLevel(skill.getId()); + final int hashCode = SkillData.getSkillHashCode(skill.getId(), Math.min(skill.getLevel(), maxLvl)); + + if (Arrays.binarySearch(_skillsByClassIdHashCodes.get(player.getClassId().ordinal()), hashCode) >= 0) { + return true; + } + + if (Arrays.binarySearch(_skillsByRaceHashCodes.get(player.getRace().ordinal()), hashCode) >= 0) { + return true; + } + + if (Arrays.binarySearch(_allSkillsHashCodes, hashCode) >= 0) { + return true; + } + + // Exclude Transfer Skills from this check. + return getTransferSkill(skill.getId(), Math.min(skill.getLevel(), maxLvl), player.getClassId()) != null; + } + + /** + * Logs current Skill Trees skills count. + */ + private void report() { + int classSkillTreeCount = 0; + for (Map classSkillTree : _classSkillTrees.values()) { + classSkillTreeCount += classSkillTree.size(); + } + + int trasferSkillTreeCount = 0; + for (Map trasferSkillTree : _transferSkillTrees.values()) { + trasferSkillTreeCount += trasferSkillTree.size(); + } + + int dwarvenOnlyFishingSkillCount = 0; + for (L2SkillLearn fishSkill : _fishingSkillTree.values()) { + if (fishSkill.getRaces().contains(Race.DWARF)) { + dwarvenOnlyFishingSkillCount++; + } + } + + int resSkillCount = 0; + for (L2SkillLearn pledgeSkill : _pledgeSkillTree.values()) { + if (pledgeSkill.isResidentialSkill()) { + resSkillCount++; + } + } + + LOG.info("Loaded {} Class Skills for {} Class Skill Trees.", classSkillTreeCount, _classSkillTrees.size()); + LOG.info("Loaded {} Sub-Class Skills.", _subClassSkillTree.size()); + LOG.info("Loaded {} Transfer Skills for {} Transfer Skill Trees.", trasferSkillTreeCount, _transferSkillTrees.size()); + LOG.info("Loaded {} Fishing Skills, {} Dwarven only Fishing Skills.", _fishingSkillTree.size(), dwarvenOnlyFishingSkillCount); + LOG.info("Loaded {} Collect Skills.", _collectSkillTree.size()); + LOG.info("Loaded {} Pledge Skills, {} for Pledge and {} Residential.", _pledgeSkillTree.size(), (_pledgeSkillTree.size() - resSkillCount), resSkillCount); + LOG.info("Loaded {} Sub-Pledge Skills.", _subPledgeSkillTree.size()); + LOG.info("Loaded {} Transform Skills.", _transformSkillTree.size()); + LOG.info("Loaded {} Noble Skills.", _nobleSkillTree.size()); + LOG.info("Loaded {} Hero Skills.", _heroSkillTree.size()); + LOG.info("Loaded {} Game Master Skills.", _gameMasterSkillTree.size()); + LOG.info("Loaded {} Game Master Aura Skills.", _gameMasterAuraSkillTree.size()); + final int commonSkills = _commonSkillTree.size(); + if (commonSkills > 0) { + LOG.info("Loaded {} Common Skills to all classes.", commonSkills); + } + } + + public static SkillTreesData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SkillTreesData INSTANCE = new SkillTreesData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/StaticObjectData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/StaticObjectData.java new file mode 100644 index 00000000..76d504b0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/StaticObjectData.java @@ -0,0 +1,107 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class loads and holds all static object data. + * @author UnAfraid + */ +public final class StaticObjectData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(StaticObjectData.class); + + private final Map _staticObjects = new HashMap<>(); + + protected StaticObjectData() { + load(); + } + + @Override + public void load() { + _staticObjects.clear(); + parseDatapackFile("data/staticObjects.xml"); + LOG.info("Loaded {} static object templates.", _staticObjects.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("object".equalsIgnoreCase(d.getNodeName())) { + final NamedNodeMap attrs = d.getAttributes(); + final StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + final Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + addObject(set); + } + } + } + } + } + + /** + * Initialize an static object based on the stats set and add it to the map. + * @param set the stats set to add. + */ + private void addObject(StatsSet set) { + final var objectId = IdFactory.getInstance().getNextId(); + final var template = new L2CharTemplate(new StatsSet()); + final var obj = new L2StaticObjectInstance(objectId, template, set.getInt("id")); + obj.setType(set.getInt("type", 0)); + obj.setName(set.getString("name")); + obj.setMap(set.getString("texture", "none"), set.getInt("map_x", 0), set.getInt("map_y", 0)); + obj.spawnMe(set.getInt("x"), set.getInt("y"), set.getInt("z")); + _staticObjects.put(obj.getObjectId(), obj); + } + + /** + * Gets the static objects. + * @return a collection of static objects. + */ + public Collection getStaticObjects() { + return _staticObjects.values(); + } + + public static StaticObjectData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final StaticObjectData INSTANCE = new StaticObjectData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/TransformData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/TransformData.java new file mode 100644 index 00000000..1aa3d01c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/TransformData.java @@ -0,0 +1,196 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.transform.Transform; +import com.l2jserver.gameserver.model.actor.transform.TransformLevelData; +import com.l2jserver.gameserver.model.actor.transform.TransformTemplate; +import com.l2jserver.gameserver.model.holders.AdditionalItemHolder; +import com.l2jserver.gameserver.model.holders.AdditionalSkillHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.network.serverpackets.ExBasicActionList; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Transformation data. + * @author UnAfraid + */ +public final class TransformData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(TransformData.class); + + private final Map _transformData = new HashMap<>(); + + protected TransformData() { + load(); + } + + @Override + public synchronized void load() { + _transformData.clear(); + parseDatapackDirectory("data/stats/transformations", false); + LOG.info("Loaded {} transform templates.", _transformData.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("transform".equalsIgnoreCase(d.getNodeName())) { + NamedNodeMap attrs = d.getAttributes(); + StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + final Transform transform = new Transform(set); + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + boolean isMale = "Male".equalsIgnoreCase(cd.getNodeName()); + if ("Male".equalsIgnoreCase(cd.getNodeName()) || "Female".equalsIgnoreCase(cd.getNodeName())) { + TransformTemplate templateData = null; + for (Node z = cd.getFirstChild(); z != null; z = z.getNextSibling()) { + switch (z.getNodeName()) { + case "common" -> { + for (Node s = z.getFirstChild(); s != null; s = s.getNextSibling()) { + switch (s.getNodeName()) { + case "base", "stats", "defense", "magicDefense", "collision", "moving" -> { + attrs = s.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + } + } + } + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + case "skills" -> { + if (templateData == null) { + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + for (Node s = z.getFirstChild(); s != null; s = s.getNextSibling()) { + if ("skill".equals(s.getNodeName())) { + attrs = s.getAttributes(); + int skillId = parseInteger(attrs, "id"); + int skillLevel = parseInteger(attrs, "level"); + templateData.addSkill(new SkillHolder(skillId, skillLevel)); + } + } + } + case "actions" -> { + if (templateData == null) { + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + set.set("actions", z.getTextContent()); + final int[] actions = set.getIntArray("actions", " "); + templateData.setBasicActionList(new ExBasicActionList(actions)); + } + case "additionalSkills" -> { + if (templateData == null) { + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + for (Node s = z.getFirstChild(); s != null; s = s.getNextSibling()) { + if ("skill".equals(s.getNodeName())) { + attrs = s.getAttributes(); + int skillId = parseInteger(attrs, "id"); + int skillLevel = parseInteger(attrs, "level"); + int minLevel = parseInteger(attrs, "minLevel"); + templateData.addAdditionalSkill(new AdditionalSkillHolder(skillId, skillLevel, minLevel)); + } + } + } + case "items" -> { + if (templateData == null) { + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + for (Node s = z.getFirstChild(); s != null; s = s.getNextSibling()) { + if ("item".equals(s.getNodeName())) { + attrs = s.getAttributes(); + int itemId = parseInteger(attrs, "id"); + boolean allowed = parseBoolean(attrs, "allowed"); + templateData.addAdditionalItem(new AdditionalItemHolder(itemId, allowed)); + } + } + } + case "levels" -> { + if (templateData == null) { + templateData = new TransformTemplate(set); + transform.setTemplate(isMale, templateData); + } + + for (Node s = z.getFirstChild(); s != null; s = s.getNextSibling()) { + if ("level".equals(s.getNodeName())) { + final var levelsSet = new StatsSet(); + attrs = s.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + final var att = attrs.item(i); + levelsSet.set(att.getNodeName(), att.getNodeValue()); + } + templateData.addLevelData(new TransformLevelData(levelsSet)); + } + } + } + } + } + } + } + _transformData.put(transform.getId(), transform); + } + } + } + } + } + + public Transform getTransform(int id) { + return _transformData.get(id); + } + + public boolean transformPlayer(int id, L2PcInstance player) { + final Transform transform = getTransform(id); + if (transform != null) { + player.transform(transform); + } + return transform != null; + } + + public static TransformData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final TransformData _instance = new TransformData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/data/xml/impl/UIData.java b/src/main/java/com/l2jserver/gameserver/data/xml/impl/UIData.java new file mode 100644 index 00000000..5da571a1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/data/xml/impl/UIData.java @@ -0,0 +1,151 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.data.xml.impl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.ActionKey; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * UI Data parser. + * @author Zoey76 + */ +public class UIData implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(UIData.class); + + private final Map> _storedKeys = new HashMap<>(); + + private final Map> _storedCategories = new HashMap<>(); + + protected UIData() { + load(); + } + + @Override + public void load() { + _storedKeys.clear(); + _storedCategories.clear(); + parseDatapackFile("data/ui/ui_en.xml"); + LOG.info("Loaded {} keys {} categories.", _storedKeys.size(), _storedCategories.size()); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("category".equalsIgnoreCase(d.getNodeName())) { + parseCategory(d); + } + } + } + } + } + + private void parseCategory(Node n) { + final int cat = parseInteger(n.getAttributes(), "id"); + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("commands".equalsIgnoreCase(d.getNodeName())) { + parseCommands(cat, d); + } else if ("keys".equalsIgnoreCase(d.getNodeName())) { + parseKeys(cat, d); + } + } + } + + private void parseCommands(int cat, Node d) { + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("cmd".equalsIgnoreCase(c.getNodeName())) { + addCategory(_storedCategories, cat, Integer.parseInt(c.getTextContent())); + } + } + } + + private void parseKeys(int cat, Node d) { + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("key".equalsIgnoreCase(c.getNodeName())) { + final ActionKey akey = new ActionKey(cat); + for (int i = 0; i < c.getAttributes().getLength(); i++) { + final Node att = c.getAttributes().item(i); + final int val = Integer.parseInt(att.getNodeValue()); + switch (att.getNodeName()) { + case "cmd" -> akey.setCommandId(val); + case "key" -> akey.setKeyId(val); + case "toggleKey1" -> akey.setToggleKey1(val); + case "toggleKey2" -> akey.setToggleKey2(val); + case "showType" -> akey.setShowStatus(val); + } + } + addKey(_storedKeys, cat, akey); + } + } + } + + /** + * Add a category to the stored categories. + * @param map the map to store the category + * @param cat the category + * @param cmd the command + */ + public static void addCategory(Map> map, int cat, int cmd) { + map.computeIfAbsent(cat, _ -> new ArrayList<>()).add(cmd); + } + + /** + * Create and insert an Action Key into the stored keys. + * @param map the map to store the key + * @param cat the category + * @param akey the action key + */ + public static void addKey(Map> map, int cat, ActionKey akey) { + map.computeIfAbsent(cat, _ -> new ArrayList<>()).add(akey); + } + + /** + * @return the categories + */ + public Map> getCategories() { + return _storedCategories; + } + + /** + * @return the keys + */ + public Map> getKeys() { + return _storedKeys; + } + + public static UIData getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final UIData INSTANCE = new UIData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/datatables/AugmentationData.java b/src/main/java/com/l2jserver/gameserver/datatables/AugmentationData.java new file mode 100644 index 00000000..864eea74 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/AugmentationData.java @@ -0,0 +1,796 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.data.xml.impl.OptionData; +import com.l2jserver.gameserver.model.L2Augmentation; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.options.Options; +import com.l2jserver.gameserver.network.clientpackets.AbstractRefinePacket; + +/** + * Loads augmentation bonuses and skills. + * @author durgus + * @author Gigiikun + * @author Sandro + * @author UnAfraid + */ +public class AugmentationData { + + // TODO(Zoey76): Implement using IXmlReader. + private static final Logger LOG = LoggerFactory.getLogger(AugmentationData.class); + + // stats + private static final int STAT_BLOCKSIZE = 3640; + private static final int STAT_SUBBLOCKSIZE = 91; + public static final int MIN_SKILL_ID = STAT_BLOCKSIZE * 4; + + // skills + private static final int BLUE_START = 14561; + private static final int SKILLS_BLOCKSIZE = 178; + + // basestats + private static final int BASESTAT_STR = 16341; + private static final int BASESTAT_MEN = 16344; + + // accessory + private static final int ACC_START = 16669; + private static final int ACC_BLOCKS_NUM = 10; + private static final int ACC_STAT_SUBBLOCKSIZE = 21; + + private static final int ACC_RING_START = ACC_START; + private static final int ACC_RING_SKILLS = 18; + private static final int ACC_RING_BLOCKSIZE = ACC_RING_SKILLS + (4 * ACC_STAT_SUBBLOCKSIZE); + private static final int ACC_RING_END = (ACC_RING_START + (ACC_BLOCKS_NUM * ACC_RING_BLOCKSIZE)) - 1; + + private static final int ACC_EAR_START = ACC_RING_END + 1; + private static final int ACC_EAR_SKILLS = 18; + private static final int ACC_EAR_BLOCKSIZE = ACC_EAR_SKILLS + (4 * ACC_STAT_SUBBLOCKSIZE); + private static final int ACC_EAR_END = (ACC_EAR_START + (ACC_BLOCKS_NUM * ACC_EAR_BLOCKSIZE)) - 1; + + private static final int ACC_NECK_START = ACC_EAR_END + 1; + private static final int ACC_NECK_SKILLS = 24; + private static final int ACC_NECK_BLOCKSIZE = ACC_NECK_SKILLS + (4 * ACC_STAT_SUBBLOCKSIZE); + + private final List> _blueSkills = new ArrayList<>(); + private final List> _purpleSkills = new ArrayList<>(); + private final List> _redSkills = new ArrayList<>(); + + private final List _augmentationChances = new ArrayList<>(); + private final List _augmentationChancesAcc = new ArrayList<>(); + + private final Map _allSkills = new HashMap<>(); + + protected AugmentationData() { + for (int i = 0; i < 10; i++) { + _blueSkills.add(new ArrayList<>()); + _purpleSkills.add(new ArrayList<>()); + _redSkills.add(new ArrayList<>()); + } + + load(); + + if (!character().retailLikeAugmentation()) { + for (int i = 0; i < 10; i++) { + LOG.info("Loaded {} blue, {} purple and {} red skills for life stone level {}.", _blueSkills.get(i).size(), _purpleSkills.get(i).size(), _redSkills.get(i).size(), i); + } + } else { + LOG.info("Loaded {} augmentations.", _augmentationChances.size()); + LOG.info("Loaded {} accessory augmentations.", _augmentationChancesAcc.size()); + } + } + + public class AugmentationChance { + private final String _WeaponType; + private final int _StoneId; + private final int _VariationId; + private final int _CategoryChance; + private final int _AugmentId; + private final float _AugmentChance; + + public AugmentationChance(String WeaponType, int StoneId, int VariationId, int CategoryChance, int AugmentId, float AugmentChance) { + _WeaponType = WeaponType; + _StoneId = StoneId; + _VariationId = VariationId; + _CategoryChance = CategoryChance; + _AugmentId = AugmentId; + _AugmentChance = AugmentChance; + } + + public String getWeaponType() { + return _WeaponType; + } + + public int getStoneId() { + return _StoneId; + } + + public int getVariationId() { + return _VariationId; + } + + public int getCategoryChance() { + return _CategoryChance; + } + + public int getAugmentId() { + return _AugmentId; + } + + public float getAugmentChance() { + return _AugmentChance; + } + } + + public class augmentationChanceAcc { + private final String _WeaponType; + private final int _StoneId; + private final int _VariationId; + private final int _CategoryChance; + private final int _AugmentId; + private final float _AugmentChance; + + public augmentationChanceAcc(String WeaponType, int StoneId, int VariationId, int CategoryChance, int AugmentId, float AugmentChance) { + _WeaponType = WeaponType; + _StoneId = StoneId; + _VariationId = VariationId; + _CategoryChance = CategoryChance; + _AugmentId = AugmentId; + _AugmentChance = AugmentChance; + } + + public String getWeaponType() { + return _WeaponType; + } + + public int getStoneId() { + return _StoneId; + } + + public int getVariationId() { + return _VariationId; + } + + public int getCategoryChance() { + return _CategoryChance; + } + + public int getAugmentId() { + return _AugmentId; + } + + public float getAugmentChance() { + return _AugmentChance; + } + } + + private final void load() { + // Load stats + DocumentBuilderFactory factory2 = DocumentBuilderFactory.newInstance(); + factory2.setValidating(false); + factory2.setIgnoringComments(true); + + // Load the skillmap + // Note: the skillmap data is only used when generating new augmentations + // the client expects a different id in order to display the skill in the + // items description... + if (!character().retailLikeAugmentation()) { + try { + int badAugmantData = 0; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + File file = new File(server().getDatapackRoot(), "data/stats/augmentation/augmentation_skillmap.xml"); + if (!file.exists()) { + LOG.warn("The augmentation skillmap file is missing!"); + return; + } + + Document doc = factory.newDocumentBuilder().parse(file); + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("augmentation".equalsIgnoreCase(d.getNodeName())) { + NamedNodeMap attrs = d.getAttributes(); + int skillId = 0, augmentationId = Integer.parseInt(attrs.getNamedItem("id").getNodeValue()); + int skillLvL = 0; + String type = "blue"; + + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("skillId".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + skillId = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + } else if ("skillLevel".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + skillLvL = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + } else if ("type".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + type = attrs.getNamedItem("val").getNodeValue(); + } + } + if (skillId == 0) { + badAugmantData++; + continue; + } else if (skillLvL == 0) { + badAugmantData++; + continue; + } + int k = (augmentationId - BLUE_START) / SKILLS_BLOCKSIZE; + + if (type.equalsIgnoreCase("blue")) { + _blueSkills.get(k).add(augmentationId); + } else if (type.equalsIgnoreCase("purple")) { + _purpleSkills.get(k).add(augmentationId); + } else { + _redSkills.get(k).add(augmentationId); + } + + _allSkills.put(augmentationId, new SkillHolder(skillId, skillLvL)); + } + } + } + } + if (badAugmantData != 0) { + LOG.warn("{} bad skill(s) were skipped.", badAugmantData); + } + } catch (Exception ex) { + LOG.warn("There has been an error parsing augmentation_skillmap.xml!", ex); + return; + } + } else { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + File aFile = new File(server().getDatapackRoot(), "data/stats/augmentation/retailchances.xml"); + if (aFile.exists()) { + Document aDoc = null; + + try { + aDoc = factory.newDocumentBuilder().parse(aFile); + } catch (Exception e) { + e.printStackTrace(); + return; + } + String aWeaponType = null; + int aStoneId = 0; + int aVariationId = 0; + int aCategoryChance = 0; + int aAugmentId = 0; + float aAugmentChance = 0; + + for (Node l = aDoc.getFirstChild(); l != null; l = l.getNextSibling()) { + if (l.getNodeName().equals("list")) { + NamedNodeMap aNodeAttributes = null; + + for (Node n = l.getFirstChild(); n != null; n = n.getNextSibling()) { + if (n.getNodeName().equals("weapon")) { + aNodeAttributes = n.getAttributes(); + + aWeaponType = aNodeAttributes.getNamedItem("type").getNodeValue(); + + for (Node c = n.getFirstChild(); c != null; c = c.getNextSibling()) { + if (c.getNodeName().equals("stone")) { + aNodeAttributes = c.getAttributes(); + + aStoneId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + + for (Node v = c.getFirstChild(); v != null; v = v.getNextSibling()) { + if (v.getNodeName().equals("variation")) { + aNodeAttributes = v.getAttributes(); + + aVariationId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + + for (Node j = v.getFirstChild(); j != null; j = j.getNextSibling()) { + if (j.getNodeName().equals("category")) { + aNodeAttributes = j.getAttributes(); + + aCategoryChance = Integer.parseInt(aNodeAttributes.getNamedItem("probability").getNodeValue()); + + for (Node e = j.getFirstChild(); e != null; e = e.getNextSibling()) { + if (e.getNodeName().equals("augment")) { + aNodeAttributes = e.getAttributes(); + + aAugmentId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + aAugmentChance = Float.parseFloat(aNodeAttributes.getNamedItem("chance").getNodeValue()); + + _augmentationChances.add(new AugmentationChance(aWeaponType, aStoneId, aVariationId, aCategoryChance, aAugmentId, aAugmentChance)); + } + } + } + } + } + } + } + } + } + } + } + } + } else { + LOG.warn("The retailchances.xml data file is missing!"); + return; + } + } + + if (character().retailLikeAugmentationAccessory()) { + DocumentBuilderFactory factory3 = DocumentBuilderFactory.newInstance(); + factory3.setValidating(false); + factory3.setIgnoringComments(true); + + File aFile3 = new File(server().getDatapackRoot(), "data/stats/augmentation/retailchances_accessory.xml"); + if (aFile3.exists()) { + Document aDoc = null; + + try { + aDoc = factory3.newDocumentBuilder().parse(aFile3); + } catch (Exception e) { + e.printStackTrace(); + return; + } + String aWeaponType = null; + int aStoneId = 0; + int aVariationId = 0; + int aCategoryChance = 0; + int aAugmentId = 0; + float aAugmentChance = 0; + + for (Node l = aDoc.getFirstChild(); l != null; l = l.getNextSibling()) { + if (l.getNodeName().equals("list")) { + NamedNodeMap aNodeAttributes = null; + for (Node n = l.getFirstChild(); n != null; n = n.getNextSibling()) { + if (n.getNodeName().equals("weapon")) { + aNodeAttributes = n.getAttributes(); + + aWeaponType = aNodeAttributes.getNamedItem("type").getNodeValue(); + + for (Node c = n.getFirstChild(); c != null; c = c.getNextSibling()) { + if (c.getNodeName().equals("stone")) { + aNodeAttributes = c.getAttributes(); + + aStoneId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + + for (Node v = c.getFirstChild(); v != null; v = v.getNextSibling()) { + if (v.getNodeName().equals("variation")) { + aNodeAttributes = v.getAttributes(); + + aVariationId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + + for (Node j = v.getFirstChild(); j != null; j = j.getNextSibling()) { + if (j.getNodeName().equals("category")) { + aNodeAttributes = j.getAttributes(); + + aCategoryChance = Integer.parseInt(aNodeAttributes.getNamedItem("probability").getNodeValue()); + + for (Node e = j.getFirstChild(); e != null; e = e.getNextSibling()) { + if (e.getNodeName().equals("augment")) { + aNodeAttributes = e.getAttributes(); + + aAugmentId = Integer.parseInt(aNodeAttributes.getNamedItem("id").getNodeValue()); + aAugmentChance = Float.parseFloat(aNodeAttributes.getNamedItem("chance").getNodeValue()); + + _augmentationChancesAcc.add(new augmentationChanceAcc(aWeaponType, aStoneId, aVariationId, aCategoryChance, aAugmentId, aAugmentChance)); + } + } + } + } + } + } + } + } + } + } + } + } + } else { + LOG.warn("The retailchances_accessory.xml data file is missing!"); + return; + } + } + } + + /** + * Generate a new random augmentation + * @param lifeStoneLevel + * @param lifeStoneGrade + * @param bodyPart + * @param lifeStoneId + * @param targetItem + * @return + */ + public L2Augmentation generateRandomAugmentation(int lifeStoneLevel, int lifeStoneGrade, int bodyPart, int lifeStoneId, L2ItemInstance targetItem) { + return switch (bodyPart) { + case L2Item.SLOT_LR_FINGER, L2Item.SLOT_LR_EAR, L2Item.SLOT_NECK -> generateRandomAccessoryAugmentation(lifeStoneLevel, bodyPart, lifeStoneId); + default -> generateRandomWeaponAugmentation(lifeStoneLevel, lifeStoneGrade, lifeStoneId, targetItem); + }; + } + + private L2Augmentation generateRandomWeaponAugmentation(int lifeStoneLevel, int lifeStoneGrade, int lifeStoneId, L2ItemInstance item) { + int stat12 = 0; + int stat34 = 0; + if (character().retailLikeAugmentation()) { + if (item.getItem().isMagicWeapon()) { + List _selectedChances12 = new ArrayList<>(); + List _selectedChances34 = new ArrayList<>(); + for (AugmentationChance ac : _augmentationChances) { + if (ac.getWeaponType().equals("mage") && (ac.getStoneId() == lifeStoneId)) { + if (ac.getVariationId() == 1) { + _selectedChances12.add(ac); + } else { + _selectedChances34.add(ac); + } + } + } + int r = Rnd.get(10000); + float s = 10000; + for (AugmentationChance ac : _selectedChances12) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat12 = ac.getAugmentId(); + } + } + List gradeChance = switch (lifeStoneGrade) { + case AbstractRefinePacket.GRADE_NONE -> character().getRetailLikeAugmentationNoGradeChance(); + case AbstractRefinePacket.GRADE_MID -> character().getRetailLikeAugmentationMidGradeChance(); + case AbstractRefinePacket.GRADE_HIGH -> character().getRetailLikeAugmentationHighGradeChance(); + case AbstractRefinePacket.GRADE_TOP -> character().getRetailLikeAugmentationTopGradeChance(); + default -> character().getRetailLikeAugmentationNoGradeChance(); + }; + + int c = Rnd.get(100); + if (c < gradeChance.get(0)) { + c = 55; + } else if (c < (gradeChance.get(0) + gradeChance.get(1))) { + c = 35; + } else if (c < (gradeChance.get(0) + gradeChance.get(1) + gradeChance.get(2))) { + c = 7; + } else { + c = 3; + } + List _selectedChances34final = new ArrayList<>(); + for (AugmentationChance ac : _selectedChances34) { + if (ac.getCategoryChance() == c) { + _selectedChances34final.add(ac); + } + } + + r = Rnd.get(10000); + s = 10000; + + for (AugmentationChance ac : _selectedChances34final) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat34 = ac.getAugmentId(); + } + } + } else { + List _selectedChances12 = new ArrayList<>(); + List _selectedChances34 = new ArrayList<>(); + for (AugmentationChance ac : _augmentationChances) { + if (ac.getWeaponType().equals("warrior") && (ac.getStoneId() == lifeStoneId)) { + if (ac.getVariationId() == 1) { + _selectedChances12.add(ac); + } else { + _selectedChances34.add(ac); + } + } + } + int r = Rnd.get(10000); + float s = 10000; + for (AugmentationChance ac : _selectedChances12) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat12 = ac.getAugmentId(); + } + } + List gradeChance = switch (lifeStoneGrade) { + case AbstractRefinePacket.GRADE_NONE -> character().getRetailLikeAugmentationNoGradeChance(); + case AbstractRefinePacket.GRADE_MID -> character().getRetailLikeAugmentationMidGradeChance(); + case AbstractRefinePacket.GRADE_HIGH -> character().getRetailLikeAugmentationHighGradeChance(); + case AbstractRefinePacket.GRADE_TOP -> character().getRetailLikeAugmentationTopGradeChance(); + default -> character().getRetailLikeAugmentationNoGradeChance(); + }; + + int c = Rnd.get(100); + if (c < gradeChance.get(0)) { + c = 55; + } else if (c < (gradeChance.get(0) + gradeChance.get(1))) { + c = 35; + } else if (c < (gradeChance.get(0) + gradeChance.get(1) + gradeChance.get(2))) { + c = 7; + } else { + c = 3; + } + List _selectedChances34final = new ArrayList<>(); + for (AugmentationChance ac : _selectedChances34) { + if (ac.getCategoryChance() == c) { + _selectedChances34final.add(ac); + } + } + r = Rnd.get(10000); + s = 10000; + for (AugmentationChance ac : _selectedChances34final) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat34 = ac.getAugmentId(); + } + } + } + return new L2Augmentation(((stat34 << 16) + stat12)); + } + boolean generateSkill = false; + boolean generateGlow = false; + + // life stone level is used for stat Id and skill level, but here the max level is 9 + lifeStoneLevel = Math.min(lifeStoneLevel, 9); + + switch (lifeStoneGrade) { + case AbstractRefinePacket.GRADE_NONE: + if (Rnd.get(1, 100) <= character().getAugmentationNGSkillChance()) { + generateSkill = true; + } + if (Rnd.get(1, 100) <= character().getAugmentationNGGlowChance()) { + generateGlow = true; + } + break; + case AbstractRefinePacket.GRADE_MID: + if (Rnd.get(1, 100) <= character().getAugmentationMidSkillChance()) { + generateSkill = true; + } + if (Rnd.get(1, 100) <= character().getAugmentationMidGlowChance()) { + generateGlow = true; + } + break; + case AbstractRefinePacket.GRADE_HIGH: + if (Rnd.get(1, 100) <= character().getAugmentationHighSkillChance()) { + generateSkill = true; + } + if (Rnd.get(1, 100) <= character().getAugmentationHighGlowChance()) { + generateGlow = true; + } + break; + case AbstractRefinePacket.GRADE_TOP: + if (Rnd.get(1, 100) <= character().getAugmentationTopSkillChance()) { + generateSkill = true; + } + if (Rnd.get(1, 100) <= character().getAugmentationTopGlowChance()) { + generateGlow = true; + } + break; + case AbstractRefinePacket.GRADE_ACC: + if (Rnd.get(1, 100) <= character().getAugmentationAccSkillChance()) { + generateSkill = true; + } + } + + if (!generateSkill && (Rnd.get(1, 100) <= character().getAugmentationBaseStatChance())) { + stat34 = Rnd.get(BASESTAT_STR, BASESTAT_MEN); + } + + // Second: decide which grade the augmentation result is going to have: + // 0:yellow, 1:blue, 2:purple, 3:red + // The chances used here are most likely custom, + // what's known is: you can't have yellow with skill(or baseStatModifier) + // noGrade stone can not have glow, mid only with skill, high has a chance(custom), top allways glow + int resultColor = Rnd.get(0, 100); + if ((stat34 == 0) && !generateSkill) { + if (resultColor <= ((15 * lifeStoneGrade) + 40)) { + resultColor = 1; + } else { + resultColor = 0; + } + } else { + if ((resultColor <= ((10 * lifeStoneGrade) + 5)) || (stat34 != 0)) { + resultColor = 3; + } else if (resultColor <= ((10 * lifeStoneGrade) + 10)) { + resultColor = 1; + } else { + resultColor = 2; + } + } + + // generate a skill if necessary + if (generateSkill) { + stat34 = switch (resultColor) { + case 1 -> _blueSkills.get(lifeStoneLevel).get(Rnd.get(0, _blueSkills.get(lifeStoneLevel).size() - 1)); + case 2 -> _purpleSkills.get(lifeStoneLevel).get(Rnd.get(0, _purpleSkills.get(lifeStoneLevel).size() - 1)); + case 3 -> _redSkills.get(lifeStoneLevel).get(Rnd.get(0, _redSkills.get(lifeStoneLevel).size() - 1)); + default -> stat34; + }; + } + + // Third: Calculate the subblock offset for the chosen color, + // and the level of the lifeStone + // from large number of retail augmentations: + // no skill part + // Id for stat12: + // A:1-910 B:911-1820 C:1821-2730 D:2731-3640 E:3641-4550 F:4551-5460 G:5461-6370 H:6371-7280 + // Id for stat34(this defines the color): + // I:7281-8190(yellow) K:8191-9100(blue) L:10921-11830(yellow) M:11831-12740(blue) + // you can combine I-K with A-D and L-M with E-H + // using C-D or G-H Id you will get a glow effect + // there seems no correlation in which grade use which Id except for the glowing restriction + // skill part + // Id for stat12: + // same for no skill part + // A same as E, B same as F, C same as G, D same as H + // A - no glow, no grade LS + // B - weak glow, mid grade LS? + // C - glow, high grade LS? + // D - strong glow, top grade LS? + + // is neither a skill nor basestat used for stat34? then generate a normal stat + int offset; + if (stat34 == 0) { + int temp = Rnd.get(2, 3); + int colorOffset = (resultColor * (10 * STAT_SUBBLOCKSIZE)) + (temp * STAT_BLOCKSIZE) + 1; + offset = (lifeStoneLevel * STAT_SUBBLOCKSIZE) + colorOffset; + + stat34 = Rnd.get(offset, (offset + STAT_SUBBLOCKSIZE) - 1); + if (generateGlow && (lifeStoneGrade >= 2)) { + offset = (lifeStoneLevel * STAT_SUBBLOCKSIZE) + ((temp - 2) * STAT_BLOCKSIZE) + (lifeStoneGrade * (10 * STAT_SUBBLOCKSIZE)) + 1; + } else { + offset = (lifeStoneLevel * STAT_SUBBLOCKSIZE) + ((temp - 2) * STAT_BLOCKSIZE) + (Rnd.get(0, 1) * (10 * STAT_SUBBLOCKSIZE)) + 1; + } + } else { + if (!generateGlow) { + offset = (lifeStoneLevel * STAT_SUBBLOCKSIZE) + (Rnd.get(0, 1) * STAT_BLOCKSIZE) + 1; + } else { + offset = (lifeStoneLevel * STAT_SUBBLOCKSIZE) + (Rnd.get(0, 1) * STAT_BLOCKSIZE) + (((lifeStoneGrade + resultColor) / 2) * (10 * STAT_SUBBLOCKSIZE)) + 1; + } + } + stat12 = Rnd.get(offset, (offset + STAT_SUBBLOCKSIZE) - 1); + + if (general().debug()) { + LOG.info("Augmentation success: stat12={}; stat34={}; resultColor={}; level={}; grade={}", stat12, stat34, resultColor, lifeStoneLevel, lifeStoneGrade); + } + return new L2Augmentation(((stat34 << 16) + stat12)); + } + + private L2Augmentation generateRandomAccessoryAugmentation(int lifeStoneLevel, int bodyPart, int lifeStoneId) { + int stat12 = 0; + int stat34 = 0; + if (character().retailLikeAugmentationAccessory()) { + List _selectedChances12 = new ArrayList<>(); + List _selectedChances34 = new ArrayList<>(); + for (augmentationChanceAcc ac : _augmentationChancesAcc) { + if (ac.getWeaponType().equals("warrior") && (ac.getStoneId() == lifeStoneId)) { + if (ac.getVariationId() == 1) { + _selectedChances12.add(ac); + } else { + _selectedChances34.add(ac); + } + } + } + int r = Rnd.get(10000); + float s = 10000; + for (augmentationChanceAcc ac : _selectedChances12) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat12 = ac.getAugmentId(); + } + } + int c = Rnd.get(100); + if (c < 55) { + c = 55; + } else if (c < 90) { + c = 35; + } else if (c < 99) { + c = 9; + } else { + c = 1; + } + List _selectedChances34final = new ArrayList<>(); + for (augmentationChanceAcc ac : _selectedChances34) { + if (ac.getCategoryChance() == c) { + _selectedChances34final.add(ac); + } + } + r = Rnd.get(10000); + s = 10000; + for (augmentationChanceAcc ac : _selectedChances34final) { + if (s > r) { + s -= (ac.getAugmentChance() * 100); + stat34 = ac.getAugmentId(); + } + } + + return new L2Augmentation(((stat34 << 16) + stat12)); + } + lifeStoneLevel = Math.min(lifeStoneLevel, 9); + int base = 0; + int skillsLength = 0; + + switch (bodyPart) { + case L2Item.SLOT_LR_FINGER: + base = ACC_RING_START + (ACC_RING_BLOCKSIZE * lifeStoneLevel); + skillsLength = ACC_RING_SKILLS; + break; + case L2Item.SLOT_LR_EAR: + base = ACC_EAR_START + (ACC_EAR_BLOCKSIZE * lifeStoneLevel); + skillsLength = ACC_EAR_SKILLS; + break; + case L2Item.SLOT_NECK: + base = ACC_NECK_START + (ACC_NECK_BLOCKSIZE * lifeStoneLevel); + skillsLength = ACC_NECK_SKILLS; + break; + default: + return null; + } + + int resultColor = Rnd.get(0, 3); + + // first augmentation (stats only) + stat12 = Rnd.get(ACC_STAT_SUBBLOCKSIZE); + Options op = null; + if (Rnd.get(1, 100) <= character().getAugmentationAccSkillChance()) { + // second augmentation (skill) + stat34 = base + Rnd.get(skillsLength); + op = OptionData.getInstance().getOptions(stat34); + } + + if ((op == null) || (!op.hasActiveSkill() && !op.hasPassiveSkill() && !op.hasActivationSkills())) { + // second augmentation (stats) + // calculating any different from stat12 value inside sub-block + // starting from next and wrapping over using remainder + stat34 = (stat12 + 1 + Rnd.get(ACC_STAT_SUBBLOCKSIZE - 1)) % ACC_STAT_SUBBLOCKSIZE; + // this is a stats - skipping skills + stat34 = base + skillsLength + (ACC_STAT_SUBBLOCKSIZE * resultColor) + stat34; + } + + // stat12 has stats only + stat12 = base + skillsLength + (ACC_STAT_SUBBLOCKSIZE * resultColor) + stat12; + + if (general().debug()) { + LOG.info("Accessory augmentation success: stat12={}; stat34={}; level={}", stat12, stat34, lifeStoneLevel); + } + return new L2Augmentation(((stat34 << 16) + stat12)); + } + + public static final AugmentationData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final AugmentationData _instance = new AugmentationData(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/datatables/BotReportTable.java b/src/main/java/com/l2jserver/gameserver/datatables/BotReportTable.java new file mode 100644 index 00000000..b8d7d318 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/BotReportTable.java @@ -0,0 +1,531 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.network.SystemMessageId.C1_WAS_REPORTED_AS_BOT; +import static com.l2jserver.gameserver.network.SystemMessageId.CANNOT_REPORT_TARGET_ALREDY_REPORTED_BY_CLAN_ALLY_MEMBER_OR_SAME_IP; +import static com.l2jserver.gameserver.network.SystemMessageId.TARGET_NOT_REPORT_CANNOT_REPORT_PEACE_PVP_ZONE_OR_OLYMPIAD_OR_CLAN_WAR_ENEMY; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CANNOT_REPORT_CHARACTER_IN_PEACE_OR_BATTLE_ZONE; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CANNOT_REPORT_CHAR_AT_THIS_TIME_1; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CANNOT_REPORT_CHAR_WHO_ACQUIRED_XP; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CANNOT_REPORT_CLAN_WAR_ENEMY; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CAN_REPORT_IN_S1_MINS_YOU_HAVE_S2_POINTS_LEFT; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_HAVE_BEEN_REPORTED_AND_CANNOT_REPORT; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_HAVE_USED_ALL_POINTS_POINTS_ARE_RESET_AT_NOON; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_HAVE_USED_REPORT_POINT_ON_C1_YOU_HAVE_C2_POINTS_LEFT; +import static java.util.concurrent.TimeUnit.DAYS; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.Attributes; +import org.xml.sax.helpers.DefaultHandler; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Bot Report table. + * @author BiggBoss + */ +public final class BotReportTable { + + // Zoey76: TODO: Split XML parsing from SQL operations, use IXmlReader instead of SAXParser. + private static final Logger LOG = LoggerFactory.getLogger(BotReportTable.class); + + private static final int COLUMN_BOT_ID = 1; + private static final int COLUMN_REPORTER_ID = 2; + private static final int COLUMN_REPORT_TIME = 3; + + public static final int ATTACK_ACTION_BLOCK_ID = -1; + public static final int TRADE_ACTION_BLOCK_ID = -2; + public static final int PARTY_ACTION_BLOCK_ID = -3; + public static final int ACTION_BLOCK_ID = -4; + public static final int CHAT_BLOCK_ID = -5; + + private static final String SQL_LOAD_REPORTED_CHAR_DATA = "SELECT * FROM bot_reported_char_data"; + private static final String SQL_INSERT_REPORTED_CHAR_DATA = "INSERT INTO bot_reported_char_data VALUES (?,?,?)"; + private static final String SQL_CLEAR_REPORTED_CHAR_DATA = "DELETE FROM bot_reported_char_data"; + + private Map _ipRegistry; + private Map _charRegistry; + private Map _reports; + private Map _punishments; + + BotReportTable() { + if (general().enableBotReportButton()) { + _ipRegistry = new HashMap<>(); + _charRegistry = new ConcurrentHashMap<>(); + _reports = new ConcurrentHashMap<>(); + _punishments = new ConcurrentHashMap<>(); + + try { + File punishments = new File("./config/botreport_punishments.xml"); + if (!punishments.exists()) { + throw new FileNotFoundException(punishments.getName()); + } + + SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); + parser.parse(punishments, new PunishmentsLoader()); + } catch (Exception ex) { + LOG.warn("Could not load punishments from /config/botreport_punishments.xml", ex); + } + + loadReportedCharData(); + scheduleResetPointTask(); + } + } + + /** + * Loads all reports of each reported bot into this cache class.
    + * Warning: Heavy method, used only on server start up + */ + private void loadReportedCharData() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery(SQL_LOAD_REPORTED_CHAR_DATA)) { + long lastResetTime = 0; + try { + String[] hour = general().getBotReportPointsResetHour().split(":"); + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour[0])); + c.set(Calendar.MINUTE, Integer.parseInt(hour[1])); + + if (System.currentTimeMillis() < c.getTimeInMillis()) { + c.set(Calendar.DAY_OF_YEAR, c.get(Calendar.DAY_OF_YEAR) - 1); + } + + lastResetTime = c.getTimeInMillis(); + } catch (Exception e) { + + } + + while (rs.next()) { + int botId = rs.getInt(COLUMN_BOT_ID); + int reporter = rs.getInt(COLUMN_REPORTER_ID); + long date = rs.getLong(COLUMN_REPORT_TIME); + if (_reports.containsKey(botId)) { + _reports.get(botId).addReporter(reporter, date); + } else { + ReportedCharData rcd = new ReportedCharData(); + rcd.addReporter(reporter, date); + _reports.put(rs.getInt(COLUMN_BOT_ID), rcd); + } + + if (date > lastResetTime) { + ReporterCharData rcd = _charRegistry.get(reporter); + if (rcd != null) { + rcd.setPoints(rcd.getPointsLeft() - 1); + } else { + rcd = new ReporterCharData(); + rcd.setPoints(6); + _charRegistry.put(reporter, rcd); + } + } + } + + LOG.info("Loaded {} bot reports.", _reports.size()); + } catch (Exception ex) { + LOG.warn("Could not load reported char data!", ex); + } + } + + /** + * Save all reports for each reported bot down to database.
    + * Warning: Heavy method, used only at server shutdown + */ + public void saveReportedCharData() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var ps = con.prepareStatement(SQL_INSERT_REPORTED_CHAR_DATA)) { + st.execute(SQL_CLEAR_REPORTED_CHAR_DATA); + + for (Map.Entry entrySet : _reports.entrySet()) { + Map reportTable = entrySet.getValue()._reporters; + for (int reporterId : reportTable.keySet()) { + ps.setInt(1, entrySet.getKey()); + ps.setInt(2, reporterId); + ps.setLong(3, reportTable.get(reporterId)); + ps.execute(); + } + } + } catch (Exception ex) { + LOG.warn("Could not update reported char data in database!", ex); + } + } + + /** + * Attempts to perform a bot report. R/W to ip and char id registry is synchronized. Triggers bot punish management
    + * @param reporter (L2PcInstance who issued the report) + * @return True, if the report was registered, False otherwise + */ + public boolean reportBot(L2PcInstance reporter) { + L2Object target = reporter.getTarget(); + + if (target == null) { + return false; + } + + L2PcInstance bot = target.getActingPlayer(); + + if ((bot == null) || (target.getObjectId() == reporter.getObjectId())) { + return false; + } + + if (bot.isInsideZone(ZoneId.PEACE) || bot.isInsideZone(ZoneId.PVP)) { + reporter.sendPacket(YOU_CANNOT_REPORT_CHARACTER_IN_PEACE_OR_BATTLE_ZONE); + return false; + } + + if (bot.isInOlympiadMode()) { + reporter.sendPacket(TARGET_NOT_REPORT_CANNOT_REPORT_PEACE_PVP_ZONE_OR_OLYMPIAD_OR_CLAN_WAR_ENEMY); + return false; + } + + if ((bot.getClan() != null) && bot.getClan().isAtWarWith(reporter.getClan())) { + reporter.sendPacket(YOU_CANNOT_REPORT_CLAN_WAR_ENEMY); + return false; + } + + if (bot.getExp() == bot.getStat().getStartingExp()) { + reporter.sendPacket(YOU_CANNOT_REPORT_CHAR_WHO_ACQUIRED_XP); + return false; + } + + ReportedCharData rcd = _reports.get(bot.getObjectId()); + ReporterCharData rcdRep = _charRegistry.get(reporter.getObjectId()); + final int reporterId = reporter.getObjectId(); + + synchronized (this) { + if (_reports.containsKey(reporterId)) { + reporter.sendPacket(YOU_HAVE_BEEN_REPORTED_AND_CANNOT_REPORT); + return false; + } + + final int ip = hashIp(reporter); + if (!timeHasPassed(_ipRegistry, ip)) { + reporter.sendPacket(CANNOT_REPORT_TARGET_ALREDY_REPORTED_BY_CLAN_ALLY_MEMBER_OR_SAME_IP); + return false; + } + + if (rcd != null) { + if (rcd.alreadyReportedBy(reporterId)) { + reporter.sendPacket(YOU_CANNOT_REPORT_CHAR_AT_THIS_TIME_1); + return false; + } + + if (!general().allowReportsFromSameClanMembers() && rcd.reportedBySameClan(reporter.getClan())) { + reporter.sendPacket(CANNOT_REPORT_TARGET_ALREDY_REPORTED_BY_CLAN_ALLY_MEMBER_OR_SAME_IP); + return false; + } + } + + if (rcdRep != null) { + if (rcdRep.getPointsLeft() == 0) { + reporter.sendPacket(YOU_HAVE_USED_ALL_POINTS_POINTS_ARE_RESET_AT_NOON); + return false; + } + + long reuse = (System.currentTimeMillis() - rcdRep.getLastReportTime()); + if (reuse < general().getBotReportDelay()) { + SystemMessage sm = SystemMessage.getSystemMessage(YOU_CAN_REPORT_IN_S1_MINS_YOU_HAVE_S2_POINTS_LEFT); + sm.addInt((int) (reuse / 60000)); + sm.addInt(rcdRep.getPointsLeft()); + reporter.sendPacket(sm); + return false; + } + } + + final long curTime = System.currentTimeMillis(); + + if (rcd == null) { + rcd = new ReportedCharData(); + _reports.put(bot.getObjectId(), rcd); + } + rcd.addReporter(reporterId, curTime); + + if (rcdRep == null) { + rcdRep = new ReporterCharData(); + } + rcdRep.registerReport(curTime); + + _ipRegistry.put(ip, curTime); + _charRegistry.put(reporterId, rcdRep); + } + + SystemMessage sm = SystemMessage.getSystemMessage(C1_WAS_REPORTED_AS_BOT); + sm.addCharName(bot); + reporter.sendPacket(sm); + + sm = SystemMessage.getSystemMessage(YOU_HAVE_USED_REPORT_POINT_ON_C1_YOU_HAVE_C2_POINTS_LEFT); + sm.addCharName(bot); + sm.addInt(rcdRep.getPointsLeft()); + reporter.sendPacket(sm); + + handleReport(bot, rcd); + + return true; + } + + /** + * Find the punishment to apply to the given bot and triggers the punish method. + * @param bot (L2PcInstance to be punished) + * @param rcd (ReportedCharData linked to this bot) + */ + private void handleReport(L2PcInstance bot, final ReportedCharData rcd) { + // Report count punishment + punishBot(bot, _punishments.get(rcd.getReportCount())); + + // Range punishments + for (int key : _punishments.keySet()) { + if ((key < 0) && (Math.abs(key) <= rcd.getReportCount())) { + punishBot(bot, _punishments.get(key)); + } + } + } + + /** + * Applies the given punish to the bot if the action is secure + * @param bot (L2PcInstance to punish) + * @param ph (PunishHolder containing the debuff and a possible system message to send) + */ + private void punishBot(L2PcInstance bot, PunishHolder ph) { + if (ph != null) { + ph._punish.applyEffects(bot, bot); + if (ph._systemMessageId > -1) { + bot.sendPacket(SystemMessageId.getSystemMessageId(ph._systemMessageId)); + } + } + } + + /** + * Adds a debuff punishment into the punishments record. If skill does not exist, will log it and return + * @param neededReports (report count to trigger this debuff) + * @param skillId + * @param skillLevel + * @param sysMsg (id of a system message to send when applying the punish) + */ + void addPunishment(int neededReports, int skillId, int skillLevel, int sysMsg) { + Skill sk = SkillData.getInstance().getSkill(skillId, skillLevel); + if (sk != null) { + _punishments.put(neededReports, new PunishHolder(sk, sysMsg)); + } else { + LOG.warn("Could not add punishment for {} report(s): Skill {}-{} does not exist!", neededReports, skillId, skillLevel); + } + } + + void resetPointsAndSchedule() { + synchronized (_charRegistry) { + for (ReporterCharData rcd : _charRegistry.values()) { + rcd.setPoints(7); + } + } + + scheduleResetPointTask(); + } + + private void scheduleResetPointTask() { + try { + String[] hour = general().getBotReportPointsResetHour().split(":"); + Calendar c = Calendar.getInstance(); + c.set(Calendar.HOUR_OF_DAY, Integer.parseInt(hour[0])); + c.set(Calendar.MINUTE, Integer.parseInt(hour[1])); + + if (System.currentTimeMillis() > c.getTimeInMillis()) { + c.set(Calendar.DAY_OF_YEAR, c.get(Calendar.DAY_OF_YEAR) + 1); + } + + ThreadPoolManager.getInstance().scheduleGeneral(new ResetPointTask(), c.getTimeInMillis() - System.currentTimeMillis()); + } catch (Exception ex) { + ThreadPoolManager.getInstance().scheduleGeneral(new ResetPointTask(), DAYS.toMillis(1)); + LOG.warn("Could not properly schedule bot report points reset task. Scheduled in 24 hours!", ex); + } + } + + /** + * Returns a integer representative number from a connection + * @param player (The L2PcInstance owner of the connection) + * @return int (hashed ip) + */ + private static int hashIp(L2PcInstance player) { + String con = player.getClient().getConnection().getInetAddress().getHostAddress(); + String[] rawByte = con.split("\\."); + int[] rawIp = new int[4]; + for (int i = 0; i < 4; i++) { + rawIp[i] = Integer.parseInt(rawByte[i]); + } + + return rawIp[0] | (rawIp[1] << 8) | (rawIp[2] << 16) | (rawIp[3] << 24); + } + + /** + * Checks and return if the abstract barrier specified by an integer (map key) has accomplished the waiting time + * @param map (a Map to study (Int = barrier, Long = fully qualified unix time) + * @param objectId (an existent map key) + * @return true if the time has passed. + */ + private static boolean timeHasPassed(Map map, int objectId) { + if (map.containsKey(objectId)) { + return (System.currentTimeMillis() - map.get(objectId)) > general().getBotReportDelay(); + } + return true; + } + + /** + * Represents the info about a reporter + */ + private static final class ReporterCharData { + private long _lastReport; + private byte _reportPoints; + + ReporterCharData() { + _reportPoints = 7; + _lastReport = 0; + } + + void registerReport(long time) { + _reportPoints -= 1; + _lastReport = time; + } + + long getLastReportTime() { + return _lastReport; + } + + byte getPointsLeft() { + return _reportPoints; + } + + void setPoints(int points) { + _reportPoints = (byte) points; + } + } + + /** + * Represents the info about a reported character + */ + private static final class ReportedCharData { + Map _reporters; + + ReportedCharData() { + _reporters = new HashMap<>(); + } + + int getReportCount() { + return _reporters.size(); + } + + boolean alreadyReportedBy(int objectId) { + return _reporters.containsKey(objectId); + } + + void addReporter(int objectId, long reportTime) { + _reporters.put(objectId, reportTime); + } + + boolean reportedBySameClan(L2Clan clan) { + if (clan == null) { + return false; + } + + for (int reporterId : _reporters.keySet()) { + if (clan.isMember(reporterId)) { + return true; + } + } + + return false; + } + } + + /** + * SAX loader to parse /config/botreport_punishments.xml file + */ + private final class PunishmentsLoader extends DefaultHandler { + PunishmentsLoader() { + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attr) { + if (qName.equals("punishment")) { + int reportCount = -1, skillId = -1, skillLevel = 1, sysMessage = -1; + try { + reportCount = Integer.parseInt(attr.getValue("neededReportCount")); + skillId = Integer.parseInt(attr.getValue("skillId")); + String level = attr.getValue("skillLevel"); + String systemMessageId = attr.getValue("sysMessageId"); + if (level != null) { + skillLevel = Integer.parseInt(level); + } + + if (systemMessageId != null) { + sysMessage = Integer.parseInt(systemMessageId); + } + } catch (Exception e) { + e.printStackTrace(); + } + + addPunishment(reportCount, skillId, skillLevel, sysMessage); + } + } + } + + static class PunishHolder { + final Skill _punish; + final int _systemMessageId; + + PunishHolder(final Skill sk, final int sysMsg) { + _punish = sk; + _systemMessageId = sysMsg; + } + } + + class ResetPointTask implements Runnable { + @Override + public void run() { + resetPointsAndSchedule(); + + } + } + + public static BotReportTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static final class SingletonHolder { + static final BotReportTable INSTANCE = new BotReportTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/datatables/EventDroplist.java b/src/main/java/com/l2jserver/gameserver/datatables/EventDroplist.java new file mode 100644 index 00000000..9bc388f0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/EventDroplist.java @@ -0,0 +1,116 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.l2jserver.gameserver.script.DateRange; +import com.l2jserver.gameserver.script.EventDrop; + +/** + * This class manage drop of Special Events created by GM for a defined period.
    + * During a Special Event all L2Attackable can drop extra Items.
    + * Those extra Items are defined in the table allNpcDateDrops.
    + * Each Special Event has a start and end date to stop to drop extra Items automatically. + */ +public class EventDroplist { + /** + * The table containing all DataDrop object + */ + private static final List ALL_NPC_DATE_DROPS = new ArrayList<>(); + + public static class DateDrop { + protected final DateRange _dateRange; + private final EventDrop _eventDrop; + + public DateDrop(DateRange dateRange, EventDrop eventDrop) { + _dateRange = dateRange; + _eventDrop = eventDrop; + } + + /** + * @return the _eventDrop + */ + public EventDrop getEventDrop() { + return _eventDrop; + } + + /** + * @return the _dateRange + */ + public DateRange getDateRange() { + return _dateRange; + } + } + + /** + * Create and Init a new DateDrop then add it to the allNpcDateDrops of EventDroplist . + * @param itemIdList The table containing all item identifier of this DateDrop + * @param count The table containing min and max value of this DateDrop + * @param chance The chance to obtain this drop + * @param dateRange The DateRange object to add to this DateDrop + */ + public void addGlobalDrop(int[] itemIdList, int[] count, int chance, DateRange dateRange) { + ALL_NPC_DATE_DROPS.add(new DateDrop(dateRange, new EventDrop(itemIdList, count[0], count[1], chance))); + } + + /** + * @param itemId the item Id for the drop + * @param min the minimum drop count + * @param max the maximum drop count + * @param chance the drop chance + * @param dateRange the event drop rate range + */ + public void addGlobalDrop(int itemId, long min, long max, int chance, DateRange dateRange) { + ALL_NPC_DATE_DROPS.add(new DateDrop(dateRange, new EventDrop(itemId, min, max, chance))); + } + + /** + * Adds an event drop for a given date range. + * @param dateRange the date range. + * @param eventDrop the event drop. + */ + public void addGlobalDrop(DateRange dateRange, EventDrop eventDrop) { + ALL_NPC_DATE_DROPS.add(new DateDrop(dateRange, eventDrop)); + } + + /** + * @return all DateDrop of EventDroplist allNpcDateDrops within the date range. + */ + public List getAllDrops() { + final List list = new ArrayList<>(); + final Date currentDate = new Date(); + for (DateDrop drop : ALL_NPC_DATE_DROPS) { + if (drop._dateRange.isWithinRange(currentDate)) { + list.add(drop); + } + } + return list; + } + + public static EventDroplist getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final EventDroplist _instance = new EventDroplist(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/datatables/ItemTable.java b/src/main/java/com/l2jserver/gameserver/datatables/ItemTable.java new file mode 100644 index 00000000..d879ccec --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/ItemTable.java @@ -0,0 +1,357 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.model.itemcontainer.Inventory.ADENA_ID; +import static com.l2jserver.gameserver.model.items.type.EtcItemType.ARROW; +import static com.l2jserver.gameserver.model.items.type.EtcItemType.SHOT; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.xml.impl.EnchantItemHPBonusData; +import com.l2jserver.gameserver.engines.DocumentEngine; +import com.l2jserver.gameserver.enums.ItemLocation; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.instance.L2EventMonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.item.ItemCreate; +import com.l2jserver.gameserver.model.items.L2Armor; +import com.l2jserver.gameserver.model.items.L2EtcItem; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.util.GMAudit; + +/** + * This class serves as a container for all item templates in the game. + */ +public class ItemTable { + + private static final Logger LOG = LoggerFactory.getLogger(ItemTable.class); + + private static final Logger LOG_ITEM = LoggerFactory.getLogger("item"); + + public static final Map SLOTS = new HashMap<>(); + + private L2Item[] _allTemplates; + + private final Map _etcItems = new HashMap<>(); + + private final Map _armors = new HashMap<>(); + + private final Map _weapons = new HashMap<>(); + + static { + SLOTS.put("shirt", L2Item.SLOT_UNDERWEAR); + SLOTS.put("lbracelet", L2Item.SLOT_L_BRACELET); + SLOTS.put("rbracelet", L2Item.SLOT_R_BRACELET); + SLOTS.put("talisman", L2Item.SLOT_DECO); + SLOTS.put("chest", L2Item.SLOT_CHEST); + SLOTS.put("fullarmor", L2Item.SLOT_FULL_ARMOR); + SLOTS.put("head", L2Item.SLOT_HEAD); + SLOTS.put("hair", L2Item.SLOT_HAIR); + SLOTS.put("hairall", L2Item.SLOT_HAIRALL); + SLOTS.put("underwear", L2Item.SLOT_UNDERWEAR); + SLOTS.put("back", L2Item.SLOT_BACK); + SLOTS.put("neck", L2Item.SLOT_NECK); + SLOTS.put("legs", L2Item.SLOT_LEGS); + SLOTS.put("feet", L2Item.SLOT_FEET); + SLOTS.put("gloves", L2Item.SLOT_GLOVES); + SLOTS.put("chest,legs", L2Item.SLOT_CHEST | L2Item.SLOT_LEGS); + SLOTS.put("belt", L2Item.SLOT_BELT); + SLOTS.put("rhand", L2Item.SLOT_R_HAND); + SLOTS.put("lhand", L2Item.SLOT_L_HAND); + SLOTS.put("lrhand", L2Item.SLOT_LR_HAND); + SLOTS.put("rear;lear", L2Item.SLOT_R_EAR | L2Item.SLOT_L_EAR); + SLOTS.put("rfinger;lfinger", L2Item.SLOT_R_FINGER | L2Item.SLOT_L_FINGER); + SLOTS.put("wolf", L2Item.SLOT_WOLF); + SLOTS.put("greatwolf", L2Item.SLOT_GREATWOLF); + SLOTS.put("hatchling", L2Item.SLOT_HATCHLING); + SLOTS.put("strider", L2Item.SLOT_STRIDER); + SLOTS.put("babypet", L2Item.SLOT_BABYPET); + SLOTS.put("none", L2Item.SLOT_NONE); + + // retail compatibility + SLOTS.put("onepiece", L2Item.SLOT_FULL_ARMOR); + SLOTS.put("hair2", L2Item.SLOT_HAIR2); + SLOTS.put("dhair", L2Item.SLOT_HAIRALL); + SLOTS.put("alldress", L2Item.SLOT_ALLDRESS); + SLOTS.put("deco1", L2Item.SLOT_DECO); + SLOTS.put("waist", L2Item.SLOT_BELT); + } + + public static ItemTable getInstance() { + return SingletonHolder._instance; + } + + protected ItemTable() { + load(); + } + + private void load() { + final var start = System.currentTimeMillis(); + + int highest = 0; + _armors.clear(); + _etcItems.clear(); + _weapons.clear(); + for (L2Item item : DocumentEngine.getInstance().loadItems()) { + if (highest < item.getId()) { + highest = item.getId(); + } + if (item instanceof L2EtcItem etc) { + _etcItems.put(item.getId(), etc); + } else if (item instanceof L2Armor armor) { + _armors.put(item.getId(), armor); + } else { + _weapons.put(item.getId(), (L2Weapon) item); + } + } + + buildFastLookupTable(highest); + + LOG.info("Loaded {} Etc items.", _etcItems.size()); + LOG.info("Loaded {} Armor items.", _armors.size()); + LOG.info("Loaded {} Weapon items.", _weapons.size()); + LOG.info("Loaded {} items total in {}ms.", (_etcItems.size() + _armors.size() + _weapons.size()), System.currentTimeMillis() - start); + } + + /** + * Builds a variable in which all items are putting in in function of their ID. + * @param size + */ + private void buildFastLookupTable(int size) { + // Create a FastLookUp Table called _allTemplates of size : value of the highest item ID + LOG.info("Highest item Id used {}.", size); + _allTemplates = new L2Item[size + 1]; + + // Insert armor item in Fast Look Up Table + for (L2Armor item : _armors.values()) { + _allTemplates[item.getId()] = item; + } + + // Insert weapon item in Fast Look Up Table + for (L2Weapon item : _weapons.values()) { + _allTemplates[item.getId()] = item; + } + + // Insert etcItem item in Fast Look Up Table + for (L2EtcItem item : _etcItems.values()) { + _allTemplates[item.getId()] = item; + } + } + + /** + * Returns the item corresponding to the item ID + * @param id : int designating the item + * @return L2Item + */ + public L2Item getTemplate(int id) { + if ((id >= _allTemplates.length) || (id < 0)) { + return null; + } + + return _allTemplates[id]; + } + + /** + * Create the L2ItemInstance corresponding to the Item Identifier and quantitiy add logs the activity. Actions : + *
  • Create and Init the L2ItemInstance corresponding to the Item Identifier and quantity
  • + *
  • Add the L2ItemInstance object to _allObjects of L2world
  • + *
  • Logs Item creation according to log settings
  • + * @param process : String Identifier of process triggering this action + * @param itemId : int Item Identifier of the item to be created + * @param count : int Quantity of items to be created for stackable items + * @param actor : L2PcInstance Player requesting the item creation + * @param reference : Object Object referencing current action like NPC selling item or previous item in transformation + * @return L2ItemInstance corresponding to the new item + */ + public L2ItemInstance createItem(String process, int itemId, long count, L2PcInstance actor, Object reference) { + // Create and Init the L2ItemInstance corresponding to the Item Identifier + final var objectId = IdFactory.getInstance().getNextId(); + final var item = new L2ItemInstance(objectId, itemId); + + if (process.equalsIgnoreCase("loot")) { + // loot privilege for raids + if ((reference instanceof L2Attackable raid) && raid.isRaid()) { + // if in CommandChannel and was killing a World/RaidBoss + if ((raid.getFirstCommandChannelAttacked() != null) && !character().autoLootRaids()) { + item.setOwnerId(raid.getFirstCommandChannelAttacked().getLeaderObjectId()); + final var itemLootShedule = ThreadPoolManager.getInstance().scheduleGeneral(new ResetOwner(item), character().getRaidLootRightsInterval()); + item.setItemLootSchedule(itemLootShedule); + } + } else if (!character().autoLoot() || ((reference instanceof L2EventMonsterInstance eventMonster) && eventMonster.eventDropOnGround())) { + item.setOwnerId(actor.getObjectId()); + final var itemLootShedule = ThreadPoolManager.getInstance().scheduleGeneral(new ResetOwner(item), 15000); + item.setItemLootSchedule(itemLootShedule); + } + } + + if (general().debug()) { + LOG.info("Item created object Id {} and item Id {}.", item.getObjectId(), itemId); + } + + // Add the L2ItemInstance object to _allObjects of L2world + L2World.getInstance().storeObject(item); + + // Set Item parameters + if (item.isStackable() && (count > 1)) { + item.setCount(count); + } + + if (general().logItems() && !process.equals("Reset")) { + if (!general().logItemsSmallLog() || (general().logItemsSmallLog() && (item.isEquipable() || (item.getId() == ADENA_ID)))) { + if ((item.getItemType() != ARROW) && (item.getItemType() != SHOT)) { + LOG_ITEM.info("CREATED {} by {}, referenced by {}.", item, actor, reference); + } + } + } + + if (actor != null) { + if (actor.isGM()) { + String referenceName = "no-reference"; + if (reference instanceof L2Object object) { + referenceName = (object.getName() != null ? object.getName() : "no-name"); + } else if (reference instanceof String) { + referenceName = (String) reference; + } + String targetName = (actor.getTarget() != null ? actor.getTarget().getName() : "no-target"); + if (general().gmAudit()) { + GMAudit.auditGMAction(actor.getName() + " [" + actor.getObjectId() + "]", process + "(id: " + itemId + " count: " + count + " name: " + item.getItemName() + " objId: " + item.getObjectId() + ")", targetName, "L2Object referencing this action is: " + referenceName); + } + } + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new ItemCreate(process, item, actor, reference), item.getItem()); + return item; + } + + public L2ItemInstance createItem(String process, int itemId, int count, L2PcInstance actor) { + return createItem(process, itemId, count, actor, null); + } + + /** + * Destroys the L2ItemInstance.
    + * Actions : + *
      + *
    • Sets L2ItemInstance parameters to be unusable
    • + *
    • Removes the L2ItemInstance object to _allObjects of L2world
    • + *
    • Logs Item deletion according to log settings
    • + *
    + * @param process a string identifier of process triggering this action. + * @param item the item instance to be destroyed. + * @param actor the player requesting the item destroy. + * @param reference the object referencing current action like NPC selling item or previous item in transformation. + */ + public void destroyItem(String process, L2ItemInstance item, L2PcInstance actor, Object reference) { + synchronized (item) { + long old = item.getCount(); + item.setCount(0); + item.setOwnerId(0); + item.setItemLocation(ItemLocation.VOID); + item.setLastChange(L2ItemInstance.REMOVED); + + L2World.getInstance().removeObject(item); + IdFactory.getInstance().releaseId(item.getObjectId()); + + if (general().logItems()) { + if (!general().logItemsSmallLog() || (general().logItemsSmallLog() && (item.isEquipable() || (item.getId() == ADENA_ID)))) { + if ((item.getItemType() != ARROW) && (item.getItemType() != SHOT)) { + LOG_ITEM.info("DELETED {} amount {} by {}, referenced by {}.", item, old, actor, reference); + } + } + } + + if (actor != null) { + if (actor.isGM()) { + String referenceName = "no-reference"; + if (reference instanceof L2Object object) { + referenceName = (object.getName() != null ? object.getName() : "no-name"); + } else if (reference instanceof String) { + referenceName = (String) reference; + } + String targetName = (actor.getTarget() != null ? actor.getTarget().getName() : "no-target"); + if (general().gmAudit()) { + GMAudit.auditGMAction(actor.getName() + " [" + actor.getObjectId() + "]", process + "(id: " + item.getId() + " count: " + item.getCount() + " itemObjId: " + item.getObjectId() + ")", targetName, "L2Object referencing this action is: " + referenceName); + } + } + } + + // if it's a pet control item, delete the pet as well + if (item.getItem().isPetItem()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement("DELETE FROM pets WHERE item_obj_id=?")) { + statement.setInt(1, item.getObjectId()); + statement.execute(); + } catch (Exception ex) { + LOG.warn("Could not delete pet object Id {}!", item.getObjectId(), ex); + } + } + } + } + + public void reload() { + load(); + EnchantItemHPBonusData.getInstance().load(); + } + + protected static class ResetOwner implements Runnable { + L2ItemInstance _item; + + public ResetOwner(L2ItemInstance item) { + _item = item; + } + + @Override + public void run() { + _item.setOwnerId(0); + _item.setItemLootSchedule(null); + } + } + + public Set getAllArmorsId() { + return _armors.keySet(); + } + + public Set getAllWeaponsId() { + return _weapons.keySet(); + } + + public int getArraySize() { + return _allTemplates.length; + } + + private static class SingletonHolder { + protected static final ItemTable _instance = new ItemTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/datatables/NpcPersonalAIData.java b/src/main/java/com/l2jserver/gameserver/datatables/NpcPersonalAIData.java new file mode 100644 index 00000000..044685af --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/NpcPersonalAIData.java @@ -0,0 +1,118 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Npc; + +/** + * This class holds parameter, specific to certain NPCs.
    + * It can be either general parameters overridden for certain NPC instance instead of template parameters(aggro range, for example), or some optional parameters, handled by datapack scripts. + * @author GKR + */ +public class NpcPersonalAIData { + private final Map> _AIData = new HashMap<>(); + + /** + * Instantiates a new table. + */ + protected NpcPersonalAIData() { + } + + /** + * Stores data for given spawn. + * @param spawnDat spawn to process + * @param data Map of AI values + */ + public void storeData(L2Spawn spawnDat, Map data) { + if ((data != null) && !data.isEmpty()) { + // check for spawn name. Since spawn name is key for AI Data, generate random name, if spawn name isn't specified + if (spawnDat.getName() == null) { + spawnDat.setName(Long.toString(Rnd.nextLong())); + } + + _AIData.put(spawnDat.getName(), data); + } + } + + /** + * Gets AI value with given spawnName and paramName + * @param spawnName spawn name to check + * @param paramName parameter to check + * @return value of given parameter for given spawn name + */ + public int getAIValue(final String spawnName, final String paramName) { + if (_AIData.containsKey(spawnName)) { + return _AIData.get(spawnName).getOrDefault(paramName, -1); + } + return -1; + } + + /** + * Verifies if there is AI value with given spawnName and paramName + * @param spawnName spawn name to check + * @param paramName parameter name to check + * @return {@code true} if parameter paramName is set for spawn spawnName, {@code false} otherwise + */ + public boolean hasAIValue(String spawnName, String paramName) { + return (spawnName != null) && _AIData.containsKey(spawnName) && _AIData.get(spawnName).containsKey(paramName); + } + + /** + * Initializes npc parameters by specified values. + * @param npc NPC to process + * @param spawn link to NPC's spawn + * @param spawnName name of spawn + */ + public void initializeNpcParameters(L2Npc npc, L2Spawn spawn, String spawnName) { + if (_AIData.containsKey(spawnName)) { + Map map = _AIData.get(spawnName); + + try { + for (String key : map.keySet()) { + switch (key) { + case "disableRandomAnimation" -> npc.setRandomAnimationEnabled((map.get(key) == 0)); + case "disableRandomWalk" -> { + npc.setIsNoRndWalk((map.get(key) == 1)); + spawn.setIsNoRndWalk((map.get(key) == 1)); + } + } + } + } catch (Exception e) { + // Do nothing + } + } + } + + /** + * Gets the single instance of NpcTable. + * @return single instance of NpcTable + */ + public static NpcPersonalAIData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final NpcPersonalAIData _instance = new NpcPersonalAIData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/datatables/SkillData.java b/src/main/java/com/l2jserver/gameserver/datatables/SkillData.java new file mode 100644 index 00000000..713d3486 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/SkillData.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.engines.DocumentEngine; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Skill data. + */ +public final class SkillData { + + private static final Logger LOG = LoggerFactory.getLogger(SkillData.class); + + private final Map _skills = new HashMap<>(); + private final Map _skillMaxLevel = new HashMap<>(); + private final Set _enchantable = new HashSet<>(); + + protected SkillData() { + load(); + } + + public void reload() { + load(); + // Reload Skill Tree as well. + SkillTreesData.getInstance().load(); + } + + private void load() { + final var start = System.currentTimeMillis(); + + final var temp = DocumentEngine.getInstance().loadAllSkills(); + + _skills.clear(); + _skills.putAll(temp); + + _skillMaxLevel.clear(); + _enchantable.clear(); + for (Skill skill : _skills.values()) { + final int skillId = skill.getId(); + final int skillLvl = skill.getLevel(); + if (skillLvl > 99) { + _enchantable.add(skillId); + continue; + } + + // only non-enchanted skills + final int maxLvl = getMaxLevel(skillId); + if (skillLvl > maxLvl) { + _skillMaxLevel.put(skillId, skillLvl); + } + } + + LOG.info("Loaded {} skill templates from XML files in {}ms.", _skills.size(), System.currentTimeMillis() - start); + } + + /** + * Provides the skill hash + * @param skill The L2Skill to be hashed + * @return getSkillHashCode(skill.getId(), skill.getLevel()) + */ + public static int getSkillHashCode(Skill skill) { + return getSkillHashCode(skill.getId(), skill.getLevel()); + } + + /** + * Centralized method for easier change of the hashing sys + * @param skillId The Skill Id + * @param skillLevel The Skill Level + * @return The Skill hash number + */ + public static int getSkillHashCode(int skillId, int skillLevel) { + return (skillId * 1021) + skillLevel; + } + + public Skill getSkill(int skillId, int level) { + final Skill result = _skills.get(getSkillHashCode(skillId, level)); + if (result != null) { + return result; + } + + // skill/level not found, fix for transformation scripts + final int maxLvl = getMaxLevel(skillId); + // requested level too high + if ((maxLvl > 0) && (level > maxLvl)) { + if (general().debug()) { + LOG.warn("Call to unexisting skill level Id {} requested level {} max level {}!", skillId, level, maxLvl); + } + return _skills.get(getSkillHashCode(skillId, maxLvl)); + } + + LOG.warn("No skill info found for skill Id {} and skill level {}!", skillId, level); + return null; + } + + public int getMaxLevel(int skillId) { + final Integer maxLevel = _skillMaxLevel.get(skillId); + return maxLevel != null ? maxLevel : 0; + } + + /** + * Verifies if the given skill ID correspond to an enchantable skill. + * @param skillId the skill ID + * @return {@code true} if the skill is enchantable, {@code false} otherwise + */ + public boolean isEnchantable(int skillId) { + return _enchantable.contains(skillId); + } + + /** + * @param addNoble + * @param hasCastle + * @return an array with siege skills. If addNoble == true, will add also Advanced headquarters. + */ + public Skill[] getSiegeSkills(boolean addNoble, boolean hasCastle) { + Skill[] temp = new Skill[2 + (addNoble ? 1 : 0) + (hasCastle ? 2 : 0)]; + int i = 0; + temp[i++] = _skills.get(SkillData.getSkillHashCode(246, 1)); + temp[i++] = _skills.get(SkillData.getSkillHashCode(247, 1)); + + if (addNoble) { + temp[i++] = _skills.get(SkillData.getSkillHashCode(326, 1)); + } + if (hasCastle) { + temp[i++] = _skills.get(SkillData.getSkillHashCode(844, 1)); + temp[i] = _skills.get(SkillData.getSkillHashCode(845, 1)); + } + return temp; + } + + public static SkillData getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final SkillData _instance = new SkillData(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/datatables/SpawnTable.java b/src/main/java/com/l2jserver/gameserver/datatables/SpawnTable.java new file mode 100644 index 00000000..76de6a13 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/datatables/SpawnTable.java @@ -0,0 +1,441 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.datatables; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.instancemanager.DayNightSpawnManager; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Spawn data retriever. + * @author Zoey76 + */ +public final class SpawnTable implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(SpawnTable.class); + + private static final String SELECT_SPAWNS = "SELECT count, npc_templateid, locx, locy, locz, heading, respawn_delay, respawn_random, loc_id, periodOfDay FROM spawnlist"; + + private static final String SELECT_CUSTOM_SPAWNS = "SELECT count, npc_templateid, locx, locy, locz, heading, respawn_delay, respawn_random, loc_id, periodOfDay FROM custom_spawnlist"; + + private static final Map> _spawnTable = new ConcurrentHashMap<>(); + + private int _xmlSpawnCount = 0; + + @Override + public void load() { + if (!general().noSpawns()) { + fillSpawnTable(false); + final int spawnCount = _spawnTable.size(); + LOG.info("Loaded {} NPC spawns.", spawnCount); + + if (general().customSpawnlistTable()) { + fillSpawnTable(true); + LOG.info("Loaded {} custom NPC spawns.", (_spawnTable.size() - spawnCount)); + } + + // Load XML list + parseDatapackDirectory("data/spawnlist", false); + LOG.info("Loaded {} NPC spawns from XML.", _xmlSpawnCount); + } + } + + /** + * Verifies if the template exists and it's spawnable. + * @param npcId the NPC ID + * @return {@code true} if the NPC ID belongs to an spawnable template, {@code false} otherwise + */ + private boolean checkTemplate(int npcId) { + L2NpcTemplate npcTemplate = NpcData.getInstance().getTemplate(npcId); + if (npcTemplate == null) { + LOG.warn("Data missing in NPC table for ID {}!", npcId); + return false; + } + + // Don't spawn + return !npcTemplate.isType("L2SiegeGuard") && !npcTemplate.isType("L2RaidBoss"); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + for (Node list = doc.getFirstChild(); list != null; list = list.getNextSibling()) { + if (list.getNodeName().equalsIgnoreCase("list")) { + attrs = list.getAttributes(); + // skip disabled spawnlists + if (!Boolean.parseBoolean(attrs.getNamedItem("enabled").getNodeValue())) { + continue; + } + for (Node param = list.getFirstChild(); param != null; param = param.getNextSibling()) { + attrs = param.getAttributes(); + if (param.getNodeName().equalsIgnoreCase("spawn")) { + String territoryName = null; + String spawnName = null; + Map map = null; + + // Check, if spawn name specified + if (attrs.getNamedItem("name") != null) { + spawnName = parseString(attrs, "name"); + } + // Check, if spawn territory specified and exists + if ((attrs.getNamedItem("zone") != null) && (ZoneManager.getInstance().getSpawnTerritory(attrs.getNamedItem("zone").getNodeValue()) != null)) { + territoryName = parseString(attrs, "zone"); + } + + for (Node npctag = param.getFirstChild(); npctag != null; npctag = npctag.getNextSibling()) { + attrs = npctag.getAttributes(); + // Check if there are any AI parameters + if (npctag.getNodeName().equalsIgnoreCase("AIData")) { + attrs = npctag.getAttributes(); + if (map == null) { + map = new HashMap<>(); + } + for (Node c = npctag.getFirstChild(); c != null; c = c.getNextSibling()) { + // Skip odd nodes + if (c.getNodeName().equals("#text")) { + continue; + } + int val = switch (c.getNodeName()) { + case "disableRandomAnimation", "disableRandomWalk" -> Boolean.parseBoolean(c.getTextContent()) ? 1 : 0; + default -> Integer.parseInt(c.getTextContent()); + }; + map.put(c.getNodeName(), val); + } + } + // Check for NPC spawns + else if (npctag.getNodeName().equalsIgnoreCase("npc")) { + // mandatory + final int templateId = parseInteger(attrs, "id"); + // coordinates are optional, if territory is specified; mandatory otherwise + int x = 0; + int y = 0; + int z = 0; + + try { + x = parseInteger(attrs, "x"); + y = parseInteger(attrs, "y"); + z = parseInteger(attrs, "z"); + } catch (NullPointerException npe) { + // x, y, z can be unspecified, if this spawn is territory based, do nothing + } + + if ((x == 0) && (y == 0) && (territoryName == null)) // Both coordinates and zone are unspecified + { + LOG.warn("Spawn could not be initialized, both coordinates and zone are unspecified for ID {}!", templateId); + continue; + } + + StatsSet spawnInfo = new StatsSet(); + spawnInfo.set("npcTemplateid", templateId); + spawnInfo.set("x", x); + spawnInfo.set("y", y); + spawnInfo.set("z", z); + spawnInfo.set("territoryName", territoryName); + spawnInfo.set("spawnName", spawnName); + + // trying to read optional parameters + if (attrs.getNamedItem("heading") != null) { + spawnInfo.set("heading", parseInteger(attrs, "heading")); + } + + if (attrs.getNamedItem("count") != null) { + spawnInfo.set("count", parseInteger(attrs, "count")); + } + + if (attrs.getNamedItem("respawnDelay") != null) { + spawnInfo.set("respawnDelay", parseInteger(attrs, "respawnDelay")); + } + + if (attrs.getNamedItem("respawnRandom") != null) { + spawnInfo.set("respawnRandom", parseInteger(attrs, "respawnRandom")); + } + + if (attrs.getNamedItem("periodOfDay") != null) { + String period = attrs.getNamedItem("periodOfDay").getNodeValue(); + if (period.equalsIgnoreCase("day") || period.equalsIgnoreCase("night")) { + spawnInfo.set("periodOfDay", period.equalsIgnoreCase("day") ? 1 : 2); + } + } + + _xmlSpawnCount += addSpawn(spawnInfo, map); + } + } + } + } + } + } + } + + /** + * Retrieves spawn data from database. + * @param isCustom if {@code true} the spawns are loaded as custom from custom spawn table + * @return the spawn count + */ + private int fillSpawnTable(boolean isCustom) { + int npcSpawnCount = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(isCustom ? SELECT_CUSTOM_SPAWNS : SELECT_SPAWNS)) { + while (rs.next()) { + StatsSet spawnInfo = new StatsSet(); + int npcId = rs.getInt("npc_templateid"); + + // Check basic requirements first + if (!checkTemplate(npcId)) { + // Don't spawn + continue; + } + + spawnInfo.set("npcTemplateid", npcId); + spawnInfo.set("count", rs.getInt("count")); + spawnInfo.set("x", rs.getInt("locx")); + spawnInfo.set("y", rs.getInt("locy")); + spawnInfo.set("z", rs.getInt("locz")); + spawnInfo.set("heading", rs.getInt("heading")); + spawnInfo.set("respawnDelay", rs.getInt("respawn_delay")); + spawnInfo.set("respawnRandom", rs.getInt("respawn_random")); + spawnInfo.set("locId", rs.getInt("loc_id")); + spawnInfo.set("periodOfDay", rs.getInt("periodOfDay")); + spawnInfo.set("isCustomSpawn", isCustom); + npcSpawnCount += addSpawn(spawnInfo); + } + } catch (Exception ex) { + LOG.warn("Spawn could not be initialized!", ex); + } + return npcSpawnCount; + } + + /** + * Creates NPC spawn + * @param spawnInfo StatsSet of spawn parameters + * @param AIData Map of specific AI parameters for this spawn + * @return count NPC instances, spawned by this spawn + */ + private int addSpawn(StatsSet spawnInfo, Map AIData) { + int ret = 0; + try { + final var spawnDat = new L2Spawn(spawnInfo.getInt("npcTemplateid")); + spawnDat.setAmount(spawnInfo.getInt("count", 1)); + spawnDat.setX(spawnInfo.getInt("x", 0)); + spawnDat.setY(spawnInfo.getInt("y", 0)); + spawnDat.setZ(spawnInfo.getInt("z", 0)); + spawnDat.setHeading(spawnInfo.getInt("heading", -1)); + spawnDat.setRespawnDelay(spawnInfo.getInt("respawnDelay", 0), spawnInfo.getInt("respawnRandom", 0)); + spawnDat.setLocationId(spawnInfo.getInt("locId", 0)); + String territoryName = spawnInfo.getString("territoryName", ""); + String spawnName = spawnInfo.getString("spawnName", ""); + spawnDat.setCustom(spawnInfo.getBoolean("isCustomSpawn", false)); + if (!spawnName.isEmpty()) { + spawnDat.setName(spawnName); + } + if (!territoryName.isEmpty()) { + spawnDat.setSpawnTerritory(ZoneManager.getInstance().getSpawnTerritory(territoryName)); + } + // Register AI Data for this spawn + NpcPersonalAIData.getInstance().storeData(spawnDat, AIData); + switch (spawnInfo.getInt("periodOfDay", 0)) { + // default + case 0 -> ret += spawnDat.init(); + // Day + case 1 -> { + DayNightSpawnManager.getInstance().addDayCreature(spawnDat); + ret = 1; + } + // Night + case 2 -> { + DayNightSpawnManager.getInstance().addNightCreature(spawnDat); + ret = 1; + } + } + + addSpawn(spawnDat); + } catch (Exception ex) { + LOG.warn("Spawn could not be initialized!", ex); + } + return ret; + } + + /** + * Wrapper for {@link #addSpawn(StatsSet, Map)}. + * @param spawnInfo StatsSet of spawn parameters + * @return count NPC instances, spawned by this spawn + */ + private int addSpawn(StatsSet spawnInfo) { + return addSpawn(spawnInfo, null); + } + + /** + * Gets the spawn data. + * @return the spawn data + */ + public Map> getSpawnTable() { + return _spawnTable; + } + + /** + * Gets the spawns for the NPC Id. + * @param npcId the NPC Id + * @return the spawn set for the given npcId + */ + public Set getSpawns(int npcId) { + return _spawnTable.getOrDefault(npcId, Collections.emptySet()); + } + + /** + * Gets the spawn count for the given NPC ID. + * @param npcId the NPC Id + * @return the spawn count + */ + public int getSpawnCount(int npcId) { + return getSpawns(npcId).size(); + } + + /** + * Finds a spawn for the given NPC ID. + * @param npcId the NPC Id + * @return a spawn for the given NPC ID or {@code null} + */ + public L2Spawn findAny(int npcId) { + return getSpawns(npcId).stream().findFirst().orElse(null); + } + + /** + * Adds a new spawn to the spawn table. + * @param spawn the spawn to add + * @param storeInDb if {@code true} it'll be saved in the database + */ + public void addNewSpawn(L2Spawn spawn, boolean storeInDb) { + addSpawn(spawn); + + if (storeInDb) { + final String spawnTable = spawn.isCustom() && general().customSpawnlistTable() ? "custom_spawnlist" : "spawnlist"; + try (var con = ConnectionFactory.getInstance().getConnection(); + var insert = con.prepareStatement("INSERT INTO " + spawnTable + "(count,npc_templateid,locx,locy,locz,heading,respawn_delay,respawn_random,loc_id) values(?,?,?,?,?,?,?,?,?)")) { + insert.setInt(1, spawn.getAmount()); + insert.setInt(2, spawn.getId()); + insert.setInt(3, spawn.getX()); + insert.setInt(4, spawn.getY()); + insert.setInt(5, spawn.getZ()); + insert.setInt(6, spawn.getHeading()); + insert.setInt(7, (int) MILLISECONDS.toSeconds(spawn.getRespawnDelay())); + insert.setInt(8, spawn.getRespawnMaxDelay() - spawn.getRespawnMinDelay()); + insert.setInt(9, spawn.getLocationId()); + insert.execute(); + } catch (Exception ex) { + LOG.warn("Could not store spawn in the DB!", ex); + } + } + } + + /** + * Delete an spawn from the spawn table. + * @param spawn the spawn to delete + * @param updateDb if {@code true} database will be updated + */ + public void deleteSpawn(L2Spawn spawn, boolean updateDb) { + if (!removeSpawn(spawn)) { + return; + } + + if (updateDb) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var delete = con.prepareStatement("DELETE FROM " + (spawn.isCustom() ? "custom_spawnlist" : "spawnlist") + " WHERE locx=? AND locy=? AND locz=? AND npc_templateid=? AND heading=?")) { + delete.setInt(1, spawn.getX()); + delete.setInt(2, spawn.getY()); + delete.setInt(3, spawn.getZ()); + delete.setInt(4, spawn.getId()); + delete.setInt(5, spawn.getHeading()); + delete.execute(); + } catch (Exception ex) { + LOG.warn("Spawn {} could not be removed from DB!", spawn, ex); + } + } + } + + /** + * Add a spawn to the spawn set if present, otherwise add a spawn set and add the spawn to the newly created spawn set. + * @param spawn the NPC spawn to add + */ + private void addSpawn(L2Spawn spawn) { + _spawnTable.computeIfAbsent(spawn.getId(), _ -> ConcurrentHashMap.newKeySet(1)).add(spawn); + } + + /** + * Remove a spawn from the spawn set, if the spawn set is empty, remove it as well. + * @param spawn the NPC spawn to remove + * @return {@code true} if the spawn was successfully removed, {@code false} otherwise + */ + private boolean removeSpawn(L2Spawn spawn) { + final Set set = _spawnTable.get(spawn.getId()); + if (set != null) { + boolean removed = set.remove(spawn); + if (set.isEmpty()) { + _spawnTable.remove(spawn.getId()); + } + return removed; + } + return false; + } + + /** + * Execute a procedure over all spawns.
    + * Do not use it! + * @param function the function to execute + * @return {@code true} if all procedures were executed, {@code false} otherwise + */ + public boolean forEachSpawn(Function function) { + for (Set set : _spawnTable.values()) { + for (L2Spawn spawn : set) { + if (!function.apply(spawn)) { + return false; + } + } + } + return true; + } + + public static SpawnTable getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final SpawnTable INSTANCE = new SpawnTable(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/engines/DocumentBase.java b/src/main/java/com/l2jserver/gameserver/engines/DocumentBase.java new file mode 100644 index 00000000..32079ad0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/engines/DocumentBase.java @@ -0,0 +1,956 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.engines; + +import static java.util.stream.Collectors.toSet; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.StringTokenizer; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.CategoryType; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.base.PlayerState; +import com.l2jserver.gameserver.model.conditions.Condition; +import com.l2jserver.gameserver.model.conditions.ConditionCategoryType; +import com.l2jserver.gameserver.model.conditions.ConditionChangeWeapon; +import com.l2jserver.gameserver.model.conditions.ConditionCheckAbnormal; +import com.l2jserver.gameserver.model.conditions.ConditionGameChance; +import com.l2jserver.gameserver.model.conditions.ConditionGameTime; +import com.l2jserver.gameserver.model.conditions.ConditionGameTime.CheckGameTime; +import com.l2jserver.gameserver.model.conditions.ConditionLogicAnd; +import com.l2jserver.gameserver.model.conditions.ConditionLogicNot; +import com.l2jserver.gameserver.model.conditions.ConditionLogicOr; +import com.l2jserver.gameserver.model.conditions.ConditionMinDistance; +import com.l2jserver.gameserver.model.conditions.ConditionOpCompanion; +import com.l2jserver.gameserver.model.conditions.ConditionOpExistNpc; +import com.l2jserver.gameserver.model.conditions.ConditionOpResurrection; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerActiveEffectId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerActiveSkillId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerAgathionEnergy; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerAgathionId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCallPc; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanCreateBase; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanCreateOutpost; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanEscape; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanRefuelAirship; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanSummon; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanSummonSiegeGolem; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanSweep; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanTakeCastle; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanTakeFort; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanTransform; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCanUntransform; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCharges; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerClassIdRestriction; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCloakStatus; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerCp; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerFlyMounted; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerGrade; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasAgathion; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasCastle; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasClanHall; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasFort; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasPet; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHasServitor; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerHp; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerInsideZoneId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerInstanceId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerInvSize; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerIsClanLeader; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerIsHero; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerLandingZone; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerLevel; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerLevelRange; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerMp; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerPkCount; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerPledgeClass; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerRace; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerRangeFromNpc; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerSex; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerSiegeSide; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerSouls; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerState; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerSubclass; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerTransformationId; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerTvTEvent; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerVehicleMounted; +import com.l2jserver.gameserver.model.conditions.ConditionPlayerWeight; +import com.l2jserver.gameserver.model.conditions.ConditionSiegeZone; +import com.l2jserver.gameserver.model.conditions.ConditionSlotItemId; +import com.l2jserver.gameserver.model.conditions.ConditionTargetAbnormal; +import com.l2jserver.gameserver.model.conditions.ConditionTargetActiveEffectId; +import com.l2jserver.gameserver.model.conditions.ConditionTargetActiveSkillId; +import com.l2jserver.gameserver.model.conditions.ConditionTargetAggro; +import com.l2jserver.gameserver.model.conditions.ConditionTargetClassIdRestriction; +import com.l2jserver.gameserver.model.conditions.ConditionTargetInvSize; +import com.l2jserver.gameserver.model.conditions.ConditionTargetLevel; +import com.l2jserver.gameserver.model.conditions.ConditionTargetLevelRange; +import com.l2jserver.gameserver.model.conditions.ConditionTargetMyParty; +import com.l2jserver.gameserver.model.conditions.ConditionTargetNpcId; +import com.l2jserver.gameserver.model.conditions.ConditionTargetNpcType; +import com.l2jserver.gameserver.model.conditions.ConditionTargetPlayable; +import com.l2jserver.gameserver.model.conditions.ConditionTargetRace; +import com.l2jserver.gameserver.model.conditions.ConditionTargetUsesWeaponKind; +import com.l2jserver.gameserver.model.conditions.ConditionTargetWeight; +import com.l2jserver.gameserver.model.conditions.ConditionUsingItemType; +import com.l2jserver.gameserver.model.conditions.ConditionUsingSkill; +import com.l2jserver.gameserver.model.conditions.ConditionUsingSlotType; +import com.l2jserver.gameserver.model.conditions.ConditionWithSkill; +import com.l2jserver.gameserver.model.effects.AbstractEffect; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.type.ArmorType; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.functions.FuncTemplate; + +/** + * Document Base. + * @author mkizub + */ +public abstract class DocumentBase { + + private static final Logger LOG = LoggerFactory.getLogger(DocumentBase.class); + + private final File _file; + + protected final Map _tables = new HashMap<>(); + + protected DocumentBase(File pFile) { + _file = pFile; + } + + public Document parse() { + Document doc = null; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + doc = factory.newDocumentBuilder().parse(_file); + parseDocument(doc); + } catch (Exception ex) { + LOG.error("Error loading file {}!", _file, ex); + } + return doc; + } + + protected abstract void parseDocument(Document doc); + + protected abstract StatsSet getStatsSet(); + + protected abstract String getTableValue(String name); + + protected abstract String getTableValue(String name, int idx); + + protected void resetTable() { + _tables.clear(); + } + + protected void setTable(String name, String[] table) { + _tables.put(name, table); + } + + protected void parseTemplate(Node n, Object template) { + parseTemplate(n, template, null); + } + + protected void parseTemplate(Node n, Object template, EffectScope effectScope) { + Condition condition = null; + n = n.getFirstChild(); + if (n == null) { + return; + } + if ("cond".equalsIgnoreCase(n.getNodeName())) { + condition = parseCondition(n.getFirstChild(), template); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + n = n.getNextSibling(); + } + for (; n != null; n = n.getNextSibling()) { + final String name = n.getNodeName().toLowerCase(); + + switch (name) { + case "effect" -> { + if (template instanceof AbstractEffect) { + throw new RuntimeException("Nested effects"); + } + attachEffect(n, template, condition, effectScope); + } + case "add", "sub", "mul", "div", "set", "share", "enchant", "enchanthp" -> attachFunc(n, template, name, condition); + } + } + } + + protected void attachFunc(Node n, Object template, String functionName, Condition attachCond) { + Stats stat = Stats.valueOfXml(n.getAttributes().getNamedItem("stat").getNodeValue()); + int order = -1; + final Node orderNode = n.getAttributes().getNamedItem("order"); + if (orderNode != null) { + order = Integer.parseInt(orderNode.getNodeValue()); + } + + String valueString = n.getAttributes().getNamedItem("val").getNodeValue(); + double value; + if (valueString.charAt(0) == '#') { + value = Double.parseDouble(getTableValue(valueString)); + } else { + value = Double.parseDouble(valueString); + } + + final Condition applyCond = parseCondition(n.getFirstChild(), template); + final FuncTemplate ft = new FuncTemplate(attachCond, applyCond, functionName, order, stat, value); + if (template instanceof L2Item item) { + item.attach(ft); + } else if (template instanceof AbstractEffect effect) { + effect.attach(ft); + } else { + throw new RuntimeException("Attaching stat to a non-effect template!!!"); + } + } + + protected void attachEffect(Node n, Object template, Condition attachCond) { + attachEffect(n, template, attachCond, null); + } + + protected void attachEffect(Node n, Object template, Condition attachCond, EffectScope effectScope) { + final NamedNodeMap attrs = n.getAttributes(); + final StatsSet set = new StatsSet(); + for (int i = 0; i < attrs.getLength(); i++) { + Node att = attrs.item(i); + set.set(att.getNodeName(), getValue(att.getNodeValue(), template)); + } + + final StatsSet parameters = parseParameters(n.getFirstChild(), template); + final Condition applyCond = parseCondition(n.getFirstChild(), template); + + if (template instanceof IIdentifiable id) { + set.set("id", id.getId()); + } + + final AbstractEffect effect = AbstractEffect.createEffect(attachCond, applyCond, set, parameters); + parseTemplate(n, effect); + if (template instanceof L2Item) { + LOG.error("Item {} with effects!", template); + } else if (template instanceof Skill skill) { + if (effectScope != null) { + skill.addEffect(effectScope, effect); + } else if (skill.isPassive()) { + skill.addEffect(EffectScope.PASSIVE, effect); + } else { + skill.addEffect(EffectScope.GENERAL, effect); + } + } + } + + /** + * Parse effect's parameters. + * @param n the node to start the parsing + * @param template the effect template + * @return the list of parameters if any, {@code null} otherwise + */ + private StatsSet parseParameters(Node n, Object template) { + StatsSet parameters = null; + while ((n != null)) { + // Parse all parameters. + if ((n.getNodeType() == Node.ELEMENT_NODE) && "param".equals(n.getNodeName())) { + if (parameters == null) { + parameters = new StatsSet(); + } + NamedNodeMap params = n.getAttributes(); + for (int i = 0; i < params.getLength(); i++) { + Node att = params.item(i); + parameters.set(att.getNodeName(), getValue(att.getNodeValue(), template)); + } + } + n = n.getNextSibling(); + } + return parameters == null ? StatsSet.EMPTY_STATSET : parameters; + } + + protected Condition parseCondition(Node n, Object template) { + while ((n != null) && (n.getNodeType() != Node.ELEMENT_NODE)) { + n = n.getNextSibling(); + } + + Condition condition = null; + if (n != null) { + switch (n.getNodeName().toLowerCase()) { + case "and" -> condition = parseLogicAnd(n, template); + case "or" -> condition = parseLogicOr(n, template); + case "not" -> condition = parseLogicNot(n, template); + case "player" -> condition = parsePlayerCondition(n, template); + case "target" -> condition = parseTargetCondition(n, template); + case "using" -> condition = parseUsingCondition(n); + case "game" -> condition = parseGameCondition(n); + } + } + return condition; + } + + protected Condition parseLogicAnd(Node n, Object template) { + ConditionLogicAnd cond = new ConditionLogicAnd(); + for (n = n.getFirstChild(); n != null; n = n.getNextSibling()) { + if (n.getNodeType() == Node.ELEMENT_NODE) { + cond.add(parseCondition(n, template)); + } + } + if ((cond.conditions == null) || (cond.conditions.length == 0)) { + LOG.error("Empty condition in file {}!", _file); + } + return cond; + } + + protected Condition parseLogicOr(Node n, Object template) { + ConditionLogicOr cond = new ConditionLogicOr(); + for (n = n.getFirstChild(); n != null; n = n.getNextSibling()) { + if (n.getNodeType() == Node.ELEMENT_NODE) { + cond.add(parseCondition(n, template)); + } + } + if ((cond.conditions == null) || (cond.conditions.length == 0)) { + LOG.error("Empty condition in file {}!", _file); + } + return cond; + } + + protected Condition parseLogicNot(Node n, Object template) { + for (n = n.getFirstChild(); n != null; n = n.getNextSibling()) { + if (n.getNodeType() == Node.ELEMENT_NODE) { + return new ConditionLogicNot(parseCondition(n, template)); + } + } + LOG.error("Empty condition in file {}!", _file); + return null; + } + + protected Condition parsePlayerCondition(Node n, Object template) { + Condition cond = null; + NamedNodeMap attrs = n.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + switch (a.getNodeName().toLowerCase()) { + case "races" -> { + final String[] racesVal = a.getNodeValue().split(","); + final Race[] races = new Race[racesVal.length]; + for (int r = 0; r < racesVal.length; r++) { + if (racesVal[r] != null) { + races[r] = Race.valueOf(racesVal[r]); + } + } + cond = joinAnd(cond, new ConditionPlayerRace(races)); + } + case "level" -> { + int lvl = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerLevel(lvl)); + } + case "levelrange" -> { + String[] range = getValue(a.getNodeValue(), template).split(";"); + if (range.length == 2) { + final int minimumLevel = Integer.decode(getValue(a.getNodeValue(), template).split(";")[0]); + final int maximumLevel = Integer.decode(getValue(a.getNodeValue(), template).split(";")[1]); + cond = joinAnd(cond, new ConditionPlayerLevelRange(minimumLevel, maximumLevel)); + } + } + case "resting" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.RESTING, val)); + } + case "flying" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.FLYING, val)); + } + case "moving" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.MOVING, val)); + } + case "running" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.RUNNING, val)); + } + case "standing" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.STANDING, val)); + } + case "behind" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.BEHIND, val)); + } + case "front" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.FRONT, val)); + } + case "chaotic" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.CHAOTIC, val)); + } + case "olympiad" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerState(PlayerState.OLYMPIAD, val)); + } + case "ishero" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerIsHero(val)); + } + case "transformationid" -> { + int id = Integer.parseInt(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerTransformationId(id)); + } + case "hp" -> { + int hp = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerHp(hp)); + } + case "mp" -> { + int hp = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerMp(hp)); + } + case "cp" -> { + int cp = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerCp(cp)); + } + case "grade" -> { + int expIndex = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerGrade(expIndex)); + } + case "pkcount" -> { + int expIndex = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerPkCount(expIndex)); + } + case "siegezone" -> { + int value = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionSiegeZone(value, true)); + } + case "siegeside" -> { + int value = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerSiegeSide(value)); + } + case "charges" -> { + int value = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerCharges(value)); + } + case "souls" -> { + int value = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerSouls(value)); + } + case "weight" -> { + int weight = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerWeight(weight)); + } + case "invsize" -> { + int size = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerInvSize(size)); + } + case "isclanleader" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerIsClanLeader(val)); + } + case "ontvtevent" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerTvTEvent(val)); + } + case "pledgeclass" -> { + int pledgeClass = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerPledgeClass(pledgeClass)); + } + case "clanhall" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + ArrayList array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionPlayerHasClanHall(array)); + } + case "fort" -> { + int fort = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerHasFort(fort)); + } + case "castle" -> { + int castle = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerHasCastle(castle)); + } + case "sex" -> { + int sex = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionPlayerSex(sex)); + } + case "flymounted" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerFlyMounted(val)); + } + case "vehiclemounted" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerVehicleMounted(val)); + } + case "landingzone" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerLandingZone(val)); + } + case "active_effect_id" -> { + int effectId = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerActiveEffectId(effectId)); + } + case "active_effect_id_lvl" -> { + String val = getValue(a.getNodeValue(), template); + int effect_id = Integer.decode(getValue(val.split(",")[0], template)); + int effect_lvl = Integer.decode(getValue(val.split(",")[1], template)); + cond = joinAnd(cond, new ConditionPlayerActiveEffectId(effect_id, effect_lvl)); + } + case "active_skill_id" -> { + int skill_id = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionPlayerActiveSkillId(skill_id)); + } + case "active_skill_id_lvl" -> { + String val = getValue(a.getNodeValue(), template); + int skill_id = Integer.decode(getValue(val.split(",")[0], template)); + int skill_lvl = Integer.decode(getValue(val.split(",")[1], template)); + cond = joinAnd(cond, new ConditionPlayerActiveSkillId(skill_id, skill_lvl)); + } + case "class_id_restriction" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + ArrayList array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionPlayerClassIdRestriction(array)); + } + case "subclass" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerSubclass(val)); + } + case "instanceid" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + ArrayList array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionPlayerInstanceId(array)); + } + case "agathionid" -> { + int agathionId = Integer.decode(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerAgathionId(agathionId)); + } + case "cloakstatus" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionPlayerCloakStatus(val)); + } + case "haspet" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + ArrayList array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionPlayerHasPet(array)); + } + case "hasservitor" -> cond = joinAnd(cond, new ConditionPlayerHasServitor()); + case "npcidradius" -> { + final StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + if (st.countTokens() == 3) { + final String[] ids = st.nextToken().split(";"); + final int[] npcIds = new int[ids.length]; + for (int index = 0; index < ids.length; index++) { + npcIds[index] = Integer.parseInt(getValue(ids[index], template)); + } + final int radius = Integer.parseInt(st.nextToken()); + final boolean val = Boolean.parseBoolean(st.nextToken()); + cond = joinAnd(cond, new ConditionPlayerRangeFromNpc(npcIds, radius, val)); + } + } + case "callpc" -> cond = joinAnd(cond, new ConditionPlayerCallPc(Boolean.parseBoolean(a.getNodeValue()))); + case "cancreatebase" -> cond = joinAnd(cond, new ConditionPlayerCanCreateBase(Boolean.parseBoolean(a.getNodeValue()))); + case "cancreateoutpost" -> cond = joinAnd(cond, new ConditionPlayerCanCreateOutpost(Boolean.parseBoolean(a.getNodeValue()))); + case "canescape" -> cond = joinAnd(cond, new ConditionPlayerCanEscape(Boolean.parseBoolean(a.getNodeValue()))); + case "canrefuelairship" -> cond = joinAnd(cond, new ConditionPlayerCanRefuelAirship(Integer.parseInt(a.getNodeValue()))); + case "canresurrect" -> cond = joinAnd(cond, new ConditionOpResurrection()); + case "cansummon" -> cond = joinAnd(cond, new ConditionPlayerCanSummon(Boolean.parseBoolean(a.getNodeValue()))); + case "cansummonsiegegolem" -> cond = joinAnd(cond, new ConditionPlayerCanSummonSiegeGolem(Boolean.parseBoolean(a.getNodeValue()))); + case "cansweep" -> cond = joinAnd(cond, new ConditionPlayerCanSweep(Boolean.parseBoolean(a.getNodeValue()))); + case "cantakecastle" -> cond = joinAnd(cond, new ConditionPlayerCanTakeCastle()); + case "cantakefort" -> cond = joinAnd(cond, new ConditionPlayerCanTakeFort(Boolean.parseBoolean(a.getNodeValue()))); + case "cantransform" -> cond = joinAnd(cond, new ConditionPlayerCanTransform(Boolean.parseBoolean(a.getNodeValue()))); + case "canuntransform" -> cond = joinAnd(cond, new ConditionPlayerCanUntransform(Boolean.parseBoolean(a.getNodeValue()))); + case "insidezoneid" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + List array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionPlayerInsideZoneId(array)); + } + case "checkabnormal" -> { + final String value = a.getNodeValue(); + if (value.contains(";")) { + final String[] values = value.split(";"); + final var type = AbnormalType.valueOf(values[0]); + final var level = Integer.decode(getValue(values[1], template)); + final var mustHave = Boolean.parseBoolean(values[2]); + cond = joinAnd(cond, new ConditionCheckAbnormal(type, level, mustHave)); + } else { + cond = joinAnd(cond, new ConditionCheckAbnormal(AbnormalType.valueOf(value), -1, true)); + } + break; + } + case "categorytype" -> { + final String[] values = a.getNodeValue().split(","); + final Set array = new HashSet<>(values.length); + for (String value : values) { + array.add(CategoryType.valueOf(getValue(value, null))); + } + cond = joinAnd(cond, new ConditionCategoryType(array)); + } + case "hasagathion" -> cond = joinAnd(cond, new ConditionPlayerHasAgathion(Boolean.parseBoolean(a.getNodeValue()))); + case "agathionenergy" -> cond = joinAnd(cond, new ConditionPlayerAgathionEnergy(Integer.decode(getValue(a.getNodeValue(), null)))); + case "companion" -> cond = joinAnd(cond, new ConditionOpCompanion(a.getNodeValue())); + case "existnpc" -> { + final var attributes = a.getNodeValue().split(";"); + final var npcIds = Arrays.stream(attributes[0].split(",")).map(Integer::valueOf).collect(toSet()); + final var radius = Integer.parseInt(attributes[1]); + final var present = Boolean.parseBoolean(attributes[2]); + cond = joinAnd(cond, new ConditionOpExistNpc(npcIds, radius, present)); + } + default -> LOG.error("Unrecognized condition {} in file {}!", a.getNodeName().toLowerCase(), _file); + } + } + + if (cond == null) { + LOG.error("Unrecognized condition in file {}!", _file); + } + return cond; + } + + protected Condition parseTargetCondition(Node n, Object template) { + Condition cond = null; + NamedNodeMap attrs = n.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + switch (a.getNodeName().toLowerCase()) { + case "aggro" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionTargetAggro(val)); + } + case "siegezone" -> { + int value = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionSiegeZone(value, false)); + } + case "level" -> { + int lvl = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionTargetLevel(lvl)); + } + case "levelrange" -> { + String[] range = getValue(a.getNodeValue(), template).split(";"); + if (range.length == 2) { + int minimumLevel = Integer.decode(getValue(a.getNodeValue(), template).split(";")[0]); + int maximumLevel = Integer.decode(getValue(a.getNodeValue(), template).split(";")[1]); + cond = joinAnd(cond, new ConditionTargetLevelRange(minimumLevel, maximumLevel)); + } + } + case "myparty" -> cond = joinAnd(cond, new ConditionTargetMyParty(a.getNodeValue())); + case "playable" -> cond = joinAnd(cond, new ConditionTargetPlayable()); + case "class_id_restriction" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + List array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionTargetClassIdRestriction(array)); + } + case "active_effect_id" -> { + int effectId = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionTargetActiveEffectId(effectId)); + } + case "active_effect_id_lvl" -> { + String val = getValue(a.getNodeValue(), template); + int effect_id = Integer.decode(getValue(val.split(",")[0], template)); + int effect_lvl = Integer.decode(getValue(val.split(",")[1], template)); + cond = joinAnd(cond, new ConditionTargetActiveEffectId(effect_id, effect_lvl)); + } + case "active_skill_id" -> { + int skill_id = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionTargetActiveSkillId(skill_id)); + } + case "active_skill_id_lvl" -> { + String val = getValue(a.getNodeValue(), template); + int skill_id = Integer.decode(getValue(val.split(",")[0], template)); + int skill_lvl = Integer.decode(getValue(val.split(",")[1], template)); + cond = joinAnd(cond, new ConditionTargetActiveSkillId(skill_id, skill_lvl)); + } + case "abnormal" -> { + int abnormalId = Integer.decode(getValue(a.getNodeValue(), template)); + cond = joinAnd(cond, new ConditionTargetAbnormal(abnormalId)); + } + case "mindistance" -> { + int distance = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionMinDistance(distance * distance)); + } + case "race" -> cond = joinAnd(cond, new ConditionTargetRace(Race.valueOf(a.getNodeValue()))); + case "using" -> { + int mask = 0; + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + for (WeaponType wt : WeaponType.values()) { + if (wt.name().equals(item)) { + mask |= wt.mask(); + break; + } + } + for (ArmorType at : ArmorType.values()) { + if (at.name().equals(item)) { + mask |= at.mask(); + break; + } + } + } + cond = joinAnd(cond, new ConditionTargetUsesWeaponKind(mask)); + } + case "npcid" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + List array = new ArrayList<>(st.countTokens()); + while (st.hasMoreTokens()) { + String item = st.nextToken().trim(); + array.add(Integer.decode(getValue(item, null))); + } + cond = joinAnd(cond, new ConditionTargetNpcId(array)); + } + case "npctype" -> { + String values = getValue(a.getNodeValue(), template).trim(); + String[] valuesSplit = values.split(","); + InstanceType[] types = new InstanceType[valuesSplit.length]; + for (int j = 0; j < valuesSplit.length; j++) { + types[j] = Enum.valueOf(InstanceType.class, valuesSplit[j]); + } + cond = joinAnd(cond, new ConditionTargetNpcType(types)); + } + case "weight" -> { + int weight = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionTargetWeight(weight)); + } + case "invsize" -> { + int size = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionTargetInvSize(size)); + } + case "checkabnormal" -> { + final String value = a.getNodeValue(); + if (value.contains(";")) { + final String[] values = value.split(";"); + final var type = AbnormalType.valueOf(values[0]); + final var level = Integer.decode(getValue(values[1], template)); + final var mustHave = Boolean.parseBoolean(values[2]); + cond = joinAnd(cond, new ConditionCheckAbnormal(type, level, mustHave)); + } else { + cond = joinAnd(cond, new ConditionCheckAbnormal(AbnormalType.valueOf(value), -1, true)); + } + break; + } + } + } + + if (cond == null) { + LOG.error("Unrecognized condition in file {}!", _file); + } + return cond; + } + + protected Condition parseUsingCondition(Node n) { + Condition cond = null; + NamedNodeMap attrs = n.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + switch (a.getNodeName().toLowerCase()) { + case "kind" -> { + int mask = 0; + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + while (st.hasMoreTokens()) { + int old = mask; + String item = st.nextToken().trim(); + for (WeaponType wt : WeaponType.values()) { + if (wt.name().equals(item)) { + mask |= wt.mask(); + } + } + + for (ArmorType at : ArmorType.values()) { + if (at.name().equals(item)) { + mask |= at.mask(); + } + } + + if (old == mask) { + LOG.warn("[parseUsingCondition=\"kind\"] Unknown item type name: {} in string {}!", item, a.getNodeValue()); + } + } + cond = joinAnd(cond, new ConditionUsingItemType(mask)); + } + case "slot" -> { + int mask = 0; + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ","); + while (st.hasMoreTokens()) { + int old = mask; + String item = st.nextToken().trim(); + if (ItemTable.SLOTS.containsKey(item)) { + mask |= ItemTable.SLOTS.get(item); + } + + if (old == mask) { + LOG.warn("[parseUsingCondition=\"slot\"] Unknown item slot name {}!", item); + } + } + cond = joinAnd(cond, new ConditionUsingSlotType(mask)); + } + case "skill" -> { + int id = Integer.parseInt(a.getNodeValue()); + cond = joinAnd(cond, new ConditionUsingSkill(id)); + } + case "slotitem" -> { + StringTokenizer st = new StringTokenizer(a.getNodeValue(), ";"); + int id = Integer.parseInt(st.nextToken().trim()); + int slot = Integer.parseInt(st.nextToken().trim()); + int enchant = 0; + if (st.hasMoreTokens()) { + enchant = Integer.parseInt(st.nextToken().trim()); + } + cond = joinAnd(cond, new ConditionSlotItemId(slot, id, enchant)); + } + case "weaponchange" -> { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionChangeWeapon(val)); + } + } + } + + if (cond == null) { + LOG.error("Unrecognized condition in file {}!", _file); + } + return cond; + } + + protected Condition parseGameCondition(Node n) { + Condition cond = null; + NamedNodeMap attrs = n.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Node a = attrs.item(i); + if ("skill".equalsIgnoreCase(a.getNodeName())) { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionWithSkill(val)); + } + if ("night".equalsIgnoreCase(a.getNodeName())) { + boolean val = Boolean.parseBoolean(a.getNodeValue()); + cond = joinAnd(cond, new ConditionGameTime(CheckGameTime.NIGHT, val)); + } + if ("chance".equalsIgnoreCase(a.getNodeName())) { + int val = Integer.decode(getValue(a.getNodeValue(), null)); + cond = joinAnd(cond, new ConditionGameChance(val)); + } + } + if (cond == null) { + LOG.error("Unrecognized condition in file {}!", _file); + } + return cond; + } + + protected void parseTable(Node n) { + NamedNodeMap attrs = n.getAttributes(); + String name = attrs.getNamedItem("name").getNodeValue(); + if (name.charAt(0) != '#') { + throw new IllegalArgumentException("Table name must start with #"); + } + StringTokenizer data = new StringTokenizer(n.getFirstChild().getNodeValue()); + List array = new ArrayList<>(data.countTokens()); + while (data.hasMoreTokens()) { + array.add(data.nextToken()); + } + setTable(name, array.toArray(new String[array.size()])); + } + + protected void parseBeanSet(Node n, StatsSet set, Integer level) { + String name = n.getAttributes().getNamedItem("name").getNodeValue().trim(); + String value = n.getAttributes().getNamedItem("val").getNodeValue().trim(); + char ch = value.isEmpty() ? ' ' : value.charAt(0); + if ((ch == '#') || (ch == '-') || Character.isDigit(ch)) { + set.set(name, String.valueOf(getValue(value, level))); + } else { + set.set(name, value); + } + } + + protected void setExtractableSkillData(StatsSet set, String value) { + set.set("capsuled_items_skill", value); + } + + protected String getValue(String value, Object template) { + // is it a table? + if (value.charAt(0) == '#') { + if (template instanceof Skill) { + return getTableValue(value); + } else if (template instanceof Integer integer) { + return getTableValue(value, integer); + } else { + throw new IllegalStateException(); + } + } + return value; + } + + protected Condition joinAnd(Condition cond, Condition c) { + if (cond == null) { + return c; + } + if (cond instanceof ConditionLogicAnd condAnd) { + condAnd.add(c); + return cond; + } + ConditionLogicAnd and = new ConditionLogicAnd(); + and.add(cond); + and.add(c); + return and; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/engines/DocumentEngine.java b/src/main/java/com/l2jserver/gameserver/engines/DocumentEngine.java new file mode 100644 index 00000000..aaf4ebc6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/engines/DocumentEngine.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.engines; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.engines.items.DocumentItem; +import com.l2jserver.gameserver.engines.skills.DocumentSkill; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.file.filter.XMLFilter; + +/** + * Document Engine. + * @author mkizub + */ +public class DocumentEngine { + + private static final Logger LOG = LoggerFactory.getLogger(DocumentEngine.class); + + private final List _itemFiles = new ArrayList<>(); + + private final List _skillFiles = new ArrayList<>(); + + public static DocumentEngine getInstance() { + return SingletonHolder._instance; + } + + protected DocumentEngine() { + hashFiles("data/stats/items", _itemFiles); + if (general().customItemsLoad()) { + hashFiles("data/stats/items/custom", _itemFiles); + } + hashFiles("data/stats/skills", _skillFiles); + if (general().customSkillsLoad()) { + hashFiles("data/stats/skills/custom", _skillFiles); + } + } + + private void hashFiles(String dirname, List hash) { + final var dir = new File(server().getDatapackRoot(), dirname); + if (!dir.exists()) { + LOG.warn("Directory {} does not exists!", dir.getAbsolutePath()); + return; + } + + final var files = dir.listFiles(new XMLFilter()); + if (files != null) { + Collections.addAll(hash, files); + } + } + + private List loadSkills(File file) { + if (file == null) { + LOG.warn("Skill file not found!"); + return null; + } + DocumentSkill doc = new DocumentSkill(file); + doc.parse(); + return doc.getSkills(); + } + + public Map loadAllSkills() { + return _skillFiles.parallelStream() + .map(this::loadSkills) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toConcurrentMap(SkillData::getSkillHashCode, Function.identity())); + } + + /** + * Return created items + * @return List of {@link L2Item} + */ + public List loadItems() { + return _itemFiles.parallelStream() + .filter(Objects::nonNull) + .map(DocumentItem::new) + .map(doc -> { + doc.parse(); + return doc.getItemList(); + }) + .flatMap(List::stream) + .toList(); + } + + private static class SingletonHolder { + protected static final DocumentEngine _instance = new DocumentEngine(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/engines/items/DocumentItem.java b/src/main/java/com/l2jserver/gameserver/engines/items/DocumentItem.java new file mode 100644 index 00000000..08b85598 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/engines/items/DocumentItem.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.engines.items; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.engines.DocumentBase; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.conditions.Condition; +import com.l2jserver.gameserver.model.items.L2Item; + +/** + * Document Item. + * @author mkizub + * @author JIV + */ +public final class DocumentItem extends DocumentBase { + + private static final Logger LOG = LoggerFactory.getLogger(DocumentItem.class); + + private Item _currentItem = null; + + private final List _itemsInFile = new ArrayList<>(); + + public DocumentItem(File file) { + super(file); + } + + @Override + protected StatsSet getStatsSet() { + return _currentItem.set; + } + + @Override + protected String getTableValue(String name) { + return _tables.get(name)[_currentItem.currentLevel]; + } + + @Override + protected String getTableValue(String name, int idx) { + return _tables.get(name)[idx - 1]; + } + + @Override + protected void parseDocument(Document doc) { + for (var n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (var d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("item".equalsIgnoreCase(d.getNodeName())) { + try { + _currentItem = new Item(); + parseItem(d); + _itemsInFile.add(_currentItem.item); + resetTable(); + } catch (Exception ex) { + LOG.warn("Cannot create item Id {}!", _currentItem.id, ex); + } + } + } + } + } + } + + protected void parseItem(Node n) throws InvocationTargetException { + int itemId = Integer.parseInt(n.getAttributes().getNamedItem("id").getNodeValue()); + String className = n.getAttributes().getNamedItem("type").getNodeValue(); + String itemName = n.getAttributes().getNamedItem("name").getNodeValue(); + + _currentItem.id = itemId; + _currentItem.name = itemName; + _currentItem.type = className; + _currentItem.set = new StatsSet(); + _currentItem.set.set("item_id", itemId); + _currentItem.set.set("name", itemName); + + Node first = n.getFirstChild(); + for (n = first; n != null; n = n.getNextSibling()) { + if ("table".equalsIgnoreCase(n.getNodeName())) { + if (_currentItem.item != null) { + throw new IllegalStateException("Item created but table node found! Item " + itemId); + } + parseTable(n); + } else if ("set".equalsIgnoreCase(n.getNodeName())) { + if (_currentItem.item != null) { + throw new IllegalStateException("Item created but set node found! Item " + itemId); + } + parseBeanSet(n, _currentItem.set, 1); + } else if ("for".equalsIgnoreCase(n.getNodeName())) { + makeItem(); + parseTemplate(n, _currentItem.item); + } else if ("cond".equalsIgnoreCase(n.getNodeName())) { + makeItem(); + Condition condition = parseCondition(n.getFirstChild(), _currentItem.item); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentItem.item.attach(condition); + } + } + // bah! in this point item doesn't have to be still created + makeItem(); + } + + private void makeItem() throws InvocationTargetException { + if (_currentItem.item != null) { + return; // item is already created + } + try { + Constructor c = Class.forName("com.l2jserver.gameserver.model.items.L2" + _currentItem.type).getConstructor(StatsSet.class); + _currentItem.item = (L2Item) c.newInstance(_currentItem.set); + } catch (Exception e) { + throw new InvocationTargetException(e); + } + } + + public List getItemList() { + return _itemsInFile; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/engines/items/Item.java b/src/main/java/com/l2jserver/gameserver/engines/items/Item.java new file mode 100644 index 00000000..c3f4efbb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/engines/items/Item.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.engines.items; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.items.L2Item; + +public class Item { + public int id; + public String type; + public String name; + public StatsSet set; + public int currentLevel; + public L2Item item; +} diff --git a/src/main/java/com/l2jserver/gameserver/engines/skills/DocumentSkill.java b/src/main/java/com/l2jserver/gameserver/engines/skills/DocumentSkill.java new file mode 100644 index 00000000..5ce3d587 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/engines/skills/DocumentSkill.java @@ -0,0 +1,1148 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.engines.skills; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.data.xml.impl.EnchantSkillGroupsData; +import com.l2jserver.gameserver.engines.DocumentBase; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.conditions.Condition; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Document Skill. + * @author mkizub + */ +public class DocumentSkill extends DocumentBase { + + public static class SkillInfo { + public int id; + public String name; + public StatsSet[] sets; + public StatsSet[] enchsets1; + public StatsSet[] enchsets2; + public StatsSet[] enchsets3; + public StatsSet[] enchsets4; + public StatsSet[] enchsets5; + public StatsSet[] enchsets6; + public StatsSet[] enchsets7; + public StatsSet[] enchsets8; + public int currentLevel; + public List skills = new ArrayList<>(); + public List currentSkills = new ArrayList<>(); + } + + private static final Logger LOG = LoggerFactory.getLogger(DocumentSkill.class); + + private SkillInfo _currentSkill; + + private final List _skillsInFile = new ArrayList<>(); + + public DocumentSkill(File file) { + super(file); + } + + private void setCurrentSkill(SkillInfo skill) { + _currentSkill = skill; + } + + @Override + protected StatsSet getStatsSet() { + return _currentSkill.sets[_currentSkill.currentLevel]; + } + + public List getSkills() { + return _skillsInFile; + } + + @Override + protected String getTableValue(String name) { + try { + return _tables.get(name)[_currentSkill.currentLevel]; + } catch (RuntimeException ex) { + LOG.error("Error in table {} of Skill Id {}!", name, _currentSkill.id, ex); + return ""; + } + } + + @Override + protected String getTableValue(String name, int idx) { + try { + return _tables.get(name)[idx - 1]; + } catch (RuntimeException ex) { + LOG.error("Wrong level count in skill Id {} name {} index {}!", _currentSkill.id, name, idx, ex); + return ""; + } + } + + @Override + protected void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("skill".equalsIgnoreCase(d.getNodeName())) { + setCurrentSkill(new SkillInfo()); + parseSkill(d); + _skillsInFile.addAll(_currentSkill.skills); + resetTable(); + } + } + } else if ("skill".equalsIgnoreCase(n.getNodeName())) { + setCurrentSkill(new SkillInfo()); + parseSkill(n); + _skillsInFile.addAll(_currentSkill.skills); + } + } + } + + protected void parseSkill(Node n) { + NamedNodeMap attrs = n.getAttributes(); + int enchantLevels1 = 0; + int enchantLevels2 = 0; + int enchantLevels3 = 0; + int enchantLevels4 = 0; + int enchantLevels5 = 0; + int enchantLevels6 = 0; + int enchantLevels7 = 0; + int enchantLevels8 = 0; + int skillId = Integer.parseInt(attrs.getNamedItem("id").getNodeValue()); + String skillName = attrs.getNamedItem("name").getNodeValue(); + String levels = attrs.getNamedItem("levels").getNodeValue(); + int lastLvl = Integer.parseInt(levels); + if (attrs.getNamedItem("enchantGroup1") != null) { + enchantLevels1 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 1, Integer.parseInt(attrs.getNamedItem("enchantGroup1").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup2") != null) { + enchantLevels2 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 2, Integer.parseInt(attrs.getNamedItem("enchantGroup2").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup3") != null) { + enchantLevels3 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 3, Integer.parseInt(attrs.getNamedItem("enchantGroup3").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup4") != null) { + enchantLevels4 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 4, Integer.parseInt(attrs.getNamedItem("enchantGroup4").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup5") != null) { + enchantLevels5 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 5, Integer.parseInt(attrs.getNamedItem("enchantGroup5").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup6") != null) { + enchantLevels6 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 6, Integer.parseInt(attrs.getNamedItem("enchantGroup6").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup7") != null) { + enchantLevels7 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 7, Integer.parseInt(attrs.getNamedItem("enchantGroup7").getNodeValue())); + } + if (attrs.getNamedItem("enchantGroup8") != null) { + enchantLevels8 = EnchantSkillGroupsData.getInstance().addNewRouteForSkill(skillId, lastLvl, 8, Integer.parseInt(attrs.getNamedItem("enchantGroup8").getNodeValue())); + } + + _currentSkill.id = skillId; + _currentSkill.name = skillName; + _currentSkill.sets = new StatsSet[lastLvl]; + _currentSkill.enchsets1 = new StatsSet[enchantLevels1]; + _currentSkill.enchsets2 = new StatsSet[enchantLevels2]; + _currentSkill.enchsets3 = new StatsSet[enchantLevels3]; + _currentSkill.enchsets4 = new StatsSet[enchantLevels4]; + _currentSkill.enchsets5 = new StatsSet[enchantLevels5]; + _currentSkill.enchsets6 = new StatsSet[enchantLevels6]; + _currentSkill.enchsets7 = new StatsSet[enchantLevels7]; + _currentSkill.enchsets8 = new StatsSet[enchantLevels8]; + + for (int i = 0; i < lastLvl; i++) { + _currentSkill.sets[i] = new StatsSet(); + _currentSkill.sets[i].set("skill_id", _currentSkill.id); + _currentSkill.sets[i].set("level", i + 1); + _currentSkill.sets[i].set("name", _currentSkill.name); + } + + if (_currentSkill.sets.length != lastLvl) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + lastLvl + " levels expected"); + } + + Node first = n.getFirstChild(); + for (n = first; n != null; n = n.getNextSibling()) { + if ("table".equalsIgnoreCase(n.getNodeName())) { + parseTable(n); + } + } + for (int i = 1; i <= lastLvl; i++) { + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + // Extractable item skills by Zoey76 + if ("capsuled_items_skill".equalsIgnoreCase(n.getAttributes().getNamedItem("name").getNodeValue())) { + setExtractableSkillData(_currentSkill.sets[i - 1], getTableValue("#extractableItems", i)); + } else { + parseBeanSet(n, _currentSkill.sets[i - 1], i); + } + } + } + } + for (int i = 0; i < enchantLevels1; i++) { + _currentSkill.enchsets1[i] = new StatsSet(); + _currentSkill.enchsets1[i].set("skill_id", _currentSkill.id); + // currentSkill.enchsets1[i] = currentSkill.sets[currentSkill.sets.length-1]; + _currentSkill.enchsets1[i].set("level", i + 101); + _currentSkill.enchsets1[i].set("name", _currentSkill.name); + // currentSkill.enchsets1[i].set("skillType", "NOTDONE"); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets1[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant1".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets1[i], i + 1); + } + } + } + + if (_currentSkill.enchsets1.length != enchantLevels1) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels1 + " levels expected"); + } + + for (int i = 0; i < enchantLevels2; i++) { + _currentSkill.enchsets2[i] = new StatsSet(); + // currentSkill.enchsets2[i] = currentSkill.sets[currentSkill.sets.length-1]; + _currentSkill.enchsets2[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets2[i].set("level", i + 201); + _currentSkill.enchsets2[i].set("name", _currentSkill.name); + // currentSkill.enchsets2[i].set("skillType", "NOTDONE"); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets2[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant2".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets2[i], i + 1); + } + } + } + + if (_currentSkill.enchsets2.length != enchantLevels2) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels2 + " levels expected"); + } + + for (int i = 0; i < enchantLevels3; i++) { + _currentSkill.enchsets3[i] = new StatsSet(); + _currentSkill.enchsets3[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets3[i].set("level", i + 301); + _currentSkill.enchsets3[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets3[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant3".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets3[i], i + 1); + } + } + } + + if (_currentSkill.enchsets3.length != enchantLevels3) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels3 + " levels expected"); + } + + for (int i = 0; i < enchantLevels4; i++) { + _currentSkill.enchsets4[i] = new StatsSet(); + _currentSkill.enchsets4[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets4[i].set("level", i + 401); + _currentSkill.enchsets4[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets4[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant4".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets4[i], i + 1); + } + } + } + + if (_currentSkill.enchsets4.length != enchantLevels4) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels4 + " levels expected"); + } + + for (int i = 0; i < enchantLevels5; i++) { + _currentSkill.enchsets5[i] = new StatsSet(); + _currentSkill.enchsets5[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets5[i].set("level", i + 501); + _currentSkill.enchsets5[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets5[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant5".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets5[i], i + 1); + } + } + } + + if (_currentSkill.enchsets5.length != enchantLevels5) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels5 + " levels expected"); + } + + for (int i = 0; i < enchantLevels6; i++) { + _currentSkill.enchsets6[i] = new StatsSet(); + _currentSkill.enchsets6[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets6[i].set("level", i + 601); + _currentSkill.enchsets6[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets6[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant6".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets6[i], i + 1); + } + } + } + + if (_currentSkill.enchsets6.length != enchantLevels6) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels6 + " levels expected"); + } + + for (int i = 0; i < enchantLevels7; i++) { + _currentSkill.enchsets7[i] = new StatsSet(); + _currentSkill.enchsets7[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets7[i].set("level", i + 701); + _currentSkill.enchsets7[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets7[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant7".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets7[i], i + 1); + } + } + } + + if (_currentSkill.enchsets7.length != enchantLevels7) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels7 + " levels expected"); + } + + for (int i = 0; i < enchantLevels8; i++) { + _currentSkill.enchsets8[i] = new StatsSet(); + _currentSkill.enchsets8[i].set("skill_id", _currentSkill.id); + _currentSkill.enchsets8[i].set("level", i + 801); + _currentSkill.enchsets8[i].set("name", _currentSkill.name); + + for (n = first; n != null; n = n.getNextSibling()) { + if ("set".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets8[i], _currentSkill.sets.length); + } + } + + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant8".equalsIgnoreCase(n.getNodeName())) { + parseBeanSet(n, _currentSkill.enchsets8[i], i + 1); + } + } + } + + if (_currentSkill.enchsets8.length != enchantLevels8) { + throw new RuntimeException("Skill id=" + skillId + " number of levels missmatch, " + enchantLevels8 + " levels expected"); + } + + makeSkills(); + for (int i = 0; i < lastLvl; i++) { + _currentSkill.currentLevel = i; + for (n = first; n != null; n = n.getNextSibling()) { + if ("cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + + for (int i = lastLvl; i < (lastLvl + enchantLevels1); i++) { + _currentSkill.currentLevel = i - lastLvl; + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant1cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant1Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant1startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant1channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant1pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant1pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant1endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant1selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1; i < (lastLvl + enchantLevels1 + enchantLevels2); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant2cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant2Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant2startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant2channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant2pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant2pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant2endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant2selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant3cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant3Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant3startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant3channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant3pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant3pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant3endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant3selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2 - enchantLevels3; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant4cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant4Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant4startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant4channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant4pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant4pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant4endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant4selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2 - enchantLevels3 - enchantLevels4; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant5cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant5Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant5startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant5channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant5pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant5pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant5endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant5selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5 + enchantLevels6); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2 - enchantLevels3 - enchantLevels4 - enchantLevels5; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant6cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant6Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant6startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant6channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant6pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant6pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant6endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant6selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5 + enchantLevels6; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5 + enchantLevels6 + enchantLevels7); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2 - enchantLevels3 - enchantLevels4 - enchantLevels5 - enchantLevels6; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant7cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant7Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant7startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant7channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant7pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant7pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant7endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant7selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundChannelingEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + for (int i = lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5 + enchantLevels6 + enchantLevels7; i < (lastLvl + enchantLevels1 + enchantLevels2 + enchantLevels3 + enchantLevels4 + enchantLevels5 + enchantLevels6 + enchantLevels7 + + enchantLevels8); i++) { + boolean foundCond = false, foundEffect = false, foundChannelingEffects = false, foundStartEffects = false, foundPveEffects = false, foundPvpEffects = false, foundEndEffects = false, foundSelfEffects = false; + _currentSkill.currentLevel = i - lastLvl - enchantLevels1 - enchantLevels2 - enchantLevels3 - enchantLevels4 - enchantLevels5 - enchantLevels6 - enchantLevels7; + for (n = first; n != null; n = n.getNextSibling()) { + if ("enchant8cond".equalsIgnoreCase(n.getNodeName())) { + foundCond = true; + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if ("enchant8Effects".equalsIgnoreCase(n.getNodeName())) { + foundEffect = true; + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if ("enchant8startEffects".equalsIgnoreCase(n.getNodeName())) { + foundStartEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if ("enchant8channelingEffects".equalsIgnoreCase(n.getNodeName())) { + foundChannelingEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if ("enchant8pveEffects".equalsIgnoreCase(n.getNodeName())) { + foundPveEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if ("enchant8pvpEffects".equalsIgnoreCase(n.getNodeName())) { + foundPvpEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if ("enchant8endEffects".equalsIgnoreCase(n.getNodeName())) { + foundEndEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if ("enchant8selfEffects".equalsIgnoreCase(n.getNodeName())) { + foundSelfEffects = true; + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + // If none found, the enchanted skill will take effects from maxLvL of norm skill + if (!foundCond || !foundEffect || !foundChannelingEffects || !foundStartEffects || !foundPveEffects || !foundPvpEffects || !foundEndEffects || !foundSelfEffects) { + _currentSkill.currentLevel = lastLvl - 1; + for (n = first; n != null; n = n.getNextSibling()) { + if (!foundCond && "cond".equalsIgnoreCase(n.getNodeName())) { + Condition condition = parseCondition(n.getFirstChild(), _currentSkill.currentSkills.get(i)); + Node msg = n.getAttributes().getNamedItem("msg"); + Node msgId = n.getAttributes().getNamedItem("msgId"); + if ((condition != null) && (msg != null)) { + condition.setMessage(msg.getNodeValue()); + } else if ((condition != null) && (msgId != null)) { + condition.setMessageId(Integer.decode(getValue(msgId.getNodeValue(), null))); + Node addName = n.getAttributes().getNamedItem("addName"); + if ((addName != null) && (Integer.decode(getValue(msgId.getNodeValue(), null)) > 0)) { + condition.addName(); + } + } + _currentSkill.currentSkills.get(i).attach(condition, false); + } else if (!foundEffect && "effects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i)); + } else if (!foundStartEffects && "startEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.START); + } else if (!foundChannelingEffects && "channelingEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.CHANNELING); + } else if (!foundPveEffects && "pveEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVE); + } else if (!foundPvpEffects && "pvpEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.PVP); + } else if (!foundEndEffects && "endEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.END); + } else if (!foundSelfEffects && "selfEffects".equalsIgnoreCase(n.getNodeName())) { + parseTemplate(n, _currentSkill.currentSkills.get(i), EffectScope.SELF); + } + } + } + } + _currentSkill.skills.addAll(_currentSkill.currentSkills); + } + + private void makeSkills() { + int count = 0; + _currentSkill.currentSkills = new ArrayList<>(_currentSkill.sets.length + _currentSkill.enchsets1.length + _currentSkill.enchsets2.length + _currentSkill.enchsets3.length + _currentSkill.enchsets4.length + _currentSkill.enchsets5.length + _currentSkill.enchsets6.length + + _currentSkill.enchsets7.length + _currentSkill.enchsets8.length); + for (int i = 0; i < _currentSkill.sets.length; i++) { + final var set = _currentSkill.sets[i]; + try { + _currentSkill.currentSkills.add(i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + int _count = count; + for (int i = 0; i < _currentSkill.enchsets1.length; i++) { + final var set = _currentSkill.enchsets1[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets2.length; i++) { + final var set = _currentSkill.enchsets2[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets3.length; i++) { + final var set = _currentSkill.enchsets3[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets4.length; i++) { + final var set = _currentSkill.enchsets4[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets5.length; i++) { + final var set = _currentSkill.enchsets5[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets6.length; i++) { + final var set = _currentSkill.enchsets6[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets7.length; i++) { + final var set = _currentSkill.enchsets7[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + _count = count; + for (int i = 0; i < _currentSkill.enchsets8.length; i++) { + final var set = _currentSkill.enchsets8[i]; + try { + _currentSkill.currentSkills.add(_count + i, new Skill(set)); + count++; + } catch (Exception ex) { + LOG.error("Skill id={} level={}!", set.getInt("skill_id"), set.getInt("level"), ex); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/AISkillScope.java b/src/main/java/com/l2jserver/gameserver/enums/AISkillScope.java new file mode 100644 index 00000000..38afac1f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/AISkillScope.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zoey76 + */ +public enum AISkillScope { + BUFF, + DEBUFF, + NEGATIVE, + ATTACK, + IMMOBILIZE, + HEAL, + RES, + COT, + UNIVERSAL, + LONG_RANGE, + SHORT_RANGE, + GENERAL, + SUICIDE +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/AIType.java b/src/main/java/com/l2jserver/gameserver/enums/AIType.java new file mode 100644 index 00000000..105b3c73 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/AIType.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zoey76 + */ +public enum AIType { + FIGHTER, + ARCHER, + BALANCED, + MAGE, + HEALER, + CORPSE +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/AuctionItemType.java b/src/main/java/com/l2jserver/gameserver/enums/AuctionItemType.java new file mode 100644 index 00000000..06095ec5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/AuctionItemType.java @@ -0,0 +1,23 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum AuctionItemType { + ClanHall +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/CategoryType.java b/src/main/java/com/l2jserver/gameserver/enums/CategoryType.java new file mode 100644 index 00000000..888166d7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/CategoryType.java @@ -0,0 +1,164 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * This class defines all category types. + * @author xban1x + */ +public enum CategoryType { + FIGHTER_GROUP, + MAGE_GROUP, + WIZARD_GROUP, + CLERIC_GROUP, + ATTACKER_GROUP, + TANKER_GROUP, + FIRST_CLASS_GROUP, + SECOND_CLASS_GROUP, + THIRD_CLASS_GROUP, + FOURTH_CLASS_GROUP, + BOUNTY_HUNTER_GROUP, + WARSMITH_GROUP, + SUMMON_NPC_GROUP, + KNIGHT_GROUP, + WHITE_MAGIC_GROUP, + HEAL_GROUP, + ASSIST_MAGIC_GROUP, + WARRIOR_GROUP, + HUMAN_2ND_GROUP, + ELF_2ND_GROUP, + DELF_2ND_GROUP, + ORC_2ND_GROUP, + DWARF_2ND_GROUP, + STRIDER, + STRIDER_GROUP, + RED_STRIDER_GROUP, + WOLF_GROUP, + GROWN_UP_WOLF_GROUP, + HATCHLING_GROUP, + BABY_PET_GROUP, + UPGRADE_BABY_PET_GROUP, + WYVERN_GROUP, + ALL_WOLF_GROUP, + WOLF, + SIN_EATER_GROUP, + PET_GROUP, + ITEM_EQUIP_PET_GROUP, + SUBJOB_GROUP_DAGGER, + SUBJOB_GROUP_BOW, + SUBJOB_GROUP_KNIGHT, + SUBJOB_GROUP_SUMMONER, + SUBJOB_GROUP_HALF_HEALER, + SUBJOB_GROUP_DANCE, + SUBJOB_GROUP_WIZARD, + HUMAN_FALL_CLASS, + HUMAN_WALL_CLASS, + HUMAN_MALL_CLASS, + HUMAN_CALL_CLASS, + ELF_FALL_CLASS, + ELF_MALL_CLASS, + ELF_WALL_CLASS, + ELF_CALL_CLASS, + DELF_FALL_CLASS, + DELF_MALL_CLASS, + DELF_WALL_CLASS, + DELF_CALL_CLASS, + ORC_FALL_CLASS, + ORC_MALL_CLASS, + DWARF_ALL_CLASS, + DWARF_BOUNTY_CLASS, + DWARF_SMITH_CLASS, + KAMAEL_ALL_CLASS, + KAMAEL_FIRST_CLASS_GROUP, + KAMAEL_SECOND_CLASS_GROUP, + KAMAEL_THIRD_CLASS_GROUP, + KAMAEL_FOURTH_CLASS_GROUP, + BEGINNER_FIGHTER, + BEGINNER_MAGE, + KAMAEL_MALE_MAIN_OCCUPATION, + KAMAEL_FEMALE_MAIN_OCCUPATION, + ARCHER_GROUP, + SHIELD_MASTER, + BARD, + FORCE_MASTER, + WEAPON_MASTER, + BOW_MASTER, + DAGGER_MASTER, + HEAL_MASTER, + WIZARD_MASTER, + BUFF_MASTER, + SUMMON_MASTER, + WARRIOR_CLOACK, + ROGUE_CLOACK, + MAGE_CLOACK, + SHIELD_MASTER2_3, + BARD2_3, + FORCE_MASTER2_3, + WEAPON_MASTER2_3, + BOW_MASTER2_3, + DAGGER_MASTER2_3, + HEAL_MASTER2_3, + WIZARD_MASTER2_3, + BUFF_MASTER2_3, + SUMMON_MASTER2_3, + ATTRIBUTE_GROUP_SUMMONER, + SUB_GROUP_WARRIOR, + SUB_GROUP_ROGUE, + SUB_GROUP_KNIGHT, + SUB_GROUP_SUMMONER, + SUB_GROUP_WIZARD, + SUB_GROUP_HEALER, + SUB_GROUP_ENCHANTER, + SUB_GROUP_HEC, + SUB_GROUP_HEW, + SUB_GROUP_HEF, + SUB_GROUP_ORC, + SUB_GROUP_WARE, + SUB_GROUP_BLACK, + SUB_GROUP_DE, + SUB_GROUP_KAMAEL, + LIGHT_TANKER_GROUP, + DARK_TANKER_GROUP, + MELEE_ATTACKER, + RECOM_KNIGHT_GROUP, + RECOM_MAGIC_GROUP, + RECOM_WARRIOR_GROUP, + RECOM_ROGUE_GROUP, + RECOM_KAMAEL_GROUP, + RECOM_ORCF_GROUP, + RECOM_ORCM_GROUP, + DEINONYCHUS_PET_GROUP, + BEASTFARM_BEAST, + BEASTFARM_INVADER, + ICEQUEEN_NPC; + + /** + * Finds category by it's name + * @param categoryName + * @return A {@code CategoryType} if category was found, {@code null} if category was not found + */ + public static CategoryType findByName(String categoryName) { + for (CategoryType type : values()) { + if (type.name().equalsIgnoreCase(categoryName)) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/DispelCategory.java b/src/main/java/com/l2jserver/gameserver/enums/DispelCategory.java new file mode 100644 index 00000000..8b0d8bf9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/DispelCategory.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zealar + */ +public enum DispelCategory { + ALL, + BUFF, + DEBUFF +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/DuelResult.java b/src/main/java/com/l2jserver/gameserver/enums/DuelResult.java new file mode 100644 index 00000000..200ab482 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/DuelResult.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zealar + */ +public enum DuelResult { + CONTINUE, + TEAM_1_WIN, + TEAM_2_WIN, + TEAM_1_SURRENDER, + TEAM_2_SURRENDER, + CANCELED, + TIMEOUT +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/DuelState.java b/src/main/java/com/l2jserver/gameserver/enums/DuelState.java new file mode 100644 index 00000000..33304788 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/DuelState.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zealar + */ +public enum DuelState { + NO_DUEL, + DUELLING, + DEAD, + WINNER, + INTERRUPTED +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/EffectCalculationType.java b/src/main/java/com/l2jserver/gameserver/enums/EffectCalculationType.java new file mode 100644 index 00000000..d1d06f37 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/EffectCalculationType.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Effect calculation type. + * @author Zealar + * @version 2.6.2.0 + */ +public enum EffectCalculationType { + DIFF, + PER +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/EventState.java b/src/main/java/com/l2jserver/gameserver/enums/EventState.java new file mode 100644 index 00000000..42e53adc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/EventState.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author UnAfraid + */ +public enum EventState { + STARTED, + FINISHED, +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/FortTeleportWhoType.java b/src/main/java/com/l2jserver/gameserver/enums/FortTeleportWhoType.java new file mode 100644 index 00000000..266ef93d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/FortTeleportWhoType.java @@ -0,0 +1,25 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum FortTeleportWhoType { + All, + Attacker, + Owner +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/HtmlActionScope.java b/src/main/java/com/l2jserver/gameserver/enums/HtmlActionScope.java new file mode 100644 index 00000000..e775d42f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/HtmlActionScope.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author HorridoJoho + */ +public enum HtmlActionScope { + NPC_HTML, + NPC_ITEM_HTML, + NPC_QUEST_HTML, + TUTORIAL_HTML, + COMM_BOARD_HTML +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/IllegalActionPunishmentType.java b/src/main/java/com/l2jserver/gameserver/enums/IllegalActionPunishmentType.java new file mode 100644 index 00000000..5e6c5f5b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/IllegalActionPunishmentType.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Illegal Action Punishment Type. + * @author xban1x + */ +public enum IllegalActionPunishmentType { + NONE, + BROADCAST, + KICK, + KICK_BAN, + JAIL +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/InstanceReenterType.java b/src/main/java/com/l2jserver/gameserver/enums/InstanceReenterType.java new file mode 100644 index 00000000..ab73b63c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/InstanceReenterType.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author FallenAngel + */ +public enum InstanceReenterType { + NONE, + ON_INSTANCE_ENTER, + ON_INSTANCE_FINISH +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/InstanceRemoveBuffType.java b/src/main/java/com/l2jserver/gameserver/enums/InstanceRemoveBuffType.java new file mode 100644 index 00000000..67638af0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/InstanceRemoveBuffType.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author St3eT + */ +public enum InstanceRemoveBuffType { + NONE, + ALL, + WHITELIST, + BLACKLIST +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/InstanceType.java b/src/main/java/com/l2jserver/gameserver/enums/InstanceType.java new file mode 100644 index 00000000..54a2f16f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/InstanceType.java @@ -0,0 +1,149 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum InstanceType { + L2Object(null), + L2ItemInstance(L2Object), + L2Character(L2Object), + L2Npc(L2Character), + L2Playable(L2Character), + L2Summon(L2Playable), + L2Decoy(L2Character), + L2PcInstance(L2Playable), + L2NpcInstance(L2Npc), + L2MerchantInstance(L2NpcInstance), + L2WarehouseInstance(L2NpcInstance), + L2StaticObjectInstance(L2Character), + L2DoorInstance(L2Character), + L2TerrainObjectInstance(L2Npc), + L2EffectPointInstance(L2Npc), + // Summons, Pets, Decoys and Traps + L2ServitorInstance(L2Summon), + L2PetInstance(L2Summon), + L2BabyPetInstance(L2PetInstance), + L2DecoyInstance(L2Decoy), + L2TrapInstance(L2Npc), + // Attackable + L2Attackable(L2Npc), + L2GuardInstance(L2Attackable), + L2QuestGuardInstance(L2GuardInstance), + L2MonsterInstance(L2Attackable), + L2ChestInstance(L2MonsterInstance), + L2ControllableMobInstance(L2MonsterInstance), + L2FeedableBeastInstance(L2MonsterInstance), + L2TamedBeastInstance(L2FeedableBeastInstance), + L2FriendlyMobInstance(L2Attackable), + L2RiftInvaderInstance(L2MonsterInstance), + L2RaidBossInstance(L2MonsterInstance), + L2GrandBossInstance(L2RaidBossInstance), + // FlyMobs + L2FlyNpcInstance(L2NpcInstance), + L2FlyMonsterInstance(L2MonsterInstance), + L2FlyRaidBossInstance(L2RaidBossInstance), + L2FlyTerrainObjectInstance(L2Npc), + // Sepulchers + L2SepulcherNpcInstance(L2NpcInstance), + L2SepulcherMonsterInstance(L2MonsterInstance), + // Festival + L2FestivalGiudeInstance(L2Npc), + L2FestivalMonsterInstance(L2MonsterInstance), + // Vehicles + L2Vehicle(L2Character), + L2BoatInstance(L2Vehicle), + L2AirShipInstance(L2Vehicle), + L2ControllableAirShipInstance(L2AirShipInstance), + // Siege + L2DefenderInstance(L2Attackable), + L2ArtefactInstance(L2NpcInstance), + L2ControlTowerInstance(L2Npc), + L2FlameTowerInstance(L2Npc), + L2SiegeFlagInstance(L2Npc), + // Fort Siege + L2FortCommanderInstance(L2DefenderInstance), + // Fort NPCs + L2FortLogisticsInstance(L2MerchantInstance), + L2FortManagerInstance(L2MerchantInstance), + // City NPCs + L2AuctioneerInstance(L2Npc), + L2ClanHallManagerInstance(L2MerchantInstance), + L2FishermanInstance(L2MerchantInstance), + L2ObservationInstance(L2Npc), + L2OlympiadManagerInstance(L2Npc), + L2PetManagerInstance(L2MerchantInstance), + L2RaceManagerInstance(L2Npc), + L2TeleporterInstance(L2Npc), + L2VillageMasterInstance(L2NpcInstance), + // Doormens + L2DoormenInstance(L2NpcInstance), + L2CastleDoormenInstance(L2DoormenInstance), + L2FortDoormenInstance(L2DoormenInstance), + L2ClanHallDoormenInstance(L2DoormenInstance), + // Custom + L2NpcBufferInstance(L2Npc), + L2EventMobInstance(L2Npc); + + private final InstanceType _parent; + private final long _typeL; + private final long _typeH; + private final long _maskL; + private final long _maskH; + + InstanceType(InstanceType parent) { + _parent = parent; + + final int high = ordinal() - (Long.SIZE - 1); + if (high < 0) { + _typeL = 1L << ordinal(); + _typeH = 0; + } else { + _typeL = 0; + _typeH = 1L << high; + } + + if ((_typeL < 0) || (_typeH < 0)) { + throw new Error("Too many instance types, failed to load " + name()); + } + + if (parent != null) { + _maskL = _typeL | parent._maskL; + _maskH = _typeH | parent._maskH; + } else { + _maskL = _typeL; + _maskH = _typeH; + } + } + + public final InstanceType getParent() { + return _parent; + } + + public final boolean isType(InstanceType it) { + return ((_maskL & it._typeL) > 0) || ((_maskH & it._typeH) > 0); + } + + public final boolean isTypes(InstanceType... it) { + for (InstanceType i : it) { + if (isType(i)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/ItemLocation.java b/src/main/java/com/l2jserver/gameserver/enums/ItemLocation.java new file mode 100644 index 00000000..f37d0bd6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/ItemLocation.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum ItemLocation { + VOID, + INVENTORY, + PAPERDOLL, + WAREHOUSE, + CLANWH, + PET, + PET_EQUIP, + LEASE, + REFUND, + MAIL, + FREIGHT +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/MacroType.java b/src/main/java/com/l2jserver/gameserver/enums/MacroType.java new file mode 100644 index 00000000..e94ee644 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/MacroType.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Macro type enumerated. + * @author Zoey76 + */ +public enum MacroType { + NONE, + SKILL, + ACTION, + TEXT, + SHORTCUT, + ITEM, + DELAY +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/ManorMode.java b/src/main/java/com/l2jserver/gameserver/enums/ManorMode.java new file mode 100644 index 00000000..f900c91b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/ManorMode.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author malyelfik + */ +public enum ManorMode { + DISABLED, + MODIFIABLE, + MAINTENANCE, + APPROVED +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/MountType.java b/src/main/java/com/l2jserver/gameserver/enums/MountType.java new file mode 100644 index 00000000..07249ff8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/MountType.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +import com.l2jserver.gameserver.data.xml.impl.CategoryData; + +/** + * @author UnAfraid + */ +public enum MountType { + NONE, + STRIDER, + WYVERN, + WOLF; + + public static MountType findByNpcId(int npcId) { + if (CategoryData.getInstance().isInCategory(CategoryType.STRIDER, npcId)) { + return STRIDER; + } else if (CategoryData.getInstance().isInCategory(CategoryType.WYVERN_GROUP, npcId)) { + return WYVERN; + } else if (CategoryData.getInstance().isInCategory(CategoryType.WOLF_GROUP, npcId)) { + return WOLF; + } + return NONE; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/PartyDistributionType.java b/src/main/java/com/l2jserver/gameserver/enums/PartyDistributionType.java new file mode 100644 index 00000000..c284db2a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/PartyDistributionType.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author NosBit + */ +public enum PartyDistributionType { + FINDERS_KEEPERS(0, 487), + RANDOM(1, 488), + RANDOM_INCLUDING_SPOIL(2, 798), + BY_TURN(3, 799), + BY_TURN_INCLUDING_SPOIL(4, 800); + + private final int _id; + private final int _sysStringId; + + /** + * Constructs a party distribution type. + * @param id the id used by packets. + * @param sysStringId the sysstring id + */ + PartyDistributionType(int id, int sysStringId) { + _id = id; + _sysStringId = sysStringId; + } + + /** + * Gets the id used by packets. + * @return the id + */ + public int getId() { + return _id; + } + + /** + * Gets the sysstring id used by system messages. + * @return the sysstring-e id + */ + public int getSysStringId() { + return _sysStringId; + } + + /** + * Finds the {@code PartyDistributionType} by its id + * @param id the id + * @return the {@code PartyDistributionType} if its found, {@code null} otherwise. + */ + public static PartyDistributionType findById(int id) { + for (PartyDistributionType partyDistributionType : values()) { + if (partyDistributionType.getId() == id) { + return partyDistributionType; + } + } + return null; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/PetitionState.java b/src/main/java/com/l2jserver/gameserver/enums/PetitionState.java new file mode 100644 index 00000000..616cfa59 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/PetitionState.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Enum with petition states. + * @author xban1x + */ +public enum PetitionState { + PENDING, + RESPONDER_CANCEL, + RESPONDER_MISSING, + RESPONDER_REJECT, + RESPONDER_COMPLETE, + PETITIONER_CANCEL, + PETITIONER_MISSING, + IN_PROCESS, + COMPLETED +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/PetitionType.java b/src/main/java/com/l2jserver/gameserver/enums/PetitionType.java new file mode 100644 index 00000000..7601feb2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/PetitionType.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Enum with petition types. + * @author xban1x + */ +public enum PetitionType { + IMMOBILITY, + RECOVERY_RELATED, + BUG_REPORT, + QUEST_RELATED, + BAD_USER, + SUGGESTIONS, + GAME_TIP, + OPERATION_RELATED, + OTHER +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/PlayerAction.java b/src/main/java/com/l2jserver/gameserver/enums/PlayerAction.java new file mode 100644 index 00000000..746aef4c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/PlayerAction.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author UnAfraid + */ +public enum PlayerAction { + ADMIN_COMMAND, + USER_ENGAGE; + + private final int _mask; + + PlayerAction() { + _mask = (1 << ordinal()); + } + + public int getMask() { + return _mask; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/PrivateStoreType.java b/src/main/java/com/l2jserver/gameserver/enums/PrivateStoreType.java new file mode 100644 index 00000000..feb4b0a5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/PrivateStoreType.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author NosBit + */ +public enum PrivateStoreType { + NONE(0), + SELL(1), + SELL_MANAGE(2), + BUY(3), + BUY_MANAGE(4), + MANUFACTURE(5), + PACKAGE_SELL(8); + + private final int _id; + + PrivateStoreType(int id) { + _id = id; + } + + public int getId() { + return _id; + } + + public static PrivateStoreType findById(int id) { + for (PrivateStoreType privateStoreType : values()) { + if (privateStoreType.getId() == id) { + return privateStoreType; + } + } + return null; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/QuestType.java b/src/main/java/com/l2jserver/gameserver/enums/QuestType.java new file mode 100644 index 00000000..d5217c7d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/QuestType.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Luis Arias + */ +public enum QuestType { + REPEATABLE, + ONE_TIME, + DAILY +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/Race.java b/src/main/java/com/l2jserver/gameserver/enums/Race.java new file mode 100644 index 00000000..92b64ad7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/Race.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Creature races enumerated. + * @author Zealar + */ +public enum Race { + HUMAN, + ELF, + DARK_ELF, + ORC, + DWARF, + KAMAEL, + ANIMAL, + BEAST, + BUG, + CASTLE_GUARD, + CONSTRUCT, + DEMONIC, + DIVINE, + DRAGON, + ELEMENTAL, + ETC, + FAIRY, + GIANT, + HUMANOID, + MERCENARY, + NONE, + PLANT, + SIEGE_WEAPON, + UNDEAD +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/Sex.java b/src/main/java/com/l2jserver/gameserver/enums/Sex.java new file mode 100644 index 00000000..ad575037 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/Sex.java @@ -0,0 +1,25 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum Sex { + MALE, + FEMALE, + ETC +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/ShortcutType.java b/src/main/java/com/l2jserver/gameserver/enums/ShortcutType.java new file mode 100644 index 00000000..c565c471 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/ShortcutType.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * Shortcut type enumerated. + * @author Zoey76 + */ +public enum ShortcutType { + NONE, + ITEM, + SKILL, + ACTION, + MACRO, + RECIPE, + BOOKMARK +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/ShotType.java b/src/main/java/com/l2jserver/gameserver/enums/ShotType.java new file mode 100644 index 00000000..e8fb6875 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/ShotType.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author UnAfraid + */ +public enum ShotType { + SOULSHOTS, + SPIRITSHOTS, + BLESSED_SPIRITSHOTS, + FISH_SOULSHOTS; + + private final int _mask; + + ShotType() { + _mask = (1 << ordinal()); + } + + public int getMask() { + return _mask; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/SiegeTeleportWhoType.java b/src/main/java/com/l2jserver/gameserver/enums/SiegeTeleportWhoType.java new file mode 100644 index 00000000..fd568c39 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/SiegeTeleportWhoType.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum SiegeTeleportWhoType { + Attacker, + Owner, + NotOwner, + Spectator +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/StartPosType.java b/src/main/java/com/l2jserver/gameserver/enums/StartPosType.java new file mode 100644 index 00000000..fff0494f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/StartPosType.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zealar + */ +public enum StartPosType { + FIXED, + RANDOM +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/StatFunction.java b/src/main/java/com/l2jserver/gameserver/enums/StatFunction.java new file mode 100644 index 00000000..6f2947c2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/StatFunction.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author Zealar + */ +public enum StatFunction { + ADD("Add", 30), + DIV("Div", 20), + ENCHANT("Enchant", 0), + ENCHANTHP("EnchantHp", 40), + MUL("Mul", 20), + SET("Set", 0), + SHARE("Share", 30), + SUB("Sub", 30); + + private final String _name; + private final int _order; + + StatFunction(String name, int order) { + _name = name; + _order = order; + } + + public String getName() { + return _name; + } + + public int getOrder() { + return _order; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/StatType.java b/src/main/java/com/l2jserver/gameserver/enums/StatType.java new file mode 100644 index 00000000..f1cb844d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/StatType.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum StatType { + + HP, + MP, + XP, + SP, + GIM // grab item modifier: + // GIM: the default function uses only the skilllevel to determine + // how many item is grabbed in each step + // with this stat changer you can multiple this +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/Team.java b/src/main/java/com/l2jserver/gameserver/enums/Team.java new file mode 100644 index 00000000..209e0900 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/Team.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author NosBit + */ +public enum Team { + NONE(0), + BLUE(1), + RED(2); + + private final int _id; + + Team(int id) { + _id = id; + } + + public int getId() { + return _id; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/TrapAction.java b/src/main/java/com/l2jserver/gameserver/enums/TrapAction.java new file mode 100644 index 00000000..c6d54645 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/TrapAction.java @@ -0,0 +1,25 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +public enum TrapAction { + TRAP_TRIGGERED, + TRAP_DETECTED, + TRAP_DISARMED +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/enums/TriggerAttackType.java b/src/main/java/com/l2jserver/gameserver/enums/TriggerAttackType.java new file mode 100644 index 00000000..d34c64f3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/TriggerAttackType.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Trigger Attack Type. + * @author Zoey76 + * @version 2.6.3.0 + */ +public enum TriggerAttackType { + NONE { + @Override + public boolean check(L2Character trigger, L2Object target) { + return false; + } + }, + ENEMY_ALL { + @Override + public boolean check(L2Character trigger, L2Object target) { + return target.isCharacter() && target.isAutoAttackable(trigger); + } + }, + MOB { + @Override + public boolean check(L2Character trigger, L2Object target) { + return target.isAttackable(); + } + }, + PK { + @Override + public boolean check(L2Character trigger, L2Object target) { + return target.isPlayer() || target.isSummon(); + } + }; + + public abstract boolean check(L2Character trigger, L2Object target); +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/TriggerTargetType.java b/src/main/java/com/l2jserver/gameserver/enums/TriggerTargetType.java new file mode 100644 index 00000000..438517be --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/TriggerTargetType.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Trigger Target Type. + * @author Zoey76 + * @version 2.6.3.0 + */ +public enum TriggerTargetType { + ENEMY { + @Override + public List getTargets(L2Character target, L2Character attacker) { + return List.of(attacker); + } + }, + SELF { + @Override + public List getTargets(L2Character target, L2Character attacker) { + return List.of(target); + } + }, + MY_PARTY { + @Override + public List getTargets(L2Character target, L2Character attacker) { + if (target.isPlayer() || target.isSummon()) { + final var player = target.getActingPlayer(); + if (player.isInParty()) { + return new ArrayList<>(player.getParty().getMembers()); + } + return List.of(player); + } + return List.of(); + } + }, + TARGET { + @Override + public List getTargets(L2Character target, L2Character attacker) { + return List.of(target); + } + }, + SUMMON { + @Override + public List getTargets(L2Character target, L2Character attacker) { + if (target.hasSummon()) { + return List.of(target.getSummon()); + } + return List.of(); + } + }; + + public abstract List getTargets(L2Character target, L2Character attacker); +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/TriggerType.java b/src/main/java/com/l2jserver/gameserver/enums/TriggerType.java new file mode 100644 index 00000000..7f89d087 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/TriggerType.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums; + +/** + * @author kombat + */ +public enum TriggerType { + // You hit an enemy + ON_HIT(1), + // You hit an enemy - was crit + ON_CRIT(2), + // You cast a skill + ON_CAST(4), + // You cast a skill - it was a physical one + ON_PHYSICAL(8), + // You cast a skill - it was a magic one + ON_MAGIC(16), + // You cast a skill - it was a magic one - good magic + ON_MAGIC_GOOD(32), + // You cast a skill - it was a magic one - offensive magic + ON_MAGIC_OFFENSIVE(64), + // You are attacked by enemy + ON_ATTACKED(128), + // You are attacked by enemy - by hit + ON_ATTACKED_HIT(256), + // You are attacked by enemy - by hit - was crit + ON_ATTACKED_CRIT(512), + // A skill was casted on you + ON_HIT_BY_SKILL(1024), + // An evil skill was casted on you + ON_HIT_BY_OFFENSIVE_SKILL(2048), + // A good skill was casted on you + ON_HIT_BY_GOOD_MAGIC(4096), + // Evading melee attack + ON_EVADED_HIT(8192), + // Effect only - on start + ON_START(16384), + // Effect only - each second + ON_ACTION_TIME(32768), + // Effect only - on exit + ON_EXIT(65536); + + private final int _mask; + + TriggerType(int mask) { + _mask = mask; + } + + public final boolean check(int event) { + return (_mask & event) != 0; // Trigger (sub-)type contains event (sub-)type + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/audio/IAudio.java b/src/main/java/com/l2jserver/gameserver/enums/audio/IAudio.java new file mode 100644 index 00000000..5702e3c8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/audio/IAudio.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums.audio; + +import com.l2jserver.gameserver.network.serverpackets.PlaySound; + +/** + * @author Zealar + */ +public interface IAudio { + /** + * @return the name of the sound of this audio object + */ + String getSoundName(); + + /** + * @return the {@link PlaySound} packet of this audio object + */ + PlaySound getPacket(); +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/audio/Music.java b/src/main/java/com/l2jserver/gameserver/enums/audio/Music.java new file mode 100644 index 00000000..f3994f24 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/audio/Music.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums.audio; + +import com.l2jserver.gameserver.network.serverpackets.PlaySound; + +/** + * @author Zealar + */ +public enum Music implements IAudio { + B03_D_10000("B03_D", 10000), + B03_F("B03_F"), + B04_S01("B04_S01"), + B06_F_2000("B06_F", 2000), + B06_S01_2000("B06_S01", 2000), + B07_S01_2000("B07_S01", 2000), + BS01_A_10000("BS01_A", 10000), + BS01_A_7000("BS01_A", 7000), + BS01_D_10000("BS01_D", 10000), + BS01_D_6000("BS01_D", 6000), + BS02_A_10000("BS02_A", 10000), + BS02_A_6000("BS02_A", 6000), + BS02_D_10000("BS02_D", 10000), + BS02_D_7000("BS02_D", 7000), + BS03_A_10000("BS03_A", 10000), + BS04_A_6000("BS04_A", 6000), + BS04_A_3000("BS04_A", 3000), + BS05_D_5000("BS05_D", 5000), + BS05_D_6000("BS05_D", 6000), + BS06_A_5000("BS06_A", 5000), + BS07_A_10000("BS07_A", 10000), + BS07_D_10000("BS07_D", 10000), + BS08_A_10000("BS08_A", 10000), + EV_01_10000("EV_01", 10000), + EV_02_10000("EV_02", 10000), + EV_03_200("EV_03", 200), + EV_04_200("EV_04", 200), + HB01_10000("HB01", 10000), + NS01_F_5000("ns01_f", 5000), + NS22_F_5000("ns22_f", 5000), + RM01_A_4000("Rm01_A", 4000), + RM01_A_8000("Rm01_A", 8000), + RM01_S_4000("RM01_S", 4000), + SSQ_Dawn_01("SSQ_Dawn_01"), + SSQ_Dusk_01("SSQ_Dusk_01"), + SSQ_Neutral_01("SSQ_Neutral_01"), + TP01_F_3000("TP01_F", 3000), + TP02_F_3000("TP02_F", 3000), + TP03_F_3000("TP03_F", 3000), + TP04_F_3000("TP04_F", 3000), + TP05_F("TP05_F"), + TP05_F_5000("TP05_F", 5000), + SF_S_01("SF_S_01"), + NS22_F("NS22_F"), + S_RACE("S_Race"), + SF_P_01("SF_P_01"), + SIEGE_VICTORY("Siege_Victory"); + + private final PlaySound _playSound; + + Music(String soundName) { + _playSound = PlaySound.createMusic(soundName, 0); + } + + Music(String soundName, int delay) { + _playSound = PlaySound.createMusic(soundName, delay); + } + + @Override + public String getSoundName() { + return _playSound.getSoundName(); + } + + @Override + public PlaySound getPacket() { + return _playSound; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/audio/Sound.java b/src/main/java/com/l2jserver/gameserver/enums/audio/Sound.java new file mode 100644 index 00000000..239cb7b1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/audio/Sound.java @@ -0,0 +1,127 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums.audio; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.network.serverpackets.PlaySound; + +public enum Sound implements IAudio { + ITEMSOUND_QUEST_ACCEPT("ItemSound.quest_accept"), + ITEMSOUND_QUEST_MIDDLE("ItemSound.quest_middle"), + ITEMSOUND_QUEST_FINISH("ItemSound.quest_finish"), + ITEMSOUND_QUEST_ITEMGET("ItemSound.quest_itemget"), + // Newbie Guide tutorial (incl. some quests), Mutated Kaneus quests, Quest 192 + ITEMSOUND_QUEST_TUTORIAL("ItemSound.quest_tutorial"), + // Quests 107, 363, 364 + ITEMSOUND_QUEST_GIVEUP("ItemSound.quest_giveup"), + // Quests 212, 217, 224, 226, 416 + ITEMSOUND_QUEST_BEFORE_BATTLE("ItemSound.quest_before_battle"), + // Quests 211, 258, 266, 330 + ITEMSOUND_QUEST_JACKPOT("ItemSound.quest_jackpot"), + // Quests 508, 509 and 510 + ITEMSOUND_QUEST_FANFARE_1("ItemSound.quest_fanfare_1"), + // Played only after class transfer via Test Server Helpers (ID 31756 and 31757) + ITEMSOUND_QUEST_FANFARE_2("ItemSound.quest_fanfare_2"), + // Quest 336 + ITEMSOUND_QUEST_FANFARE_MIDDLE("ItemSound.quest_fanfare_middle"), + // Quest 114 + ITEMSOUND_ARMOR_WOOD("ItemSound.armor_wood_3"), + // Quest 21 + ITEMSOUND_ARMOR_CLOTH("ItemSound.item_drop_equip_armor_cloth"), + AMDSOUND_ED_CHIMES("AmdSound.ed_chimes_05"), + HORROR_01("horror_01"), // played when spawned monster sees player + // Quest 22 + AMBSOUND_HORROR_01("AmbSound.dd_horror_01"), + AMBSOUND_HORROR_03("AmbSound.d_horror_03"), + AMBSOUND_HORROR_15("AmbSound.d_horror_15"), + // Quest 23 + ITEMSOUND_ARMOR_LEATHER("ItemSound.itemdrop_armor_leather"), + ITEMSOUND_WEAPON_SPEAR("ItemSound.itemdrop_weapon_spear"), + AMBSOUND_MT_CREAK("AmbSound.mt_creak01"), + AMBSOUND_EG_DRON("AmbSound.eg_dron_02"), + SKILLSOUND_HORROR_02("SkillSound5.horror_02"), + CHRSOUND_MHFIGHTER_CRY("ChrSound.MHFighter_cry"), + // Quest 24 + AMDSOUND_WIND_LOOT("AmdSound.d_wind_loot_02"), + INTERFACESOUND_CHARSTAT_OPEN("InterfaceSound.charstat_open_01"), + // Quest 25 + AMDSOUND_HORROR_02("AmdSound.dd_horror_02"), + CHRSOUND_FDELF_CRY("ChrSound.FDElf_Cry"), + // Quest 115 + AMBSOUND_WINGFLAP("AmbSound.t_wingflap_04"), + AMBSOUND_THUNDER("AmbSound.thunder_02"), + // Quest 120 + AMBSOUND_DRONE("AmbSound.ed_drone_02"), + AMBSOUND_CRYSTAL_LOOP("AmbSound.cd_crystal_loop"), + AMBSOUND_PERCUSSION_01("AmbSound.dt_percussion_01"), + AMBSOUND_PERCUSSION_02("AmbSound.ac_percussion_02"), + // Quest 648 and treasure chests + ITEMSOUND_BROKEN_KEY("ItemSound2.broken_key"), + // Quest 184 + ITEMSOUND_SIREN("ItemSound3.sys_siren"), + // Quest 648 + ITEMSOUND_ENCHANT_SUCCESS("ItemSound3.sys_enchant_success"), + ITEMSOUND_ENCHANT_FAILED("ItemSound3.sys_enchant_failed"), + // Best farm mobs + ITEMSOUND_SOW_SUCCESS("ItemSound3.sys_sow_success"), + // Quest 25 + SKILLSOUND_HORROR_1("SkillSound5.horror_01"), + // Quests 21 and 23 + SKILLSOUND_HORROR_2("SkillSound5.horror_02"), + // Quest 22 + SKILLSOUND_ANTARAS_FEAR("SkillSound3.antaras_fear"), + // Quest 505 + SKILLSOUND_JEWEL_CELEBRATE("SkillSound2.jewel.celebrate"), + // Quest 373 + SKILLSOUND_LIQUID_MIX("SkillSound5.liquid_mix_01"), + SKILLSOUND_LIQUID_SUCCESS("SkillSound5.liquid_success_01"), + SKILLSOUND_LIQUID_FAIL("SkillSound5.liquid_fail_01"), + // Quest 111 + ETCSOUND_ELROKI_SONG_FULL("EtcSound.elcroki_song_full"), + ETCSOUND_ELROKI_SONG_1ST("EtcSound.elcroki_song_1st"), + ETCSOUND_ELROKI_SONG_2ND("EtcSound.elcroki_song_2nd"), + ETCSOUND_ELROKI_SONG_3RD("EtcSound.elcroki_song_3rd"), + + ITEMSOUND2_RACE_START("ItemSound2.race_start"), + + // Ships + ITEMSOUND_SHIP_ARRIVAL_DEPARTURE("itemsound.ship_arrival_departure"), + ITEMSOUND_SHIP_5MIN("itemsound.ship_5min"), + ITEMSOUND_SHIP_1MIN("itemsound.ship_1min"); + + private final PlaySound _playSound; + + Sound(String soundName) { + _playSound = PlaySound.createSound(soundName); + } + + public PlaySound withObject(L2Object obj) { + return PlaySound.createSound(getSoundName(), obj); + } + + @Override + public String getSoundName() { + return _playSound.getSoundName(); + } + + @Override + public PlaySound getPacket() { + return _playSound; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/enums/audio/Voice.java b/src/main/java/com/l2jserver/gameserver/enums/audio/Voice.java new file mode 100644 index 00000000..a200e430 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/enums/audio/Voice.java @@ -0,0 +1,92 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.enums.audio; + +import com.l2jserver.gameserver.network.serverpackets.PlaySound; + +/** + * @author Zealar + */ +public enum Voice implements IAudio { + TUTORIAL_VOICE_001A_2000("tutorial_voice_001a", 2000), + TUTORIAL_VOICE_001B_2000("tutorial_voice_001b", 2000), + TUTORIAL_VOICE_001C_2000("tutorial_voice_001c", 2000), + TUTORIAL_VOICE_001D_2000("tutorial_voice_001d", 2000), + TUTORIAL_VOICE_001E_2000("tutorial_voice_001e", 2000), + TUTORIAL_VOICE_001F_2000("tutorial_voice_001f", 2000), + TUTORIAL_VOICE_001G_2000("tutorial_voice_001g", 2000), + TUTORIAL_VOICE_001H_2000("tutorial_voice_001h", 2000), + TUTORIAL_VOICE_001I_2000("tutorial_voice_001i", 2000), + TUTORIAL_VOICE_001K_2000("tutorial_voice_001k", 2000), + TUTORIAL_VOICE_002_1000("tutorial_voice_002", 1000), + TUTORIAL_VOICE_003_2000("tutorial_voice_003", 2000), + TUTORIAL_VOICE_004_5000("tutorial_voice_004", 5000), + TUTORIAL_VOICE_005_5000("tutorial_voice_005", 5000), + TUTORIAL_VOICE_006_1000("tutorial_voice_006", 1000), + TUTORIAL_VOICE_006_3500("tutorial_voice_006", 3500), + TUTORIAL_VOICE_007_3500("tutorial_voice_007", 3500), + TUTORIAL_VOICE_008_1000("tutorial_voice_008", 1000), + TUTORIAL_VOICE_009A("tutorial_voice_009a"), + TUTORIAL_VOICE_009B("tutorial_voice_009b"), + TUTORIAL_VOICE_009C("tutorial_voice_009c"), + TUTORIAL_VOICE_010A("tutorial_voice_010a"), + TUTORIAL_VOICE_010B("tutorial_voice_010b"), + TUTORIAL_VOICE_010C("tutorial_voice_010c"), + TUTORIAL_VOICE_010D("tutorial_voice_010d"), + TUTORIAL_VOICE_010E("tutorial_voice_010e"), + TUTORIAL_VOICE_010F("tutorial_voice_010f"), + TUTORIAL_VOICE_010G("tutorial_voice_010g"), + TUTORIAL_VOICE_011_1000("tutorial_voice_011", 1000), + TUTORIAL_VOICE_012_1000("tutorial_voice_012", 1000), + TUTORIAL_VOICE_013_1000("tutorial_voice_013", 1000), + TUTORIAL_VOICE_014_1000("tutorial_voice_014", 1000), + TUTORIAL_VOICE_016_1000("tutorial_voice_016", 1000), + TUTORIAL_VOICE_017_1000("tutorial_voice_017", 1000), + TUTORIAL_VOICE_018_1000("tutorial_voice_018", 1000), + TUTORIAL_VOICE_019_1000("tutorial_voice_019", 1000), + TUTORIAL_VOICE_020_1000("tutorial_voice_020", 1000), + TUTORIAL_VOICE_021_1000("tutorial_voice_021", 1000), + TUTORIAL_VOICE_023_1000("tutorial_voice_023", 1000), + TUTORIAL_VOICE_024_1000("tutorial_voice_024", 1000), + TUTORIAL_VOICE_025_1000("tutorial_voice_025", 1000), + TUTORIAL_VOICE_026_1000("tutorial_voice_026", 1000), + TUTORIAL_VOICE_027_1000("tutorial_voice_027", 1000), + TUTORIAL_VOICE_028_1000("tutorial_voice_028", 1000), + TUTORIAL_VOICE_030_1000("tutorial_voice_030", 1000); + + private final PlaySound _playSound; + + Voice(String soundName) { + _playSound = PlaySound.createVoice(soundName, 0); + } + + Voice(String soundName, int delay) { + _playSound = PlaySound.createVoice(soundName, delay); + } + + @Override + public String getSoundName() { + return _playSound.getSoundName(); + } + + @Override + public PlaySound getPacket() { + return _playSound; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/ActionHandler.java b/src/main/java/com/l2jserver/gameserver/handler/ActionHandler.java new file mode 100644 index 00000000..7820ef01 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/ActionHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.enums.InstanceType; + +/** + * @author UnAfraid + */ +public class ActionHandler implements IHandler { + private final Map _actions; + + public static ActionHandler getInstance() { + return SingletonHolder._instance; + } + + protected ActionHandler() { + _actions = new HashMap<>(); + } + + @Override + public void registerHandler(IActionHandler handler) { + _actions.put(handler.getInstanceType(), handler); + } + + @Override + public synchronized void removeHandler(IActionHandler handler) { + _actions.remove(handler.getInstanceType()); + } + + @Override + public IActionHandler getHandler(InstanceType iType) { + IActionHandler result = null; + for (InstanceType t = iType; t != null; t = t.getParent()) { + result = _actions.get(t); + if (result != null) { + break; + } + } + return result; + } + + @Override + public int size() { + return _actions.size(); + } + + private static class SingletonHolder { + protected static final ActionHandler _instance = new ActionHandler(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/ActionShiftHandler.java b/src/main/java/com/l2jserver/gameserver/handler/ActionShiftHandler.java new file mode 100644 index 00000000..2afcd24c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/ActionShiftHandler.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.enums.InstanceType; + +/** + * @author UnAfraid + */ +public class ActionShiftHandler implements IHandler { + private final Map _actionsShift; + + protected ActionShiftHandler() { + _actionsShift = new HashMap<>(); + } + + @Override + public void registerHandler(IActionShiftHandler handler) { + _actionsShift.put(handler.getInstanceType(), handler); + } + + @Override + public synchronized void removeHandler(IActionShiftHandler handler) { + _actionsShift.remove(handler.getInstanceType()); + } + + @Override + public IActionShiftHandler getHandler(InstanceType iType) { + IActionShiftHandler result = null; + for (InstanceType t = iType; t != null; t = t.getParent()) { + result = _actionsShift.get(t); + if (result != null) { + break; + } + } + return result; + } + + @Override + public int size() { + return _actionsShift.size(); + } + + public static ActionShiftHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final ActionShiftHandler _instance = new ActionShiftHandler(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/AdminCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/AdminCommandHandler.java new file mode 100644 index 00000000..329984b6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/AdminCommandHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author UnAfraid + */ +public class AdminCommandHandler implements IHandler { + private final Map _datatable; + + protected AdminCommandHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IAdminCommandHandler handler) { + String[] ids = handler.getAdminCommandList(); + for (String id : ids) { + _datatable.put(id, handler); + } + } + + @Override + public synchronized void removeHandler(IAdminCommandHandler handler) { + String[] ids = handler.getAdminCommandList(); + for (String id : ids) { + _datatable.remove(id); + } + } + + @Override + public IAdminCommandHandler getHandler(String adminCommand) { + String command = adminCommand; + if (adminCommand.contains(" ")) { + command = adminCommand.substring(0, adminCommand.indexOf(" ")); + } + return _datatable.get(command); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static AdminCommandHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final AdminCommandHandler _instance = new AdminCommandHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/BypassHandler.java b/src/main/java/com/l2jserver/gameserver/handler/BypassHandler.java new file mode 100644 index 00000000..df81f9a6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/BypassHandler.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author nBd + * @author UnAfraid + */ +public class BypassHandler implements IHandler { + private final Map _datatable; + + protected BypassHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IBypassHandler handler) { + for (String element : handler.getBypassList()) { + _datatable.put(element.toLowerCase(), handler); + } + } + + @Override + public synchronized void removeHandler(IBypassHandler handler) { + for (String element : handler.getBypassList()) { + _datatable.remove(element.toLowerCase()); + } + } + + @Override + public IBypassHandler getHandler(String command) { + if (command.contains("?")) { + command = command.substring(0, command.indexOf("?")); + } else if (command.contains(" ")) { + command = command.substring(0, command.indexOf(" ")); + } + return _datatable.get(command.toLowerCase()); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static BypassHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final BypassHandler _instance = new BypassHandler(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/ChatHandler.java b/src/main/java/com/l2jserver/gameserver/handler/ChatHandler.java new file mode 100644 index 00000000..bf59b8e8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/ChatHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class handles all chat handlers + * @author durgus + * @author UnAfraid + */ +public class ChatHandler implements IHandler { + private final Map _datatable; + + protected ChatHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IChatHandler handler) { + int[] ids = handler.getChatTypeList(); + for (int id : ids) { + _datatable.put(id, handler); + } + } + + @Override + public synchronized void removeHandler(IChatHandler handler) { + int[] ids = handler.getChatTypeList(); + for (int id : ids) { + _datatable.remove(id); + } + } + + /** + * Get the chat handler for the given chat type + * @param chatType + * @return + */ + @Override + public IChatHandler getHandler(Integer chatType) { + return _datatable.get(chatType); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static ChatHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final ChatHandler _instance = new ChatHandler(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/CommunityBoardHandler.java b/src/main/java/com/l2jserver/gameserver/handler/CommunityBoardHandler.java new file mode 100644 index 00000000..79e04706 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/CommunityBoardHandler.java @@ -0,0 +1,196 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.util.Util; + +/** + * Community Board handler. + * @author Zoey76 + */ +public final class CommunityBoardHandler implements IHandler { + + private static final Logger LOG = LoggerFactory.getLogger(CommunityBoardHandler.class); + + /** The registered handlers. */ + private final Map _datatable = new HashMap<>(); + /** The bypasses used by the players. */ + private final Map _bypasses = new ConcurrentHashMap<>(); + + protected CommunityBoardHandler() { + // Prevent external initialization. + } + + @Override + public void registerHandler(IParseBoardHandler handler) { + for (String cmd : handler.getCommunityBoardCommands()) { + _datatable.put(cmd.toLowerCase(), handler); + } + } + + @Override + public synchronized void removeHandler(IParseBoardHandler handler) { + for (String cmd : handler.getCommunityBoardCommands()) { + _datatable.remove(cmd.toLowerCase()); + } + } + + @Override + public IParseBoardHandler getHandler(String cmd) { + for (IParseBoardHandler cb : _datatable.values()) { + for (String command : cb.getCommunityBoardCommands()) { + if (cmd.toLowerCase().startsWith(command.toLowerCase())) { + return cb; + } + } + } + return null; + } + + @Override + public int size() { + return _datatable.size(); + } + + /** + * Verifies if the string is a registered community board command. + * @param cmd the command to verify + * @return {@code true} if the command has been registered, {@code false} otherwise + */ + public boolean isCommunityBoardCommand(String cmd) { + return getHandler(cmd) != null; + } + + /** + * Parses a community board command. + * @param command the command + * @param player the player + */ + public void handleParseCommand(String command, L2PcInstance player) { + if (player == null) { + return; + } + + if (!general().enableCommunityBoard()) { + player.sendPacket(SystemMessageId.CB_OFFLINE); + return; + } + + final IParseBoardHandler cb = getHandler(command); + if (cb == null) { + LOG.warn("Couldn't find parse handler for command {}!", command); + return; + } + + cb.parseCommunityBoardCommand(command, player); + } + + /** + * Writes a command into the client. + * @param player the player + * @param url the command URL + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument + * @param arg4 the fourth argument + * @param arg5 the fifth argument + */ + public void handleWriteCommand(L2PcInstance player, String url, String arg1, String arg2, String arg3, String arg4, String arg5) { + if (player == null) { + return; + } + + if (!general().enableCommunityBoard()) { + player.sendPacket(SystemMessageId.CB_OFFLINE); + return; + } + + String cmd; + switch (url) { + case "Topic" -> cmd = "_bbstop"; + case "Post" -> cmd = "_bbspos"; // TODO: Implement. + case "Region" -> cmd = "_bbsloc"; + case "Notice" -> cmd = "_bbsclan"; + default -> { + separateAndSend("

    The command: " + url + " is not implemented yet.


    ", player); + return; + } + } + + final IParseBoardHandler cb = getHandler(cmd); + if (cb == null) { + LOG.warn("Couldn't find write handler for command {}!", cmd); + return; + } + + if (!(cb instanceof IWriteBoardHandler)) { + LOG.warn("{} doesn't implement write!", cb.getClass().getSimpleName()); + return; + } + ((IWriteBoardHandler) cb).writeCommunityBoardCommand(player, arg1, arg2, arg3, arg4, arg5); + } + + /** + * Sets the last bypass used by the player. + * @param player the player + * @param title the title + * @param bypass the bypass + */ + public void addBypass(L2PcInstance player, String title, String bypass) { + _bypasses.put(player.getObjectId(), title + "&" + bypass); + } + + /** + * Removes the last bypass used by the player. + * @param player the player + * @return the last bypass used + */ + public String removeBypass(L2PcInstance player) { + return _bypasses.remove(player.getObjectId()); + } + + /** + * Separates and send an HTML into multiple packets, to display into the community board.
    + * The limit is 16383 characters. + * @param html the HTML to send + * @param player the player + */ + public static void separateAndSend(String html, L2PcInstance player) { + Util.sendCBHtml(player, html); + } + + public static CommunityBoardHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final CommunityBoardHandler _instance = new CommunityBoardHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/EffectHandler.java b/src/main/java/com/l2jserver/gameserver/handler/EffectHandler.java new file mode 100644 index 00000000..6f307894 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/EffectHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.effects.AbstractEffect; +import com.l2jserver.gameserver.scripting.ScriptEngineManager; + +/** + * @author BiggBoss + * @author UnAfraid + */ +public final class EffectHandler implements IHandler, String> { + private final Map> _handlers; + + protected EffectHandler() { + _handlers = new HashMap<>(); + } + + @Override + public void registerHandler(Class handler) { + _handlers.put(handler.getSimpleName(), handler); + } + + @Override + public synchronized void removeHandler(Class handler) { + _handlers.remove(handler.getSimpleName()); + } + + @Override + public Class getHandler(String name) { + return _handlers.get(name); + } + + @Override + public int size() { + return _handlers.size(); + } + + public void executeScript() throws Exception { + ScriptEngineManager.getInstance().executeScript("com/l2jserver/datapack/handlers/EffectMasterHandler.java"); + } + + public static EffectHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + private static final class SingletonHolder { + protected static final EffectHandler INSTANCE = new EffectHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IActionHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IActionHandler.java new file mode 100644 index 00000000..512082f1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IActionHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public interface IActionHandler { + boolean action(L2PcInstance activeChar, L2Object target, boolean interact); + + InstanceType getInstanceType(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/IActionShiftHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IActionShiftHandler.java new file mode 100644 index 00000000..f42afce6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IActionShiftHandler.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public interface IActionShiftHandler { + boolean action(L2PcInstance activeChar, L2Object target, boolean interact); + + InstanceType getInstanceType(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/IAdminCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IAdminCommandHandler.java new file mode 100644 index 00000000..24b9540e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IAdminCommandHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public interface IAdminCommandHandler { + /** + * this is the worker method that is called when someone uses an admin command. + * @param activeChar + * @param command + * @return command success + */ + boolean useAdminCommand(String command, L2PcInstance activeChar); + + /** + * this method is called at initialization to register all the item ids automatically + * @return all known itemIds + */ + String[] getAdminCommandList(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IBypassHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IBypassHandler.java new file mode 100644 index 00000000..09f31098 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IBypassHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Bypass Handler interface. + * @author nBd + */ +public interface IBypassHandler { + /** + * This is the worker method that is called when someone uses an bypass command. + * @param command + * @param activeChar + * @param bypassOrigin + * @return success + */ + boolean useBypass(String command, L2PcInstance player, L2Character bypassOrigin); + + /** + * This method is called at initialization to register all bypasses automatically. + * @return all known bypasses + */ + String[] getBypassList(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/IChatHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IChatHandler.java new file mode 100644 index 00000000..9534aac3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IChatHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Interface for chat handlers + * @author durgus + */ +public interface IChatHandler { + /** + * Handles a specific type of chat messages + * @param type + * @param activeChar + * @param target + * @param text + */ + void handleChat(int type, L2PcInstance activeChar, String target, String text); + + /** + * Returns a list of all chat types registered to this handler + * @return + */ + int[] getChatTypeList(); +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/handler/IHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IHandler.java new file mode 100644 index 00000000..31551ed6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IHandler.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +/** + * Handler Interface. + * @author UnAfraid + * @author Zoey76 + * @param + * @param + */ +public interface IHandler { + @SuppressWarnings("unchecked") + default void registerByClass(Class clazz) throws Exception { + final Object object = clazz.getDeclaredConstructor().newInstance(); + registerHandler((K) object); + } + + /** + * Registers the handler. + * @param handler the handler + */ + void registerHandler(K handler); + + /** + * Removes the handler. + * @param handler the handler to remove + */ + void removeHandler(K handler); + + /** + * Gets the handler for the given object. + * @param val the object + * @return the handler + */ + K getHandler(V val); + + /** + * Gets the amount of handlers. + * @return the amount of handlers + */ + int size(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IItemHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IItemHandler.java new file mode 100644 index 00000000..6d49e89f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IItemHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * Mother class of all Item Handlers. + */ +public interface IItemHandler { + /** + * Launch task associated to the item. + * @param playable the non-NPC character using the item + * @param item L2ItemInstance designating the item to use + * @param forceUse ctrl hold on item use + * @return {@code true} if the item all conditions are met and the item is used, {@code false} otherwise. + */ + boolean useItem(L2Playable playable, L2ItemInstance item, boolean forceUse); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IParseBoardHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IParseBoardHandler.java new file mode 100644 index 00000000..e953f96d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IParseBoardHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Community Board interface. + * @author Zoey76 + */ +public interface IParseBoardHandler { + /** + * Parses a community board command. + * @param command the command + * @param player the player + * @return + */ + boolean parseCommunityBoardCommand(String command, L2PcInstance player); + + /** + * Gets the community board commands. + * @return the community board commands + */ + String[] getCommunityBoardCommands(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IPunishmentHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IPunishmentHandler.java new file mode 100644 index 00000000..32793530 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IPunishmentHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.punishment.PunishmentTask; +import com.l2jserver.gameserver.model.punishment.PunishmentType; + +/** + * Punishment Handler interface. + * @author UnAfraid + */ +public interface IPunishmentHandler { + void onStart(PunishmentTask task); + + void onEnd(PunishmentTask task); + + PunishmentType getType(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/ITelnetHandler.java b/src/main/java/com/l2jserver/gameserver/handler/ITelnetHandler.java new file mode 100644 index 00000000..e98e32df --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/ITelnetHandler.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.io.PrintWriter; +import java.net.Socket; + +/** + * Telnet Handler interface. + * @author UnAfraid + */ +public interface ITelnetHandler { + /** + * this is the worker method that is called when someone uses an bypass command + * @param command + * @param _print + * @param _cSocket + * @param __uptime + * @return success + */ + boolean useCommand(String command, PrintWriter _print, Socket _cSocket, int __uptime); + + /** + * this method is called at initialization to register all bypasses automatically + * @return all known bypasses + */ + String[] getCommandList(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IUserCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IUserCommandHandler.java new file mode 100644 index 00000000..8d74786e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IUserCommandHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public interface IUserCommandHandler { + /** + * this is the worker method that is called when someone uses an admin command. + * @param id + * @param activeChar + * @return command success + */ + boolean useUserCommand(int id, L2PcInstance activeChar); + + /** + * this method is called at initialization to register all the item ids automatically + * @return all known itemIds + */ + int[] getUserCommandList(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IVoicedCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IVoicedCommandHandler.java new file mode 100644 index 00000000..aac0a239 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IVoicedCommandHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public interface IVoicedCommandHandler { + /** + * this is the worker method that is called when someone uses an admin command. + * @param player + * @param command + * @param params + * @return command success + */ + boolean useVoicedCommand(String command, L2PcInstance player, String params); + + /** + * this method is called at initialization to register all the item ids automatically + * @return all known itemIds + */ + String[] getVoicedCommandList(); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/IWriteBoardHandler.java b/src/main/java/com/l2jserver/gameserver/handler/IWriteBoardHandler.java new file mode 100644 index 00000000..2ba527fa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/IWriteBoardHandler.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Community Board interface. + * @author Zoey76 + */ +public interface IWriteBoardHandler extends IParseBoardHandler { + /** + * Writes a community board command into the client. + * @param player the player + * @param arg1 the first argument + * @param arg2 the second argument + * @param arg3 the third argument + * @param arg4 the fourth argument + * @param arg5 the fifth argument + * @return + */ + boolean writeCommunityBoardCommand(L2PcInstance player, String arg1, String arg2, String arg3, String arg4, String arg5); +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/ItemHandler.java b/src/main/java/com/l2jserver/gameserver/handler/ItemHandler.java new file mode 100644 index 00000000..61854218 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/ItemHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.items.L2EtcItem; + +/** + * Item handlers. + * @author UnAfraid + */ +public class ItemHandler implements IHandler { + private final Map _datatable; + + protected ItemHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IItemHandler handler) { + _datatable.put(handler.getClass().getSimpleName(), handler); + } + + @Override + public synchronized void removeHandler(IItemHandler handler) { + _datatable.remove(handler.getClass().getSimpleName()); + } + + @Override + public IItemHandler getHandler(L2EtcItem item) { + if ((item == null) || (item.getHandlerName() == null)) { + return null; + } + return _datatable.get(item.getHandlerName()); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static ItemHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ItemHandler INSTANCE = new ItemHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/PunishmentHandler.java b/src/main/java/com/l2jserver/gameserver/handler/PunishmentHandler.java new file mode 100644 index 00000000..61357262 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/PunishmentHandler.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.punishment.PunishmentType; + +/** + * This class manages handlers of punishments. + * @author UnAfraid + */ +public class PunishmentHandler implements IHandler { + private final Map _handlers = new HashMap<>(); + + protected PunishmentHandler() { + + } + + @Override + public void registerHandler(IPunishmentHandler handler) { + _handlers.put(handler.getType(), handler); + } + + @Override + public synchronized void removeHandler(IPunishmentHandler handler) { + _handlers.remove(handler.getType()); + } + + @Override + public IPunishmentHandler getHandler(PunishmentType val) { + return _handlers.get(val); + } + + @Override + public int size() { + return _handlers.size(); + } + + public static PunishmentHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final PunishmentHandler _instance = new PunishmentHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/TelnetHandler.java b/src/main/java/com/l2jserver/gameserver/handler/TelnetHandler.java new file mode 100644 index 00000000..fa5ec36c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/TelnetHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author UnAfraid + */ +public class TelnetHandler implements IHandler { + private final Map _telnetHandlers; + + protected TelnetHandler() { + _telnetHandlers = new HashMap<>(); + } + + @Override + public void registerHandler(ITelnetHandler handler) { + for (String element : handler.getCommandList()) { + _telnetHandlers.put(element.toLowerCase(), handler); + } + } + + @Override + public synchronized void removeHandler(ITelnetHandler handler) { + for (String element : handler.getCommandList()) { + _telnetHandlers.remove(element.toLowerCase()); + } + } + + @Override + public ITelnetHandler getHandler(String command) { + if (command.contains(" ")) { + command = command.substring(0, command.indexOf(" ")); + } + + return _telnetHandlers.get(command.toLowerCase()); + } + + @Override + public int size() { + return _telnetHandlers.size(); + } + + public static TelnetHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final TelnetHandler _instance = new TelnetHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/UserCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/UserCommandHandler.java new file mode 100644 index 00000000..484eb911 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/UserCommandHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author UnAfraid + */ +public class UserCommandHandler implements IHandler { + private final Map _datatable; + + protected UserCommandHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IUserCommandHandler handler) { + int[] ids = handler.getUserCommandList(); + for (int id : ids) { + _datatable.put(id, handler); + } + } + + @Override + public synchronized void removeHandler(IUserCommandHandler handler) { + int[] ids = handler.getUserCommandList(); + for (int id : ids) { + _datatable.remove(id); + } + } + + @Override + public IUserCommandHandler getHandler(Integer userCommand) { + return _datatable.get(userCommand); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static UserCommandHandler getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final UserCommandHandler _instance = new UserCommandHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/handler/VoicedCommandHandler.java b/src/main/java/com/l2jserver/gameserver/handler/VoicedCommandHandler.java new file mode 100644 index 00000000..95714ad2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/handler/VoicedCommandHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.handler; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author UnAfraid + */ +public class VoicedCommandHandler implements IHandler { + private final Map _datatable; + + protected VoicedCommandHandler() { + _datatable = new HashMap<>(); + } + + @Override + public void registerHandler(IVoicedCommandHandler handler) { + for (String id : handler.getVoicedCommandList()) { + _datatable.put(id, handler); + } + } + + @Override + public synchronized void removeHandler(IVoicedCommandHandler handler) { + for (String id : handler.getVoicedCommandList()) { + _datatable.remove(id); + } + } + + @Override + public IVoicedCommandHandler getHandler(String voicedCommand) { + String command = voicedCommand; + if (voicedCommand.contains(" ")) { + command = voicedCommand.substring(0, voicedCommand.indexOf(" ")); + } + return _datatable.get(command); + } + + @Override + public int size() { + return _datatable.size(); + } + + public static VoicedCommandHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final VoicedCommandHandler INSTANCE = new VoicedCommandHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/idfactory/BitSetIDFactory.java b/src/main/java/com/l2jserver/gameserver/idfactory/BitSetIDFactory.java new file mode 100644 index 00000000..526f1b23 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/idfactory/BitSetIDFactory.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.idfactory; + +import java.util.BitSet; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.util.file.filter.PrimeFinder; + +/** + * BitSet ID Factory. + * @version 2.6.1.0 + */ +public class BitSetIDFactory extends IdFactory { + + private static final Logger LOG = LoggerFactory.getLogger(BitSetIDFactory.class); + + private BitSet _freeIds; + + private AtomicInteger _freeIdCount; + + private AtomicInteger _nextFreeId; + + protected class BitSetCapacityCheck implements Runnable { + @Override + public void run() { + synchronized (BitSetIDFactory.this) { + if (reachingBitSetCapacity()) { + increaseBitSetCapacity(); + } + } + } + } + + protected BitSetIDFactory() { + synchronized (BitSetIDFactory.class) { + ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new BitSetCapacityCheck(), 30000, 30000); + initialize(); + } + LOG.info("{} Ids available.", _freeIds.size()); + } + + public void initialize() { + try { + _freeIds = new BitSet(PrimeFinder.nextPrime(100000)); + _freeIds.clear(); + _freeIdCount = new AtomicInteger(FREE_OBJECT_ID_SIZE); + + for (int usedObjectId : extractUsedObjectIDTable()) { + int objectID = usedObjectId - FIRST_OID; + if (objectID < 0) { + LOG.warn("Object Id {} in DB is less than minimum ID of {}!", usedObjectId, FIRST_OID); + continue; + } + _freeIds.set(usedObjectId - FIRST_OID); + _freeIdCount.decrementAndGet(); + } + + _nextFreeId = new AtomicInteger(_freeIds.nextClearBit(0)); + _initialized = true; + } catch (Exception ex) { + _initialized = false; + LOG.error("Could not be initialized properly!", ex); + } + } + + @Override + public synchronized void releaseId(int objectId) { + if ((objectId - FIRST_OID) > -1) { + _freeIds.clear(objectId - FIRST_OID); + _freeIdCount.incrementAndGet(); + } else { + LOG.warn("Release objectID {} failed (< {}).", objectId, FIRST_OID); + } + } + + @Override + public synchronized int getNextId() { + int newID = _nextFreeId.get(); + _freeIds.set(newID); + _freeIdCount.decrementAndGet(); + + int nextFree = _freeIds.nextClearBit(newID); + + if (nextFree < 0) { + nextFree = _freeIds.nextClearBit(0); + } + if (nextFree < 0) { + if (_freeIds.size() < FREE_OBJECT_ID_SIZE) { + increaseBitSetCapacity(); + } else { + throw new NullPointerException("Ran out of valid Id's."); + } + } + + _nextFreeId.set(nextFree); + + return newID + FIRST_OID; + } + + @Override + public synchronized int size() { + return _freeIdCount.get(); + } + + /** + * @return + */ + protected synchronized int usedIdCount() { + return (size() - FIRST_OID); + } + + /** + * @return + */ + protected synchronized boolean reachingBitSetCapacity() { + return PrimeFinder.nextPrime((usedIdCount() * 11) / 10) > _freeIds.size(); + } + + protected synchronized void increaseBitSetCapacity() { + BitSet newBitSet = new BitSet(PrimeFinder.nextPrime((usedIdCount() * 11) / 10)); + newBitSet.or(_freeIds); + _freeIds = newBitSet; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/idfactory/CompactionIDFactory.java b/src/main/java/com/l2jserver/gameserver/idfactory/CompactionIDFactory.java new file mode 100644 index 00000000..b165e217 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/idfactory/CompactionIDFactory.java @@ -0,0 +1,141 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.idfactory; + +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +/** + * 18.8.2010 - JIV: Disabling until someone update it + */ +@Deprecated +public class CompactionIDFactory extends IdFactory { + + private static final Logger LOG = LoggerFactory.getLogger(CompactionIDFactory.class); + + private int _curOID; + + private final int _freeSize; + + protected CompactionIDFactory() { + _curOID = FIRST_OID; + _freeSize = 0; + + try (var con = ConnectionFactory.getInstance().getConnection()) { + Integer[] tmp_obj_ids = extractUsedObjectIDTable(); + + int N = tmp_obj_ids.length; + for (int idx = 0; idx < N; idx++) { + N = insertUntil(tmp_obj_ids, idx, N, con); + } + _curOID++; + LOG.info("Next usable Object Id is {}.", _curOID); + _initialized = true; + } catch (Exception ex) { + LOG.error("Could not initialize properly!", ex); + } + } + + private int insertUntil(Integer[] tmp_obj_ids, int idx, int N, Connection con) throws SQLException { + int id = tmp_obj_ids[idx]; + if (id == _curOID) { + _curOID++; + return N; + } + // check these IDs not present in DB + if (server().badIdChecking()) { + for (String check : ID_CHECKS) { + try (var ps = con.prepareStatement(check)) { + ps.setInt(1, _curOID); + ps.setInt(2, id); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + int badId = rs.getInt(1); + LOG.error("Bad Id {} in DB found by {}!", badId, check); + throw new RuntimeException(); + } + } + } + } + } + + int hole = id - _curOID; + if (hole > (N - idx)) { + hole = N - idx; + } + for (int i = 1; i <= hole; i++) { + id = tmp_obj_ids[N - i]; + LOG.info("Compacting DB object Id={} into {}.", id, _curOID); + for (String update : ID_UPDATES) { + try (var ps = con.prepareStatement(update)) { + ps.setInt(1, _curOID); + ps.setInt(2, id); + ps.execute(); + } + } + _curOID++; + } + if (hole < (N - idx)) { + _curOID++; + } + return N - hole; + } + + @Override + public synchronized int getNextId() { + // @formatter:off + /* + * if (_freeSize == 0) + */ + return _curOID++; + /* + * else + * return _freeOIDs[--_freeSize]; + */ + // @formatter:on + } + + @Override + public synchronized void releaseId(int id) { + // Don't release Ids until we are sure it isn't messing up + // @formatter:off + /* + if (_freeSize >= _freeOIDs.length) + { + int[] tmp = new int[_freeSize + STACK_SIZE_INCREMENT]; + System.arraycopy(_freeOIDs, 0, tmp, 0, _freeSize); + _freeOIDs = tmp; + } + _freeOIDs[_freeSize++] = id; + */ + // @formatter:on + } + + @Override + public int size() { + return (_freeSize + LAST_OID) - FIRST_OID; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/idfactory/IdFactory.java b/src/main/java/com/l2jserver/gameserver/idfactory/IdFactory.java new file mode 100644 index 00000000..b0d2d138 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/idfactory/IdFactory.java @@ -0,0 +1,337 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.idfactory; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +/** + * Id Factory. + * @version 2.6.1.0 + */ +public abstract class IdFactory { + + private static final Logger LOG = LoggerFactory.getLogger(IdFactory.class); + + @Deprecated + protected static final String[] ID_UPDATES = { + "UPDATE items SET owner_id = ? WHERE owner_id = ?", + "UPDATE items SET object_id = ? WHERE object_id = ?", + "UPDATE character_quests SET charId = ? WHERE charId = ?", + "UPDATE character_contacts SET charId = ? WHERE charId = ?", + "UPDATE character_contacts SET friendId = ? WHERE contactId = ?", + "UPDATE character_friends SET charId = ? WHERE charId = ?", + "UPDATE character_friends SET friendId = ? WHERE friendId = ?", + "UPDATE character_hennas SET charId = ? WHERE charId = ?", + "UPDATE character_recipebook SET charId = ? WHERE charId = ?", + "UPDATE character_recipeshoplist SET charId = ? WHERE charId = ?", + "UPDATE character_shortcuts SET charId = ? WHERE charId = ?", + "UPDATE character_shortcuts SET shortcut_id = ? WHERE shortcut_id = ? AND type = 1", // items + "UPDATE character_macroses SET charId = ? WHERE charId = ?", + "UPDATE character_skills SET charId = ? WHERE charId = ?", + "UPDATE character_skills_save SET charId = ? WHERE charId = ?", + "UPDATE character_subclasses SET charId = ? WHERE charId = ?", + "UPDATE character_ui_actions SET charId = ? WHERE charId = ?", + "UPDATE character_ui_categories SET charId = ? WHERE charId = ?", + "UPDATE characters SET charId = ? WHERE charId = ?", + "UPDATE characters SET clanid = ? WHERE clanid = ?", + "UPDATE clan_data SET clan_id = ? WHERE clan_id = ?", + "UPDATE siege_clans SET clan_id = ? WHERE clan_id = ?", + "UPDATE clan_data SET ally_id = ? WHERE ally_id = ?", + "UPDATE clan_data SET leader_id = ? WHERE leader_id = ?", + "UPDATE pets SET item_obj_id = ? WHERE item_obj_id = ?", + "UPDATE character_hennas SET charId = ? WHERE charId = ?", + "UPDATE itemsonground SET object_id = ? WHERE object_id = ?", + "UPDATE auction_bid SET bidderId = ? WHERE bidderId = ?", + "UPDATE auction_watch SET charObjId = ? WHERE charObjId = ?", + "UPDATE olympiad_fights SET charOneId = ? WHERE charOneId = ?", + "UPDATE olympiad_fights SET charTwoId = ? WHERE charTwoId = ?", + "UPDATE heroes_diary SET charId = ? WHERE charId = ?", + "UPDATE olympiad_nobles SET charId = ? WHERE charId = ?", + "UPDATE character_offline_trade SET charId = ? WHERE charId = ?", + "UPDATE character_offline_trade_items SET charId = ? WHERE charId = ?", + "UPDATE clanhall SET ownerId = ? WHERE ownerId = ?" + }; + + protected static final String[] ID_CHECKS = { + "SELECT owner_id FROM items WHERE object_id >= ? AND object_id < ?", + "SELECT object_id FROM items WHERE object_id >= ? AND object_id < ?", + "SELECT charId FROM character_quests WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_contacts WHERE charId >= ? AND charId < ?", + "SELECT contactId FROM character_contacts WHERE contactId >= ? AND contactId < ?", + "SELECT charId FROM character_friends WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_friends WHERE friendId >= ? AND friendId < ?", + "SELECT charId FROM character_hennas WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_recipebook WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_recipeshoplist WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_shortcuts WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_macroses WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_skills WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_skills_save WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_subclasses WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_ui_actions WHERE charId >= ? AND charId < ?", + "SELECT charId FROM character_ui_categories WHERE charId >= ? AND charId < ?", + "SELECT charId FROM characters WHERE charId >= ? AND charId < ?", + "SELECT clanid FROM characters WHERE clanid >= ? AND clanid < ?", + "SELECT clan_id FROM clan_data WHERE clan_id >= ? AND clan_id < ?", + "SELECT clan_id FROM siege_clans WHERE clan_id >= ? AND clan_id < ?", + "SELECT ally_id FROM clan_data WHERE ally_id >= ? AND ally_id < ?", + "SELECT leader_id FROM clan_data WHERE leader_id >= ? AND leader_id < ?", + "SELECT item_obj_id FROM pets WHERE item_obj_id >= ? AND item_obj_id < ?", + "SELECT object_id FROM itemsonground WHERE object_id >= ? AND object_id < ?" + }; + + //@formatter:off + private static final String[][] ID_EXTRACTS = + { + {"characters","charId"}, + {"items","object_id"}, + {"clan_data","clan_id"}, + {"itemsonground","object_id"}, + {"messages","messageId"} + }; + //@formatter:on + + private static final String[] TIMESTAMPS_CLEAN = { + "DELETE FROM character_instance_time WHERE time <= ?", + "DELETE FROM character_skills_save WHERE restore_type = 1 AND systime <= ?" + }; + + protected boolean _initialized; + + public static final int FIRST_OID = 0x10000000; + public static final int LAST_OID = 0x7FFFFFFF; + public static final int FREE_OBJECT_ID_SIZE = LAST_OID - FIRST_OID; + + protected static IdFactory _instance; + + protected IdFactory() { + setAllCharacterOffline(); + if (general().databaseCleanUp()) { + if (customs().allowWedding()) { + cleanInvalidWeddings(); + } + cleanUpDB(); + } + cleanUpTimeStamps(); + } + + static { + switch (server().getIdFactory()) { + case BitSet -> _instance = new BitSetIDFactory(); + case Stack -> _instance = new StackIDFactory(); + case Compaction -> throw new UnsupportedOperationException("Compaction IdFactory is disabled."); + } + } + + private void setAllCharacterOffline() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate("UPDATE characters SET online = 0"); + LOG.info("Updated characters online status."); + } catch (Exception ex) { + LOG.warn("Could not update characters online status!", ex); + } + } + + private void cleanUpDB() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var stmt = con.createStatement()) { + long cleanupStart = System.currentTimeMillis(); + int cleanCount = 0; + // Misc/Account Related + // Please read the descriptions above each before uncommenting them. If you are still + // unsure of what exactly it does, leave it commented out. This is for those who know + // what they are doing. :) + + // Deletes only accounts that HAVE been logged into and have no characters associated + // with the account. + // cleanCount += + // stmt.executeUpdate("DELETE FROM accounts WHERE accounts.lastactive > 0 AND accounts.login NOT IN (SELECT account_name FROM characters);"); + + // Deletes any accounts that don't have characters. Whether or not the player has ever + // logged into the account. + // cleanCount += + // stmt.executeUpdate("DELETE FROM accounts WHERE accounts.login NOT IN (SELECT account_name FROM characters);"); + + // Deletes banned accounts that have not been logged into for xx amount of days + // (specified at the end of the script, default is set to 90 days). This prevents + // accounts from being deleted that were accidentally or temporarily banned. + // cleanCount += + // stmt.executeUpdate("DELETE FROM accounts WHERE accounts.accessLevel < 0 AND DATEDIFF(CURRENT_DATE( ) , FROM_UNIXTIME(`lastactive`/1000)) > 90;"); + // cleanCount += + // stmt.executeUpdate("DELETE FROM characters WHERE characters.account_name NOT IN (SELECT login FROM accounts);"); + + // If the character does not exist... + cleanCount += stmt.executeUpdate("DELETE FROM account_gsdata WHERE account_gsdata.account_name NOT IN (SELECT account_name FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_contacts WHERE character_contacts.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_contacts WHERE character_contacts.contactId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_friends WHERE character_friends.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_friends WHERE character_friends.friendId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_hennas WHERE character_hennas.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_macroses WHERE character_macroses.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_quests WHERE character_quests.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_recipebook WHERE character_recipebook.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_recipeshoplist WHERE character_recipeshoplist.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_shortcuts WHERE character_shortcuts.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_skills WHERE character_skills.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_skills_save WHERE character_skills_save.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_subclasses WHERE character_subclasses.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_raid_points WHERE character_raid_points.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_instance_time WHERE character_instance_time.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_ui_actions WHERE character_ui_actions.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_ui_categories WHERE character_ui_categories.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM items WHERE items.owner_id NOT IN (SELECT charId FROM characters) AND items.owner_id NOT IN (SELECT clan_id FROM clan_data) AND items.owner_id != -1;"); + cleanCount += stmt.executeUpdate("DELETE FROM items WHERE items.owner_id = -1 AND loc LIKE 'MAIL' AND loc_data NOT IN (SELECT messageId FROM messages WHERE senderId = -1);"); + cleanCount += stmt.executeUpdate("DELETE FROM item_auction_bid WHERE item_auction_bid.playerObjId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM item_attributes WHERE item_attributes.itemId NOT IN (SELECT object_id FROM items);"); + cleanCount += stmt.executeUpdate("DELETE FROM item_elementals WHERE item_elementals.itemId NOT IN (SELECT object_id FROM items);"); + cleanCount += stmt.executeUpdate("DELETE FROM cursed_weapons WHERE cursed_weapons.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM heroes WHERE heroes.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM olympiad_nobles WHERE olympiad_nobles.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM olympiad_nobles_eom WHERE olympiad_nobles_eom.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM pets WHERE pets.item_obj_id NOT IN (SELECT object_id FROM items);"); + cleanCount += stmt.executeUpdate("DELETE FROM seven_signs WHERE seven_signs.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM merchant_lease WHERE merchant_lease.player_id NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_reco_bonus WHERE character_reco_bonus.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_data WHERE clan_data.leader_id NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_data WHERE clan_data.clan_id NOT IN (SELECT clanid FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM olympiad_fights WHERE olympiad_fights.charOneId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM olympiad_fights WHERE olympiad_fights.charTwoId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM heroes_diary WHERE heroes_diary.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_offline_trade WHERE character_offline_trade.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_offline_trade_items WHERE character_offline_trade_items.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_quest_global_data WHERE character_quest_global_data.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_tpbookmark WHERE character_tpbookmark.charId NOT IN (SELECT charId FROM characters);"); + cleanCount += stmt.executeUpdate("DELETE FROM character_variables WHERE character_variables.charId NOT IN (SELECT charId FROM characters);"); + + // If the clan does not exist... + cleanCount += stmt.executeUpdate("DELETE FROM clan_privs WHERE clan_privs.clan_id NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_skills WHERE clan_skills.clan_id NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_subpledges WHERE clan_subpledges.clan_id NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_wars WHERE clan_wars.clan1 NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_wars WHERE clan_wars.clan2 NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clanhall_functions WHERE clanhall_functions.hall_id NOT IN (SELECT id FROM clanhall WHERE ownerId <> 0 union all SELECT clanHallId FROM siegable_clanhall WHERE ownerId <> 0);"); + cleanCount += stmt.executeUpdate("DELETE FROM siege_clans WHERE siege_clans.clan_id NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM clan_notices WHERE clan_notices.clan_id NOT IN (SELECT clan_id FROM clan_data);"); + cleanCount += stmt.executeUpdate("DELETE FROM auction_bid WHERE auction_bid.bidderId NOT IN (SELECT clan_id FROM clan_data);"); + // Untested, leaving commented out until confirmation that it's safe/works properly. Was + // initially removed because of a bug. Search for idfactory.java changes in the trac for + // further info. + // cleanCount += + // stmt.executeUpdate("DELETE FROM auction WHERE auction.id IN (SELECT id FROM clanhall WHERE ownerId <> 0) AND auction.sellerId=0;"); + // cleanCount += + // stmt.executeUpdate("DELETE FROM auction_bid WHERE auctionId NOT IN (SELECT id FROM auction)"); + + // Forum Related + cleanCount += stmt.executeUpdate("DELETE FROM forums WHERE forums.forum_owner_id NOT IN (SELECT clan_id FROM clan_data) AND forums.forum_parent=2;"); + cleanCount += stmt.executeUpdate("DELETE FROM forums WHERE forums.forum_owner_id NOT IN (SELECT charId FROM characters) AND forums.forum_parent=3;"); + cleanCount += stmt.executeUpdate("DELETE FROM posts WHERE posts.post_forum_id NOT IN (SELECT forum_id FROM forums);"); + cleanCount += stmt.executeUpdate("DELETE FROM topic WHERE topic.topic_forum_id NOT IN (SELECT forum_id FROM forums);"); + + // Update needed items after cleaning has taken place. + stmt.executeUpdate("UPDATE clan_data SET auction_bid_at = 0 WHERE auction_bid_at NOT IN (SELECT auctionId FROM auction_bid);"); + stmt.executeUpdate("UPDATE clan_data SET new_leader_id = 0 WHERE new_leader_id <> 0 AND new_leader_id NOT IN (SELECT charId FROM characters);"); + stmt.executeUpdate("UPDATE clan_subpledges SET leader_id=0 WHERE clan_subpledges.leader_id NOT IN (SELECT charId FROM characters) AND leader_id > 0;"); + stmt.executeUpdate("UPDATE castle SET taxpercent=0 WHERE castle.id NOT IN (SELECT hasCastle FROM clan_data);"); + stmt.executeUpdate("UPDATE characters SET clanid=0, clan_privs=0, wantspeace=0, subpledge=0, lvl_joined_academy=0, apprentice=0, sponsor=0, clan_join_expiry_time=0, clan_create_expiry_time=0 WHERE characters.clanid > 0 AND characters.clanid NOT IN (SELECT clan_id FROM clan_data);"); + stmt.executeUpdate("UPDATE clanhall SET ownerId=0, paidUntil=0, paid=0 WHERE clanhall.ownerId NOT IN (SELECT clan_id FROM clan_data);"); + stmt.executeUpdate("UPDATE fort SET owner=0 WHERE owner NOT IN (SELECT clan_id FROM clan_data);"); + + LOG.info("Cleaned {} elements from database in {}s.", cleanCount, (System.currentTimeMillis() - cleanupStart) / 1000); + } catch (Exception ex) { + LOG.warn("Could not clean up database!", ex); + } + } + + private void cleanInvalidWeddings() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate("DELETE FROM mods_wedding WHERE player1Id NOT IN (SELECT charId FROM characters)"); + s.executeUpdate("DELETE FROM mods_wedding WHERE player2Id NOT IN (SELECT charId FROM characters)"); + LOG.info("Cleaned up invalid weddings."); + } catch (Exception ex) { + LOG.warn("Could not clean up invalid weddings!", ex); + } + } + + private void cleanUpTimeStamps() { + try (var con = ConnectionFactory.getInstance().getConnection()) { + int cleanCount = 0; + for (String line : TIMESTAMPS_CLEAN) { + try (var stmt = con.prepareStatement(line)) { + stmt.setLong(1, System.currentTimeMillis()); + cleanCount += stmt.executeUpdate(); + } + } + LOG.info("Cleaned {} expired timestamps from database.", cleanCount); + } catch (Exception e) { + } + } + + protected final Integer[] extractUsedObjectIDTable() throws Exception { + final List temp = new ArrayList<>(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + + String extractUsedObjectIdsQuery = ""; + + for (String[] tblClmn : ID_EXTRACTS) { + extractUsedObjectIdsQuery += "SELECT " + tblClmn[1] + " FROM " + tblClmn[0] + " UNION "; + } + + extractUsedObjectIdsQuery = extractUsedObjectIdsQuery.substring(0, extractUsedObjectIdsQuery.length() - 7); // Remove the last " UNION " + try (var rs = s.executeQuery(extractUsedObjectIdsQuery)) { + while (rs.next()) { + temp.add(rs.getInt(1)); + } + } + } + Collections.sort(temp); + return temp.toArray(new Integer[temp.size()]); + } + + public boolean isInitialized() { + return _initialized; + } + + public static IdFactory getInstance() { + return _instance; + } + + public abstract int getNextId(); + + /** + * return a used Object ID back to the pool + * @param id + */ + public abstract void releaseId(int id); + + public abstract int size(); +} diff --git a/src/main/java/com/l2jserver/gameserver/idfactory/IdFactoryType.java b/src/main/java/com/l2jserver/gameserver/idfactory/IdFactoryType.java new file mode 100644 index 00000000..04384243 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/idfactory/IdFactoryType.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.idfactory; + +/** + * Id Factory Type. + * @author Zoey76 + * @version 2.6.1.0 + */ +public enum IdFactoryType { + Compaction, + BitSet, + Stack +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/idfactory/StackIDFactory.java b/src/main/java/com/l2jserver/gameserver/idfactory/StackIDFactory.java new file mode 100644 index 00000000..cf0bb462 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/idfactory/StackIDFactory.java @@ -0,0 +1,140 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.idfactory; + +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Stack; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +/** + * Stack ID Factory. + * @version 2.6.1.0 + */ +public class StackIDFactory extends IdFactory { + + private static final Logger LOG = LoggerFactory.getLogger(IdFactory.class); + + private int _curOID; + + private int _tempOID; + + private final Stack _freeOIDStack = new Stack<>(); + + protected StackIDFactory() { + _curOID = FIRST_OID; + _tempOID = FIRST_OID; + + try (var con = ConnectionFactory.getInstance().getConnection()) { + Integer[] tmp_obj_ids = extractUsedObjectIDTable(); + if (tmp_obj_ids.length > 0) { + _curOID = tmp_obj_ids[tmp_obj_ids.length - 1]; + } + LOG.info("Max Id = {}.", _curOID); + + int N = tmp_obj_ids.length; + for (int idx = 0; idx < N; idx++) { + N = insertUntil(tmp_obj_ids, idx, N, con); + } + + _curOID++; + LOG.info("Next usable Object Id is {}.", _curOID); + _initialized = true; + } catch (Exception ex) { + LOG.error("Could not be initialized properly!", ex); + } + } + + private int insertUntil(Integer[] tmp_obj_ids, int idx, int N, Connection con) throws SQLException { + int id = tmp_obj_ids[idx]; + if (id == _tempOID) { + _tempOID++; + return N; + } + // check these IDs not present in DB + if (server().badIdChecking()) { + for (String check : ID_CHECKS) { + try (var ps = con.prepareStatement(check)) { + ps.setInt(1, _tempOID); + // ps.setInt(1, _curOID); + ps.setInt(2, id); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + int badId = rs.getInt(1); + LOG.error("Bad Id {} in DB found by {}!", badId, check); + throw new RuntimeException(); + } + } + } + } + } + + // int hole = id - _curOID; + int hole = id - _tempOID; + if (hole > (N - idx)) { + hole = N - idx; + } + for (int i = 1; i <= hole; i++) { + // log.info("Free ID added " + (_tempOID)); + _freeOIDStack.push(_tempOID); + _tempOID++; + // _curOID++; + } + if (hole < (N - idx)) { + _tempOID++; + } + return N - hole; + } + + public static IdFactory getInstance() { + return _instance; + } + + @Override + public synchronized int getNextId() { + int id; + if (!_freeOIDStack.empty()) { + id = _freeOIDStack.pop(); + } else { + id = _curOID; + _curOID = _curOID + 1; + } + return id; + } + + /** + * return a used Object ID back to the pool + * @param id + */ + @Override + public synchronized void releaseId(int id) { + _freeOIDStack.push(id); + } + + @Override + public int size() { + return (FREE_OBJECT_ID_SIZE - _curOID) + FIRST_OID + _freeOIDStack.size(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/AirShipManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/AirShipManager.java new file mode 100644 index 00000000..290380bf --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/AirShipManager.java @@ -0,0 +1,267 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.AirShipTeleportList; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.VehiclePathPoint; +import com.l2jserver.gameserver.model.actor.instance.L2AirShipInstance; +import com.l2jserver.gameserver.model.actor.instance.L2ControllableAirShipInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.network.serverpackets.ExAirShipTeleportList; + +public class AirShipManager { + + private static final Logger LOG = LoggerFactory.getLogger(AirShipManager.class); + + private static final String LOAD_DB = "SELECT * FROM airships"; + private static final String ADD_DB = "INSERT INTO airships (owner_id,fuel) VALUES (?,?)"; + private static final String UPDATE_DB = "UPDATE airships SET fuel=? WHERE owner_id=?"; + + private final L2CharTemplate _airShipTemplate; + private final Map _airShipsInfo = new HashMap<>(); + private final Map _airShips = new HashMap<>(); + private final Map _teleports = new HashMap<>(); + + protected AirShipManager() { + StatsSet npcDat = new StatsSet(); + npcDat.set("npcId", 9); + npcDat.set("level", 0); + npcDat.set("jClass", "boat"); + + npcDat.set("baseSTR", 0); + npcDat.set("baseCON", 0); + npcDat.set("baseDEX", 0); + npcDat.set("baseINT", 0); + npcDat.set("baseWIT", 0); + npcDat.set("baseMEN", 0); + + npcDat.set("baseShldDef", 0); + npcDat.set("baseShldRate", 0); + npcDat.set("baseAccCombat", 38); + npcDat.set("baseEvasRate", 38); + npcDat.set("baseCritRate", 38); + + npcDat.set("collisionRadius", 0); + npcDat.set("collisionHeight", 0); + npcDat.set("sex", "male"); + npcDat.set("type", ""); + npcDat.set("baseAtkRange", 0); + npcDat.set("baseMpMax", 0); + npcDat.set("baseCpMax", 0); + npcDat.set("rewardExp", 0); + npcDat.set("rewardSp", 0); + npcDat.set("basePAtk", 0); + npcDat.set("baseMAtk", 0); + npcDat.set("basePAtkSpd", 0); + npcDat.set("aggroRange", 0); + npcDat.set("baseMAtkSpd", 0); + npcDat.set("rhand", 0); + npcDat.set("lhand", 0); + npcDat.set("armor", 0); + npcDat.set("baseWalkSpd", 0); + npcDat.set("baseRunSpd", 0); + npcDat.set("name", "AirShip"); + npcDat.set("baseHpMax", 50000); + npcDat.set("baseHpReg", 3.e-3f); + npcDat.set("baseMpReg", 3.e-3f); + npcDat.set("basePDef", 100); + npcDat.set("baseMDef", 100); + _airShipTemplate = new L2CharTemplate(npcDat); + + load(); + } + + public L2AirShipInstance getNewAirShip(int x, int y, int z, int heading) { + final var objectId = IdFactory.getInstance().getNextId(); + final var airShip = new L2AirShipInstance(objectId, _airShipTemplate); + airShip.setHeading(heading); + airShip.setXYZInvisible(x, y, z); + airShip.spawnMe(); + airShip.getStat().setMoveSpeed(280); + airShip.getStat().setRotationSpeed(2000); + return airShip; + } + + public L2AirShipInstance getNewAirShip(int x, int y, int z, int heading, int ownerId) { + final StatsSet info = _airShipsInfo.get(ownerId); + if (info == null) { + return null; + } + + final L2AirShipInstance airShip; + if (_airShips.containsKey(ownerId)) { + airShip = _airShips.get(ownerId); + airShip.refreshID(); + } else { + final var objectId = IdFactory.getInstance().getNextId(); + airShip = new L2ControllableAirShipInstance(objectId, _airShipTemplate, ownerId); + _airShips.put(ownerId, airShip); + + airShip.setMaxFuel(600); + airShip.setFuel(info.getInt("fuel")); + airShip.getStat().setMoveSpeed(280); + airShip.getStat().setRotationSpeed(2000); + } + + airShip.setHeading(heading); + airShip.setXYZInvisible(x, y, z); + airShip.spawnMe(); + return airShip; + } + + public void removeAirShip(L2AirShipInstance ship) { + if (ship.getOwnerId() != 0) { + storeInDb(ship.getOwnerId()); + final StatsSet info = _airShipsInfo.get(ship.getOwnerId()); + if (info != null) { + info.set("fuel", ship.getFuel()); + } + } + } + + public boolean hasAirShipLicense(int ownerId) { + return _airShipsInfo.containsKey(ownerId); + } + + public void registerLicense(int ownerId) { + if (!_airShipsInfo.containsKey(ownerId)) { + final StatsSet info = new StatsSet(); + info.set("fuel", 600); + + _airShipsInfo.put(ownerId, info); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ADD_DB)) { + ps.setInt(1, ownerId); + ps.setInt(2, info.getInt("fuel")); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("Could not add new airship license!", ex); + } + } + } + + public boolean hasAirShip(int ownerId) { + final L2AirShipInstance ship = _airShips.get(ownerId); + return (ship != null) && (ship.isVisible() || ship.isTeleporting()); + } + + public void registerAirShipTeleportList(int dockId, int locationId, VehiclePathPoint[][] tp, int[] fuelConsumption) { + if (tp.length != fuelConsumption.length) { + return; + } + + _teleports.put(dockId, new AirShipTeleportList(locationId, fuelConsumption, tp)); + } + + public void sendAirShipTeleportList(L2PcInstance player) { + if ((player == null) || !player.isInAirShip()) { + return; + } + + final L2AirShipInstance ship = player.getAirShip(); + if (!ship.isCaptain(player) || !ship.isInDock() || ship.isMoving()) { + return; + } + + int dockId = ship.getDockId(); + if (!_teleports.containsKey(dockId)) { + return; + } + + final AirShipTeleportList all = _teleports.get(dockId); + player.sendPacket(new ExAirShipTeleportList(all.getLocation(), all.getRoute(), all.getFuel())); + } + + public VehiclePathPoint[] getTeleportDestination(int dockId, int index) { + final AirShipTeleportList all = _teleports.get(dockId); + if (all == null) { + return null; + } + + if ((index < -1) || (index >= all.getRoute().length)) { + return null; + } + + return all.getRoute()[index + 1]; + } + + public int getFuelConsumption(int dockId, int index) { + final AirShipTeleportList all = _teleports.get(dockId); + if (all == null) { + return 0; + } + + if ((index < -1) || (index >= all.getFuel().length)) { + return 0; + } + + return all.getFuel()[index + 1]; + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(LOAD_DB)) { + StatsSet info; + while (rs.next()) { + info = new StatsSet(); + info.set("fuel", rs.getInt("fuel")); + _airShipsInfo.put(rs.getInt("owner_id"), info); + } + } catch (Exception ex) { + LOG.warn("Could not load airships table!", ex); + } + LOG.info("Loaded {} private airships.", _airShipsInfo.size()); + } + + private void storeInDb(int ownerId) { + StatsSet info = _airShipsInfo.get(ownerId); + if (info == null) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_DB)) { + ps.setInt(1, info.getInt("fuel")); + ps.setInt(2, ownerId); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("Could not update airships table!", ex); + } + } + + public static AirShipManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final AirShipManager _instance = new AirShipManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/AntiFeedManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/AntiFeedManager.java new file mode 100644 index 00000000..968f1e1a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/AntiFeedManager.java @@ -0,0 +1,245 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.L2GameClient; + +public final class AntiFeedManager { + public static final int GAME_ID = 0; + public static final int OLYMPIAD_ID = 1; + public static final int TVT_ID = 2; + public static final int L2EVENT_ID = 3; + + private final Map _lastDeathTimes = new ConcurrentHashMap<>(); + private final Map> _eventIPs = new ConcurrentHashMap<>(); + + protected AntiFeedManager() { + } + + /** + * Set time of the last player's death to current + * @param objectId Player's objectId + */ + public void setLastDeathTime(int objectId) { + _lastDeathTimes.put(objectId, System.currentTimeMillis()); + } + + /** + * Check if current kill should be counted as non-feeded. + * @param attacker Attacker character + * @param target Target character + * @return True if kill is non-feeded. + */ + public boolean check(L2Character attacker, L2Character target) { + if (!customs().antiFeedEnable()) { + return true; + } + + if (target == null) { + return false; + } + + final var targetPlayer = target.getActingPlayer(); + if (targetPlayer == null) { + return false; + } + + if ((customs().getAntiFeedInterval() > 0) && _lastDeathTimes.containsKey(targetPlayer.getObjectId())) { + if ((System.currentTimeMillis() - _lastDeathTimes.get(targetPlayer.getObjectId())) < SECONDS.toMillis(customs().getAntiFeedInterval())) { + return false; + } + } + + if (customs().antiFeedDualbox() && (attacker != null)) { + final var attackerPlayer = attacker.getActingPlayer(); + if (attackerPlayer == null) { + return false; + } + + final var targetClient = targetPlayer.getClient(); + final var attackerClient = attackerPlayer.getClient(); + if ((targetClient == null) || (attackerClient == null) || targetClient.isDetached() || attackerClient.isDetached()) { + // unable to check ip address + return !customs().antiFeedDisconnectedAsDualbox(); + } + + return !targetClient.getConnectionAddress().equals(attackerClient.getConnectionAddress()); + } + + return true; + } + + /** + * Clears all timestamps + */ + public void clear() { + _lastDeathTimes.clear(); + } + + /** + * Register new event for dualbox check. Should be called only once. + * @param eventId + */ + public void registerEvent(int eventId) { + _eventIPs.putIfAbsent(eventId, new ConcurrentHashMap<>()); + } + + /** + * @param eventId + * @param player + * @param max + * @return If number of all simultaneous connections from player's IP address lower than max then increment connection count and return true.
    + * False if number of all simultaneous connections from player's IP address higher than max. + */ + public boolean tryAddPlayer(int eventId, L2PcInstance player, int max) { + return tryAddClient(eventId, player.getClient(), max); + } + + /** + * @param eventId + * @param client + * @param max + * @return If number of all simultaneous connections from player's IP address lower than max then increment connection count and return true.
    + * False if number of all simultaneous connections from player's IP address higher than max. + */ + public boolean tryAddClient(int eventId, L2GameClient client, int max) { + if (client == null) { + return false; // unable to determine IP address + } + + final var event = _eventIPs.get(eventId); + if (event == null) { + return false; // no such event registered + } + + final var addrHash = client.getConnectionAddress().hashCode(); + final var connectionCount = event.computeIfAbsent(addrHash, _ -> new AtomicInteger()); + final int whiteListCount = customs().getDualboxCheckWhitelist().getOrDefault(addrHash, 0); + if (whiteListCount < 0) { + connectionCount.incrementAndGet(); + return true; + } + + final var limit = max + whiteListCount; + return connectionCount.getAndUpdate(current -> current < limit ? current + 1 : current) < limit; + } + + /** + * Decreasing number of active connection from player's IP address + * @param eventId + * @param player + * @return true if success and false if any problem detected. + */ + public boolean removePlayer(int eventId, L2PcInstance player) { + return removeClient(eventId, player.getClient()); + } + + /** + * Decreasing number of active connection from player's IP address + * @param eventId + * @param client + * @return true if success and false if any problem detected. + */ + public boolean removeClient(int eventId, L2GameClient client) { + if (client == null) { + return false; // unable to determine IP address + } + + final var event = _eventIPs.get(eventId); + if (event == null) { + return false; // no such event registered + } + + final var addrHash = client.getConnectionAddress().hashCode(); + + return event.computeIfPresent(addrHash, (_, v) -> { + if (v.decrementAndGet() == 0) { + return null; + } + return v; + }) != null; + } + + /** + * Remove player connection IP address from all registered events lists. + * @param client + */ + public void onDisconnect(L2GameClient client) { + if (client == null) { + return; + } + + _eventIPs.forEach((k, _) -> removeClient(k, client)); + } + + /** + * Clear all entries for this eventId. + * @param eventId + */ + public void clear(int eventId) { + final Map event = _eventIPs.get(eventId); + if (event != null) { + event.clear(); + } + } + + /** + * @param player + * @param max + * @return maximum number of allowed connections (whitelist + max) + */ + public int getLimit(L2PcInstance player, int max) { + return getLimit(player.getClient(), max); + } + + /** + * @param client + * @param max + * @return maximum number of allowed connections (whitelist + max) + */ + public int getLimit(L2GameClient client, int max) { + if (client == null) { + return max; + } + + final var addrHash = client.getConnectionAddress().hashCode(); + int limit = max; + if (customs().getDualboxCheckWhitelist().containsKey(addrHash)) { + limit += customs().getDualboxCheckWhitelist().get(addrHash); + } + return limit; + } + + public static AntiFeedManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final AntiFeedManager _instance = new AntiFeedManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/AuctionManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/AuctionManager.java new file mode 100644 index 00000000..6678abdd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/AuctionManager.java @@ -0,0 +1,162 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.entity.Auction; + +/** + * Zoey76: TODO: Rewrite it and unharcode it. + */ +public final class AuctionManager { + + private static final Logger LOG = LoggerFactory.getLogger(AuctionManager.class); + + private final List _auctions = new ArrayList<>(); + + private static final String[] ITEM_INIT_DATA = { + "(22, 0, 'NPC', 'NPC Clan', 'ClanHall', 22, 0, 'Moonstone Hall', 1, 20000000, 0, 1073037600000)", + "(23, 0, 'NPC', 'NPC Clan', 'ClanHall', 23, 0, 'Onyx Hall', 1, 20000000, 0, 1073037600000)", + "(24, 0, 'NPC', 'NPC Clan', 'ClanHall', 24, 0, 'Topaz Hall', 1, 20000000, 0, 1073037600000)", + "(25, 0, 'NPC', 'NPC Clan', 'ClanHall', 25, 0, 'Ruby Hall', 1, 20000000, 0, 1073037600000)", + "(26, 0, 'NPC', 'NPC Clan', 'ClanHall', 26, 0, 'Crystal Hall', 1, 20000000, 0, 1073037600000)", + "(27, 0, 'NPC', 'NPC Clan', 'ClanHall', 27, 0, 'Onyx Hall', 1, 20000000, 0, 1073037600000)", + "(28, 0, 'NPC', 'NPC Clan', 'ClanHall', 28, 0, 'Sapphire Hall', 1, 20000000, 0, 1073037600000)", + "(29, 0, 'NPC', 'NPC Clan', 'ClanHall', 29, 0, 'Moonstone Hall', 1, 20000000, 0, 1073037600000)", + "(30, 0, 'NPC', 'NPC Clan', 'ClanHall', 30, 0, 'Emerald Hall', 1, 20000000, 0, 1073037600000)", + "(31, 0, 'NPC', 'NPC Clan', 'ClanHall', 31, 0, 'The Atramental Barracks', 1, 8000000, 0, 1073037600000)", + "(32, 0, 'NPC', 'NPC Clan', 'ClanHall', 32, 0, 'The Scarlet Barracks', 1, 8000000, 0, 1073037600000)", + "(33, 0, 'NPC', 'NPC Clan', 'ClanHall', 33, 0, 'The Viridian Barracks', 1, 8000000, 0, 1073037600000)", + "(36, 0, 'NPC', 'NPC Clan', 'ClanHall', 36, 0, 'The Golden Chamber', 1, 50000000, 0, 1106827200000)", + "(37, 0, 'NPC', 'NPC Clan', 'ClanHall', 37, 0, 'The Silver Chamber', 1, 50000000, 0, 1106827200000)", + "(38, 0, 'NPC', 'NPC Clan', 'ClanHall', 38, 0, 'The Mithril Chamber', 1, 50000000, 0, 1106827200000)", + "(39, 0, 'NPC', 'NPC Clan', 'ClanHall', 39, 0, 'Silver Manor', 1, 50000000, 0, 1106827200000)", + "(40, 0, 'NPC', 'NPC Clan', 'ClanHall', 40, 0, 'Gold Manor', 1, 50000000, 0, 1106827200000)", + "(41, 0, 'NPC', 'NPC Clan', 'ClanHall', 41, 0, 'The Bronze Chamber', 1, 50000000, 0, 1106827200000)", + "(42, 0, 'NPC', 'NPC Clan', 'ClanHall', 42, 0, 'The Golden Chamber', 1, 50000000, 0, 1106827200000)", + "(43, 0, 'NPC', 'NPC Clan', 'ClanHall', 43, 0, 'The Silver Chamber', 1, 50000000, 0, 1106827200000)", + "(44, 0, 'NPC', 'NPC Clan', 'ClanHall', 44, 0, 'The Mithril Chamber', 1, 50000000, 0, 1106827200000)", + "(45, 0, 'NPC', 'NPC Clan', 'ClanHall', 45, 0, 'The Bronze Chamber', 1, 50000000, 0, 1106827200000)", + "(46, 0, 'NPC', 'NPC Clan', 'ClanHall', 46, 0, 'Silver Manor', 1, 50000000, 0, 1106827200000)", + "(47, 0, 'NPC', 'NPC Clan', 'ClanHall', 47, 0, 'Moonstone Hall', 1, 50000000, 0, 1106827200000)", + "(48, 0, 'NPC', 'NPC Clan', 'ClanHall', 48, 0, 'Onyx Hall', 1, 50000000, 0, 1106827200000)", + "(49, 0, 'NPC', 'NPC Clan', 'ClanHall', 49, 0, 'Emerald Hall', 1, 50000000, 0, 1106827200000)", + "(50, 0, 'NPC', 'NPC Clan', 'ClanHall', 50, 0, 'Sapphire Hall', 1, 50000000, 0, 1106827200000)", + "(51, 0, 'NPC', 'NPC Clan', 'ClanHall', 51, 0, 'Mont Chamber', 1, 50000000, 0, 1106827200000)", + "(52, 0, 'NPC', 'NPC Clan', 'ClanHall', 52, 0, 'Astaire Chamber', 1, 50000000, 0, 1106827200000)", + "(53, 0, 'NPC', 'NPC Clan', 'ClanHall', 53, 0, 'Aria Chamber', 1, 50000000, 0, 1106827200000)", + "(54, 0, 'NPC', 'NPC Clan', 'ClanHall', 54, 0, 'Yiana Chamber', 1, 50000000, 0, 1106827200000)", + "(55, 0, 'NPC', 'NPC Clan', 'ClanHall', 55, 0, 'Roien Chamber', 1, 50000000, 0, 1106827200000)", + "(56, 0, 'NPC', 'NPC Clan', 'ClanHall', 56, 0, 'Luna Chamber', 1, 50000000, 0, 1106827200000)", + "(57, 0, 'NPC', 'NPC Clan', 'ClanHall', 57, 0, 'Traban Chamber', 1, 50000000, 0, 1106827200000)", + "(58, 0, 'NPC', 'NPC Clan', 'ClanHall', 58, 0, 'Eisen Hall', 1, 20000000, 0, 1106827200000)", + "(59, 0, 'NPC', 'NPC Clan', 'ClanHall', 59, 0, 'Heavy Metal Hall', 1, 20000000, 0, 1106827200000)", + "(60, 0, 'NPC', 'NPC Clan', 'ClanHall', 60, 0, 'Molten Ore Hall', 1, 20000000, 0, 1106827200000)", + "(61, 0, 'NPC', 'NPC Clan', 'ClanHall', 61, 0, 'Titan Hall', 1, 20000000, 0, 1106827200000)" + }; + + // @formatter:off + private static final int[] ITEM_INIT_DATA_ID = + { + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61 + }; + // @formatter:on + + protected AuctionManager() { + load(); + } + + public void reload() { + _auctions.clear(); + load(); + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT id FROM auction ORDER BY id")) { + while (rs.next()) { + _auctions.add(new Auction(rs.getInt("id"))); + } + LOG.info("Loaded {} auction(s).", _auctions.size()); + } catch (Exception ex) { + LOG.warn("There has been an error loading auctions from database!", ex); + } + } + + public Auction getAuction(int auctionId) { + int index = getAuctionIndex(auctionId); + if (index >= 0) { + return _auctions.get(index); + } + return null; + } + + public int getAuctionIndex(int auctionId) { + Auction auction; + for (int i = 0; i < _auctions.size(); i++) { + auction = _auctions.get(i); + if ((auction != null) && (auction.getId() == auctionId)) { + return i; + } + } + return -1; + } + + public List getAuctions() { + return _auctions; + } + + public void initNPC(int id) { + int i; + for (i = 0; i < ITEM_INIT_DATA_ID.length; i++) { + if (ITEM_INIT_DATA_ID[i] == id) { + break; + } + } + if ((i >= ITEM_INIT_DATA_ID.length) || (ITEM_INIT_DATA_ID[i] != id)) { + LOG.warn("Clan hall auction not found for Id {}!", id); + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate("INSERT INTO `auction` VALUES " + ITEM_INIT_DATA[i]); + _auctions.add(new Auction(id)); + LOG.info("Created auction for clan hall Id {}.", id); + } catch (Exception ex) { + LOG.error("There has been an error storing auction!", ex); + } + } + + public static AuctionManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final AuctionManager INSTANCE = new AuctionManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/BoatManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/BoatManager.java new file mode 100644 index 00000000..09f680f0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/BoatManager.java @@ -0,0 +1,183 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.VehiclePathPoint; +import com.l2jserver.gameserver.model.actor.instance.L2BoatInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; + +public class BoatManager { + private final Map _boats = new ConcurrentHashMap<>(); + private final boolean[] _docksBusy = new boolean[3]; + + public static final int TALKING_ISLAND = 1; + public static final int GLUDIN_HARBOR = 2; + public static final int RUNE_HARBOR = 3; + + public static BoatManager getInstance() { + return SingletonHolder._instance; + } + + protected BoatManager() { + Arrays.fill(_docksBusy, false); + } + + public L2BoatInstance getNewBoat(int boatId, int x, int y, int z, int heading) { + if (!general().allowBoat()) { + return null; + } + + StatsSet npcDat = new StatsSet(); + npcDat.set("npcId", boatId); + npcDat.set("level", 0); + npcDat.set("jClass", "boat"); + + npcDat.set("baseSTR", 0); + npcDat.set("baseCON", 0); + npcDat.set("baseDEX", 0); + npcDat.set("baseINT", 0); + npcDat.set("baseWIT", 0); + npcDat.set("baseMEN", 0); + + npcDat.set("baseShldDef", 0); + npcDat.set("baseShldRate", 0); + npcDat.set("baseAccCombat", 38); + npcDat.set("baseEvasRate", 38); + npcDat.set("baseCritRate", 38); + + // npcDat.set("name", ""); + npcDat.set("collision_radius", 0); + npcDat.set("collision_height", 0); + npcDat.set("sex", "male"); + npcDat.set("type", ""); + npcDat.set("baseAtkRange", 0); + npcDat.set("baseMpMax", 0); + npcDat.set("baseCpMax", 0); + npcDat.set("rewardExp", 0); + npcDat.set("rewardSp", 0); + npcDat.set("basePAtk", 0); + npcDat.set("baseMAtk", 0); + npcDat.set("basePAtkSpd", 0); + npcDat.set("aggroRange", 0); + npcDat.set("baseMAtkSpd", 0); + npcDat.set("rhand", 0); + npcDat.set("lhand", 0); + npcDat.set("armor", 0); + npcDat.set("baseWalkSpd", 0); + npcDat.set("baseRunSpd", 0); + npcDat.set("baseHpMax", 50000); + npcDat.set("baseHpReg", 3.e-3f); + npcDat.set("baseMpReg", 3.e-3f); + npcDat.set("basePDef", 100); + npcDat.set("baseMDef", 100); + + final var objectId = IdFactory.getInstance().getNextId(); + final var boat = new L2BoatInstance(objectId, new L2CharTemplate(npcDat)); + boat.setHeading(heading); + boat.setXYZInvisible(x, y, z); + boat.spawnMe(); + + _boats.put(boat.getObjectId(), boat); + return boat; + } + + /** + * @param boatId + * @return + */ + public L2BoatInstance getBoat(int boatId) { + return _boats.get(boatId); + } + + /** + * Lock/unlock dock so only one ship can be docked + * @param h Dock Id + * @param value True if dock is locked + */ + public void dockShip(int h, boolean value) { + try { + _docksBusy[h] = value; + } catch (ArrayIndexOutOfBoundsException e) { + } + } + + /** + * Check if dock is busy + * @param h Dock Id + * @return Trye if dock is locked + */ + public boolean dockBusy(int h) { + try { + return _docksBusy[h]; + } catch (ArrayIndexOutOfBoundsException e) { + return false; + } + } + + /** + * Broadcast one packet in both path points + * @param point1 + * @param point2 + * @param packet + */ + public void broadcastPacket(VehiclePathPoint point1, VehiclePathPoint point2, L2GameServerPacket packet) { + broadcastPacketsToPlayers(point1, point2, packet); + } + + /** + * Broadcast several packets in both path points + * @param point1 + * @param point2 + * @param packets + */ + public void broadcastPackets(VehiclePathPoint point1, VehiclePathPoint point2, L2GameServerPacket... packets) { + broadcastPacketsToPlayers(point1, point2, packets); + } + + private void broadcastPacketsToPlayers(VehiclePathPoint point1, VehiclePathPoint point2, L2GameServerPacket... packets) { + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + if (Math.hypot(player.getX() - point1.getX(), player.getY() - point1.getY()) < general().getBoatBroadcastRadius()) { + for (L2GameServerPacket p : packets) { + player.sendPacket(p); + } + } else { + if (Math.hypot(player.getX() - point2.getX(), player.getY() - point2.getY()) < general().getBoatBroadcastRadius()) { + for (L2GameServerPacket p : packets) { + player.sendPacket(p); + } + } + } + } + } + + private static class SingletonHolder { + protected static final BoatManager _instance = new BoatManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManager.java new file mode 100644 index 00000000..f7983959 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManager.java @@ -0,0 +1,278 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.InstanceListManager; +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +public final class CastleManager implements InstanceListManager { + + private static final Logger LOG = LoggerFactory.getLogger(CastleManager.class); + + private final List _castles = new ArrayList<>(); + + private final Map _castleSiegeDate = new ConcurrentHashMap<>(); + + private static final int[] _castleCirclets = { + 0, + 6838, + 6835, + 6839, + 6837, + 6840, + 6834, + 6836, + 8182, + 8183 + }; + + public int findNearestCastleIndex(L2Object obj) { + return findNearestCastleIndex(obj, Long.MAX_VALUE); + } + + public int findNearestCastleIndex(L2Object obj, long maxDistance) { + int index = getCastleIndex(obj); + if (index < 0) { + double distance; + Castle castle; + for (int i = 0; i < _castles.size(); i++) { + castle = _castles.get(i); + if (castle == null) { + continue; + } + distance = castle.getDistance(obj); + if (maxDistance > distance) { + maxDistance = (long) distance; + index = i; + } + } + } + return index; + } + + public Castle getCastleById(int castleId) { + for (Castle temp : _castles) { + if (temp.getResidenceId() == castleId) { + return temp; + } + } + return null; + } + + public Castle getCastleByOwner(L2Clan clan) { + for (Castle temp : _castles) { + if (temp.getOwnerId() == clan.getId()) { + return temp; + } + } + return null; + } + + public Castle getCastle(String name) { + for (Castle temp : _castles) { + if (temp.getName().equalsIgnoreCase(name.trim())) { + return temp; + } + } + return null; + } + + public Castle getCastle(int x, int y, int z) { + for (Castle temp : _castles) { + if (temp.checkIfInZone(x, y, z)) { + return temp; + } + } + return null; + } + + public Castle getCastle(L2Object activeObject) { + return getCastle(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public int getCastleIndex(int castleId) { + Castle castle; + for (int i = 0; i < _castles.size(); i++) { + castle = _castles.get(i); + if ((castle != null) && (castle.getResidenceId() == castleId)) { + return i; + } + } + return -1; + } + + public int getCastleIndex(L2Object activeObject) { + return getCastleIndex(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public int getCastleIndex(int x, int y, int z) { + Castle castle; + for (int i = 0; i < _castles.size(); i++) { + castle = _castles.get(i); + if ((castle != null) && castle.checkIfInZone(x, y, z)) { + return i; + } + } + return -1; + } + + public List getCastles() { + return _castles; + } + + public boolean hasOwnedCastle() { + boolean hasOwnedCastle = false; + for (Castle castle : _castles) { + if (castle.getOwnerId() > 0) { + hasOwnedCastle = true; + break; + } + } + return hasOwnedCastle; + } + + public void validateTaxes(int sealStrifeOwner) { + int maxTax = switch (sealStrifeOwner) { + case SevenSigns.CABAL_DUSK -> 5; + case SevenSigns.CABAL_DAWN -> 25; + default -> 15; // no owner + }; + for (Castle castle : _castles) { + if (castle.getTaxPercent() > maxTax) { + castle.setTaxPercent(maxTax); + } + } + } + + public int getCirclet() { + return getCircletByCastleId(1); + } + + public int getCircletByCastleId(int castleId) { + if ((castleId > 0) && (castleId < 10)) { + return _castleCirclets[castleId]; + } + + return 0; + } + + // remove this castle's circlets from the clan + public void removeCirclet(L2Clan clan, int castleId) { + for (L2ClanMember member : clan.getMembers()) { + removeCirclet(member, castleId); + } + } + + public void removeCirclet(L2ClanMember member, int castleId) { + if (member == null) { + return; + } + L2PcInstance player = member.getPlayerInstance(); + int circletId = getCircletByCastleId(castleId); + + if (circletId != 0) { + // online-player circlet removal + if (player != null) { + try { + L2ItemInstance circlet = player.getInventory().getItemByItemId(circletId); + if (circlet != null) { + if (circlet.isEquipped()) { + player.getInventory().unEquipItemInSlot(circlet.getLocationSlot()); + } + player.destroyItemByItemId("CastleCircletRemoval", circletId, 1, player, true); + } + return; + } catch (NullPointerException e) { + // continue removing offline + } + } + // else offline-player circlet removal + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM items WHERE owner_id = ? and item_id = ?")) { + ps.setInt(1, member.getObjectId()); + ps.setInt(2, circletId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Failed to remove castle circlets offline for player {}!", player, ex); + } + } + } + + @Override + public void loadInstances() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT id FROM castle ORDER BY id")) { + while (rs.next()) { + _castles.add(new Castle(rs.getInt("id"))); + } + LOG.info("Loaded {} castles.", _castles.size()); + } catch (Exception ex) { + LOG.warn("There has been an error loading castles from database!", ex); + } + } + + @Override + public void updateReferences() { + } + + @Override + public void activateInstances() { + for (final Castle castle : _castles) { + castle.activateInstance(); + } + } + + public void registerSiegeDate(int castleId, long siegeDate) { + _castleSiegeDate.put(castleId, siegeDate); + } + + public int getSiegeDates(long siegeDate) { + int count = 0; + for (long date : _castleSiegeDate.values()) { + if (Math.abs(date - siegeDate) < 1000) { + count++; + } + } + return count; + } + + public static CastleManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final CastleManager _instance = new CastleManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManorManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManorManager.java new file mode 100644 index 00000000..6ced46a0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/CastleManorManager.java @@ -0,0 +1,683 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.ManorMode; +import com.l2jserver.gameserver.model.CropProcure; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.L2Seed; +import com.l2jserver.gameserver.model.SeedProduction; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.interfaces.IStorable; +import com.l2jserver.gameserver.model.itemcontainer.ItemContainer; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Castle manor system. + * @author malyelfik + */ +public final class CastleManorManager implements IXmlReader, IStorable { + + private static final Logger LOG = LoggerFactory.getLogger(CastleManorManager.class); + + private static final String INSERT_PRODUCT = "INSERT INTO castle_manor_production VALUES (?, ?, ?, ?, ?, ?)"; + + private static final String INSERT_CROP = "INSERT INTO castle_manor_procure VALUES (?, ?, ?, ?, ?, ?, ?)"; + + // Current manor status + private ManorMode _mode = ManorMode.APPROVED; + // Temporary date + private Calendar _nextModeChange = null; + // Seeds holder + private static final Map _seeds = new HashMap<>(); + // Manor period settings + private final Map> _procure = new HashMap<>(); + private final Map> _procureNext = new HashMap<>(); + private final Map> _production = new HashMap<>(); + private final Map> _productionNext = new HashMap<>(); + + public CastleManorManager() { + if (general().allowManor()) { + load(); // Load seed data (XML) + loadDb(); // Load castle manor data (DB) + + // Set mode and start timer + final Calendar currentTime = Calendar.getInstance(); + final int hour = currentTime.get(Calendar.HOUR_OF_DAY); + final int min = currentTime.get(Calendar.MINUTE); + final int maintenanceMin = general().getManorRefreshMin() + general().getManorMaintenanceMin(); + + if (((hour >= general().getManorRefreshTime()) && (min >= maintenanceMin)) || (hour < general().getManorApproveTime()) || ((hour == general().getManorApproveTime()) && (min <= general().getManorApproveMin()))) { + _mode = ManorMode.MODIFIABLE; + } else if ((hour == general().getManorRefreshTime()) && ((min >= general().getManorRefreshMin()) && (min < maintenanceMin))) { + _mode = ManorMode.MAINTENANCE; + } + + // Schedule mode change + scheduleModeChange(); + + // Schedule autosave + if (!general().manorSaveAllActions()) { + ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(this::storeMe, general().getManorSavePeriodRate(), general().getManorSavePeriodRate(), TimeUnit.HOURS); + } + + // Send debug message + if (general().debug()) { + LOG.info("Current mode {}.", _mode); + } + } else { + _mode = ManorMode.DISABLED; + LOG.info("Manor system is deactivated."); + } + } + + @Override + public void load() { + parseDatapackFile("data/seeds.xml"); + LOG.info("Loaded {} seeds.", _seeds.size()); + } + + @Override + public void parseDocument(Document doc) { + StatsSet set; + NamedNodeMap attrs; + Node att; + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("castle".equalsIgnoreCase(d.getNodeName())) { + final int castleId = parseInteger(d.getAttributes(), "id"); + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + if ("crop".equalsIgnoreCase(c.getNodeName())) { + set = new StatsSet(); + set.set("castleId", castleId); + + attrs = c.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + att = attrs.item(i); + set.set(att.getNodeName(), att.getNodeValue()); + } + _seeds.put(set.getInt("seedId"), new L2Seed(set)); + } + } + } + } + } + } + } + + private void loadDb() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var stProduction = con.prepareStatement("SELECT * FROM castle_manor_production WHERE castle_id=?"); + var stProcure = con.prepareStatement("SELECT * FROM castle_manor_procure WHERE castle_id=?")) { + for (Castle castle : CastleManager.getInstance().getCastles()) { + final int castleId = castle.getResidenceId(); + + // Clear params + stProduction.clearParameters(); + stProcure.clearParameters(); + + // Seed production + final List pCurrent = new ArrayList<>(); + final List pNext = new ArrayList<>(); + stProduction.setInt(1, castleId); + try (var rs = stProduction.executeQuery()) { + while (rs.next()) { + final int seedId = rs.getInt("seed_id"); + if (_seeds.containsKey(seedId)) // Don't load unknown seeds + { + final SeedProduction sp = new SeedProduction(seedId, rs.getLong("amount"), rs.getLong("price"), rs.getInt("start_amount")); + if (rs.getBoolean("next_period")) { + pNext.add(sp); + } else { + pCurrent.add(sp); + } + } else { + LOG.warn("Unknown seed Id {}!", seedId); + } + } + } + _production.put(castleId, pCurrent); + _productionNext.put(castleId, pNext); + + // Seed procure + final List current = new ArrayList<>(); + final List next = new ArrayList<>(); + stProcure.setInt(1, castleId); + try (var rs = stProcure.executeQuery()) { + final Set cropIds = getCropIds(); + while (rs.next()) { + final int cropId = rs.getInt("crop_id"); + if (cropIds.contains(cropId)) // Don't load unknown crops + { + final CropProcure cp = new CropProcure(cropId, rs.getLong("amount"), rs.getInt("reward_type"), rs.getLong("start_amount"), rs.getLong("price")); + if (rs.getBoolean("next_period")) { + next.add(cp); + } else { + current.add(cp); + } + } else { + LOG.warn("Unknown crop Id {}!", cropId); + } + } + } + _procure.put(castleId, current); + _procureNext.put(castleId, next); + } + LOG.info("Manor data loaded."); + } catch (Exception ex) { + LOG.warn("Unable to load manor data!", ex); + } + } + + // ------------------------------------------------------- + // Manor methods + // ------------------------------------------------------- + + private void scheduleModeChange() { + // Calculate next mode change + _nextModeChange = Calendar.getInstance(); + _nextModeChange.set(Calendar.SECOND, 0); + switch (_mode) { + case MODIFIABLE -> { + _nextModeChange.set(Calendar.HOUR_OF_DAY, general().getManorApproveTime()); + _nextModeChange.set(Calendar.MINUTE, general().getManorApproveMin()); + if (_nextModeChange.before(Calendar.getInstance())) { + _nextModeChange.add(Calendar.DATE, 1); + } + } + case MAINTENANCE -> { + _nextModeChange.set(Calendar.HOUR_OF_DAY, general().getManorRefreshTime()); + _nextModeChange.set(Calendar.MINUTE, general().getManorRefreshMin() + general().getManorMaintenanceMin()); + } + case APPROVED -> { + _nextModeChange.set(Calendar.HOUR_OF_DAY, general().getManorRefreshTime()); + _nextModeChange.set(Calendar.MINUTE, general().getManorRefreshMin()); + } + } + // Schedule mode change + ThreadPoolManager.getInstance().scheduleGeneral(this::changeMode, (_nextModeChange.getTimeInMillis() - System.currentTimeMillis())); + } + + public void changeMode() { + switch (_mode) { + case APPROVED: { + // Change mode + _mode = ManorMode.MAINTENANCE; + + // Update manor period + for (Castle castle : CastleManager.getInstance().getCastles()) { + final L2Clan owner = castle.getOwner(); + if (owner == null) { + continue; + } + + final int castleId = castle.getResidenceId(); + final ItemContainer cwh = owner.getWarehouse(); + for (CropProcure crop : _procure.get(castleId)) { + if (crop.getStartAmount() > 0) { + // Adding bought crops to clan warehouse + if (crop.getStartAmount() != crop.getAmount()) { + long count = (long) ((crop.getStartAmount() - crop.getAmount()) * 0.9); + if ((count < 1) && (Rnd.nextInt(99) < 90)) { + count = 1; + } + + if (count > 0) { + cwh.addItem("Manor", getSeedByCrop(crop.getId()).getMatureId(), count, null, null); + } + } + // Reserved and not used money giving back to treasury + if (crop.getAmount() > 0) { + castle.addToTreasuryNoTax(crop.getAmount() * crop.getPrice()); + } + } + } + + // Change next period to current and prepare next period data + final List _nextProduction = _productionNext.get(castleId); + final List _nextProcure = _procureNext.get(castleId); + + _production.put(castleId, _nextProduction); + _procure.put(castleId, _nextProcure); + + if (castle.getTreasury() < getManorCost(castleId, false)) { + _productionNext.put(castleId, Collections.emptyList()); + _procureNext.put(castleId, Collections.emptyList()); + } else { + final List production = new ArrayList<>(_nextProduction); + for (SeedProduction s : production) { + s.setAmount(s.getStartAmount()); + } + _productionNext.put(castleId, production); + + final List procure = new ArrayList<>(_nextProcure); + for (CropProcure cr : procure) { + cr.setAmount(cr.getStartAmount()); + } + _procureNext.put(castleId, procure); + } + } + + // Save changes + storeMe(); + break; + } + case MAINTENANCE: { + // Notify clan leader about manor mode change + for (Castle castle : CastleManager.getInstance().getCastles()) { + final L2Clan owner = castle.getOwner(); + if (owner != null) { + final L2ClanMember clanLeader = owner.getLeader(); + if ((clanLeader != null) && clanLeader.isOnline()) { + clanLeader.getPlayerInstance().sendPacket(SystemMessageId.THE_MANOR_INFORMATION_HAS_BEEN_UPDATED); + } + } + } + _mode = ManorMode.MODIFIABLE; + break; + } + case MODIFIABLE: { + _mode = ManorMode.APPROVED; + + for (Castle castle : CastleManager.getInstance().getCastles()) { + final L2Clan owner = castle.getOwner(); + if (owner == null) { + continue; + } + + int slots = 0; + final int castleId = castle.getResidenceId(); + final ItemContainer cwh = owner.getWarehouse(); + for (CropProcure crop : _procureNext.get(castleId)) { + if ((crop.getStartAmount() > 0) && (cwh.getItemsByItemId(getSeedByCrop(crop.getId()).getMatureId()) == null)) { + slots++; + } + } + + final long manorCost = getManorCost(castleId, true); + if (!cwh.validateCapacity(slots) && (castle.getTreasury() < manorCost)) { + _productionNext.get(castleId).clear(); + _procureNext.get(castleId).clear(); + + // Notify clan leader + final L2ClanMember clanLeader = owner.getLeader(); + if ((clanLeader != null) && clanLeader.isOnline()) { + clanLeader.getPlayerInstance().sendPacket(SystemMessageId.THE_AMOUNT_IS_NOT_SUFFICIENT_AND_SO_THE_MANOR_IS_NOT_IN_OPERATION); + } + } else { + castle.addToTreasuryNoTax(-manorCost); + } + } + + // Store changes + if (general().manorSaveAllActions()) { + storeMe(); + } + break; + } + } + scheduleModeChange(); + if (general().debug()) { + LOG.info("Manor mode changed to {}!", _mode); + } + } + + public void setNextSeedProduction(List list, int castleId) { + _productionNext.put(castleId, list); + if (general().manorSaveAllActions()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var dps = con.prepareStatement("DELETE FROM castle_manor_production WHERE castle_id = ? AND next_period = 1"); + var ips = con.prepareStatement(INSERT_PRODUCT)) { + // Delete old data + dps.setInt(1, castleId); + dps.executeUpdate(); + + // Insert new data + if (!list.isEmpty()) { + for (SeedProduction sp : list) { + ips.setInt(1, castleId); + ips.setInt(2, sp.getId()); + ips.setLong(3, sp.getAmount()); + ips.setLong(4, sp.getStartAmount()); + ips.setLong(5, sp.getPrice()); + ips.setBoolean(6, true); + ips.addBatch(); + } + ips.executeBatch(); + } + } catch (Exception ex) { + LOG.error("Unable to store manor data!", ex); + } + } + } + + public void setNextCropProcure(List list, int castleId) { + _procureNext.put(castleId, list); + if (general().manorSaveAllActions()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var dps = con.prepareStatement("DELETE FROM castle_manor_procure WHERE castle_id = ? AND next_period = 1"); + var ips = con.prepareStatement(INSERT_CROP)) { + // Delete old data + dps.setInt(1, castleId); + dps.executeUpdate(); + + // Insert new data + if (!list.isEmpty()) { + for (CropProcure cp : list) { + ips.setInt(1, castleId); + ips.setInt(2, cp.getId()); + ips.setLong(3, cp.getAmount()); + ips.setLong(4, cp.getStartAmount()); + ips.setLong(5, cp.getPrice()); + ips.setInt(6, cp.getReward()); + ips.setBoolean(7, true); + ips.addBatch(); + } + ips.executeBatch(); + } + } catch (Exception ex) { + LOG.warn("Unable to store manor data!", ex); + } + } + } + + public void updateCurrentProduction(int castleId, Collection items) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle_manor_production SET amount = ? WHERE castle_id = ? AND seed_id = ? AND next_period = 0")) { + for (SeedProduction sp : items) { + ps.setLong(1, sp.getAmount()); + ps.setInt(2, castleId); + ps.setInt(3, sp.getId()); + ps.addBatch(); + } + ps.executeBatch(); + } catch (Exception ex) { + LOG.warn("Unable to store manor data!", ex); + } + } + + public void updateCurrentProcure(int castleId, Collection items) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle_manor_procure SET amount = ? WHERE castle_id = ? AND crop_id = ? AND next_period = 0")) { + for (CropProcure sp : items) { + ps.setLong(1, sp.getAmount()); + ps.setInt(2, castleId); + ps.setInt(3, sp.getId()); + ps.addBatch(); + } + ps.executeBatch(); + } catch (Exception ex) { + LOG.warn("Unable to store manor data!", ex); + } + } + + public List getSeedProduction(int castleId, boolean nextPeriod) { + return (nextPeriod) ? _productionNext.get(castleId) : _production.get(castleId); + } + + public SeedProduction getSeedProduct(int castleId, int seedId, boolean nextPeriod) { + for (SeedProduction sp : getSeedProduction(castleId, nextPeriod)) { + if (sp.getId() == seedId) { + return sp; + } + } + return null; + } + + public List getCropProcure(int castleId, boolean nextPeriod) { + return (nextPeriod) ? _procureNext.get(castleId) : _procure.get(castleId); + } + + public CropProcure getCropProcure(int castleId, int cropId, boolean nextPeriod) { + for (CropProcure cp : getCropProcure(castleId, nextPeriod)) { + if (cp.getId() == cropId) { + return cp; + } + } + return null; + } + + public long getManorCost(int castleId, boolean nextPeriod) { + final List procure = getCropProcure(castleId, nextPeriod); + final List production = getSeedProduction(castleId, nextPeriod); + + long total = 0; + for (SeedProduction seed : production) { + final L2Seed s = getSeed(seed.getId()); + total += (s == null) ? 1 : (s.getSeedReferencePrice() * seed.getStartAmount()); + } + for (CropProcure crop : procure) { + total += (crop.getPrice() * crop.getStartAmount()); + } + return total; + } + + @Override + public boolean storeMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ds = con.prepareStatement("DELETE FROM castle_manor_production"); + var is = con.prepareStatement(INSERT_PRODUCT); + var dp = con.prepareStatement("DELETE FROM castle_manor_procure"); + var ip = con.prepareStatement(INSERT_CROP)) { + // Delete old seeds + ds.executeUpdate(); + + // Current production + for (Map.Entry> entry : _production.entrySet()) { + for (SeedProduction sp : entry.getValue()) { + is.setInt(1, entry.getKey()); + is.setInt(2, sp.getId()); + is.setLong(3, sp.getAmount()); + is.setLong(4, sp.getStartAmount()); + is.setLong(5, sp.getPrice()); + is.setBoolean(6, false); + is.addBatch(); + } + } + + // Next production + for (Map.Entry> entry : _productionNext.entrySet()) { + for (SeedProduction sp : entry.getValue()) { + is.setInt(1, entry.getKey()); + is.setInt(2, sp.getId()); + is.setLong(3, sp.getAmount()); + is.setLong(4, sp.getStartAmount()); + is.setLong(5, sp.getPrice()); + is.setBoolean(6, true); + is.addBatch(); + } + } + + // Execute production batch + is.executeBatch(); + + // Delete old procure + dp.executeUpdate(); + + // Current procure + for (Map.Entry> entry : _procure.entrySet()) { + for (CropProcure cp : entry.getValue()) { + ip.setInt(1, entry.getKey()); + ip.setInt(2, cp.getId()); + ip.setLong(3, cp.getAmount()); + ip.setLong(4, cp.getStartAmount()); + ip.setLong(5, cp.getPrice()); + ip.setInt(6, cp.getReward()); + ip.setBoolean(7, false); + ip.addBatch(); + } + } + + // Next procure + for (Map.Entry> entry : _procureNext.entrySet()) { + for (CropProcure cp : entry.getValue()) { + ip.setInt(1, entry.getKey()); + ip.setInt(2, cp.getId()); + ip.setLong(3, cp.getAmount()); + ip.setLong(4, cp.getStartAmount()); + ip.setLong(5, cp.getPrice()); + ip.setInt(6, cp.getReward()); + ip.setBoolean(7, true); + ip.addBatch(); + } + } + + // Execute procure batch + ip.executeBatch(); + + return true; + } catch (Exception ex) { + LOG.warn("Unable to store manor data!", ex); + return false; + } + } + + public void resetManorData(int castleId) { + if (!general().allowManor()) { + return; + } + + _procure.get(castleId).clear(); + _procureNext.get(castleId).clear(); + _production.get(castleId).clear(); + _productionNext.get(castleId).clear(); + + if (general().allowManor()) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ds = con.prepareStatement("DELETE FROM castle_manor_production WHERE castle_id = ?"); + var dc = con.prepareStatement("DELETE FROM castle_manor_procure WHERE castle_id = ?")) { + // Delete seeds + ds.setInt(1, castleId); + ds.executeUpdate(); + + // Delete procure + dc.setInt(1, castleId); + dc.executeUpdate(); + } catch (Exception ex) { + LOG.warn("Unable to store manor data!", ex); + } + } + } + + public boolean isUnderMaintenance() { + return _mode.equals(ManorMode.MAINTENANCE); + } + + public boolean isManorApproved() { + return _mode.equals(ManorMode.APPROVED); + } + + public boolean isModifiablePeriod() { + return _mode.equals(ManorMode.MODIFIABLE); + } + + public String getCurrentModeName() { + return _mode.toString(); + } + + public String getNextModeChange() { + return new SimpleDateFormat("dd/MM HH:mm:ss").format(_nextModeChange.getTime()); + } + + // ------------------------------------------------------- + // Seed methods + // ------------------------------------------------------- + + public List getCrops() { + final List seeds = new ArrayList<>(); + final List cropIds = new ArrayList<>(); + for (L2Seed seed : _seeds.values()) { + if (!cropIds.contains(seed.getCropId())) { + seeds.add(seed); + cropIds.add(seed.getCropId()); + } + } + cropIds.clear(); + return seeds; + } + + public Set getSeedsForCastle(int castleId) { + return _seeds.values().stream().filter(s -> s.getCastleId() == castleId).collect(Collectors.toSet()); + } + + public Set getSeedIds() { + return _seeds.keySet(); + } + + public Set getCropIds() { + return _seeds.values().stream().map(L2Seed::getCropId).collect(Collectors.toSet()); + } + + public L2Seed getSeed(int seedId) { + return _seeds.get(seedId); + } + + public L2Seed getSeedByCrop(int cropId, int castleId) { + for (L2Seed s : getSeedsForCastle(castleId)) { + if (s.getCropId() == cropId) { + return s; + } + } + return null; + } + + public L2Seed getSeedByCrop(int cropId) { + for (L2Seed s : _seeds.values()) { + if (s.getCropId() == cropId) { + return s; + } + } + return null; + } + + public static CastleManorManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final CastleManorManager INSTANCE = new CastleManorManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallManager.java new file mode 100644 index 00000000..1f760dd5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallManager.java @@ -0,0 +1,234 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.entity.Auction; +import com.l2jserver.gameserver.model.entity.ClanHall; +import com.l2jserver.gameserver.model.entity.clanhall.AuctionableHall; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.model.zone.type.L2ClanHallZone; + +/** + * Clan Hall Manger. + * @author Steuf + */ +public final class ClanHallManager { + + private static final Logger LOG = LoggerFactory.getLogger(ClanHallManager.class); + + private final Map _clanHall = new ConcurrentHashMap<>(); + + private final Map _freeClanHall = new ConcurrentHashMap<>(); + + private final Map _allAuctionableClanHalls = new HashMap<>(); + + private static final Map _allClanHalls = new HashMap<>(); + + private boolean _loaded = false; + + public boolean loaded() { + return _loaded; + } + + protected ClanHallManager() { + load(); + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM clanhall ORDER BY id")) { + int id, ownerId, lease; + while (rs.next()) { + StatsSet set = new StatsSet(); + + id = rs.getInt("id"); + ownerId = rs.getInt("ownerId"); + lease = rs.getInt("lease"); + + set.set("id", id); + set.set("name", rs.getString("name")); + set.set("ownerId", ownerId); + set.set("lease", lease); + set.set("desc", rs.getString("desc")); + set.set("location", rs.getString("location")); + set.set("paidUntil", rs.getLong("paidUntil")); + set.set("grade", rs.getInt("Grade")); + set.set("paid", rs.getBoolean("paid")); + AuctionableHall ch = new AuctionableHall(set); + _allAuctionableClanHalls.put(id, ch); + addClanHall(ch); + + if (ch.getOwnerId() > 0) { + _clanHall.put(id, ch); + continue; + } + _freeClanHall.put(id, ch); + + Auction auc = AuctionManager.getInstance().getAuction(id); + if ((auc == null) && (lease > 0)) { + AuctionManager.getInstance().initNPC(id); + } + } + LOG.info("Loaded {} clan halls.", getClanHalls().size()); + LOG.info("Loaded {} free clan halls.", getFreeClanHalls().size()); + _loaded = true; + } catch (Exception ex) { + LOG.warn("There has been an error loading clan halls from database!", ex); + } + } + + public Map getAllClanHalls() { + return _allClanHalls; + } + + public Map getFreeClanHalls() { + return _freeClanHall; + } + + /** + * @return all ClanHalls that have owner + */ + public Map getClanHalls() { + return _clanHall; + } + + public Map getAllAuctionableClanHalls() { + return _allAuctionableClanHalls; + } + + public void addClanHall(ClanHall hall) { + _allClanHalls.put(hall.getId(), hall); + } + + public boolean isFree(int chId) { + return _freeClanHall.containsKey(chId); + } + + public synchronized void setFree(int chId) { + _freeClanHall.put(chId, _clanHall.get(chId)); + ClanTable.getInstance().getClan(_freeClanHall.get(chId).getOwnerId()).setHideoutId(0); + _freeClanHall.get(chId).free(); + _clanHall.remove(chId); + } + + public synchronized void setOwner(int chId, L2Clan clan) { + if (!_clanHall.containsKey(chId)) { + _clanHall.put(chId, _freeClanHall.get(chId)); + _freeClanHall.remove(chId); + } else { + _clanHall.get(chId).free(); + } + ClanTable.getInstance().getClan(clan.getId()).setHideoutId(chId); + _clanHall.get(chId).setOwner(clan); + } + + public ClanHall getClanHallById(int clanHallId) { + return _allClanHalls.get(clanHallId); + } + + public AuctionableHall getAuctionableHallById(int clanHallId) { + return _allAuctionableClanHalls.get(clanHallId); + } + + public ClanHall getClanHall(int x, int y, int z) { + for (ClanHall temp : _allClanHalls.values()) { + if (temp.checkIfInZone(x, y, z)) { + return temp; + } + } + return null; + } + + public ClanHall getClanHall(L2Object activeObject) { + return getClanHall(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public AuctionableHall getNearbyClanHall(int x, int y, int maxDist) { + L2ClanHallZone zone; + + for (Map.Entry ch : _clanHall.entrySet()) { + zone = ch.getValue().getZone(); + if ((zone != null) && (zone.getDistanceToZone(x, y) < maxDist)) { + return ch.getValue(); + } + } + for (Map.Entry ch : _freeClanHall.entrySet()) { + zone = ch.getValue().getZone(); + if ((zone != null) && (zone.getDistanceToZone(x, y) < maxDist)) { + return ch.getValue(); + } + } + return null; + } + + public ClanHall getNearbyAbstractHall(int x, int y, int maxDist) { + for (Map.Entry ch : _allClanHalls.entrySet()) { + final var zone = ch.getValue().getZone(); + if ((zone != null) && (zone.getDistanceToZone(x, y) < maxDist)) { + return ch.getValue(); + } + } + return null; + } + + public AuctionableHall getClanHallByOwner(L2Clan clan) { + for (Map.Entry ch : _clanHall.entrySet()) { + if (clan.getId() == ch.getValue().getOwnerId()) { + return ch.getValue(); + } + } + return null; + } + + public ClanHall getAbstractHallByOwner(L2Clan clan) { + // Separate loops to avoid iterating over free clan halls + for (Map.Entry ch : _clanHall.entrySet()) { + if (clan.getId() == ch.getValue().getOwnerId()) { + return ch.getValue(); + } + } + for (Map.Entry ch : ClanHallSiegeManager.getInstance().getConquerableHalls().entrySet()) { + if (clan.getId() == ch.getValue().getOwnerId()) { + return ch.getValue(); + } + } + return null; + } + + public static ClanHallManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ClanHallManager INSTANCE = new ClanHallManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallSiegeManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallSiegeManager.java new file mode 100644 index 00000000..2ef23862 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/ClanHallSiegeManager.java @@ -0,0 +1,172 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.clanhall; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.clanhall.ClanHallSiegeEngine; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Clan Hall Siege Manager. + * @author BiggBoss + */ +public final class ClanHallSiegeManager { + + private static final Logger LOG = LoggerFactory.getLogger(ClanHallSiegeManager.class); + + private static final String SQL_LOAD_HALLS = "SELECT * FROM siegable_clanhall"; + + private final Map _siegableHalls = new HashMap<>(); + + protected ClanHallSiegeManager() { + loadClanHalls(); + } + + private void loadClanHalls() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(SQL_LOAD_HALLS)) { + _siegableHalls.clear(); + + while (rs.next()) { + final int id = rs.getInt("clanHallId"); + + StatsSet set = new StatsSet(); + + set.set("id", id); + set.set("name", rs.getString("name")); + set.set("ownerId", rs.getInt("ownerId")); + set.set("desc", rs.getString("desc")); + set.set("location", rs.getString("location")); + set.set("nextSiege", rs.getLong("nextSiege")); + set.set("siegeLength", rs.getLong("siege_length")); + set.set("scheduleConfig", rs.getString("schedule_config")); + SiegableHall hall = new SiegableHall(set); + _siegableHalls.put(id, hall); + ClanHallManager.getInstance().addClanHall(hall); + } + LOG.info("Loaded {} conquerable clan halls.", _siegableHalls.size()); + } catch (Exception ex) { + LOG.warn("Could not load siegable clan halls!", ex); + } + } + + public Map getConquerableHalls() { + return _siegableHalls; + } + + public SiegableHall getSiegableHall(int clanHall) { + return getConquerableHalls().get(clanHall); + } + + public SiegableHall getNearbyClanHall(L2Character activeChar) { + return getNearbyClanHall(activeChar.getX(), activeChar.getY(), 10000); + } + + public SiegableHall getNearbyClanHall(int x, int y, int maxDist) { + for (Map.Entry ch : _siegableHalls.entrySet()) { + final var zone = ch.getValue().getZone(); + if ((zone != null) && (zone.getDistanceToZone(x, y) < maxDist)) { + return ch.getValue(); + } + } + return null; + } + + public ClanHallSiegeEngine getSiege(L2Character character) { + SiegableHall hall = getNearbyClanHall(character); + if (hall == null) { + return null; + } + return hall.getSiege(); + } + + public void registerClan(L2Clan clan, SiegableHall hall, L2PcInstance player) { + if (clan.getLevel() < clanhall().getMinClanLevel()) { + player.sendMessage("Only clans of level " + clanhall().getMinClanLevel() + " or higher may register for a castle siege"); + } else if (hall.isWaitingBattle()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.DEADLINE_FOR_SIEGE_S1_PASSED); + sm.addString(hall.getName()); + player.sendPacket(sm); + } else if (hall.isInSiege()) { + player.sendPacket(SystemMessageId.NOT_SIEGE_REGISTRATION_TIME2); + } else if (hall.getOwnerId() == clan.getId()) { + player.sendPacket(SystemMessageId.CLAN_THAT_OWNS_CASTLE_IS_AUTOMATICALLY_REGISTERED_DEFENDING); + } else if ((clan.getCastleId() != 0) || (clan.getHideoutId() != 0)) { + player.sendPacket(SystemMessageId.CLAN_THAT_OWNS_CASTLE_CANNOT_PARTICIPATE_OTHER_SIEGE); + } else if (hall.getSiege().checkIsAttacker(clan)) { + player.sendPacket(SystemMessageId.ALREADY_REQUESTED_SIEGE_BATTLE); + } else if (isClanParticipating(clan)) { + player.sendPacket(SystemMessageId.APPLICATION_DENIED_BECAUSE_ALREADY_SUBMITTED_A_REQUEST_FOR_ANOTHER_SIEGE_BATTLE); + } else if (hall.getSiege().getAttackers().size() >= clanhall().getMaxAttackers()) { + player.sendPacket(SystemMessageId.ATTACKER_SIDE_FULL); + } else { + hall.addAttacker(clan); + } + } + + public void unRegisterClan(L2Clan clan, SiegableHall hall) { + if (!hall.isRegistering()) { + return; + } + hall.removeAttacker(clan); + } + + public boolean isClanParticipating(L2Clan clan) { + for (SiegableHall hall : getConquerableHalls().values()) { + if ((hall.getSiege() != null) && hall.getSiege().checkIsAttacker(clan)) { + return true; + } + } + return false; + } + + public void onServerShutDown() { + for (SiegableHall hall : getConquerableHalls().values()) { + // Rainbow springs has his own attackers table + if ((hall.getId() == 62) || (hall.getSiege() == null)) { + continue; + } + + hall.getSiege().saveAttackers(); + } + } + + public static ClanHallSiegeManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static final class SingletonHolder { + protected static final ClanHallSiegeManager INSTANCE = new ClanHallSiegeManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/CoupleManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/CoupleManager.java new file mode 100644 index 00000000..3eda6bcb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/CoupleManager.java @@ -0,0 +1,133 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Couple; + +/** + * Couple Manager. + * @author evill33t + */ +public final class CoupleManager { + + private static final Logger LOG = LoggerFactory.getLogger(CoupleManager.class); + + private final List _couples = new CopyOnWriteArrayList<>(); + + private CoupleManager() { + load(); + } + + public void reload() { + _couples.clear(); + load(); + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.createStatement(); + var rs = ps.executeQuery("SELECT id FROM mods_wedding ORDER BY id")) { + while (rs.next()) { + getCouples().add(new Couple(rs.getInt("id"))); + } + LOG.info("Loaded: {} couples(s)", getCouples().size()); + } catch (Exception e) { + LOG.error("Exception: CoupleManager.load(): {}", e.getMessage(), e); + } + } + + public Couple getCouple(int coupleId) { + int index = getCoupleIndex(coupleId); + if (index >= 0) { + return getCouples().get(index); + } + return null; + } + + public void createCouple(L2PcInstance player1, L2PcInstance player2) { + if ((player1 != null) && (player2 != null)) { + if ((player1.getPartnerId() == 0) && (player2.getPartnerId() == 0)) { + int player1id = player1.getObjectId(); + int player2id = player2.getObjectId(); + + Couple couple = new Couple(player1, player2); + getCouples().add(couple); + player1.setPartnerId(player2id); + player2.setPartnerId(player1id); + player1.setCoupleId(couple.getId()); + player2.setCoupleId(couple.getId()); + } + } + } + + public void deleteCouple(int coupleId) { + int index = getCoupleIndex(coupleId); + Couple couple = getCouples().get(index); + if (couple != null) { + L2PcInstance player1 = L2World.getInstance().getPlayer(couple.getPlayer1Id()); + L2PcInstance player2 = L2World.getInstance().getPlayer(couple.getPlayer2Id()); + if (player1 != null) { + player1.setPartnerId(0); + player1.setMarried(false); + player1.setCoupleId(0); + + } + if (player2 != null) { + player2.setPartnerId(0); + player2.setMarried(false); + player2.setCoupleId(0); + + } + couple.divorce(); + getCouples().remove(index); + } + } + + public int getCoupleIndex(int coupleId) { + int i = 0; + for (Couple temp : getCouples()) { + if ((temp != null) && (temp.getId() == coupleId)) { + return i; + } + i++; + } + return -1; + } + + public List getCouples() { + return _couples; + } + + public static CoupleManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final CoupleManager _instance = new CoupleManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/CursedWeaponsManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/CursedWeaponsManager.java new file mode 100644 index 00000000..38a24a08 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/CursedWeaponsManager.java @@ -0,0 +1,362 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.CursedWeapon; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FeedableBeastInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FestivalMonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FortCommanderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2GrandBossInstance; +import com.l2jserver.gameserver.model.actor.instance.L2GuardInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2RiftInvaderInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * UnAfraid: TODO: Rewrite with DocumentParser + * @author Micht + */ +public final class CursedWeaponsManager { + + private static final Logger LOG = LoggerFactory.getLogger(CursedWeaponsManager.class); + + private Map _cursedWeapons; + + protected CursedWeaponsManager() { + init(); + } + + private void init() { + _cursedWeapons = new HashMap<>(); + + if (!general().allowCursedWeapons()) { + return; + } + + load(); + restore(); + controlPlayers(); + LOG.info("Loaded {} cursed weapon(s).", _cursedWeapons.size()); + } + + public void reload() { + init(); + } + + private void load() { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + File file = new File(server().getDatapackRoot(), "data/cursedWeapons.xml"); + if (!file.exists()) { + LOG.warn("Couldn't find {}!", file.getAbsoluteFile()); + return; + } + + Document doc = factory.newDocumentBuilder().parse(file); + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("item".equalsIgnoreCase(d.getNodeName())) { + NamedNodeMap attrs = d.getAttributes(); + int id = Integer.parseInt(attrs.getNamedItem("id").getNodeValue()); + int skillId = Integer.parseInt(attrs.getNamedItem("skillId").getNodeValue()); + String name = attrs.getNamedItem("name").getNodeValue(); + + CursedWeapon cw = new CursedWeapon(id, skillId, name); + + int val; + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("dropRate".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + val = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + cw.setDropRate(val); + } else if ("duration".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + val = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + cw.setDuration(val); + } else if ("durationLost".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + val = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + cw.setDurationLost(val); + } else if ("disappearChance".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + val = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + cw.setDisappearChance(val); + } else if ("stageKills".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + val = Integer.parseInt(attrs.getNamedItem("val").getNodeValue()); + cw.setStageKills(val); + } + } + + // Store cursed weapon + _cursedWeapons.put(id, cw); + } + } + } + } + } catch (Exception ex) { + LOG.warn("There has been an error parsing cursed weapons file!", ex); + } + } + + private void restore() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT itemId, charId, playerKarma, playerPkKills, nbKills, endTime FROM cursed_weapons")) { + // Retrieve the L2PcInstance from the characters table of the database + CursedWeapon cw; + while (rs.next()) { + cw = _cursedWeapons.get(rs.getInt("itemId")); + cw.setPlayerId(rs.getInt("charId")); + cw.setPlayerKarma(rs.getInt("playerKarma")); + cw.setPlayerPkKills(rs.getInt("playerPkKills")); + cw.setNbKills(rs.getInt("nbKills")); + cw.setEndTime(rs.getLong("endTime")); + cw.reActivate(); + } + } catch (Exception ex) { + LOG.warn("Could not restore cursed weapons data!", ex); + } + } + + private void controlPlayers() { + // TODO: See comments below... + // This entire for loop should NOT be necessary, since it is already handled by + // CursedWeapon.endOfLife(). However, if we indeed *need* to duplicate it for safety, + // then we'd better make sure that it FULLY cleans up inactive cursed weapons! + // Undesired effects result otherwise, such as player with no zariche but with karma + // or a lost-child entry in the cursed weapons table, without a corresponding one in items... + + // Retrieve the L2PcInstance from the characters table of the database + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT owner_id FROM items WHERE item_id=?")) { + for (CursedWeapon cw : _cursedWeapons.values()) { + if (cw.isActivated()) { + continue; + } + + // Do an item check to be sure that the cursed weapon isn't hold by someone + int itemId = cw.getItemId(); + ps.setInt(1, itemId); + try (var rset = ps.executeQuery()) { + if (rset.next()) { + // A player has the cursed weapon in his inventory ... + int playerId = rset.getInt("owner_id"); + LOG.warn("Player {} owns the cursed weapon {} but he shouldn't!", playerId, itemId); + + // Delete the item + try (var delete = con.prepareStatement("DELETE FROM items WHERE owner_id=? AND item_id=?")) { + delete.setInt(1, playerId); + delete.setInt(2, itemId); + if (delete.executeUpdate() != 1) { + LOG.warn("There has been an error while deleting cursed weapon {} from player Id {}!", itemId, playerId); + } + } + + // Restore the player's old karma and pk count + try (var update = con.prepareStatement("UPDATE characters SET karma=?, pkkills=? WHERE charId=?")) { + update.setInt(1, cw.getPlayerKarma()); + update.setInt(2, cw.getPlayerPkKills()); + update.setInt(3, playerId); + if (update.executeUpdate() != 1) { + LOG.warn("There has been an error while updating karma & pkkills for player Id {}!", cw.getPlayerId()); + } + } + // clean up the cursed weapons table. + removeFromDb(itemId); + } + } + ps.clearParameters(); + } + } catch (Exception ex) { + LOG.warn("Could not check cursed weapons data!", ex); + } + } + + public synchronized void checkDrop(L2Attackable attackable, L2PcInstance player) { + // Cursed weapons cannot drop in instance + if (attackable.getInstanceId() != 0) { + return; + } + if ((attackable instanceof L2DefenderInstance) || (attackable instanceof L2RiftInvaderInstance) || (attackable instanceof L2FestivalMonsterInstance) || (attackable instanceof L2GuardInstance) || (attackable instanceof L2GrandBossInstance) || (attackable instanceof L2FeedableBeastInstance) + || (attackable instanceof L2FortCommanderInstance)) { + return; + } + + for (CursedWeapon cw : _cursedWeapons.values()) { + if (cw.isActive()) { + continue; + } + + if (cw.checkDrop(attackable, player)) { + break; + } + } + } + + public void activate(L2PcInstance player, L2ItemInstance item) { + CursedWeapon cw = _cursedWeapons.get(item.getId()); + if (player.isCursedWeaponEquipped()) // cannot own 2 cursed swords + { + CursedWeapon cw2 = _cursedWeapons.get(player.getCursedWeaponEquippedId()); + // TODO: give the bonus level in a more appropriate manner. + // The following code adds "_stageKills" levels. This will also show in the char status. + // I do not have enough info to know if the bonus should be shown in the pk count, or if it + // should be a full "_stageKills" bonus or just the remaining from the current count till the of the current stage... + // This code is a TEMP fix, so that the cursed weapon's bonus level can be observed with as little change in the code as possible, until proper info arises. + cw2.setNbKills(cw2.getStageKills() - 1); + cw2.increaseKills(); + + // erase the newly obtained cursed weapon + cw.setPlayer(player); // NECESSARY in order to find which inventory the weapon is in! + cw.endOfLife(); // expire the weapon and clean up. + } else { + cw.activate(player, item); + } + } + + public void drop(int itemId, L2Character killer) { + CursedWeapon cw = _cursedWeapons.get(itemId); + + cw.dropIt(killer); + } + + public void increaseKills(int itemId) { + CursedWeapon cw = _cursedWeapons.get(itemId); + + cw.increaseKills(); + } + + public int getLevel(int itemId) { + CursedWeapon cw = _cursedWeapons.get(itemId); + + return cw.getLevel(); + } + + public static void announce(SystemMessage sm) { + Broadcast.toAllOnlinePlayers(sm); + } + + public void checkPlayer(L2PcInstance player) { + if (player == null) { + return; + } + + for (CursedWeapon cw : _cursedWeapons.values()) { + if (cw.isActivated() && (player.getObjectId() == cw.getPlayerId())) { + cw.setPlayer(player); + cw.setItem(player.getInventory().getItemByItemId(cw.getItemId())); + cw.giveSkill(); + player.setCursedWeaponEquippedId(cw.getItemId()); + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_MINUTE_OF_USAGE_TIME_ARE_LEFT_FOR_S1); + sm.addString(cw.getName()); + // sm.addItemName(cw.getItemId()); + sm.addInt((int) ((cw.getEndTime() - System.currentTimeMillis()) / 60000)); + player.sendPacket(sm); + } + } + } + + public int checkOwnsWeaponId(int ownerId) { + for (CursedWeapon cw : _cursedWeapons.values()) { + if (cw.isActivated() && (ownerId == cw.getPlayerId())) { + return cw.getItemId(); + } + } + return -1; + } + + public static void removeFromDb(int itemId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM cursed_weapons WHERE itemId = ?")) { + ps.setInt(1, itemId); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("Failed to remove cursed weapon Id {}!", itemId, ex); + } + } + + public void saveData() { + for (CursedWeapon cw : _cursedWeapons.values()) { + cw.saveData(); + } + } + + public boolean isCursed(int itemId) { + return _cursedWeapons.containsKey(itemId); + } + + public Collection getCursedWeapons() { + return _cursedWeapons.values(); + } + + public Set getCursedWeaponsIds() { + return _cursedWeapons.keySet(); + } + + public CursedWeapon getCursedWeapon(int itemId) { + return _cursedWeapons.get(itemId); + } + + public void givePassive(int itemId) { + try { + _cursedWeapons.get(itemId).giveSkill(); + } catch (Exception e) { + /***/ + } + } + + public static CursedWeaponsManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final CursedWeaponsManager _instance = new CursedWeaponsManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/DayNightSpawnManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/DayNightSpawnManager.java new file mode 100644 index 00000000..9a61aebb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/DayNightSpawnManager.java @@ -0,0 +1,219 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2RaidBossInstance; + +/** + * @author godson + */ +public final class DayNightSpawnManager { + + private static final Logger LOG = LoggerFactory.getLogger(DayNightSpawnManager.class); + + private final List _dayCreatures = new ArrayList<>(); + + private final List _nightCreatures = new ArrayList<>(); + + private final Map _bosses = new HashMap<>(); + + private DayNightSpawnManager() { + // Prevent external initialization. + } + + public void addDayCreature(L2Spawn spawnDat) { + _dayCreatures.add(spawnDat); + } + + public void addNightCreature(L2Spawn spawnDat) { + _nightCreatures.add(spawnDat); + } + + /** + * Spawn Day Creatures, and Unspawn Night Creatures + */ + public void spawnDayCreatures() { + spawnCreatures(_nightCreatures, _dayCreatures, "night", "day"); + } + + /** + * Spawn Night Creatures, and Unspawn Day Creatures + */ + public void spawnNightCreatures() { + spawnCreatures(_dayCreatures, _nightCreatures, "day", "night"); + } + + /** + * Manage Spawn/Respawn + * @param unSpawnCreatures List with spawns must be unspawned + * @param spawnCreatures List with spawns must be spawned + * @param unspawnLogInfo String for log info for unspawned L2NpcInstance + * @param spawnLogInfo String for log info for spawned L2NpcInstance + */ + private void spawnCreatures(List unSpawnCreatures, List spawnCreatures, String unspawnLogInfo, String spawnLogInfo) { + try { + if (!unSpawnCreatures.isEmpty()) { + int i = 0; + for (L2Spawn spawn : unSpawnCreatures) { + if (spawn == null) { + continue; + } + + spawn.stopRespawn(); + L2Npc last = spawn.getLastSpawn(); + if (last != null) { + last.deleteMe(); + i++; + } + } + LOG.info("Removed {} {} creatures.", i, unspawnLogInfo); + } + + int i = 0; + for (L2Spawn spawnDat : spawnCreatures) { + if (spawnDat == null) { + continue; + } + spawnDat.startRespawn(); + spawnDat.doSpawn(); + i++; + } + + LOG.info("Spawned {} {} creatures.", i, spawnLogInfo); + } catch (Exception ex) { + LOG.warn("There has been an error while spawning creatures!", ex); + } + } + + private void changeMode(int mode) { + if (_nightCreatures.isEmpty() && _dayCreatures.isEmpty() && _bosses.isEmpty()) { + return; + } + + switch (mode) { + case 0 -> { + spawnDayCreatures(); + specialNightBoss(0); + } + case 1 -> { + spawnNightCreatures(); + specialNightBoss(1); + } + } + } + + public DayNightSpawnManager trim() { + ((ArrayList) _nightCreatures).trimToSize(); + ((ArrayList) _dayCreatures).trimToSize(); + return this; + } + + public void notifyChangeMode() { + if (GameTimeController.getInstance().isNight()) { + changeMode(1); + } else { + changeMode(0); + } + } + + public void cleanUp() { + _nightCreatures.clear(); + _dayCreatures.clear(); + _bosses.clear(); + } + + private void specialNightBoss(int mode) { + try { + for (L2Spawn spawn : _bosses.keySet()) { + var boss = _bosses.get(spawn); + if ((boss == null) && (mode == 1)) { + boss = (L2RaidBossInstance) spawn.doSpawn(); + RaidBossSpawnManager.getInstance().notifySpawnNightBoss(boss); + _bosses.put(spawn, boss); + continue; + } + + if ((boss == null) && (mode == 0)) { + continue; + } + + if ((boss != null) && (boss.getId() == 25328) && boss.getRaidStatus().equals(RaidBossSpawnManager.StatusEnum.ALIVE)) { + handleHellmann(boss, mode); + } + return; + } + } catch (Exception ex) { + LOG.warn("There has been an error while spawning special night boss!", ex); + } + } + + private void handleHellmann(L2RaidBossInstance boss, int mode) { + switch (mode) { + case 0 -> { + boss.deleteMe(); + LOG.info("Deleting Hellmann raidboss."); + } + case 1 -> { + if (!boss.isVisible()) { + boss.stopAllEffects(); + boss.setIsDead(false); + boss.setDecayed(false); + boss.setCurrentHpMp(boss.getMaxHp(), boss.getMaxMp()); + boss.spawnMe(); + RaidBossSpawnManager.getInstance().notifySpawnNightBoss(boss); + } + LOG.info("Spawning Hellmann raidboss."); + } + } + } + + public L2RaidBossInstance handleBoss(L2Spawn bossSpawn) { + if (_bosses.containsKey(bossSpawn)) { + return _bosses.get(bossSpawn); + } + + if (GameTimeController.getInstance().isNight()) { + L2RaidBossInstance raidboss = (L2RaidBossInstance) bossSpawn.doSpawn(); + _bosses.put(bossSpawn, raidboss); + + return raidboss; + } + _bosses.put(bossSpawn, null); + return null; + } + + public static DayNightSpawnManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final DayNightSpawnManager INSTANCE = new DayNightSpawnManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/DimensionalRiftManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/DimensionalRiftManager.java new file mode 100644 index 00000000..07264110 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/DimensionalRiftManager.java @@ -0,0 +1,300 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.model.DimensionalRiftRoom; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.DimensionalRift; + +/** + * Dimensional Rift manager. + * @author kombat + */ +public final class DimensionalRiftManager { + + private static final Logger LOG = LoggerFactory.getLogger(DimensionalRiftManager.class); + + private final Map> _rooms = new HashMap<>(7); + + protected DimensionalRiftManager() { + loadRooms(); + loadSpawns(); + } + + public DimensionalRiftRoom getRoom(byte type, byte room) { + return _rooms.get(type) == null ? null : _rooms.get(type).get(room); + } + + private void loadRooms() { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + File file = new File(server().getDatapackRoot(), "data/dimensionalRiftRooms.xml"); + if (!file.exists()) { + LOG.warn("Could not find file {}!", file.getAbsoluteFile()); + return; + } + + Document doc = factory.newDocumentBuilder().parse(file); + NamedNodeMap attrs; + + byte type, id; + int xMin, xMax, yMin, yMax, zMin, zMax, xT, yT, zT; + boolean boss; + + for (Node list = doc.getFirstChild(); list != null; list = list.getNextSibling()) { + if ("list".equalsIgnoreCase(list.getNodeName())) { + for (Node room = list.getFirstChild(); room != null; room = room.getNextSibling()) { + if ("room".equalsIgnoreCase(room.getNodeName())) { + attrs = room.getAttributes(); + + type = Byte.parseByte(attrs.getNamedItem("type").getNodeValue()); + id = Byte.parseByte(attrs.getNamedItem("id").getNodeValue()); + + xMin = Integer.parseInt(attrs.getNamedItem("xMin").getNodeValue()); + xMax = Integer.parseInt(attrs.getNamedItem("xMax").getNodeValue()); + yMin = Integer.parseInt(attrs.getNamedItem("yMin").getNodeValue()); + yMax = Integer.parseInt(attrs.getNamedItem("yMax").getNodeValue()); + zMin = Integer.parseInt(attrs.getNamedItem("zMin").getNodeValue()); + zMax = Integer.parseInt(attrs.getNamedItem("zMax").getNodeValue()); + xT = Integer.parseInt(attrs.getNamedItem("xT").getNodeValue()); + yT = Integer.parseInt(attrs.getNamedItem("yT").getNodeValue()); + zT = Integer.parseInt(attrs.getNamedItem("zT").getNodeValue()); + + boss = Boolean.parseBoolean(attrs.getNamedItem("boss").getNodeValue()); + + if (!_rooms.containsKey(type)) { + _rooms.put(type, new HashMap<>(9)); + } + + _rooms.get(type).put(id, new DimensionalRiftRoom(type, id, xMin, xMax, yMin, yMax, zMin, zMax, xT, yT, zT, boss)); + } + } + } + } + } catch (Exception ex) { + LOG.warn("There was an error on loading Dimensional Rift rooms!", ex); + } + + int typeSize = _rooms.size(); + int roomSize = 0; + + for (byte b : _rooms.keySet()) { + roomSize += _rooms.get(b).size(); + } + + LOG.info("Loaded {} room types with {} rooms.", typeSize, roomSize); + } + + public void loadSpawns() { + int countGood = 0, countBad = 0; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + File file = new File(server().getDatapackRoot(), "data/dimensionalRiftSpawns.xml"); + if (!file.exists()) { + LOG.warn("Could not find file {}!", file.getAbsoluteFile()); + return; + } + + Document doc = factory.newDocumentBuilder().parse(file); + NamedNodeMap attrs; + byte type, roomId; + int mobId, x, y, z, delay, count; + + for (Node rift = doc.getFirstChild(); rift != null; rift = rift.getNextSibling()) { + if ("rift".equalsIgnoreCase(rift.getNodeName())) { + for (Node area = rift.getFirstChild(); area != null; area = area.getNextSibling()) { + if ("area".equalsIgnoreCase(area.getNodeName())) { + attrs = area.getAttributes(); + type = Byte.parseByte(attrs.getNamedItem("type").getNodeValue()); + + for (Node room = area.getFirstChild(); room != null; room = room.getNextSibling()) { + if ("room".equalsIgnoreCase(room.getNodeName())) { + attrs = room.getAttributes(); + roomId = Byte.parseByte(attrs.getNamedItem("id").getNodeValue()); + + for (Node spawn = room.getFirstChild(); spawn != null; spawn = spawn.getNextSibling()) { + if ("spawn".equalsIgnoreCase(spawn.getNodeName())) { + attrs = spawn.getAttributes(); + mobId = Integer.parseInt(attrs.getNamedItem("mobId").getNodeValue()); + delay = Integer.parseInt(attrs.getNamedItem("delay").getNodeValue()); + count = Integer.parseInt(attrs.getNamedItem("count").getNodeValue()); + + if (!_rooms.containsKey(type)) { + LOG.warn("Type {} not found!", type); + } else if (!_rooms.get(type).containsKey(roomId)) { + LOG.warn("Room {} in type {} not found!", roomId, type); + } + + for (int i = 0; i < count; i++) { + final DimensionalRiftRoom riftRoom = _rooms.get(type).get(roomId); + x = riftRoom.getRandomX(); + y = riftRoom.getRandomY(); + z = riftRoom.getTeleportCoordinates().getZ(); + + if (_rooms.containsKey(type) && _rooms.get(type).containsKey(roomId)) { + final L2Spawn spawnDat = new L2Spawn(mobId); + spawnDat.setAmount(1); + spawnDat.setX(x); + spawnDat.setY(y); + spawnDat.setZ(z); + spawnDat.setHeading(-1); + spawnDat.setRespawnDelay(delay); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _rooms.get(type).get(roomId).getSpawns().add(spawnDat); + countGood++; + } else { + countBad++; + } + } + } + } + } + } + } + } + } + } + } catch (Exception ex) { + LOG.warn("There was an error on loading Dimensional Rift spawns!", ex); + } + + LOG.info("Loaded {} Dimensional Rift spawns.", countGood); + + if (countBad > 0) { + LOG.warn("There has been {} errors in DImensinal Rift spawns!", countBad); + } + } + + public void reload() { + for (byte b : _rooms.keySet()) { + for (byte i : _rooms.get(b).keySet()) { + _rooms.get(b).get(i).getSpawns().clear(); + } + _rooms.get(b).clear(); + } + _rooms.clear(); + loadRooms(); + loadSpawns(); + } + + public boolean checkIfInRiftZone(int x, int y, int z, boolean ignorePeaceZone) { + if (ignorePeaceZone) { + return _rooms.get((byte) 0).get((byte) 1).checkIfInZone(x, y, z); + } + + return _rooms.get((byte) 0).get((byte) 1).checkIfInZone(x, y, z) && !_rooms.get((byte) 0).get((byte) 0).checkIfInZone(x, y, z); + } + + public boolean checkIfInPeaceZone(int x, int y, int z) { + return _rooms.get((byte) 0).get((byte) 0).checkIfInZone(x, y, z); + } + + public void teleportToWaitingRoom(L2PcInstance player) { + player.teleToLocation(getRoom((byte) 0, (byte) 0).getTeleportCoordinates()); + } + + public synchronized void start(L2PcInstance player, byte type, L2Npc npc) { + byte room; + List emptyRooms; + do { + emptyRooms = getFreeRooms(type); + room = emptyRooms.get(Rnd.get(1, emptyRooms.size()) - 1); + } + + // find empty room + while (_rooms.get(type).get(room).getParty() != null); + new DimensionalRift(npc, player.getParty(), type, room); + } + + public void killRift(DimensionalRift d) { + if (d.getTeleportTimerTask() != null) { + d.getTeleportTimerTask().cancel(); + } + d.setTeleportTimerTask(null); + + if (d.getTeleportTimer() != null) { + d.getTeleportTimer().cancel(); + } + d.setTeleportTimer(null); + + if (d.getSpawnTimerTask() != null) { + d.getSpawnTimerTask().cancel(); + } + d.setSpawnTimerTask(null); + + if (d.getSpawnTimer() != null) { + d.getSpawnTimer().cancel(); + } + d.setSpawnTimer(null); + } + + public boolean isAllowedEnter(byte type) { + int count = 0; + for (DimensionalRiftRoom room : _rooms.get(type).values()) { + if (room.getParty() != null) { + count++; + } + } + return (count < (_rooms.get(type).size() - 1)); + } + + public List getFreeRooms(byte type) { + List list = new ArrayList<>(); + for (DimensionalRiftRoom room : _rooms.get(type).values()) { + if (room.getParty() == null) { + list.add(room.getRoom()); + } + } + return list; + } + + public static DimensionalRiftManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final DimensionalRiftManager _instance = new DimensionalRiftManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/DuelManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/DuelManager.java new file mode 100644 index 00000000..8f6c30d4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/DuelManager.java @@ -0,0 +1,149 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.enums.PrivateStoreType; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Duel; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public final class DuelManager { + private final Map _duels = new ConcurrentHashMap<>(); + private final AtomicInteger _currentDuelId = new AtomicInteger(); + + DuelManager() { + // Hide constructor + } + + public Duel getDuel(int duelId) { + return _duels.get(duelId); + } + + public void addDuel(L2PcInstance playerA, L2PcInstance playerB, boolean partyDuel) { + if ((playerA == null) || (playerB == null)) { + return; + } + + final int duelId = _currentDuelId.incrementAndGet(); + _duels.put(duelId, new Duel(playerA, playerB, partyDuel, duelId)); + } + + public void removeDuel(Duel duel) { + _duels.remove(duel.getId()); + } + + public void doSurrender(L2PcInstance player) { + if ((player == null) || !player.isInDuel()) { + return; + } + final Duel duel = getDuel(player.getDuelId()); + duel.doSurrender(player); + } + + /** + * Updates player states. + * @param player - the dying player + */ + public void onPlayerDefeat(L2PcInstance player) { + if ((player == null) || !player.isInDuel()) { + return; + } + final Duel duel = getDuel(player.getDuelId()); + if (duel != null) { + duel.onPlayerDefeat(player); + } + } + + /** + * Broadcasts a packet to the team opposing the given player. + * @param player + * @param packet + */ + public void broadcastToOpposingTeam(L2PcInstance player, L2GameServerPacket packet) { + if ((player == null) || !player.isInDuel()) { + return; + } + final Duel duel = getDuel(player.getDuelId()); + + if (duel == null) { + return; + } + if (duel.getTeamA().contains(player)) { + duel.broadcastToTeam2(packet); + } else { + duel.broadcastToTeam1(packet); + } + } + + /** + * Checks if this player might join / start a duel. + * @param player + * @param target + * @param partyDuel + * @return true if the player might join/start a duel. + */ + public static boolean canDuel(L2PcInstance player, L2PcInstance target, boolean partyDuel) { + SystemMessageId reason = null; + if (target.isInCombat() || target.isJailed()) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_CURRENTLY_ENGAGED_IN_BATTLE; + } else if (target.isTransformed()) { + reason = SystemMessageId.C1_CANNOT_DUEL_WHILE_POLYMORPHED; + } else if (target.isDead() || ((target.getCurrentHp() < (target.getMaxHp() / 2.0)) || (target.getCurrentMp() < (target.getMaxMp() / 2.0)))) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_HP_OR_MP_IS_BELOW_50_PERCENT; + } else if (target.isInDuel()) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_ALREADY_ENGAGED_IN_A_DUEL; + } else if (target.isInOlympiadMode()) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_PARTICIPATING_IN_THE_OLYMPIAD; + } else if (target.isCursedWeaponEquipped() || (target.getKarma() > 0) || (target.getPvpFlag() > 0)) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_IN_A_CHAOTIC_STATE; + } else if (target.getPrivateStoreType() != PrivateStoreType.NONE) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_CURRENTLY_ENGAGED_IN_A_PRIVATE_STORE_OR_MANUFACTURE; + } else if (target.isMounted() || target.isInBoat()) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_CURRENTLY_RIDING_A_BOAT_STEED_OR_STRIDER; + } else if (target.isFishing()) { + reason = SystemMessageId.C1_CANNOT_DUEL_BECAUSE_C1_IS_CURRENTLY_FISHING; + } else if ((!partyDuel && (target.isInsideZone(ZoneId.PEACE) || target.isInsideZone(ZoneId.WATER))) || target.isInsideZone(ZoneId.PVP) || target.isInsideZone(ZoneId.SIEGE)) { + reason = SystemMessageId.C1_CANNOT_MAKE_A_CHALLANGE_TO_A_DUEL_BECAUSE_C1_IS_CURRENTLY_IN_A_DUEL_PROHIBITED_AREA; + } + + if (reason != null) { + SystemMessage msg = SystemMessage.getSystemMessage(reason); + msg.addString(target.getName()); + player.sendPacket(msg); + return false; + } + + return true; + } + + public static DuelManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + static final DuelManager _instance = new DuelManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/FortManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/FortManager.java new file mode 100644 index 00000000..0ea27141 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/FortManager.java @@ -0,0 +1,170 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.InstanceListManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.entity.Fort; + +public final class FortManager implements InstanceListManager { + + private static final Logger LOG = LoggerFactory.getLogger(FortManager.class); + + private final List _forts = new CopyOnWriteArrayList<>(); + + public int findNearestFortIndex(L2Object obj) { + return findNearestFortIndex(obj, Long.MAX_VALUE); + } + + public int findNearestFortIndex(L2Object obj, long maxDistance) { + int index = getFortIndex(obj); + if (index < 0) { + double distance; + Fort fort; + for (int i = 0; i < _forts.size(); i++) { + fort = _forts.get(i); + if (fort == null) { + continue; + } + distance = fort.getDistance(obj); + if (maxDistance > distance) { + maxDistance = (long) distance; + index = i; + } + } + } + return index; + } + + public Fort getFortById(int fortId) { + for (Fort f : _forts) { + if (f.getResidenceId() == fortId) { + return f; + } + } + return null; + } + + public Fort getFortByOwner(L2Clan clan) { + for (Fort f : _forts) { + if (f.getOwnerClan() == clan) { + return f; + } + } + return null; + } + + public Fort getFort(String name) { + for (Fort f : _forts) { + if (f.getName().equalsIgnoreCase(name.trim())) { + return f; + } + } + return null; + } + + public Fort getFort(int x, int y, int z) { + for (Fort f : _forts) { + if (f.checkIfInZone(x, y, z)) { + return f; + } + } + return null; + } + + public Fort getFort(L2Object activeObject) { + return getFort(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public int getFortIndex(int fortId) { + Fort fort; + for (int i = 0; i < _forts.size(); i++) { + fort = _forts.get(i); + if ((fort != null) && (fort.getResidenceId() == fortId)) { + return i; + } + } + return -1; + } + + public int getFortIndex(L2Object activeObject) { + return getFortIndex(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public int getFortIndex(int x, int y, int z) { + Fort fort; + for (int i = 0; i < _forts.size(); i++) { + fort = _forts.get(i); + if ((fort != null) && fort.checkIfInZone(x, y, z)) { + return i; + } + } + return -1; + } + + public List getForts() { + return _forts; + } + + @Override + public void loadInstances() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT id FROM fort ORDER BY id")) { + while (rs.next()) { + _forts.add(new Fort(rs.getInt("id"))); + } + + LOG.info("Loaded {} fortress.", _forts.size()); + + for (Fort fort : _forts) { + fort.getSiege().getSiegeGuardManager().loadSiegeGuard(); + } + } catch (Exception ex) { + LOG.warn("There has been an error loading fort instances!", ex); + } + } + + @Override + public void updateReferences() { + } + + @Override + public void activateInstances() { + for (final Fort fort : _forts) { + fort.activateInstance(); + } + } + + public static FortManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final FortManager _instance = new FortManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeGuardManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeGuardManager.java new file mode 100644 index 00000000..3af97352 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeGuardManager.java @@ -0,0 +1,116 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.entity.Fort; + +public final class FortSiegeGuardManager { + + private static final Logger LOG = LoggerFactory.getLogger(FortSiegeGuardManager.class); + + private final Fort _fort; + + private final Map> _siegeGuards = new HashMap<>(); + + public FortSiegeGuardManager(Fort fort) { + _fort = fort; + } + + public void spawnSiegeGuard() { + try { + final List monsterList = _siegeGuards.get(getFort().getResidenceId()); + if (monsterList != null) { + for (L2Spawn spawnDat : monsterList) { + spawnDat.doSpawn(); + if (spawnDat.getRespawnDelay() == 0) { + spawnDat.stopRespawn(); + } else { + spawnDat.startRespawn(); + } + } + } + } catch (Exception e) { + LOG.warn("Error spawning siege guards for fort {}: {}", getFort().getName(), e.getMessage(), e); + } + } + + /** + * Unspawn guards. + */ + public void unspawnSiegeGuard() { + try { + final List monsterList = _siegeGuards.get(getFort().getResidenceId()); + if (monsterList != null) { + for (L2Spawn spawnDat : monsterList) { + spawnDat.stopRespawn(); + if (spawnDat.getLastSpawn() != null) { + spawnDat.getLastSpawn().doDie(spawnDat.getLastSpawn()); + } + } + } + } catch (Exception e) { + LOG.warn("Error despawning siege guards for fort {}: {}", getFort().getName(), e.getMessage(), e); + } + } + + void loadSiegeGuard() { + _siegeGuards.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT npcId, x, y, z, heading, respawnDelay FROM fort_siege_guards WHERE fortId = ?")) { + final int fortId = getFort().getResidenceId(); + ps.setInt(1, fortId); + try (var rs = ps.executeQuery()) { + final List siegeGuardSpawns = new ArrayList<>(); + while (rs.next()) { + final L2Spawn spawn = new L2Spawn(rs.getInt("npcId")); + spawn.setAmount(1); + spawn.setX(rs.getInt("x")); + spawn.setY(rs.getInt("y")); + spawn.setZ(rs.getInt("z")); + spawn.setHeading(rs.getInt("heading")); + spawn.setRespawnDelay(rs.getInt("respawnDelay")); + spawn.setLocationId(0); + + siegeGuardSpawns.add(spawn); + } + _siegeGuards.put(fortId, siegeGuardSpawns); + } + } catch (Exception e) { + LOG.warn("Error loading siege guard for fort {}: {}", getFort().getName(), e.getMessage(), e); + } + } + + public Fort getFort() { + return _fort; + } + + public Map> getSiegeGuardSpawn() { + return _siegeGuards; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeManager.java new file mode 100644 index 00000000..0d9b8d1c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/FortSiegeManager.java @@ -0,0 +1,240 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.fortSiege; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.CombatFlag; +import com.l2jserver.gameserver.model.FortSiegeSpawn; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.FortSiege; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public final class FortSiegeManager { + + private static final Logger LOG = LoggerFactory.getLogger(FortSiegeManager.class); + + // Fort Siege settings + private Map> _commanderSpawnList; + private Map> _flagList; + private final List _sieges = new ArrayList<>(); + + private FortSiegeManager() { + load(); + } + + public void addSiegeSkills(L2PcInstance character) { + character.addSkill(CommonSkill.SEAL_OF_RULER.getSkill(), false); + character.addSkill(CommonSkill.BUILD_HEADQUARTERS.getSkill(), false); + } + + /** + * @param clan The L2Clan of the player + * @param fortid id of the fort to check against + * @return true if the clan is registered or owner of a fort + */ + public boolean checkIsRegistered(L2Clan clan, int fortid) { + if (clan == null) { + return false; + } + + boolean register = false; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT clan_id FROM fortsiege_clans where clan_id=? and fort_id=?")) { + ps.setInt(1, clan.getId()); + ps.setInt(2, fortid); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + register = true; + } + } + } catch (Exception e) { + LOG.warn("Exception: checkIsRegistered(): {}", e.getMessage(), e); + } + return register; + } + + public void removeSiegeSkills(L2PcInstance character) { + character.removeSkill(CommonSkill.SEAL_OF_RULER.getSkill()); + character.removeSkill(CommonSkill.BUILD_HEADQUARTERS.getSkill()); + } + + private void load() { + // Siege spawns settings + _commanderSpawnList = new ConcurrentHashMap<>(); + _flagList = new ConcurrentHashMap<>(); + + for (Fort fort : FortManager.getInstance().getForts()) { + List commanderSpawns = new ArrayList<>(); + List flagSpawns = new ArrayList<>(); + for (int i = 1; i < 5; i++) { + final String spawnParams = fortSiege().getProperty(fort.getName().replace(" ", "") + "Commander" + i); + if (spawnParams == null) { + break; + } + + final StringTokenizer st = new StringTokenizer(spawnParams.trim(), ","); + try { + int x = Integer.parseInt(st.nextToken()); + int y = Integer.parseInt(st.nextToken()); + int z = Integer.parseInt(st.nextToken()); + int heading = Integer.parseInt(st.nextToken()); + int npc_id = Integer.parseInt(st.nextToken()); + commanderSpawns.add(new FortSiegeSpawn(fort.getResidenceId(), x, y, z, heading, npc_id, i)); + } catch (Exception e) { + LOG.warn("Error while loading commander(s) for {} fort.", fort.getName()); + } + } + + _commanderSpawnList.put(fort.getResidenceId(), commanderSpawns); + + for (int i = 1; i < 4; i++) { + final String spawnParams = fortSiege().getProperty(fort.getName().replace(" ", "") + "Flag" + i); + if (spawnParams == null) { + break; + } + final StringTokenizer st = new StringTokenizer(spawnParams.trim(), ","); + + try { + int x = Integer.parseInt(st.nextToken()); + int y = Integer.parseInt(st.nextToken()); + int z = Integer.parseInt(st.nextToken()); + int flag_id = Integer.parseInt(st.nextToken()); + + flagSpawns.add(new CombatFlag(fort.getResidenceId(), x, y, z, 0, flag_id)); + } catch (Exception e) { + LOG.warn("Error while loading flag(s) for {} fort.", fort.getName()); + } + } + _flagList.put(fort.getResidenceId(), flagSpawns); + } + } + + public List getCommanderSpawnList(int _fortId) { + return _commanderSpawnList.get(_fortId); + } + + public List getFlagList(int _fortId) { + return _flagList.get(_fortId); + } + + public FortSiege getSiege(L2Object activeObject) { + return getSiege(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public FortSiege getSiege(int x, int y, int z) { + for (Fort fort : FortManager.getInstance().getForts()) { + if (fort.getSiege().checkIfInZone(x, y, z)) { + return fort.getSiege(); + } + } + return null; + } + + public List getSieges() { + return _sieges; + } + + public void addSiege(FortSiege fortSiege) { + _sieges.add(fortSiege); + } + + public boolean isCombat(int itemId) { + return (itemId == 9819); + } + + public boolean activateCombatFlag(L2PcInstance player, L2ItemInstance item) { + if (!checkIfCanPickup(player)) { + return false; + } + + final Fort fort = FortManager.getInstance().getFort(player); + + final List fcf = _flagList.get(fort.getResidenceId()); + for (CombatFlag cf : fcf) { + if (cf.getCombatFlagInstance() == item) { + cf.activate(player, item); + } + } + return true; + } + + public boolean checkIfCanPickup(L2PcInstance player) { + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_FORTRESS_BATTLE_OF_S1_HAS_FINISHED); + sm.addItemName(9819); + // Cannot own 2 combat flag + if (player.isCombatFlagEquipped()) { + player.sendPacket(sm); + return false; + } + + // here check if is siege is in progress + // here check if is siege is attacker + final Fort fort = FortManager.getInstance().getFort(player); + + if ((fort == null) || (fort.getResidenceId() <= 0)) { + player.sendPacket(sm); + return false; + } else if (!fort.getSiege().isInProgress()) { + player.sendPacket(sm); + return false; + } else if (fort.getSiege().getAttackerClan(player.getClan()) == null) { + player.sendPacket(sm); + return false; + } + return true; + } + + public void dropCombatFlag(L2PcInstance player, int fortId) { + final Fort fort = FortManager.getInstance().getFortById(fortId); + final List fcf = _flagList.get(fort.getResidenceId()); + for (CombatFlag cf : fcf) { + if (cf.getPlayerObjectId() == player.getObjectId()) { + cf.dropIt(); + if (fort.getSiege().isInProgress()) { + cf.spawnMe(); + } + } + } + } + + public static FortSiegeManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final FortSiegeManager _instance = new FortSiegeManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/FourSepulchersManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/FourSepulchersManager.java new file mode 100644 index 00000000..ea69a20e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/FourSepulchersManager.java @@ -0,0 +1,1280 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.instancemanager.tasks.FourSepulchersChangeAttackTimeTask; +import com.l2jserver.gameserver.instancemanager.tasks.FourSepulchersChangeCoolDownTimeTask; +import com.l2jserver.gameserver.instancemanager.tasks.FourSepulchersChangeEntryTimeTask; +import com.l2jserver.gameserver.instancemanager.tasks.FourSepulchersChangeWarmUpTimeTask; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2SepulcherMonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2SepulcherNpcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.quest.Quest; +import com.l2jserver.gameserver.model.quest.QuestState; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * Zoey76: TODO: Use Location DTO instead of array of int. + * @author sandman + */ +public final class FourSepulchersManager { + + private static final Logger LOG = LoggerFactory.getLogger(FourSepulchersManager.class); + + private static final int QUEST_ID = 620; + + private static final int ENTRANCE_PASS = 7075; + + private static final int USED_PASS = 7261; + + private static final int CHAPEL_KEY = 7260; + + private static final int ANTIQUE_BROOCH = 7262; + + private boolean _firstTimeRun; + private boolean _inEntryTime = false; + private boolean _inWarmUpTime = false; + private boolean _inAttackTime = false; + private boolean _inCoolDownTime = false; + + private ScheduledFuture _changeCoolDownTimeTask = null; + private ScheduledFuture _changeEntryTimeTask = null; + private ScheduledFuture _changeWarmUpTimeTask = null; + private ScheduledFuture _changeAttackTimeTask = null; + + // @formatter:off + private final int[][] _startHallSpawn = + { + { 181632, -85587, -7218 }, + { 179963, -88978, -7218 }, + { 173217, -86132, -7218 }, + { 175608, -82296, -7218 } + }; + + private final int[][][] _shadowSpawnLoc = + { + { + { 25339, 191231, -85574, -7216, 33380 }, + { 25349, 189534, -88969, -7216, 32768 }, + { 25346, 173195, -76560, -7215, 49277 }, + { 25342, 175591, -72744, -7215, 49317 } + }, + { + { 25342, 191231, -85574, -7216, 33380 }, + { 25339, 189534, -88969, -7216, 32768 }, + { 25349, 173195, -76560, -7215, 49277 }, + { 25346, 175591, -72744, -7215, 49317 } + }, + { + { 25346, 191231, -85574, -7216, 33380 }, + { 25342, 189534, -88969, -7216, 32768 }, + { 25339, 173195, -76560, -7215, 49277 }, + { 25349, 175591, -72744, -7215, 49317 } + }, + { + { 25349, 191231, -85574, -7216, 33380 }, + { 25346, 189534, -88969, -7216, 32768 }, + { 25342, 173195, -76560, -7215, 49277 }, + { 25339, 175591, -72744, -7215, 49317 } + }, + }; + // @formatter:on + + protected Map _archonSpawned = new ConcurrentHashMap<>(); + protected Map _hallInUse = new ConcurrentHashMap<>(); + protected Map _challengers = new ConcurrentHashMap<>(); + protected Map _startHallSpawns = new HashMap<>(); + protected Map _hallGateKeepers = new HashMap<>(); + protected Map _keyBoxNpc = new HashMap<>(); + protected Map _victim = new HashMap<>(); + protected Map _executionerSpawns = new HashMap<>(); + protected Map _keyBoxSpawns = new HashMap<>(); + protected Map _mysteriousBoxSpawns = new HashMap<>(); + protected Map _shadowSpawns = new HashMap<>(); + protected Map> _dukeFinalMobs = new HashMap<>(); + protected Map> _dukeMobs = new HashMap<>(); + protected Map> _emperorsGraveNpcs = new HashMap<>(); + protected Map> _magicalMonsters = new HashMap<>(); + protected Map> _physicalMonsters = new HashMap<>(); + protected Map> _viscountMobs = new HashMap<>(); + + protected List _physicalSpawns; + protected List _magicalSpawns; + protected List _managers; + protected List _dukeFinalSpawns; + protected List _emperorsGraveSpawns; + protected List _allMobs = new CopyOnWriteArrayList<>(); + + private long _attackTimeEnd = 0; + private long _coolDownTimeEnd = 0; + private long _entryTimeEnd = 0; + private long _warmUpTimeEnd = 0; + + private final byte _newCycleMin = 55; + + public void init() { + if (_changeCoolDownTimeTask != null) { + _changeCoolDownTimeTask.cancel(true); + } + if (_changeEntryTimeTask != null) { + _changeEntryTimeTask.cancel(true); + } + if (_changeWarmUpTimeTask != null) { + _changeWarmUpTimeTask.cancel(true); + } + if (_changeAttackTimeTask != null) { + _changeAttackTimeTask.cancel(true); + } + + _changeCoolDownTimeTask = null; + _changeEntryTimeTask = null; + _changeWarmUpTimeTask = null; + _changeAttackTimeTask = null; + + _inEntryTime = false; + _inWarmUpTime = false; + _inAttackTime = false; + _inCoolDownTime = false; + + _firstTimeRun = true; + initFixedInfo(); + loadMysteriousBox(); + initKeyBoxSpawns(); + loadPhysicalMonsters(); + loadMagicalMonsters(); + initLocationShadowSpawns(); + initExecutionerSpawns(); + loadDukeMonsters(); + loadEmperorsGraveMonsters(); + spawnManagers(); + timeSelector(); + } + + // phase select on server launch + protected void timeSelector() { + timeCalculator(); + long currentTime = Calendar.getInstance().getTimeInMillis(); + // if current time >= time of entry beginning and if current time < time of entry beginning + time of entry end + if ((currentTime >= _coolDownTimeEnd) && (currentTime < _entryTimeEnd)) // entry time check + { + clean(); + _changeEntryTimeTask = ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeEntryTimeTask(), 0); + LOG.info("Beginning in entry time"); + } else if ((currentTime >= _entryTimeEnd) && (currentTime < _warmUpTimeEnd)) // warmup time check + { + clean(); + _changeWarmUpTimeTask = ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeWarmUpTimeTask(), 0); + LOG.info("Beginning in warm-up time."); + } else if ((currentTime >= _warmUpTimeEnd) && (currentTime < _attackTimeEnd)) // attack time check + { + clean(); + _changeAttackTimeTask = ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeAttackTimeTask(), 0); + LOG.info("Beginning in attack time."); + } else + // else cooldown time and without cleanup because it's already implemented + { + _changeCoolDownTimeTask = ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeCoolDownTimeTask(), 0); + LOG.info("Beginning in cooldown time."); + } + } + + // phase end times calculator + protected void timeCalculator() { + Calendar tmp = Calendar.getInstance(); + if (tmp.get(Calendar.MINUTE) < _newCycleMin) { + tmp.set(Calendar.HOUR, Calendar.getInstance().get(Calendar.HOUR) - 1); + } + tmp.set(Calendar.MINUTE, _newCycleMin); + _coolDownTimeEnd = tmp.getTimeInMillis(); + _entryTimeEnd = _coolDownTimeEnd + general().getTimeOfEntry(); + _warmUpTimeEnd = _entryTimeEnd + general().getTimeOfWarmUp(); + _attackTimeEnd = _warmUpTimeEnd + general().getTimeOfAttack(); + } + + public void clean() { + for (var i = 31921; i < 31925; i++) { + if (!_startHallSpawns.containsKey(i)) { + LOG.warn("NPC Id {} is not inside {}!", i, _startHallSpawns); + continue; + } + final var location = _startHallSpawns.get(i); + GrandBossManager.getInstance().getZone(location[0], location[1], location[2]).oustAllPlayers(); + } + + deleteAllMobs(); + + closeAllDoors(); + + _hallInUse.clear(); + _hallInUse.put(31921, false); + _hallInUse.put(31922, false); + _hallInUse.put(31923, false); + _hallInUse.put(31924, false); + + _archonSpawned.replaceAll((_, _) -> false); + } + + protected void spawnManagers() { + _managers = new CopyOnWriteArrayList<>(); + + for (int npcId = 31921; npcId <= 31924; npcId++) { + try { + final L2Spawn spawnDat = new L2Spawn(npcId); + spawnDat.setAmount(1); + spawnDat.setRespawnDelay(60); + switch (npcId) { + // conquerors + case 31921 -> { + spawnDat.setX(181061); + spawnDat.setY(-85595); + spawnDat.setZ(-7200); + spawnDat.setHeading(-32584); + } + // emperors + case 31922 -> { + spawnDat.setX(179292); + spawnDat.setY(-88981); + spawnDat.setZ(-7200); + spawnDat.setHeading(-33272); + } + // sages + case 31923 -> { + spawnDat.setX(173202); + spawnDat.setY(-87004); + spawnDat.setZ(-7200); + spawnDat.setHeading(-16248); + } + // judges + case 31924 -> { + spawnDat.setX(175606); + spawnDat.setY(-82853); + spawnDat.setZ(-7200); + spawnDat.setHeading(-16248); + } + } + _managers.add(spawnDat); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + spawnDat.doSpawn(); + spawnDat.startRespawn(); + LOG.info("Spawned {}.", spawnDat.getTemplate().getName()); + } catch (Exception ex) { + LOG.warn("There has been an error while spawning managers!", ex); + } + } + } + + protected void initFixedInfo() { + _startHallSpawns.put(31921, _startHallSpawn[0]); + _startHallSpawns.put(31922, _startHallSpawn[1]); + _startHallSpawns.put(31923, _startHallSpawn[2]); + _startHallSpawns.put(31924, _startHallSpawn[3]); + + _hallInUse.put(31921, false); + _hallInUse.put(31922, false); + _hallInUse.put(31923, false); + _hallInUse.put(31924, false); + + _hallGateKeepers.put(31925, 25150012); + _hallGateKeepers.put(31926, 25150013); + _hallGateKeepers.put(31927, 25150014); + _hallGateKeepers.put(31928, 25150015); + _hallGateKeepers.put(31929, 25150016); + _hallGateKeepers.put(31930, 25150002); + _hallGateKeepers.put(31931, 25150003); + _hallGateKeepers.put(31932, 25150004); + _hallGateKeepers.put(31933, 25150005); + _hallGateKeepers.put(31934, 25150006); + _hallGateKeepers.put(31935, 25150032); + _hallGateKeepers.put(31936, 25150033); + _hallGateKeepers.put(31937, 25150034); + _hallGateKeepers.put(31938, 25150035); + _hallGateKeepers.put(31939, 25150036); + _hallGateKeepers.put(31940, 25150022); + _hallGateKeepers.put(31941, 25150023); + _hallGateKeepers.put(31942, 25150024); + _hallGateKeepers.put(31943, 25150025); + _hallGateKeepers.put(31944, 25150026); + + _keyBoxNpc.put(18120, 31455); + _keyBoxNpc.put(18121, 31455); + _keyBoxNpc.put(18122, 31455); + _keyBoxNpc.put(18123, 31455); + _keyBoxNpc.put(18124, 31456); + _keyBoxNpc.put(18125, 31456); + _keyBoxNpc.put(18126, 31456); + _keyBoxNpc.put(18127, 31456); + _keyBoxNpc.put(18128, 31457); + _keyBoxNpc.put(18129, 31457); + _keyBoxNpc.put(18130, 31457); + _keyBoxNpc.put(18131, 31457); + _keyBoxNpc.put(18149, 31458); + _keyBoxNpc.put(18150, 31459); + _keyBoxNpc.put(18151, 31459); + _keyBoxNpc.put(18152, 31459); + _keyBoxNpc.put(18153, 31459); + _keyBoxNpc.put(18154, 31460); + _keyBoxNpc.put(18155, 31460); + _keyBoxNpc.put(18156, 31460); + _keyBoxNpc.put(18157, 31460); + _keyBoxNpc.put(18158, 31461); + _keyBoxNpc.put(18159, 31461); + _keyBoxNpc.put(18160, 31461); + _keyBoxNpc.put(18161, 31461); + _keyBoxNpc.put(18162, 31462); + _keyBoxNpc.put(18163, 31462); + _keyBoxNpc.put(18164, 31462); + _keyBoxNpc.put(18165, 31462); + _keyBoxNpc.put(18183, 31463); + _keyBoxNpc.put(18184, 31464); + _keyBoxNpc.put(18212, 31465); + _keyBoxNpc.put(18213, 31465); + _keyBoxNpc.put(18214, 31465); + _keyBoxNpc.put(18215, 31465); + _keyBoxNpc.put(18216, 31466); + _keyBoxNpc.put(18217, 31466); + _keyBoxNpc.put(18218, 31466); + _keyBoxNpc.put(18219, 31466); + + _victim.put(18150, 18158); + _victim.put(18151, 18159); + _victim.put(18152, 18160); + _victim.put(18153, 18161); + _victim.put(18154, 18162); + _victim.put(18155, 18163); + _victim.put(18156, 18164); + _victim.put(18157, 18165); + } + + private void loadMysteriousBox() { + _mysteriousBoxSpawns.clear(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT id, count, npc_templateid, locx, locy, locz, heading, respawn_delay, key_npc_id FROM four_sepulchers_spawnlist Where spawntype = ? ORDER BY id")) { + ps.setInt(1, 0); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final L2Spawn spawnDat = new L2Spawn(rs.getInt("npc_templateid")); + spawnDat.setAmount(rs.getInt("count")); + spawnDat.setX(rs.getInt("locx")); + spawnDat.setY(rs.getInt("locy")); + spawnDat.setZ(rs.getInt("locz")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(rs.getInt("respawn_delay")); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + int keyNpcId = rs.getInt("key_npc_id"); + _mysteriousBoxSpawns.put(keyNpcId, spawnDat); + } + } + LOG.info("Loaded {} Mysterious-Box spawns.", _mysteriousBoxSpawns.size()); + } catch (Exception ex) { + LOG.warn("Misterious-Boc spawn could not be initialized!", ex); + } + } + + private void initKeyBoxSpawns() { + for (Entry keyNpc : _keyBoxNpc.entrySet()) { + try { + final L2Spawn spawnDat = new L2Spawn(keyNpc.getValue()); + spawnDat.setAmount(1); + spawnDat.setX(0); + spawnDat.setY(0); + spawnDat.setZ(0); + spawnDat.setHeading(0); + spawnDat.setRespawnDelay(3600); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _keyBoxSpawns.put(keyNpc.getKey(), spawnDat); + } catch (Exception ex) { + LOG.warn("Key Box spawn could not be initialized!", ex); + } + } + } + + private void loadPhysicalMonsters() { + _physicalMonsters.clear(); + + int loaded = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("SELECT Distinct key_npc_id FROM four_sepulchers_spawnlist Where spawntype = ? ORDER BY key_npc_id")) { + ps1.setInt(1, 1); + try (var rs1 = ps1.executeQuery(); + var ps2 = con.prepareStatement("SELECT id, count, npc_templateid, locx, locy, locz, heading, respawn_delay, key_npc_id FROM four_sepulchers_spawnlist Where key_npc_id = ? and spawntype = ? ORDER BY id")) { + while (rs1.next()) { + int keyNpcId = rs1.getInt("key_npc_id"); + + ps2.setInt(1, keyNpcId); + ps2.setInt(2, 1); + try (var rs2 = ps2.executeQuery()) { + _physicalSpawns = new ArrayList<>(); + while (rs2.next()) { + final L2Spawn spawnDat = new L2Spawn(rs2.getInt("npc_templateid")); + spawnDat.setAmount(rs2.getInt("count")); + spawnDat.setX(rs2.getInt("locx")); + spawnDat.setY(rs2.getInt("locy")); + spawnDat.setZ(rs2.getInt("locz")); + spawnDat.setHeading(rs2.getInt("heading")); + spawnDat.setRespawnDelay(rs2.getInt("respawn_delay")); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _physicalSpawns.add(spawnDat); + loaded++; + } + } + ps2.clearParameters(); + _physicalMonsters.put(keyNpcId, _physicalSpawns); + } + } + LOG.info("Loaded {} Physical type monsters spawns.", loaded); + } catch (Exception ex) { + LOG.warn("Physical monsters spawn could not be initialized!", ex); + } + } + + private void loadMagicalMonsters() { + _magicalMonsters.clear(); + + int loaded = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("SELECT Distinct key_npc_id FROM four_sepulchers_spawnlist Where spawntype = ? ORDER BY key_npc_id")) { + ps1.setInt(1, 2); + try (var rs1 = ps1.executeQuery(); + var ps2 = con.prepareStatement("SELECT id, count, npc_templateid, locx, locy, locz, heading, respawn_delay, key_npc_id FROM four_sepulchers_spawnlist WHERE key_npc_id = ? AND spawntype = ? ORDER BY id")) { + while (rs1.next()) { + int keyNpcId = rs1.getInt("key_npc_id"); + + ps2.setInt(1, keyNpcId); + ps2.setInt(2, 2); + try (var rset2 = ps2.executeQuery()) { + _magicalSpawns = new ArrayList<>(); + + while (rset2.next()) { + final L2Spawn spawnDat = new L2Spawn(rset2.getInt("npc_templateid")); + spawnDat.setAmount(rset2.getInt("count")); + spawnDat.setX(rset2.getInt("locx")); + spawnDat.setY(rset2.getInt("locy")); + spawnDat.setZ(rset2.getInt("locz")); + spawnDat.setHeading(rset2.getInt("heading")); + spawnDat.setRespawnDelay(rset2.getInt("respawn_delay")); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _magicalSpawns.add(spawnDat); + loaded++; + } + } + ps2.clearParameters(); + _magicalMonsters.put(keyNpcId, _magicalSpawns); + } + } + LOG.info("Loaded {} Magical monsters spawns.", loaded); + } catch (Exception ex) { + LOG.warn("Magical monsters spawn could not be initialized!", ex); + } + } + + private void loadDukeMonsters() { + _dukeFinalMobs.clear(); + _archonSpawned.clear(); + + int loaded = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("SELECT Distinct key_npc_id FROM four_sepulchers_spawnlist Where spawntype = ? ORDER BY key_npc_id")) { + ps1.setInt(1, 5); + try (var rs1 = ps1.executeQuery(); + var ps2 = con.prepareStatement("SELECT id, count, npc_templateid, locx, locy, locz, heading, respawn_delay, key_npc_id FROM four_sepulchers_spawnlist WHERE key_npc_id = ? AND spawntype = ? ORDER BY id")) { + while (rs1.next()) { + int keyNpcId = rs1.getInt("key_npc_id"); + + ps2.setInt(1, keyNpcId); + ps2.setInt(2, 5); + try (var rset2 = ps2.executeQuery()) { + ps2.clearParameters(); + + _dukeFinalSpawns = new ArrayList<>(); + + while (rset2.next()) { + final L2Spawn spawnDat = new L2Spawn(rset2.getInt("npc_templateid")); + spawnDat.setAmount(rset2.getInt("count")); + spawnDat.setX(rset2.getInt("locx")); + spawnDat.setY(rset2.getInt("locy")); + spawnDat.setZ(rset2.getInt("locz")); + spawnDat.setHeading(rset2.getInt("heading")); + spawnDat.setRespawnDelay(rset2.getInt("respawn_delay")); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _dukeFinalSpawns.add(spawnDat); + loaded++; + } + } + ps2.clearParameters(); + _dukeFinalMobs.put(keyNpcId, _dukeFinalSpawns); + _archonSpawned.put(keyNpcId, false); + } + } + LOG.info("Loaded {} Church of duke monsters spawns.", loaded); + } catch (Exception ex) { + LOG.warn("Duke monsters spawn could not be initialized", ex); + } + } + + private void loadEmperorsGraveMonsters() { + _emperorsGraveNpcs.clear(); + + int loaded = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("SELECT Distinct key_npc_id FROM four_sepulchers_spawnlist Where spawntype = ? ORDER BY key_npc_id")) { + ps1.setInt(1, 6); + try (var rs1 = ps1.executeQuery(); + var ps2 = con.prepareStatement("SELECT id, count, npc_templateid, locx, locy, locz, heading, respawn_delay, key_npc_id FROM four_sepulchers_spawnlist WHERE key_npc_id = ? and spawntype = ? ORDER BY id")) { + while (rs1.next()) { + int keyNpcId = rs1.getInt("key_npc_id"); + + ps2.setInt(1, keyNpcId); + ps2.setInt(2, 6); + try (var rs2 = ps2.executeQuery()) { + _emperorsGraveSpawns = new ArrayList<>(); + + while (rs2.next()) { + final L2Spawn spawnDat = new L2Spawn(rs2.getInt("npc_templateid")); + spawnDat.setAmount(rs2.getInt("count")); + spawnDat.setX(rs2.getInt("locx")); + spawnDat.setY(rs2.getInt("locy")); + spawnDat.setZ(rs2.getInt("locz")); + spawnDat.setHeading(rs2.getInt("heading")); + spawnDat.setRespawnDelay(rs2.getInt("respawn_delay")); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _emperorsGraveSpawns.add(spawnDat); + loaded++; + } + } + ps2.clearParameters(); + _emperorsGraveNpcs.put(keyNpcId, _emperorsGraveSpawns); + } + } + LOG.info("Loaded {} Emperor's Grave spawns.", loaded); + } catch (Exception ex) { + LOG.warn("Emperor's Grave spawns could not be initialized!", ex); + } + } + + protected void initLocationShadowSpawns() { + int locNo = Rnd.get(4); + final int[] gateKeeper = { + 31929, + 31934, + 31939, + 31944 + }; + + _shadowSpawns.clear(); + + for (int i = 0; i <= 3; i++) { + try { + final L2Spawn spawnDat = new L2Spawn(_shadowSpawnLoc[locNo][i][0]); + spawnDat.setAmount(1); + spawnDat.setX(_shadowSpawnLoc[locNo][i][1]); + spawnDat.setY(_shadowSpawnLoc[locNo][i][2]); + spawnDat.setZ(_shadowSpawnLoc[locNo][i][3]); + spawnDat.setHeading(_shadowSpawnLoc[locNo][i][4]); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _shadowSpawns.put(gateKeeper[i], spawnDat); + } catch (Exception ex) { + LOG.warn("There has been an error spawning shadows!", ex); + } + } + } + + protected void initExecutionerSpawns() { + for (int keyNpcId : _victim.keySet()) { + try { + final L2Spawn spawnDat = new L2Spawn(_victim.get(keyNpcId)); + spawnDat.setAmount(1); + spawnDat.setX(0); + spawnDat.setY(0); + spawnDat.setZ(0); + spawnDat.setHeading(0); + spawnDat.setRespawnDelay(3600); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + _executionerSpawns.put(keyNpcId, spawnDat); + } catch (Exception ex) { + LOG.warn("Executioner spawns could not be initialized!", ex); + } + } + } + + public ScheduledFuture getChangeAttackTimeTask() { + return _changeAttackTimeTask; + } + + public void setChangeAttackTimeTask(ScheduledFuture task) { + _changeAttackTimeTask = task; + } + + public ScheduledFuture getChangeCoolDownTimeTask() { + return _changeCoolDownTimeTask; + } + + public void setChangeCoolDownTimeTask(ScheduledFuture task) { + _changeCoolDownTimeTask = task; + } + + public ScheduledFuture getChangeEntryTimeTask() { + return _changeEntryTimeTask; + } + + public void setChangeEntryTimeTask(ScheduledFuture task) { + _changeEntryTimeTask = task; + } + + public ScheduledFuture getChangeWarmUpTimeTask() { + return _changeWarmUpTimeTask; + } + + public void setChangeWarmUpTimeTask(ScheduledFuture task) { + _changeWarmUpTimeTask = task; + } + + public long getAttackTimeEnd() { + return _attackTimeEnd; + } + + public void setAttackTimeEnd(long attackTimeEnd) { + _attackTimeEnd = attackTimeEnd; + } + + public byte getCycleMin() { + return _newCycleMin; + } + + public long getEntrytTimeEnd() { + return _entryTimeEnd; + } + + public void setEntryTimeEnd(long entryTimeEnd) { + _entryTimeEnd = entryTimeEnd; + } + + public long getWarmUpTimeEnd() { + return _warmUpTimeEnd; + } + + public void setWarmUpTimeEnd(long warmUpTimeEnd) { + _warmUpTimeEnd = warmUpTimeEnd; + } + + public boolean isAttackTime() { + return _inAttackTime; + } + + public void setIsAttackTime(boolean attackTime) { + _inAttackTime = attackTime; + } + + public boolean isCoolDownTime() { + return _inCoolDownTime; + } + + public void setIsCoolDownTime(boolean isCoolDownTime) { + _inCoolDownTime = isCoolDownTime; + } + + public boolean isEntryTime() { + return _inEntryTime; + } + + public void setIsEntryTime(boolean entryTime) { + _inEntryTime = entryTime; + } + + public boolean isFirstTimeRun() { + return _firstTimeRun; + } + + public void setIsFirstTimeRun(boolean isFirstTimeRun) { + _firstTimeRun = isFirstTimeRun; + } + + public boolean isWarmUpTime() { + return _inWarmUpTime; + } + + public void setIsWarmUpTime(boolean isWarmUpTime) { + _inWarmUpTime = isWarmUpTime; + } + + public synchronized void tryEntry(L2Npc npc, L2PcInstance player) { + Quest hostQuest = QuestManager.getInstance().getQuest(QUEST_ID); + if (hostQuest == null) { + LOG.warn("Could not find quest Id {}!", QUEST_ID); + return; + } + + int npcId = npc.getId(); + switch (npcId) { + // ID ok + case 31921: + case 31922: + case 31923: + case 31924: + break; + // ID not ok + default: { + if (!player.isGM()) { + LOG.warn("Player {} tried to cheat in four sepulchers.", player); + Util.handleIllegalPlayerAction(player, "Warning!! Character " + player.getName() + " tried to enter four sepulchers with invalid npc id."); + } + return; + } + } + + if (_hallInUse.get(npcId)) { + showHtmlFile(player, npcId + "-FULL.htm", npc, null); + return; + } + + if (general().getNumberOfNecessaryPartyMembers() > 1) { + if (!player.isInParty() || (player.getParty().getMemberCount() < general().getNumberOfNecessaryPartyMembers())) { + showHtmlFile(player, npcId + "-SP.htm", npc, null); + return; + } + + if (!player.getParty().isLeader(player)) { + showHtmlFile(player, npcId + "-NL.htm", npc, null); + return; + } + + for (L2PcInstance mem : player.getParty().getMembers()) { + QuestState qs = mem.getQuestState(hostQuest.getName()); + if ((qs == null) || (!qs.isStarted() && !qs.isCompleted())) { + showHtmlFile(player, npcId + "-NS.htm", npc, mem); + return; + } + if (mem.getInventory().getItemByItemId(ENTRANCE_PASS) == null) { + showHtmlFile(player, npcId + "-SE.htm", npc, mem); + return; + } + + if (player.getWeightPenalty() >= 3) { + mem.sendPacket(SystemMessageId.INVENTORY_LESS_THAN_80_PERCENT); + return; + } + } + } else if ((general().getNumberOfNecessaryPartyMembers() <= 1) && player.isInParty()) { + if (!player.getParty().isLeader(player)) { + showHtmlFile(player, npcId + "-NL.htm", npc, null); + return; + } + for (L2PcInstance mem : player.getParty().getMembers()) { + QuestState qs = mem.getQuestState(hostQuest.getName()); + if ((qs == null) || (!qs.isStarted() && !qs.isCompleted())) { + showHtmlFile(player, npcId + "-NS.htm", npc, mem); + return; + } + if (mem.getInventory().getItemByItemId(ENTRANCE_PASS) == null) { + showHtmlFile(player, npcId + "-SE.htm", npc, mem); + return; + } + + if (player.getWeightPenalty() >= 3) { + mem.sendPacket(SystemMessageId.INVENTORY_LESS_THAN_80_PERCENT); + return; + } + } + } else { + QuestState qs = player.getQuestState(hostQuest.getName()); + if ((qs == null) || (!qs.isStarted() && !qs.isCompleted())) { + showHtmlFile(player, npcId + "-NS.htm", npc, player); + return; + } + if (player.getInventory().getItemByItemId(ENTRANCE_PASS) == null) { + showHtmlFile(player, npcId + "-SE.htm", npc, player); + return; + } + + if (player.getWeightPenalty() >= 3) { + player.sendPacket(SystemMessageId.INVENTORY_LESS_THAN_80_PERCENT); + return; + } + } + + if (!isEntryTime()) { + showHtmlFile(player, npcId + "-NE.htm", npc, null); + return; + } + + showHtmlFile(player, npcId + "-OK.htm", npc, null); + + entry(npcId, player); + } + + private void entry(int npcId, L2PcInstance player) { + int[] Location = _startHallSpawns.get(npcId); + int driftx; + int drifty; + + if (general().getNumberOfNecessaryPartyMembers() > 1) { + final List members = new LinkedList<>(); + for (L2PcInstance mem : player.getParty().getMembers()) { + if (!mem.isDead() && Util.checkIfInRange(700, player, mem, true)) { + members.add(mem); + } + } + + for (L2PcInstance mem : members) { + GrandBossManager.getInstance().getZone(Location[0], Location[1], Location[2]).allowPlayerEntry(mem, 30); + driftx = Rnd.get(-80, 80); + drifty = Rnd.get(-80, 80); + mem.teleToLocation(Location[0] + driftx, Location[1] + drifty, Location[2]); + mem.destroyItemByItemId("Quest", ENTRANCE_PASS, 1, mem, true); + if (mem.getInventory().getItemByItemId(ANTIQUE_BROOCH) == null) { + mem.addItem("Quest", USED_PASS, 1, mem, true); + } + + L2ItemInstance hallsKey = mem.getInventory().getItemByItemId(CHAPEL_KEY); + if (hallsKey != null) { + mem.destroyItemByItemId("Quest", CHAPEL_KEY, hallsKey.getCount(), mem, true); + } + } + + _challengers.put(npcId, player); + + _hallInUse.put(npcId, true); + } + if ((general().getNumberOfNecessaryPartyMembers() <= 1) && player.isInParty()) { + final List members = new LinkedList<>(); + for (L2PcInstance mem : player.getParty().getMembers()) { + if (!mem.isDead() && Util.checkIfInRange(700, player, mem, true)) { + members.add(mem); + } + } + + for (L2PcInstance mem : members) { + GrandBossManager.getInstance().getZone(Location[0], Location[1], Location[2]).allowPlayerEntry(mem, 30); + driftx = Rnd.get(-80, 80); + drifty = Rnd.get(-80, 80); + mem.teleToLocation(Location[0] + driftx, Location[1] + drifty, Location[2]); + mem.destroyItemByItemId("Quest", ENTRANCE_PASS, 1, mem, true); + if (mem.getInventory().getItemByItemId(ANTIQUE_BROOCH) == null) { + mem.addItem("Quest", USED_PASS, 1, mem, true); + } + + L2ItemInstance hallsKey = mem.getInventory().getItemByItemId(CHAPEL_KEY); + if (hallsKey != null) { + mem.destroyItemByItemId("Quest", CHAPEL_KEY, hallsKey.getCount(), mem, true); + } + } + + _challengers.put(npcId, player); + + _hallInUse.put(npcId, true); + } else { + GrandBossManager.getInstance().getZone(Location[0], Location[1], Location[2]).allowPlayerEntry(player, 30); + driftx = Rnd.get(-80, 80); + drifty = Rnd.get(-80, 80); + player.teleToLocation(Location[0] + driftx, Location[1] + drifty, Location[2]); + player.destroyItemByItemId("Quest", ENTRANCE_PASS, 1, player, true); + if (player.getInventory().getItemByItemId(ANTIQUE_BROOCH) == null) { + player.addItem("Quest", USED_PASS, 1, player, true); + } + + L2ItemInstance hallsKey = player.getInventory().getItemByItemId(CHAPEL_KEY); + if (hallsKey != null) { + player.destroyItemByItemId("Quest", CHAPEL_KEY, hallsKey.getCount(), player, true); + } + + _challengers.put(npcId, player); + + _hallInUse.put(npcId, true); + } + } + + public void spawnMysteriousBox(int npcId) { + if (!isAttackTime()) { + return; + } + + L2Spawn spawnDat = _mysteriousBoxSpawns.get(npcId); + if (spawnDat != null) { + _allMobs.add(spawnDat.doSpawn()); + spawnDat.stopRespawn(); + } + } + + public void spawnMonster(int npcId) { + if (!isAttackTime()) { + return; + } + + final List mobs = new CopyOnWriteArrayList<>(); + final List monsterList; + if (Rnd.get(2) == 0) { + monsterList = _physicalMonsters.get(npcId); + } else { + monsterList = _magicalMonsters.get(npcId); + } + + if (monsterList != null) { + boolean spawnKeyBoxMob = false; + boolean spawnedKeyBoxMob = false; + + for (L2Spawn spawnDat : monsterList) { + if (spawnedKeyBoxMob) { + spawnKeyBoxMob = false; + } else { + switch (npcId) { + case 31469: + case 31474: + case 31479: + case 31484: + if (Rnd.get(48) == 0) { + spawnKeyBoxMob = true; + } + break; + default: + spawnKeyBoxMob = false; + } + } + + L2SepulcherMonsterInstance mob = null; + + if (spawnKeyBoxMob) { + try { + final L2Spawn keyBoxMobSpawn = new L2Spawn(18149); + keyBoxMobSpawn.setAmount(1); + keyBoxMobSpawn.setLocation(spawnDat.getLocation()); + keyBoxMobSpawn.setRespawnDelay(3600); + SpawnTable.getInstance().addNewSpawn(keyBoxMobSpawn, false); + mob = (L2SepulcherMonsterInstance) keyBoxMobSpawn.doSpawn(); + keyBoxMobSpawn.stopRespawn(); + } catch (Exception ex) { + LOG.warn("Monster spawns could not be initialized!", ex); + } + + spawnedKeyBoxMob = true; + } else { + mob = (L2SepulcherMonsterInstance) spawnDat.doSpawn(); + spawnDat.stopRespawn(); + } + + if (mob != null) { + mob.mysteriousBoxId = npcId; + switch (npcId) { + case 31469, 31474, 31479, 31484, 31472, 31477, 31482, 31487 -> mobs.add(mob); + } + _allMobs.add(mob); + } + } + + switch (npcId) { + case 31469, 31474, 31479, 31484 -> _viscountMobs.put(npcId, mobs); + case 31472, 31477, 31482, 31487 -> _dukeMobs.put(npcId, mobs); + } + } + } + + public synchronized boolean isViscountMobsAnnihilated(int npcId) { + List mobs = _viscountMobs.get(npcId); + if (mobs == null) { + return true; + } + + for (L2SepulcherMonsterInstance mob : mobs) { + if (!mob.isDead()) { + return false; + } + } + + return true; + } + + public synchronized boolean isDukeMobsAnnihilated(int npcId) { + List mobs = _dukeMobs.get(npcId); + if (mobs == null) { + return true; + } + + for (L2SepulcherMonsterInstance mob : mobs) { + if (!mob.isDead()) { + return false; + } + } + + return true; + } + + public void spawnKeyBox(L2Npc activeChar) { + if (!isAttackTime()) { + return; + } + + L2Spawn spawnDat = _keyBoxSpawns.get(activeChar.getId()); + if (spawnDat != null) { + spawnDat.setAmount(1); + spawnDat.setX(activeChar.getX()); + spawnDat.setY(activeChar.getY()); + spawnDat.setZ(activeChar.getZ()); + spawnDat.setHeading(activeChar.getHeading()); + spawnDat.setRespawnDelay(3600); + _allMobs.add(spawnDat.doSpawn()); + spawnDat.stopRespawn(); + } + } + + public void spawnExecutionerOfHalisha(L2Npc activeChar) { + if (!isAttackTime()) { + return; + } + + L2Spawn spawnDat = _executionerSpawns.get(activeChar.getId()); + if (spawnDat != null) { + spawnDat.setAmount(1); + spawnDat.setX(activeChar.getX()); + spawnDat.setY(activeChar.getY()); + spawnDat.setZ(activeChar.getZ()); + spawnDat.setHeading(activeChar.getHeading()); + spawnDat.setRespawnDelay(3600); + _allMobs.add(spawnDat.doSpawn()); + spawnDat.stopRespawn(); + } + } + + public void spawnArchonOfHalisha(int npcId) { + if (!isAttackTime()) { + return; + } + + if (_archonSpawned.get(npcId)) { + return; + } + + List monsterList = _dukeFinalMobs.get(npcId); + if (monsterList != null) { + for (L2Spawn spawnDat : monsterList) { + L2SepulcherMonsterInstance mob = (L2SepulcherMonsterInstance) spawnDat.doSpawn(); + spawnDat.stopRespawn(); + + if (mob != null) { + mob.mysteriousBoxId = npcId; + _allMobs.add(mob); + } + } + _archonSpawned.put(npcId, true); + } + } + + public void spawnEmperorsGraveNpc(int npcId) { + if (!isAttackTime()) { + return; + } + + List monsterList = _emperorsGraveNpcs.get(npcId); + if (monsterList != null) { + for (L2Spawn spawnDat : monsterList) { + _allMobs.add(spawnDat.doSpawn()); + spawnDat.stopRespawn(); + } + } + } + + public void locationShadowSpawns() { + int locNo = Rnd.get(4); + final int[] gateKeeper = { + 31929, + 31934, + 31939, + 31944 + }; + + for (int i = 0; i <= 3; i++) { + int keyNpcId = gateKeeper[i]; + L2Spawn spawnDat = _shadowSpawns.get(keyNpcId); + spawnDat.setX(_shadowSpawnLoc[locNo][i][1]); + spawnDat.setY(_shadowSpawnLoc[locNo][i][2]); + spawnDat.setZ(_shadowSpawnLoc[locNo][i][3]); + spawnDat.setHeading(_shadowSpawnLoc[locNo][i][4]); + _shadowSpawns.put(keyNpcId, spawnDat); + } + } + + public void spawnShadow(int npcId) { + if (!isAttackTime()) { + return; + } + + L2Spawn spawnDat = _shadowSpawns.get(npcId); + if (spawnDat != null) { + L2SepulcherMonsterInstance mob = (L2SepulcherMonsterInstance) spawnDat.doSpawn(); + spawnDat.stopRespawn(); + + if (mob != null) { + mob.mysteriousBoxId = npcId; + _allMobs.add(mob); + } + } + } + + public void deleteAllMobs() { + for (L2Npc mob : _allMobs) { + try { + if (mob.getSpawn() != null) { + mob.getSpawn().stopRespawn(); + } + mob.deleteMe(); + } catch (Exception ex) { + LOG.warn("Failed deleting mob!", ex); + } + } + _allMobs.clear(); + } + + protected void closeAllDoors() { + for (int doorId : _hallGateKeepers.values()) { + try { + L2DoorInstance door = DoorData.getInstance().getDoor(doorId); + if (door != null) { + door.closeMe(); + } else { + LOG.warn("Attempted to close undefined door Id {}!", doorId); + } + } catch (Exception ex) { + LOG.warn("Failed closing door Id {}!", doorId, ex); + } + } + } + + protected byte minuteSelect(byte min) { + if (((double) min % 5) != 0) // if doesn't divides on 5 fully + { + // mad table for selecting proper minutes... + // may be there is a better way to do this + switch (min) { + case 6, 7 -> min = 5; + case 8, 9, 11, 12 -> min = 10; + case 13, 14, 16, 17 -> min = 15; + case 18, 19, 21, 22 -> min = 20; + case 23, 24, 26, 27 -> min = 25; + case 28, 29, 31, 32 -> min = 30; + case 33, 34, 36, 37 -> min = 35; + case 38, 39, 41, 42 -> min = 40; + case 43, 44, 46, 47 -> min = 45; + case 48, 49, 51, 52 -> min = 50; + case 53, 54, 56, 57 -> min = 55; + } + } + return min; + } + + public void managerSay(byte min) { + // for attack phase, sending message every 5 minutes + if (_inAttackTime) { + if (min < 5) { + return; // do not shout when < 5 minutes + } + + min = minuteSelect(min); + + NpcStringId msg = NpcStringId.MINUTES_HAVE_PASSED; + + if (min == 90) { + msg = NpcStringId.GAME_OVER_THE_TELEPORT_WILL_APPEAR_MOMENTARILY; + } + + for (L2Spawn temp : _managers) { + if (temp == null) { + LOG.warn("Manager is null on attack time!"); + continue; + } + + if (!(temp.getLastSpawn() instanceof L2SepulcherNpcInstance)) { + LOG.warn("Manager is not Sepulcher instance on attack time!"); + continue; + } + + // hall not used right now, so its manager will not tell you anything :) + // if you don't need this - delete next two lines. + if (!_hallInUse.get(temp.getId())) { + continue; + } + + ((L2SepulcherNpcInstance) temp.getLastSpawn()).sayInShout(msg); + } + } + + else if (_inEntryTime) { + NpcStringId msg1 = NpcStringId.YOU_MAY_NOW_ENTER_THE_SEPULCHER; + NpcStringId msg2 = NpcStringId.IF_YOU_PLACE_YOUR_HAND_ON_THE_STONE_STATUE_IN_FRONT_OF_EACH_SEPULCHER_YOU_WILL_BE_ABLE_TO_ENTER; + for (L2Spawn temp : _managers) { + if (temp == null) { + LOG.warn("Manager is null on entry time!"); + continue; + } + if (!(temp.getLastSpawn() instanceof L2SepulcherNpcInstance)) { + LOG.warn("Manager is not Sepulcher instance on entry time!"); + continue; + } + ((L2SepulcherNpcInstance) temp.getLastSpawn()).sayInShout(msg1); + ((L2SepulcherNpcInstance) temp.getLastSpawn()).sayInShout(msg2); + } + } + } + + public Map getHallGateKeepers() { + return _hallGateKeepers; + } + + public void showHtmlFile(L2PcInstance player, String file, L2Npc npc, L2PcInstance member) { + final NpcHtmlMessage html = new NpcHtmlMessage(npc.getObjectId()); + html.setFile(player.getHtmlPrefix(), "data/html/SepulcherNpc/" + file); + if (member != null) { + html.replace("%member%", member.getName()); + } + player.sendPacket(html); + } + + public static FourSepulchersManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final FourSepulchersManager _instance = new FourSepulchersManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/GlobalVariablesManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/GlobalVariablesManager.java new file mode 100644 index 00000000..2eedaa03 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/GlobalVariablesManager.java @@ -0,0 +1,103 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.Map.Entry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.variables.AbstractVariables; + +/** + * Global Variables Manager. + * @author xban1x + */ +public final class GlobalVariablesManager extends AbstractVariables { + + private static final Logger LOG = LoggerFactory.getLogger(GlobalVariablesManager.class); + + private static final String SELECT_QUERY = "SELECT * FROM global_variables"; + + private static final String DELETE_QUERY = "DELETE FROM global_variables"; + + private static final String INSERT_QUERY = "INSERT INTO global_variables (var, value) VALUES (?, ?)"; + + protected GlobalVariablesManager() { + restoreMe(); + } + + @Override + public boolean restoreMe() { + // Restore previous variables. + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery(SELECT_QUERY)) { + while (rs.next()) { + set(rs.getString("var"), rs.getString("value")); + } + } catch (Exception ex) { + LOG.warn("Couldn't restore global variables!", ex); + return false; + } finally { + compareAndSetChanges(true, false); + } + LOG.info("Loaded {} variables.", getSet().size()); + return true; + } + + @Override + public boolean storeMe() { + // No changes, nothing to store. + if (!hasChanges()) { + return false; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var del = con.createStatement(); + var st = con.prepareStatement(INSERT_QUERY)) { + // Clear previous entries. + del.execute(DELETE_QUERY); + + // Insert all variables. + for (Entry entry : getSet().entrySet()) { + st.setString(1, entry.getKey()); + st.setString(2, String.valueOf(entry.getValue())); + st.addBatch(); + } + st.executeBatch(); + } catch (Exception ex) { + LOG.warn("Couldn't save global variables to database!", ex); + return false; + } finally { + compareAndSetChanges(true, false); + } + LOG.info("Stored {} variables.", getSet().size()); + return true; + } + + public static GlobalVariablesManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final GlobalVariablesManager _instance = new GlobalVariablesManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/GraciaSeedsManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/GraciaSeedsManager.java new file mode 100644 index 00000000..e92f6647 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/GraciaSeedsManager.java @@ -0,0 +1,248 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.graciaSeeds; + +import java.util.Calendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.tasks.UpdateSoDStateTask; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.quest.Quest; + +public final class GraciaSeedsManager { + + private static final Logger LOG = LoggerFactory.getLogger(GraciaSeedsManager.class); + + private static final String ENERGY_SEEDS = "EnergySeeds"; + private static final String SOD_DEFENCE = "Defence"; + + private static final byte SOITYPE = 2; + + private static final byte SOATYPE = 3; + + // Seed of Destruction + private static final int EDRIC = 32527; + private static final Location EDRIC_SPAWN_LOCATION = new Location(-248525, 250048, 4307, 24576); + private static final byte SODTYPE = 1; + private L2Npc edricSpawn = null; + private int _SoDTiatKilled = 0; + private int _SoDState = 1; + private final Calendar _SoDLastStateChangeDate; + + private GraciaSeedsManager() { + _SoDLastStateChangeDate = Calendar.getInstance(); + loadData(); + handleSodStages(); + } + + private void saveData(byte seedType) { + switch (seedType) { + case SODTYPE: + // Seed of Destruction + GlobalVariablesManager.getInstance().set("SoDState", _SoDState); + GlobalVariablesManager.getInstance().set("SoDTiatKilled", _SoDTiatKilled); + GlobalVariablesManager.getInstance().set("SoDLSCDate", _SoDLastStateChangeDate.getTimeInMillis()); + break; + case SOITYPE: + // Seed of Infinity + break; + case SOATYPE: + // Seed of Annihilation + break; + default: + LOG.warn("Unknown SeedType in SaveData: {}", seedType); + break; + } + } + + private void loadData() { + // Seed of Destruction variables + if (GlobalVariablesManager.getInstance().hasVariable("SoDState")) { + _SoDState = GlobalVariablesManager.getInstance().getInt("SoDState"); + _SoDTiatKilled = GlobalVariablesManager.getInstance().getInt("SoDTiatKilled", _SoDTiatKilled); + _SoDLastStateChangeDate.setTimeInMillis(GlobalVariablesManager.getInstance().getLong("SoDLSCDate")); + } else { + // save Initial values + saveData(SODTYPE); + } + } + + private void handleSodStages() { + switch (_SoDState) { + case 1: + // Despawn Edric(Remnant Manager) and do nothing else, players should kill Tiat a few times + despawnSoDRemnantManager(); + break; + case 2: + // Conquest Complete state, if too much time is passed than change to defense state + long timePast = System.currentTimeMillis() - _SoDLastStateChangeDate.getTimeInMillis(); + if (timePast >= graciaSeeds().getStage2Length()) { + // change to Defend state + setSoDState(5, true, false); + } else { + // Schedule change to Defend state + ThreadPoolManager.getInstance().scheduleEffect(new UpdateSoDStateTask(), graciaSeeds().getStage2Length() - timePast); + } + // Spawn Edric(Remnant Manager) for solo instances; + spawnSoDRemnantManager(); + break; + case 3, 4, 5: + // Spawn Edric(Remnant Manager) else is handled by datapack + spawnSoDRemnantManager(); + break; + default: + LOG.warn("Unknown Seed of Destruction state({})! ", _SoDState); + } + } + + private void updateSoDDefence(int state) { + if ((state >= 3) && (state <= 5)) { + final Quest quest = QuestManager.getInstance().getQuest(SOD_DEFENCE); + if (quest == null) { + LOG.warn(": missing Defence Quest!"); + } else { + quest.notifyEvent("start", null, null); + } + } else { + LOG.warn("Invalid Seed of Destruction defence state({}), should be 3, 4 or 5", state); + } + } + + private void stopSoDInvasion() { + final Quest defQuest = QuestManager.getInstance().getQuest(SOD_DEFENCE); + if (defQuest == null) { + LOG.warn("Missing Defence Quest!"); + } else { + defQuest.notifyEvent("stop", null, null); + } + } + + public void updateSodState() { + final Quest esQuest = QuestManager.getInstance().getQuest(ENERGY_SEEDS); + if (esQuest == null) { + LOG.warn("Missing EnergySeeds Quest!"); + } else { + esQuest.notifyEvent("StopSoDAi", null, null); + stopSoDInvasion(); + } + } + + public void increaseSoDTiatKilled() { + if (_SoDState == 1) { + _SoDTiatKilled++; + if (_SoDTiatKilled >= graciaSeeds().getTiatKillCountForNextState()) { + setSoDState(2, false, true); + } + saveData(SODTYPE); + } + } + + private void setSoDOpenState() { + Quest esQuest = QuestManager.getInstance().getQuest(ENERGY_SEEDS); + if (esQuest == null) { + LOG.warn(getClass().getSimpleName() + ": missing EnergySeeds Quest!"); + } else { + esQuest.notifyEvent("StartSoDAi", null, null); + stopSoDInvasion(); + } + } + + private void spawnSoDRemnantManager() { + try { + if ((edricSpawn == null) || edricSpawn.isDecayed()) { + final L2Spawn spawn = new L2Spawn(EDRIC); + spawn.setInstanceId(0); + spawn.setLocation(EDRIC_SPAWN_LOCATION); + spawn.stopRespawn(); + edricSpawn = spawn.spawnOne(false); + } + } catch (Exception e) { + LOG.warn("Could not spawn NPC Edric #" + EDRIC + "; error: {}", e.getMessage()); + } + } + + private void despawnSoDRemnantManager() { + if (edricSpawn != null) { + edricSpawn.deleteMe(); + } + } + + public int getSoDTiatKilled() { + return _SoDTiatKilled; + } + + public void setSoDState(int value, boolean doSave, boolean updateDate) { + LOG.info("New Seed of Destruction state -> {}.", value); + if (updateDate) { + _SoDLastStateChangeDate.setTimeInMillis(System.currentTimeMillis()); + } + _SoDState = value; + + if (_SoDState == 1) { + // reset number of Tiat kills + _SoDTiatKilled = 0; + updateSodState(); + } else if (_SoDState == 2) { + setSoDOpenState(); + } else if (_SoDState > 2) { + updateSoDDefence(_SoDState); + } + + handleSodStages(); + + if (doSave) { + saveData(SODTYPE); + } + } + + public long getSoDTimeForNextStateChange() { + // this should not happen! + return switch (_SoDState) { + case 2 -> ((_SoDLastStateChangeDate.getTimeInMillis() + graciaSeeds().getStage2Length()) - System.currentTimeMillis()); + default -> -1; + }; + } + + public Calendar getSoDLastStateChangeDate() { + return _SoDLastStateChangeDate; + } + + public int getSoDState() { + return _SoDState; + } + + /** + * Gets the single instance of {@code GraciaSeedsManager}. + * @return single instance of {@code GraciaSeedsManager} + */ + public static GraciaSeedsManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final GraciaSeedsManager _instance = new GraciaSeedsManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/GrandBossManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/GrandBossManager.java new file mode 100644 index 00000000..4604b39c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/GrandBossManager.java @@ -0,0 +1,319 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.instancemanager.tasks.GrandBossManagerStoreTask; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2GrandBossInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.IStorable; +import com.l2jserver.gameserver.model.zone.type.L2BossZone; + +/** + * Grand Boss manager. + * @author DaRkRaGe + * @author Emperorc + */ +public final class GrandBossManager implements IStorable { + + private static final Logger LOG = LoggerFactory.getLogger(GrandBossManager.class); + + private static final String DELETE_GRAND_BOSS_LIST = "DELETE FROM grandboss_list"; + + private static final String INSERT_GRAND_BOSS_LIST = "INSERT INTO grandboss_list (player_id,zone) VALUES (?,?)"; + + private static final String UPDATE_GRAND_BOSS_DATA = "UPDATE grandboss_data set loc_x = ?, loc_y = ?, loc_z = ?, heading = ?, respawn_time = ?, currentHP = ?, currentMP = ?, status = ? where boss_id = ?"; + + private static final String UPDATE_GRAND_BOSS_DATA2 = "UPDATE grandboss_data set status = ? where boss_id = ?"; + + protected static final Map BOSSES = new ConcurrentHashMap<>(); + + protected static Map _storedInfo = new HashMap<>(); + + private final Map _bossStatus = new ConcurrentHashMap<>(); + + private final Map _zones = new ConcurrentHashMap<>(); + + protected GrandBossManager() { + init(); + } + + private void init() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * from grandboss_data ORDER BY boss_id")) { + while (rs.next()) { + // Read all info from DB, and store it for AI to read and decide what to do + // faster than accessing DB in real time + StatsSet info = new StatsSet(); + int bossId = rs.getInt("boss_id"); + info.set("loc_x", rs.getInt("loc_x")); + info.set("loc_y", rs.getInt("loc_y")); + info.set("loc_z", rs.getInt("loc_z")); + info.set("heading", rs.getInt("heading")); + info.set("respawn_time", rs.getLong("respawn_time")); + double HP = rs.getDouble("currentHP"); // jython doesn't recognize doubles + int true_HP = (int) HP; // so use java's ability to type cast + info.set("currentHP", true_HP); // to convert double to int + double MP = rs.getDouble("currentMP"); + int true_MP = (int) MP; + info.set("currentMP", true_MP); + int status = rs.getInt("status"); + _bossStatus.put(bossId, status); + _storedInfo.put(bossId, info); + LOG.info("{} ({}) status is {}.", NpcData.getInstance().getTemplate(bossId).getName(), bossId, status); + if (status > 0) { + LOG.info("Next spawn date of {} is {}.", NpcData.getInstance().getTemplate(bossId).getName(), new Date(info.getLong("respawn_time"))); + } + } + LOG.info("Loaded {} grand boss instances.", _storedInfo.size()); + } catch (Exception ex) { + LOG.warn("There has been an error while initializing GrandBossManager!", ex); + } + ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new GrandBossManagerStoreTask(), 5 * 60 * 1000, 5 * 60 * 1000); + } + + public void initZones() { + final Map> zones = new HashMap<>(); + for (Integer zoneId : _zones.keySet()) { + zones.put(zoneId, new ArrayList<>()); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * from grandboss_list ORDER BY player_id")) { + while (rs.next()) { + int id = rs.getInt("player_id"); + int zoneId = rs.getInt("zone"); + zones.get(zoneId).add(id); + } + LOG.info("Initialized {} grand boss zones.", _zones.size()); + } catch (Exception ex) { + LOG.warn("There has been an error while initializing GrandBoss zones!", ex); + } + + for (Entry e : _zones.entrySet()) { + e.getValue().setAllowedPlayers(zones.get(e.getKey())); + } + + zones.clear(); + } + + public void addZone(L2BossZone zone) { + _zones.put(zone.getId(), zone); + } + + public L2BossZone getZone(int zoneId) { + return _zones.get(zoneId); + } + + public L2BossZone getZone(L2Character character) { + return _zones.values().stream().filter(z -> z.isCharacterInZone(character)).findFirst().orElse(null); + } + + public L2BossZone getZone(Location loc) { + return getZone(loc.getX(), loc.getY(), loc.getZ()); + } + + public L2BossZone getZone(int x, int y, int z) { + return _zones.values().stream().filter(zone -> zone.isInsideZone(x, y, z)).findFirst().orElse(null); + } + + public boolean checkIfInZone(String zoneType, L2Object obj) { + final L2BossZone temp = getZone(obj.getX(), obj.getY(), obj.getZ()); + return (temp != null) && temp.getName().equalsIgnoreCase(zoneType); + } + + public boolean checkIfInZone(L2PcInstance player) { + return (player != null) && (getZone(player.getX(), player.getY(), player.getZ()) != null); + } + + public int getBossStatus(int bossId) { + return _bossStatus.get(bossId); + } + + public void setBossStatus(int bossId, int status) { + _bossStatus.put(bossId, status); + LOG.info("Updated {} ({}) status to {}.", NpcData.getInstance().getTemplate(bossId).getName(), bossId, status); + updateDb(bossId, true); + } + + /** + * Adds a L2GrandBossInstance to the list of bosses. + * @param boss + */ + public void addBoss(L2GrandBossInstance boss) { + if (boss != null) { + BOSSES.put(boss.getId(), boss); + } + } + + public L2GrandBossInstance getBoss(int bossId) { + return BOSSES.get(bossId); + } + + public StatsSet getStatsSet(int bossId) { + return _storedInfo.get(bossId); + } + + public void setStatsSet(int bossId, StatsSet info) { + _storedInfo.put(bossId, info); + updateDb(bossId, false); + } + + @Override + public boolean storeMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate(DELETE_GRAND_BOSS_LIST); + + try (var insert = con.prepareStatement(INSERT_GRAND_BOSS_LIST)) { + for (Entry e : _zones.entrySet()) { + List list = e.getValue().getAllowedPlayers(); + if ((list == null) || list.isEmpty()) { + continue; + } + for (Integer player : list) { + insert.setInt(1, player); + insert.setInt(2, e.getKey()); + insert.executeUpdate(); + insert.clearParameters(); + } + } + } + for (Entry e : _storedInfo.entrySet()) { + final L2GrandBossInstance boss = BOSSES.get(e.getKey()); + StatsSet info = e.getValue(); + if ((boss == null) || (info == null)) { + try (var update = con.prepareStatement(UPDATE_GRAND_BOSS_DATA2)) { + update.setInt(1, _bossStatus.get(e.getKey())); + update.setInt(2, e.getKey()); + update.executeUpdate(); + update.clearParameters(); + } + } else { + try (var update = con.prepareStatement(UPDATE_GRAND_BOSS_DATA)) { + update.setInt(1, boss.getX()); + update.setInt(2, boss.getY()); + update.setInt(3, boss.getZ()); + update.setInt(4, boss.getHeading()); + update.setLong(5, info.getLong("respawn_time")); + double hp = boss.getCurrentHp(); + double mp = boss.getCurrentMp(); + if (boss.isDead()) { + hp = boss.getMaxHp(); + mp = boss.getMaxMp(); + } + update.setDouble(6, hp); + update.setDouble(7, mp); + update.setInt(8, _bossStatus.get(e.getKey())); + update.setInt(9, e.getKey()); + update.executeUpdate(); + update.clearParameters(); + } + } + } + } catch (Exception ex) { + LOG.warn("Could not store grand bosses to database!", ex); + return false; + } + return true; + } + + private void updateDb(int bossId, boolean statusOnly) { + try (var con = ConnectionFactory.getInstance().getConnection()) { + L2GrandBossInstance boss = BOSSES.get(bossId); + StatsSet info = _storedInfo.get(bossId); + + if (statusOnly || (boss == null) || (info == null)) { + try (var ps = con.prepareStatement(UPDATE_GRAND_BOSS_DATA2)) { + ps.setInt(1, _bossStatus.get(bossId)); + ps.setInt(2, bossId); + ps.executeUpdate(); + } + } else { + try (var ps = con.prepareStatement(UPDATE_GRAND_BOSS_DATA)) { + ps.setInt(1, boss.getX()); + ps.setInt(2, boss.getY()); + ps.setInt(3, boss.getZ()); + ps.setInt(4, boss.getHeading()); + ps.setLong(5, info.getLong("respawn_time")); + double hp = boss.getCurrentHp(); + double mp = boss.getCurrentMp(); + if (boss.isDead()) { + hp = boss.getMaxHp(); + mp = boss.getMaxMp(); + } + ps.setDouble(6, hp); + ps.setDouble(7, mp); + ps.setInt(8, _bossStatus.get(bossId)); + ps.setInt(9, bossId); + ps.executeUpdate(); + } + } + } catch (Exception ex) { + LOG.warn("Could not update grand bosses to database!", ex); + } + } + + /** + * Saves all Grand Boss info and then clears all info from memory, including all schedules. + */ + public void cleanUp() { + storeMe(); + + BOSSES.clear(); + _storedInfo.clear(); + _bossStatus.clear(); + _zones.clear(); + } + + public Map getZones() { + return _zones; + } + + /** + * Gets the single instance of {@code GrandBossManager}. + * @return single instance of {@code GrandBossManager} + */ + public static GrandBossManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final GrandBossManager _instance = new GrandBossManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/HandysBlockCheckerManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/HandysBlockCheckerManager.java new file mode 100644 index 00000000..49ca0a04 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/HandysBlockCheckerManager.java @@ -0,0 +1,337 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.instancemanager.tasks.PenaltyRemoveTask; +import com.l2jserver.gameserver.model.ArenaParticipantsHolder; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.PcInventory; +import com.l2jserver.gameserver.model.olympiad.OlympiadManager; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameAddPlayer; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameChangeTeam; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameRemovePlayer; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * This class manage the player add/remove, team change and event arena status, as the clearance of the participants list or liberate the arena. + * @author BiggBoss + */ +public final class HandysBlockCheckerManager { + // All the participants and their team classified by arena + private static final ArenaParticipantsHolder[] _arenaPlayers = new ArenaParticipantsHolder[4]; + + // Arena votes to start the game + private static final Map _arenaVotes = new HashMap<>(); + + // Arena Status, True = is being used, otherwise, False + private static final Map _arenaStatus = new HashMap<>(); + + // Registration request penalty (10 seconds) + protected static Set _registrationPenalty = Collections.synchronizedSet(new HashSet<>()); + + /** + * Return the number of event-start votes for the specified arena id + * @param arenaId + * @return int (number of votes) + */ + public synchronized int getArenaVotes(int arenaId) { + return _arenaVotes.get(arenaId); + } + + /** + * Add a new vote to start the event for the specified arena id + * @param arena + */ + public synchronized void increaseArenaVotes(int arena) { + int newVotes = _arenaVotes.get(arena) + 1; + ArenaParticipantsHolder holder = _arenaPlayers[arena]; + + if ((newVotes > (holder.getAllPlayers().size() / 2)) && !holder.getEvent().isStarted()) { + clearArenaVotes(arena); + if ((holder.getBlueTeamSize() == 0) || (holder.getRedTeamSize() == 0)) { + return; + } + if (general().isHBCEFairPlay()) { + holder.checkAndShuffle(); + } + ThreadPoolManager.getInstance().executeGeneral(holder.getEvent().new StartEvent()); + } else { + _arenaVotes.put(arena, newVotes); + } + } + + /** + * Will clear the votes queue (of event start) for the specified arena id + * @param arena + */ + public synchronized void clearArenaVotes(int arena) { + _arenaVotes.put(arena, 0); + } + + protected HandysBlockCheckerManager() { + // Initialize arena status + _arenaStatus.put(0, false); + _arenaStatus.put(1, false); + _arenaStatus.put(2, false); + _arenaStatus.put(3, false); + + // Initialize arena votes + _arenaVotes.put(0, 0); + _arenaVotes.put(1, 0); + _arenaVotes.put(2, 0); + _arenaVotes.put(3, 0); + } + + /** + * Returns the players holder + * @param arena + * @return ArenaParticipantsHolder + */ + public ArenaParticipantsHolder getHolder(int arena) { + return _arenaPlayers[arena]; + } + + /** + * Initializes the participants holder + */ + public void startUpParticipantsQueue() { + for (int i = 0; i < 4; ++i) { + _arenaPlayers[i] = new ArenaParticipantsHolder(i); + } + } + + /** + * Add the player to the specified arena (through the specified arena manager) and send the needed server -> client packets + * @param player + * @param arenaId + * @return + */ + public boolean addPlayerToArena(L2PcInstance player, int arenaId) { + ArenaParticipantsHolder holder = _arenaPlayers[arenaId]; + + synchronized (holder) { + boolean isRed; + + for (int i = 0; i < 4; i++) { + if (_arenaPlayers[i].getAllPlayers().contains(player)) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_IS_ALREADY_REGISTERED_ON_THE_MATCH_WAITING_LIST); + msg.addCharName(player); + player.sendPacket(msg); + return false; + } + } + + if (player.isCursedWeaponEquipped()) { + player.sendPacket(SystemMessageId.CANNOT_REGISTER_PROCESSING_CURSED_WEAPON); + return false; + } + + if (player.isOnEvent() || player.isInOlympiadMode()) { + player.sendMessage("Couldnt register you due other event participation"); + return false; + } + + if (OlympiadManager.getInstance().isRegistered(player)) { + OlympiadManager.getInstance().unRegisterNoble(player); + player.sendPacket(SystemMessageId.COLISEUM_OLYMPIAD_KRATEIS_APPLICANTS_CANNOT_PARTICIPATE); + } + + // if(UnderGroundColiseum.getInstance().isRegisteredPlayer(player)) + // { + // UngerGroundColiseum.getInstance().removeParticipant(player); + // player.sendPacket(SystemMessageId.COLISEUM_OLYMPIAD_KRATEIS_APPLICANTS_CANNOT_PARTICIPATE)); + // } + // if(KrateiCubeManager.getInstance().isRegisteredPlayer(player)) + // { + // KrateiCubeManager.getInstance().removeParticipant(player); + // player.sendPacket(SystemMessageId.COLISEUM_OLYMPIAD_KRATEIS_APPLICANTS_CANNOT_PARTICIPATE)); + // } + + if (_registrationPenalty.contains(player.getObjectId())) { + player.sendPacket(SystemMessageId.CANNOT_REQUEST_REGISTRATION_10_SECS_AFTER); + return false; + } + + if (holder.getBlueTeamSize() < holder.getRedTeamSize()) { + holder.addPlayer(player, 1); + isRed = false; + } else { + holder.addPlayer(player, 0); + isRed = true; + } + holder.broadCastPacketToTeam(new ExCubeGameAddPlayer(player, isRed)); + return true; + } + } + + /** + * Will remove the specified player from the specified team and arena and will send the needed packet to all his team mates / enemy team mates + * @param player + * @param arenaId + * @param team + */ + public void removePlayer(L2PcInstance player, int arenaId, int team) { + ArenaParticipantsHolder holder = _arenaPlayers[arenaId]; + synchronized (holder) { + boolean isRed = team == 0; + + holder.removePlayer(player, team); + holder.broadCastPacketToTeam(new ExCubeGameRemovePlayer(player, isRed)); + + // End event if theres an empty team + int teamSize = isRed ? holder.getRedTeamSize() : holder.getBlueTeamSize(); + if (teamSize == 0) { + holder.getEvent().endEventAbnormally(); + } + + _registrationPenalty.add(player.getObjectId()); + schedulePenaltyRemoval(player.getObjectId()); + } + } + + /** + * Will change the player from one team to other (if possible) and will send the needed packets + * @param player + * @param arena + * @param team + */ + public void changePlayerToTeam(L2PcInstance player, int arena, int team) { + ArenaParticipantsHolder holder = _arenaPlayers[arena]; + + synchronized (holder) { + boolean isFromRed = holder.getRedPlayers().contains(player); + + if (isFromRed && (holder.getBlueTeamSize() == 6)) { + player.sendMessage("The team is full"); + return; + } else if (!isFromRed && (holder.getRedTeamSize() == 6)) { + player.sendMessage("The team is full"); + return; + } + + int futureTeam = isFromRed ? 1 : 0; + holder.addPlayer(player, futureTeam); + + if (isFromRed) { + holder.removePlayer(player, 0); + } else { + holder.removePlayer(player, 1); + } + holder.broadCastPacketToTeam(new ExCubeGameChangeTeam(player, isFromRed)); + } + } + + /** + * Will erase all participants from the specified holder + * @param arenaId + */ + public synchronized void clearPaticipantQueueByArenaId(int arenaId) { + _arenaPlayers[arenaId].clearPlayers(); + } + + /** + * Returns true if arena is holding an event at this momment + * @param arenaId + * @return boolean + */ + public boolean arenaIsBeingUsed(int arenaId) { + if ((arenaId < 0) || (arenaId > 3)) { + return false; + } + return _arenaStatus.get(arenaId); + } + + /** + * Set the specified arena as being used + * @param arenaId + */ + public void setArenaBeingUsed(int arenaId) { + _arenaStatus.put(arenaId, true); + } + + /** + * Set as free the specified arena for future events + * @param arenaId + */ + public void setArenaFree(int arenaId) { + _arenaStatus.put(arenaId, false); + } + + /** + * Called when played logs out while participating in Block Checker Event + * @param player + */ + public void onDisconnect(L2PcInstance player) { + int arena = player.getBlockCheckerArena(); + int team = getHolder(arena).getPlayerTeam(player); + HandysBlockCheckerManager.getInstance().removePlayer(player, arena, team); + if (player.getTeam() != Team.NONE) { + player.stopAllEffects(); + // Remove team aura + player.setTeam(Team.NONE); + + // Remove the event items + PcInventory inv = player.getInventory(); + + if (inv.getItemByItemId(13787) != null) { + long count = inv.getInventoryItemCount(13787, 0); + inv.destroyItemByItemId("Handys Block Checker", 13787, count, player, player); + } + if (inv.getItemByItemId(13788) != null) { + long count = inv.getInventoryItemCount(13788, 0); + inv.destroyItemByItemId("Handys Block Checker", 13788, count, player, player); + } + player.setInsideZone(ZoneId.PVP, false); + // Teleport Back + player.teleToLocation(-57478, -60367, -2370); + } + } + + public void removePenalty(int objectId) { + _registrationPenalty.remove(objectId); + } + + private void schedulePenaltyRemoval(int objId) { + ThreadPoolManager.getInstance().scheduleGeneral(new PenaltyRemoveTask(objId), 10000); + } + + /** + * Gets the single instance of {@code HandysBlockCheckerManager}. + * @return single instance of {@code HandysBlockCheckerManager} + */ + public static HandysBlockCheckerManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final HandysBlockCheckerManager _instance = new HandysBlockCheckerManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/InstanceManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/InstanceManager.java new file mode 100644 index 00000000..71712251 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/InstanceManager.java @@ -0,0 +1,280 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.instancezone.InstanceWorld; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Instance Manager. + * @author evill33t + * @author GodKratos + */ +public final class InstanceManager implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(InstanceManager.class); + + private static final Map INSTANCES = new ConcurrentHashMap<>(); + + private final Map _instanceWorlds = new ConcurrentHashMap<>(); + + private int _dynamic = 300000; + + private static final Map _instanceIdNames = new HashMap<>(); + + private final Map> _playerInstanceTimes = new ConcurrentHashMap<>(); + + private static final String ADD_INSTANCE_TIME = "INSERT INTO character_instance_time (charId,instanceId,time) values (?,?,?) ON DUPLICATE KEY UPDATE time=?"; + + private static final String RESTORE_INSTANCE_TIMES = "SELECT instanceId,time FROM character_instance_time WHERE charId=?"; + + private static final String DELETE_INSTANCE_TIME = "DELETE FROM character_instance_time WHERE charId=? AND instanceId=?"; + + protected InstanceManager() { + // Creates the multiverse. + INSTANCES.put(-1, new Instance(-1, "multiverse")); + LOG.info("Multiverse Instance created."); + // Creates the universe. + INSTANCES.put(0, new Instance(0, "universe")); + LOG.info("Universe Instance created."); + load(); + } + + @Override + public void load() { + _instanceIdNames.clear(); + parseDatapackFile("data/instancenames.xml"); + LOG.info("Loaded {} instance names.", _instanceIdNames.size()); + } + + public long getInstanceTime(int playerObjId, int id) { + if (!_playerInstanceTimes.containsKey(playerObjId)) { + restoreInstanceTimes(playerObjId); + } + if (_playerInstanceTimes.get(playerObjId).containsKey(id)) { + return _playerInstanceTimes.get(playerObjId).get(id); + } + return -1; + } + + public Map getAllInstanceTimes(int playerObjId) { + if (!_playerInstanceTimes.containsKey(playerObjId)) { + restoreInstanceTimes(playerObjId); + } + return _playerInstanceTimes.get(playerObjId); + } + + public void setInstanceTime(int playerObjId, int id, long time) { + if (!_playerInstanceTimes.containsKey(playerObjId)) { + restoreInstanceTimes(playerObjId); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(ADD_INSTANCE_TIME)) { + ps.setInt(1, playerObjId); + ps.setInt(2, id); + ps.setLong(3, time); + ps.setLong(4, time); + ps.execute(); + _playerInstanceTimes.get(playerObjId).put(id, time); + } catch (Exception ex) { + LOG.warn("Could not insert character instance time data!", ex); + } + } + + public void deleteInstanceTime(int playerObjId, int id) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_INSTANCE_TIME)) { + ps.setInt(1, playerObjId); + ps.setInt(2, id); + ps.execute(); + _playerInstanceTimes.get(playerObjId).remove(id); + } catch (Exception ex) { + LOG.warn("Could not delete character instance time data!", ex); + } + } + + public void restoreInstanceTimes(int playerObjId) { + if (_playerInstanceTimes.containsKey(playerObjId)) { + return; // already restored + } + _playerInstanceTimes.put(playerObjId, new ConcurrentHashMap<>()); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(RESTORE_INSTANCE_TIMES)) { + ps.setInt(1, playerObjId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int id = rs.getInt("instanceId"); + long time = rs.getLong("time"); + if (time < System.currentTimeMillis()) { + deleteInstanceTime(playerObjId, id); + } else { + _playerInstanceTimes.get(playerObjId).put(id, time); + } + } + } + } catch (Exception ex) { + LOG.warn("Could not delete character instance time data!", ex); + } + } + + public String getInstanceIdName(int id) { + if (_instanceIdNames.containsKey(id)) { + return _instanceIdNames.get(id); + } + return ("UnknownInstance"); + } + + @Override + public void parseDocument(Document doc) { + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equals(n.getNodeName())) { + NamedNodeMap attrs; + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("instance".equals(d.getNodeName())) { + attrs = d.getAttributes(); + _instanceIdNames.put(parseInteger(attrs, "id"), attrs.getNamedItem("name").getNodeValue()); + } + } + } + } + } + + public void addWorld(InstanceWorld world) { + _instanceWorlds.put(world.getInstanceId(), world); + } + + public InstanceWorld getWorld(int instanceId) { + return _instanceWorlds.get(instanceId); + } + + /** + * Check if the player have a World Instance where it's allowed to enter. + * @param player the player to check + * @return the instance world + */ + public InstanceWorld getPlayerWorld(L2PcInstance player) { + for (InstanceWorld temp : _instanceWorlds.values()) { + if ((temp != null) && (temp.isAllowed(player.getObjectId()))) { + return temp; + } + } + return null; + } + + public void destroyInstance(int instanceid) { + if (instanceid <= 0) { + return; + } + final Instance temp = INSTANCES.get(instanceid); + if (temp != null) { + temp.removeNpcs(); + temp.removePlayers(); + temp.removeDoors(); + temp.cancelTimer(); + INSTANCES.remove(instanceid); + _instanceWorlds.remove(instanceid); + } + } + + public Instance getInstance(int instanceid) { + return INSTANCES.get(instanceid); + } + + public Map getInstances() { + return INSTANCES; + } + + public int getPlayerInstance(int objectId) { + for (Instance temp : INSTANCES.values()) { + if (temp == null) { + continue; + } + // check if the player is in any active instance + if (temp.containsPlayer(objectId)) { + return temp.getId(); + } + } + // 0 is default instance aka the world + return 0; + } + + public boolean createInstance(int id) { + if (getInstance(id) != null) { + return false; + } + + final Instance instance = new Instance(id); + INSTANCES.put(id, instance); + return true; + } + + public boolean createInstanceFromTemplate(int id, String template) { + if (getInstance(id) != null) { + return false; + } + + final Instance instance = new Instance(id); + INSTANCES.put(id, instance); + instance.loadInstanceTemplate(template); + return true; + } + + /** + * Create a new instance with a dynamic instance id based on a template (or null) + * @param template xml file + * @return + */ + public int createDynamicInstance(String template) { + while (getInstance(_dynamic) != null) { + _dynamic++; + if (_dynamic == Integer.MAX_VALUE) { + LOG.warn("More then {} instances has been created!", Integer.MAX_VALUE - 300000); + _dynamic = 300000; + } + } + final Instance instance = new Instance(_dynamic); + INSTANCES.put(_dynamic, instance); + if (template != null) { + instance.loadInstanceTemplate(template); + } + return _dynamic; + } + + public static InstanceManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final InstanceManager _instance = new InstanceManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/ItemAuctionManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/ItemAuctionManager.java new file mode 100644 index 00000000..38fe4c75 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/ItemAuctionManager.java @@ -0,0 +1,146 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.parsers.DocumentBuilderFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.itemauction.ItemAuctionInstance; + +/** + * Item Auction Manager. + * @author Forsaiken + */ +public final class ItemAuctionManager { + + private static final Logger LOG = LoggerFactory.getLogger(ItemAuctionManager.class); + + private final Map _managerInstances = new HashMap<>(); + private final AtomicInteger _auctionIds; + + protected ItemAuctionManager() { + _auctionIds = new AtomicInteger(1); + + if (!general().itemAuctionEnabled()) { + LOG.info("Auction Manager disabled by config."); + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT auctionId FROM item_auction ORDER BY auctionId DESC LIMIT 0, 1")) { + if (rs.next()) { + _auctionIds.set(rs.getInt(1) + 1); + } + } catch (Exception e) { + LOG.error("Failed loading auctions!", e); + } + + final File file = new File(server().getDatapackRoot(), "data/ItemAuctions.xml"); + if (!file.exists()) { + LOG.warn("Missing ItemAuctions.xml!"); + return; + } + + // TODO(Zoey76): Use IXmlReader. + final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(false); + factory.setIgnoringComments(true); + + try { + final Document doc = factory.newDocumentBuilder().parse(file); + for (Node na = doc.getFirstChild(); na != null; na = na.getNextSibling()) { + if ("list".equalsIgnoreCase(na.getNodeName())) { + for (Node nb = na.getFirstChild(); nb != null; nb = nb.getNextSibling()) { + if ("instance".equalsIgnoreCase(nb.getNodeName())) { + final NamedNodeMap nab = nb.getAttributes(); + final int instanceId = Integer.parseInt(nab.getNamedItem("id").getNodeValue()); + + if (_managerInstances.containsKey(instanceId)) { + throw new Exception("Dublicated instanceId " + instanceId); + } + + final ItemAuctionInstance instance = new ItemAuctionInstance(instanceId, _auctionIds, nb); + _managerInstances.put(instanceId, instance); + } + } + } + } + LOG.info("Loaded " + _managerInstances.size() + " auction manager instance(s)."); + } catch (Exception e) { + LOG.error("Failed loading auctions from xml!", e); + } + } + + public void shutdown() { + for (ItemAuctionInstance instance : _managerInstances.values()) { + instance.shutdown(); + } + } + + public ItemAuctionInstance getManagerInstance(final int instanceId) { + return _managerInstances.get(instanceId); + } + + public int getNextAuctionId() { + return _auctionIds.getAndIncrement(); + } + + public static void deleteAuction(final int auctionId) { + try (var con = ConnectionFactory.getInstance().getConnection()) { + try (var ps = con.prepareStatement("DELETE FROM item_auction WHERE auctionId=?")) { + ps.setInt(1, auctionId); + ps.execute(); + } + + try (var ps = con.prepareStatement("DELETE FROM item_auction_bid WHERE auctionId=?")) { + ps.setInt(1, auctionId); + ps.execute(); + } + } catch (Exception e) { + LOG.error("Failed deleting auction ID {}!", auctionId, e); + } + } + + /** + * Gets the single instance of {@code ItemAuctionManager}. + * @return single instance of {@code ItemAuctionManager} + */ + public static ItemAuctionManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ItemAuctionManager INSTANCE = new ItemAuctionManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/ItemsOnGroundManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/ItemsOnGroundManager.java new file mode 100644 index 00000000..8e41dab7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/ItemsOnGroundManager.java @@ -0,0 +1,214 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ItemsAutoDestroy; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * This class manage all items on ground. + * @author Enforcer + */ +public final class ItemsOnGroundManager implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(ItemsOnGroundManager.class); + + private final List _items = new CopyOnWriteArrayList<>(); + + private ItemsOnGroundManager() { + if (general().getSaveDroppedItemInterval() > 0) { + ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(this, general().getSaveDroppedItemInterval(), general().getSaveDroppedItemInterval()); + } + load(); + } + + private void load() { + // If SaveDroppedItem is false, may want to delete all items previously stored to avoid add old items on reactivate + if (!general().saveDroppedItem() && general().clearDroppedItemTable()) { + emptyTable(); + } + + if (!general().saveDroppedItem()) { + return; + } + + // if DestroyPlayerDroppedItem was previously false, items currently protected will be added to ItemsAutoDestroy + if (general().destroyPlayerDroppedItem()) { + String str; + if (!general().destroyEquipableItem()) { + // Recycle misc. items only + str = "UPDATE itemsonground SET drop_time = ? WHERE drop_time = -1 AND equipable = 0"; + } else { + // Recycle all items including equipable + str = "UPDATE itemsonground SET drop_time = ? WHERE drop_time = -1"; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(str)) { + ps.setLong(1, System.currentTimeMillis()); + ps.execute(); + } catch (Exception e) { + LOG.error("Error while updating table ItemsOnGround: {}", e.getMessage(), e); + } + } + + // Add items to world + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT object_id,item_id,count,enchant_level,x,y,z,drop_time,equipable FROM itemsonground")) { + int count = 0; + try (var rs = ps.executeQuery()) { + L2ItemInstance item; + while (rs.next()) { + item = new L2ItemInstance(rs.getInt(1), rs.getInt(2)); + L2World.getInstance().storeObject(item); + // this check and.. + if (item.isStackable() && (rs.getInt(3) > 1)) { + item.setCount(rs.getInt(3)); + } + // this, are really necessary? + if (rs.getInt(4) > 0) { + item.setEnchantLevel(rs.getInt(4)); + } + item.setXYZ(rs.getInt(5), rs.getInt(6), rs.getInt(7)); + item.setWorldRegion(L2World.getInstance().getRegion(item.getLocation())); + item.getWorldRegion().addVisibleObject(item); + final long dropTime = rs.getLong(8); + item.setDropTime(dropTime); + item.setProtected(dropTime == -1); + item.setIsVisible(true); + L2World.getInstance().addVisibleObject(item, item.getWorldRegion()); + _items.add(item); + count++; + // add to ItemsAutoDestroy only items not protected + if (!general().getProtectedItems().contains(item.getId())) { + if (dropTime > -1) { + if (((general().getAutoDestroyDroppedItemAfter() > 0) && !item.getItem().hasExImmediateEffect()) || ((general().getAutoDestroyHerbTime() > 0) && item.getItem().hasExImmediateEffect())) { + ItemsAutoDestroy.getInstance().addItem(item); + } + } + } + } + } + LOG.info("Loaded {} items.", count); + } catch (Exception e) { + LOG.error("Error while loading ItemsOnGround: {}", e.getMessage(), e); + } + + if (general().emptyDroppedItemTableAfterLoad()) { + emptyTable(); + } + } + + public void save(L2ItemInstance item) { + if (!general().saveDroppedItem()) { + return; + } + _items.add(item); + } + + public void removeObject(L2ItemInstance item) { + if (general().saveDroppedItem()) { + _items.remove(item); + } + } + + public void saveInDb() { + run(); + } + + public void cleanUp() { + _items.clear(); + } + + private void emptyTable() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate("DELETE FROM itemsonground"); + } catch (Exception e1) { + LOG.error("Error while cleaning table ItemsOnGround: {}", e1.getMessage(), e1); + } + } + + @Override + public synchronized void run() { + if (!general().saveDroppedItem()) { + return; + } + + emptyTable(); + + if (_items.isEmpty()) { + return; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO itemsonground(object_id,item_id,count,enchant_level,x,y,z,drop_time,equipable) VALUES(?,?,?,?,?,?,?,?,?)")) { + for (L2ItemInstance item : _items) { + if (item == null) { + continue; + } + + if (CursedWeaponsManager.getInstance().isCursed(item.getId())) { + continue; // Cursed Items not saved to ground, prevent double save + } + + try { + ps.setInt(1, item.getObjectId()); + ps.setInt(2, item.getId()); + ps.setLong(3, item.getCount()); + ps.setInt(4, item.getEnchantLevel()); + ps.setInt(5, item.getX()); + ps.setInt(6, item.getY()); + ps.setInt(7, item.getZ()); + ps.setLong(8, (item.isProtected() ? -1 : item.getDropTime())); // item is protected or AutoDestroyed + ps.setLong(9, (item.isEquipable() ? 1 : 0)); // set equip-able + ps.execute(); + ps.clearParameters(); + } catch (Exception e) { + LOG.error("Error while inserting into table ItemsOnGround: {}", e.getMessage(), e); + } + } + } catch (Exception e) { + LOG.error("SQL error while storing items on ground: {}", e.getMessage(), e); + } + } + + /** + * Gets the single instance of {@code ItemsOnGroundManager}. + * @return single instance of {@code ItemsOnGroundManager} + */ + public static ItemsOnGroundManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final ItemsOnGroundManager _instance = new ItemsOnGroundManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/MailManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/MailManager.java new file mode 100644 index 00000000..e9090541 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/MailManager.java @@ -0,0 +1,216 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.tasks.MessageDeletionTask; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Message; +import com.l2jserver.gameserver.network.serverpackets.ExNoticePostArrived; + +/** + * Mail Manager. + * @author Migi + * @author DS + */ +public final class MailManager { + + private static final Logger LOG = LoggerFactory.getLogger(MailManager.class); + + private final Map _messages = new ConcurrentHashMap<>(); + + protected MailManager() { + load(); + } + + private void load() { + int count = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.createStatement(); + var rs = ps.executeQuery("SELECT * FROM messages ORDER BY expiration")) { + while (rs.next()) { + final Message msg = new Message(rs); + + int msgId = msg.getId(); + _messages.put(msgId, msg); + + count++; + + long expiration = msg.getExpiration(); + + if (expiration < System.currentTimeMillis()) { + ThreadPoolManager.getInstance().scheduleGeneral(new MessageDeletionTask(msgId), 10000); + } else { + ThreadPoolManager.getInstance().scheduleGeneral(new MessageDeletionTask(msgId), expiration - System.currentTimeMillis()); + } + } + } catch (Exception ex) { + LOG.warn("There has been an error loading from database!", ex); + } + LOG.info("Successfully loaded {} messages.", count); + } + + public Message getMessage(int msgId) { + return _messages.get(msgId); + } + + public Collection getMessages() { + return _messages.values(); + } + + public boolean hasUnreadPost(L2PcInstance player) { + final int objectId = player.getObjectId(); + for (Message msg : getMessages()) { + if ((msg != null) && (msg.getReceiverId() == objectId) && msg.isUnread()) { + return true; + } + } + return false; + } + + public int getInboxSize(int objectId) { + int size = 0; + for (Message msg : getMessages()) { + if ((msg != null) && (msg.getReceiverId() == objectId) && !msg.isDeletedByReceiver()) { + size++; + } + } + return size; + } + + public int getOutboxSize(int objectId) { + int size = 0; + for (Message msg : getMessages()) { + if ((msg != null) && (msg.getSenderId() == objectId) && !msg.isDeletedBySender()) { + size++; + } + } + return size; + } + + public List getInbox(int objectId) { + final List inbox = new ArrayList<>(); + for (Message msg : getMessages()) { + if ((msg != null) && (msg.getReceiverId() == objectId) && !msg.isDeletedByReceiver()) { + inbox.add(msg); + } + } + return inbox; + } + + public List getOutbox(int objectId) { + final List outbox = new ArrayList<>(); + for (Message msg : getMessages()) { + if ((msg != null) && (msg.getSenderId() == objectId) && !msg.isDeletedBySender()) { + outbox.add(msg); + } + } + return outbox; + } + + public void sendMessage(Message msg) { + _messages.put(msg.getId(), msg); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = Message.getStatement(msg, con)) { + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error saving message Id {}!", msg.getId(), ex); + } + + final L2PcInstance receiver = L2World.getInstance().getPlayer(msg.getReceiverId()); + if (receiver != null) { + receiver.sendPacket(ExNoticePostArrived.valueOf(true)); + } + + ThreadPoolManager.getInstance().scheduleGeneral(new MessageDeletionTask(msg.getId()), msg.getExpiration() - System.currentTimeMillis()); + } + + public void markAsReadInDb(int msgId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE messages SET isUnread=FALSE WHERE messageId = ?")) { + ps.setInt(1, msgId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error marking as read message Id {}!", msgId, ex); + } + } + + public void markAsDeletedBySenderInDb(int msgId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE messages SET isDeletedBySender=TRUE WHERE messageId = ?")) { + ps.setInt(1, msgId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error marking as deleted by sender message Id {}!", msgId, ex); + } + } + + public void markAsDeletedByReceiverInDb(int msgId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE messages SET isDeletedByReceiver=TRUE WHERE messageId = ?")) { + ps.setInt(1, msgId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error marking as deleted by receiver message Id {}!", msgId, ex); + } + } + + public void removeAttachmentsInDb(int msgId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE messages SET hasAttachments=FALSE WHERE messageId = ?")) { + ps.setInt(1, msgId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error removing attachments in message Id {}!", msgId, ex); + } + } + + public void deleteMessageInDb(int msgId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM messages WHERE messageId = ?")) { + ps.setInt(1, msgId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("There has been an error deleting message Id {}!", msgId, ex); + } + + _messages.remove(msgId); + IdFactory.getInstance().releaseId(msgId); + } + + public static MailManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final MailManager INSTANCE = new MailManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/MapRegionManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/MapRegionManager.java new file mode 100644 index 00000000..58d09236 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/MapRegionManager.java @@ -0,0 +1,358 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.model.L2MapRegion; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2SiegeFlagInstance; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.model.zone.type.L2ClanHallZone; +import com.l2jserver.gameserver.model.zone.type.L2RespawnZone; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * Map Region Manager. + * @author Nyaran + */ +public final class MapRegionManager implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(MapRegionManager.class); + + private static final Map REGIONS = new HashMap<>(); + + private static final String DEFAULT_RESPAWN = "talking_island_town"; + + protected MapRegionManager() { + load(); + } + + @Override + public void load() { + REGIONS.clear(); + parseDatapackDirectory("data/mapregion", false); + LOG.info("Loaded {} map regions.", REGIONS.size()); + } + + @Override + public void parseDocument(Document doc) { + NamedNodeMap attrs; + String name; + String town; + int locId; + int castle; + int bbs; + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("region".equalsIgnoreCase(d.getNodeName())) { + attrs = d.getAttributes(); + name = attrs.getNamedItem("name").getNodeValue(); + town = attrs.getNamedItem("town").getNodeValue(); + locId = parseInteger(attrs, "locId"); + castle = parseInteger(attrs, "castle"); + bbs = parseInteger(attrs, "bbs"); + + L2MapRegion region = new L2MapRegion(name, town, locId, castle, bbs); + for (Node c = d.getFirstChild(); c != null; c = c.getNextSibling()) { + attrs = c.getAttributes(); + if ("respawnPoint".equalsIgnoreCase(c.getNodeName())) { + int spawnX = parseInteger(attrs, "X"); + int spawnY = parseInteger(attrs, "Y"); + int spawnZ = parseInteger(attrs, "Z"); + + boolean other = parseBoolean(attrs, "isOther", false); + boolean chaotic = parseBoolean(attrs, "isChaotic", false); + boolean banish = parseBoolean(attrs, "isBanish", false); + + if (other) { + region.addOtherSpawn(spawnX, spawnY, spawnZ); + } else if (chaotic) { + region.addChaoticSpawn(spawnX, spawnY, spawnZ); + } else if (banish) { + region.addBanishSpawn(spawnX, spawnY, spawnZ); + } else { + region.addSpawn(spawnX, spawnY, spawnZ); + } + } else if ("map".equalsIgnoreCase(c.getNodeName())) { + region.addMap(parseInteger(attrs, "X"), parseInteger(attrs, "Y")); + } else if ("banned".equalsIgnoreCase(c.getNodeName())) { + region.addBannedRace(attrs.getNamedItem("race").getNodeValue(), attrs.getNamedItem("point").getNodeValue()); + } + } + REGIONS.put(name, region); + } + } + } + } + } + + public L2MapRegion getMapRegion(int locX, int locY) { + for (L2MapRegion region : REGIONS.values()) { + if (region.isZoneInRegion(getMapRegionX(locX), getMapRegionY(locY))) { + return region; + } + } + return null; + } + + public int getMapRegionLocId(int locX, int locY) { + L2MapRegion region = getMapRegion(locX, locY); + if (region != null) { + return region.getLocId(); + } + return 0; + } + + public L2MapRegion getMapRegion(L2Object obj) { + return getMapRegion(obj.getX(), obj.getY()); + } + + public int getMapRegionLocId(L2Object obj) { + return getMapRegionLocId(obj.getX(), obj.getY()); + } + + public int getMapRegionX(int posX) { + return (posX >> 15) + 9 + 11;// + centerTileX; + } + + public int getMapRegionY(int posY) { + return (posY >> 15) + 10 + 8;// + centerTileX; + } + + /** + * Get town name by character position + * @param activeChar + * @return + */ + public String getClosestTownName(L2Character activeChar) { + L2MapRegion region = getMapRegion(activeChar); + + if (region == null) { + return "Aden Castle Town"; + } + + return region.getTown(); + } + + public int getAreaCastle(L2Character activeChar) { + L2MapRegion region = getMapRegion(activeChar); + if (region == null) { + return 0; + } + return region.getCastle(); + } + + public Location getTeleToLocation(L2Character activeChar, TeleportWhereType teleportWhere) { + Location loc; + if (activeChar.isPlayer()) { + final L2PcInstance player = activeChar.getActingPlayer(); + + // flying players in gracia cant use teleports to aden continent + if ((player.getClan() != null) && !player.isFlyingMounted() && !player.isFlying()) { + // If teleport to clan hall + if (teleportWhere == TeleportWhereType.CLANHALL) { + final var clanhall = ClanHallManager.getInstance().getAbstractHallByOwner(player.getClan()); + if (clanhall != null) { + L2ClanHallZone zone = clanhall.getZone(); + if ((zone != null) && !player.isFlyingMounted()) { + if (player.getKarma() > 0) { + return zone.getChaoticSpawnLoc(); + } + return zone.getSpawnLoc(); + } + } + } + + // If teleport to castle + if (teleportWhere == TeleportWhereType.CASTLE) { + var castle = CastleManager.getInstance().getCastleByOwner(player.getClan()); + // Otherwise check if player is on castle or fortress ground + // and player's clan is defender + if (castle == null) { + castle = CastleManager.getInstance().getCastle(player); + if (!((castle != null) && castle.getSiege().isInProgress() && (castle.getSiege().getDefenderClan(player.getClan()) != null))) { + castle = null; + } + } + + if ((castle != null) && (castle.getResidenceId() > 0)) { + if (player.getKarma() > 0) { + return castle.getResidenceZone().getChaoticSpawnLoc(); + } + return castle.getResidenceZone().getSpawnLoc(); + } + } + + // If teleport to fortress + if (teleportWhere == TeleportWhereType.FORTRESS) { + var fort = FortManager.getInstance().getFortByOwner(player.getClan()); + // Otherwise check if player is on castle or fortress ground + // and player's clan is defender + if (fort == null) { + fort = FortManager.getInstance().getFort(player); + if (!((fort != null) && fort.getSiege().isInProgress() && (fort.getOwnerClan() == player.getClan()))) { + fort = null; + } + } + + if ((fort != null) && (fort.getResidenceId() > 0)) { + if (player.getKarma() > 0) { + return fort.getResidenceZone().getChaoticSpawnLoc(); + } + return fort.getResidenceZone().getSpawnLoc(); + } + } + + // If teleport to SiegeHQ + if (teleportWhere == TeleportWhereType.SIEGEFLAG) { + final var castle = CastleManager.getInstance().getCastle(player); + final var fort = FortManager.getInstance().getFort(player); + final var clanhall = ClanHallManager.getInstance().getNearbyAbstractHall(activeChar.getX(), activeChar.getY(), 10000); + L2SiegeFlagInstance tw_flag = TerritoryWarManager.getInstance().getHQForClan(player.getClan()); + if (tw_flag != null) { + return tw_flag.getLocation(); + } else if (castle != null) { + if (castle.getSiege().isInProgress()) { + // Check if player's clan is attacker + List flags = castle.getSiege().getFlag(player.getClan()); + if ((flags != null) && !flags.isEmpty()) { + // Spawn to flag - Need more work to get player to the nearest flag + return flags.get(0).getLocation(); + } + } + + } else if (fort != null) { + if (fort.getSiege().isInProgress()) { + // Check if player's clan is attacker + List flags = fort.getSiege().getFlag(player.getClan()); + if ((flags != null) && !flags.isEmpty()) { + // Spawn to flag - Need more work to get player to the nearest flag + return flags.get(0).getLocation(); + } + } + } else if ((clanhall != null) && clanhall.isSiegableHall()) { + SiegableHall sHall = (SiegableHall) clanhall; + List flags = sHall.getSiege().getFlag(player.getClan()); + if ((flags != null) && !flags.isEmpty()) { + return flags.get(0).getLocation(); + } + } + } + } + + // Karma player land out of city + if (player.getKarma() > 0) { + try { + L2RespawnZone zone = ZoneManager.getInstance().getZone(player, L2RespawnZone.class); + if (zone != null) { + return getRestartRegion(activeChar, zone.getRespawnPoint((L2PcInstance) activeChar)).getChaoticSpawnLoc(); + } + return getMapRegion(activeChar).getChaoticSpawnLoc(); + } catch (Exception e) { + if (player.isFlyingMounted()) { + return REGIONS.get("union_base_of_kserth").getChaoticSpawnLoc(); + } + return REGIONS.get(DEFAULT_RESPAWN).getChaoticSpawnLoc(); + } + } + + // Checking if needed to be respawned in "far" town from the castle; + final var castle = CastleManager.getInstance().getCastle(player); + if (castle != null) { + if (castle.getSiege().isInProgress()) { + // Check if player's clan is participating + if ((castle.getSiege().checkIsDefender(player.getClan()) || castle.getSiege().checkIsAttacker(player.getClan())) && (SevenSigns.getInstance().getSealOwner(SevenSigns.SEAL_STRIFE) == SevenSigns.CABAL_DAWN)) { + return castle.getResidenceZone().getOtherSpawnLoc(); + } + } + } + + // Checking if in an instance + if (player.getInstanceId() > 0) { + Instance inst = InstanceManager.getInstance().getInstance(player.getInstanceId()); + if (inst != null) { + loc = inst.getExitLoc(); + if (loc != null) { + return loc; + } + } + } + } + + // Get the nearest town + try { + L2RespawnZone zone = ZoneManager.getInstance().getZone(activeChar, L2RespawnZone.class); + if (zone != null) { + return getRestartRegion(activeChar, zone.getRespawnPoint((L2PcInstance) activeChar)).getSpawnLoc(); + } + return getMapRegion(activeChar).getSpawnLoc(); + } catch (Exception e) { + // Port to the default respawn if no closest town found. + return REGIONS.get(DEFAULT_RESPAWN).getSpawnLoc(); + } + } + + public L2MapRegion getRestartRegion(L2Character activeChar, String point) { + try { + L2PcInstance player = ((L2PcInstance) activeChar); + L2MapRegion region = REGIONS.get(point); + + if (region.getBannedRace().containsKey(player.getRace())) { + getRestartRegion(player, region.getBannedRace().get(player.getRace())); + } + return region; + } catch (Exception e) { + return REGIONS.get(DEFAULT_RESPAWN); + } + } + + /** + * @param regionName the map region name. + * @return if exists the map region identified by that name, null otherwise. + */ + public L2MapRegion getMapRegionByName(String regionName) { + return REGIONS.get(regionName); + } + + public static MapRegionManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final MapRegionManager INSTANCE = new MapRegionManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/MercTicketManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/MercTicketManager.java new file mode 100644 index 00000000..86ce9ca6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/MercTicketManager.java @@ -0,0 +1,380 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.enums.ItemLocation; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * This class is similar to the SiegeGuardManager, except it handles the loading of the mercenary tickets that are dropped on castle floors by the castle lords.
    + * These tickets (a.k.a. badges) need to be read after each server reboot except when the server crashed in the middle of an ongoing siege.
    + * In addition, this class keeps track of the added tickets, in order to properly limit the number of mercenaries in each castle and the number of mercenaries from each mercenary type.
    + * Finally, we provide auxiliary functions to identify the castle in which each item (and its corresponding NPC) belong to, in order to help avoid mixing them up. + * @author yellowperil + * @author Fulminus + */ +public final class MercTicketManager { + + private static final Logger LOG = LoggerFactory.getLogger(MercTicketManager.class); + + private static final List DROPPED_TICKETS = new CopyOnWriteArrayList<>(); + + // TODO: move all these values into siege.properties + // max tickets per merc type = 10 + (castleid * 2)? + // max ticker per castle = 40 + (castleid * 20)? + // @formatter:off + private static final int[] MAX_MERC_PER_TYPE = + { + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, // Gludio + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, // Dion + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, // Giran + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, // Oren + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // Aden + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // Innadril + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // Goddard + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // Rune + 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20 // Schuttgart + }; + + //TODO: not retail like: clan lvl 5 - 30 ticks max, lvl 7+ - 50 max + protected static final int[] MERCS_MAX_PER_CASTLE = + { + 100, // Gludio + 150, // Dion + 200, // Giran + 300, // Oren + 400, // Aden + 400, // Innadril + 400, // Goddard + 400, // Rune + 400 // Schuttgart + }; + + private static final int[] ITEM_IDS = + { + 3960, 3961, 3962, 3963, 3964, 3965, 3966, 3967, 3968, 3969, 6115, 6116, 6117, 6118, 6119, 6120, 6121, 6122, 6123, 6124, 6038, 6039, 6040, 6041, 6042, 6043, 6044, 6045, 6046, 6047, 6175, 6176, 6177, 6178, 6179, 6180, 6181, 6182, 6183, 6184, 6235, 6236, 6237, 6238, 6239, 6240, 6241, 6242, 6243, 6244, 6295, 6296, // Gludio + 3973, 3974, 3975, 3976, 3977, 3978, 3979, 3980, 3981, 3982, 6125, 6126, 6127, 6128, 6129, 6130, 6131, 6132, 6133, 6134, 6051, 6052, 6053, 6054, 6055, 6056, 6057, 6058, 6059, 6060, 6185, 6186, 6187, 6188, 6189, 6190, 6191, 6192, 6193, 6194, 6245, 6246, 6247, 6248, 6249, 6250, 6251, 6252, 6253, 6254, 6297, 6298, // Dion + 3986, 3987, 3988, 3989, 3990, 3991, 3992, 3993, 3994, 3995, 6135, 6136, 6137, 6138, 6139, 6140, 6141, 6142, 6143, 6144, 6064, 6065, 6066, 6067, 6068, 6069, 6070, 6071, 6072, 6073, 6195, 6196, 6197, 6198, 6199, 6200, 6201, 6202, 6203, 6204, 6255, 6256, 6257, 6258, 6259, 6260, 6261, 6262, 6263, 6264, 6299, 6300, // Giran + 3999, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 6145, 6146, 6147, 6148, 6149, 6150, 6151, 6152, 6153, 6154, 6077, 6078, 6079, 6080, 6081, 6082, 6083, 6084, 6085, 6086, 6205, 6206, 6207, 6208, 6209, 6210, 6211, 6212, 6213, 6214, 6265, 6266, 6267, 6268, 6269, 6270, 6271, 6272, 6273, 6274, 6301, 6302, // Oren + 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020, 4021, 6155, 6156, 6157, 6158, 6159, 6160, 6161, 6162, 6163, 6164, 6090, 6091, 6092, 6093, 6094, 6095, 6096, 6097, 6098, 6099, 6215, 6216, 6217, 6218, 6219, 6220, 6221, 6222, 6223, 6224, 6275, 6276, 6277, 6278, 6279, 6280, 6281, 6282, 6283, 6284, 6303, 6304, // Aden + 5205, 5206, 5207, 5208, 5209, 5210, 5211, 5212, 5213, 5214, 6165, 6166, 6167, 6168, 6169, 6170, 6171, 6172, 6173, 6174, 6105, 6106, 6107, 6108, 6109, 6110, 6111, 6112, 6113, 6114, 6225, 6226, 6227, 6228, 6229, 6230, 6231, 6232, 6233, 6234, 6285, 6286, 6287, 6288, 6289, 6290, 6291, 6292, 6293, 6294, 6305, 6306, // Innadril + 6779, 6780, 6781, 6782, 6783, 6784, 6785, 6786, 6787, 6788, 6802, 6803, 6804, 6805, 6806, 6807, 6808, 6809, 6810, 6811, 6792, 6793, 6794, 6795, 6796, 6797, 6798, 6799, 6800, 6801, 6812, 6813, 6814, 6815, 6816, 6817, 6818, 6819, 6820, 6821, 6822, 6823, 6824, 6825, 6826, 6827, 6828, 6829, 6830, 6831, 6832, 6833, // Goddard + 7973, 7974, 7975, 7976, 7977, 7978, 7979, 7980, 7981, 7982, 7998, 7999, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 7988, 7989, 7990, 7991, 7992, 7993, 7994, 7995, 7996, 7997, 8008, 8009, 8010, 8011, 8012, 8013, 8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025, 8026, 8027, 8028, 8029, // Rune + 7918, 7919, 7920, 7921, 7922, 7923, 7924, 7925, 7926, 7927, 7941, 7942, 7943, 7944, 7945, 7946, 7947, 7948, 7949, 7950, 7931, 7932, 7933, 7934, 7935, 7936, 7937, 7938, 7939, 7940, 7951, 7952, 7953, 7954, 7955, 7956, 7957, 7958, 7959, 7960, 7961, 7962, 7963, 7964, 7965, 7966, 7967, 7968, 7969, 7970, 7971, 7972 // Schuttgart + }; + + private static final int[] NPC_IDS = + { + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Gludio + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Dion + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Giran + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Oren + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Aden + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Innadril + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Goddard + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Rune + 35010, 35011, 35012, 35013, 35014, 35015, 35016, 35017, 35018, 35019, 35020, 35021, 35022, 35023, 35024, 35025, 35026, 35027, 35028, 35029, 35030,35031,35032,35033,35034,35035,35036,35037,35038,35039, 35040, 35041, 35042, 35043, 35044, 35045, 35046, 35047, 35048, 35049, 35050, 35051, 35052, 35053, 35054, 35055, 35056, 35057, 35058, 35059, 35060, 35061, // Schuttgart + }; + // @formatter:on + + private static final int GUARDIAN_TYPES_COUNT = 52; + + protected MercTicketManager() { + load(); + } + + // returns the castleId for the passed ticket item id + public int getTicketCastleId(int itemId) { + for (int i = 0; i < 9; i++) // CastleID`s from 1 to 9 minus; + { + for (int i2 = 0; i2 < 50; i2 += 10) { + // Simplified if statement; + if (((itemId >= ITEM_IDS[i2 + (i * GUARDIAN_TYPES_COUNT)]) && (itemId <= ITEM_IDS[i2 + 9 + (i * GUARDIAN_TYPES_COUNT)]))) { + return i + 1; + } + } + if ((itemId >= ITEM_IDS[50]) && (itemId <= ITEM_IDS[51])) { + return i + 1; + } + } + return -1; + } + + public void reload() { + DROPPED_TICKETS.clear(); + load(); + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM castle_siege_guards Where isHired = 1")) { + int npcId; + int itemId; + int x, y, z; + int[] mercPlaced = new int[20]; + // start index to begin the search for the itemId corresponding to this NPC + // this will help with: + // a) skip unnecessary iterations in the search loop + // b) avoid finding the wrong itemId whenever tickets of different spawn the same npc! + int startindex = 0; + + while (rs.next()) { + npcId = rs.getInt("npcId"); + x = rs.getInt("x"); + y = rs.getInt("y"); + z = rs.getInt("z"); + Castle castle = CastleManager.getInstance().getCastle(x, y, z); + if (castle != null) { + if (mercPlaced[castle.getResidenceId() - 1] >= MERCS_MAX_PER_CASTLE[castle.getResidenceId() - 1]) { + continue; + } + startindex = GUARDIAN_TYPES_COUNT * (castle.getResidenceId() - 1); + mercPlaced[castle.getResidenceId() - 1] += 1; + } + + // find the FIRST ticket itemId with spawns the saved NPC in the saved location + for (int i = startindex; i < (startindex + GUARDIAN_TYPES_COUNT); i++) { + if (NPC_IDS[i] == npcId) // Find the index of the item used + { + // only handle tickets if a siege is not ongoing in this npc's castle + if ((castle != null) && !(castle.getSiege().isInProgress())) { + itemId = ITEM_IDS[i]; + // create the ticket in the gameworld + final var objectId = IdFactory.getInstance().getNextId(); + final var dropticket = new L2ItemInstance(objectId, itemId); + dropticket.setItemLocation(ItemLocation.VOID); + dropticket.dropMe(null, x, y, z); + dropticket.setDropTime(0); // avoids it from being removed by the auto item destroyer + L2World.getInstance().storeObject(dropticket); + DROPPED_TICKETS.add(dropticket); + } + break; + } + } + } + rs.close(); + s.close(); + + LOG.info("Loaded {} mercenary tickets.", DROPPED_TICKETS.size()); + } catch (Exception ex) { + LOG.warn("There has been an error loading mercenary tickets!", ex); + } + } + + /** + * Checks if the passed item has reached the limit of number of dropped tickets that this SPECIFIC item may have in its castle + * @param itemId + * @return + */ + public boolean isAtTypeLimit(int itemId) { + int limit = -1; + // find the max value for this item + for (int i = 0; i < ITEM_IDS.length; i++) { + if (ITEM_IDS[i] == itemId) // Find the index of the item used + { + limit = MAX_MERC_PER_TYPE[i]; + break; + } + } + + if (limit <= 0) { + return true; + } + + int count = 0; + for (L2ItemInstance ticket : DROPPED_TICKETS) { + if ((ticket != null) && (ticket.getId() == itemId)) { + count++; + } + } + return count >= limit; + } + + /** + * Checks if the passed item belongs to a castle which has reached its limit of number of dropped tickets. + * @param itemId + * @return + */ + public boolean isAtCasleLimit(int itemId) { + int castleId = getTicketCastleId(itemId); + if (castleId <= 0) { + return true; + } + int limit = MERCS_MAX_PER_CASTLE[castleId - 1]; + if (limit <= 0) { + return true; + } + + int count = 0; + for (L2ItemInstance ticket : DROPPED_TICKETS) { + if ((ticket != null) && (getTicketCastleId(ticket.getId()) == castleId)) { + count++; + } + } + return count >= limit; + } + + public int getMaxAllowedMerc(int castleId) { + return MERCS_MAX_PER_CASTLE[castleId - 1]; + } + + public boolean isTooCloseToAnotherTicket(int x, int y, int z) { + for (L2ItemInstance item : DROPPED_TICKETS) { + double dx = x - item.getX(); + double dy = y - item.getY(); + double dz = z - item.getZ(); + + if (((dx * dx) + (dy * dy) + (dz * dz)) < (25 * 25)) { + return true; + } + } + return false; + } + + /** + * addTicket actions 1) find the npc that needs to be saved in the mercenary spawns, given this item 2) Use the passed character's location info to add the spawn 3) create a copy of the item to drop in the world returns the id of the mercenary npc that was added to the spawn returns -1 if this + * fails. + * @param itemId + * @param activeChar + * @return + */ + public int addTicket(int itemId, L2PcInstance activeChar) { + int x = activeChar.getX(); + int y = activeChar.getY(); + int z = activeChar.getZ(); + int heading = activeChar.getHeading(); + + Castle castle = CastleManager.getInstance().getCastle(activeChar); + if (castle == null) { + return -1; + } + + for (int i = 0; i < ITEM_IDS.length; i++) { + // Find the index of the item used + if (ITEM_IDS[i] == itemId) { + spawnMercenary(NPC_IDS[i], x, y, z, 3000); + + // Hire merc for this castle. NpcId is at the same index as the item used. + castle.getSiege().getSiegeGuardManager().hireMerc(x, y, z, heading, NPC_IDS[i]); + + // create the ticket in the gameworld + final var objectId = IdFactory.getInstance().getNextId(); + final var dropticket = new L2ItemInstance(objectId, itemId); + dropticket.setItemLocation(ItemLocation.VOID); + dropticket.dropMe(null, x, y, z); + dropticket.setDropTime(0); // avoids it from beeing removed by the auto item destroyer + L2World.getInstance().storeObject(dropticket); // add to the world + // and keep track of this ticket in the list + DROPPED_TICKETS.add(dropticket); + + return NPC_IDS[i]; + } + } + return -1; + } + + private void spawnMercenary(int npcId, int x, int y, int z, int despawnDelay) { + final var template = NpcData.getInstance().getTemplate(npcId); + if (template != null) { + final var objectId = IdFactory.getInstance().getNextId(); + final var npc = new L2DefenderInstance(objectId, template); + npc.setCurrentHpMp(npc.getMaxHp(), npc.getMaxMp()); + npc.setDecayed(false); + npc.spawnMe(x, y, (z + 20)); + + if (despawnDelay > 0) { + npc.scheduleDespawn(despawnDelay); + } + } + } + + /** + * Delete all tickets from a castle; remove the items from the world and remove references to them from this class + * @param castleId + */ + public void deleteTickets(int castleId) { + for (L2ItemInstance item : DROPPED_TICKETS) { + if ((item != null) && (getTicketCastleId(item.getId()) == castleId)) { + item.decayMe(); + L2World.getInstance().removeObject(item); + DROPPED_TICKETS.remove(item); + } + } + } + + /** + * remove a single ticket and its associated spawn from the world (used when the castle lord picks up a ticket, for example) + * @param item + */ + public void removeTicket(L2ItemInstance item) { + int itemId = item.getId(); + int npcId = -1; + + // find the FIRST ticket itemId with spawns the saved NPC in the saved location + for (int i = 0; i < ITEM_IDS.length; i++) { + if (ITEM_IDS[i] == itemId) // Find the index of the item used + { + npcId = NPC_IDS[i]; + break; + } + } + // find the castle where this item is + Castle castle = CastleManager.getInstance().getCastleById(getTicketCastleId(itemId)); + + if ((npcId > 0) && (castle != null)) { + (new SiegeGuardManager(castle)).removeMerc(npcId, item.getX(), item.getY(), item.getZ()); + } + + DROPPED_TICKETS.remove(item); + } + + public int[] getItemIds() { + return ITEM_IDS; + } + + public List getDroppedTickets() { + return DROPPED_TICKETS; + } + + /** + * Gets the single instance of {@code MercTicketManager}. + * @return single instance of {@code MercTicketManager} + */ + public static MercTicketManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final MercTicketManager _instance = new MercTicketManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/PetitionManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/PetitionManager.java new file mode 100644 index 00000000..3f1c67f4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/PetitionManager.java @@ -0,0 +1,405 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.enums.PetitionState; +import com.l2jserver.gameserver.model.Petition; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Petition Manager + * @author Tempy + */ +public final class PetitionManager { + private static final Logger LOG = LoggerFactory.getLogger(PetitionManager.class); + + private final Map _pendingPetitions; + private final Map _completedPetitions; + + private PetitionManager() { + _pendingPetitions = new HashMap<>(); + _completedPetitions = new HashMap<>(); + } + + public void clearCompletedPetitions() { + final int numPetitions = getPendingPetitionCount(); + + getCompletedPetitions().clear(); + LOG.info("Completed petition data cleared. {} petition(s) removed.", numPetitions); + } + + public void clearPendingPetitions() { + final int numPetitions = getPendingPetitionCount(); + + getPendingPetitions().clear(); + LOG.info("Pending petition queue cleared. {} petition(s) removed.", numPetitions); + } + + public boolean acceptPetition(L2PcInstance respondingAdmin, int petitionId) { + if (!isValidPetition(petitionId)) { + return false; + } + + final Petition currPetition = getPendingPetitions().get(petitionId); + + if (currPetition.getResponder() != null) { + return false; + } + + currPetition.setResponder(respondingAdmin); + currPetition.setState(PetitionState.IN_PROCESS); + + // Petition application accepted. (Send to Petitioner) + currPetition.sendPetitionerPacket(SystemMessage.getSystemMessage(SystemMessageId.PETITION_APP_ACCEPTED)); + + // Petition application accepted. Reciept No. is + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PETITION_ACCEPTED_RECENT_NO_S1); + sm.addInt(currPetition.getId()); + currPetition.sendResponderPacket(sm); + + // Petition consultation with underway. + sm = SystemMessage.getSystemMessage(SystemMessageId.STARTING_PETITION_WITH_C1); + sm.addString(currPetition.getPetitioner().getName()); + currPetition.sendResponderPacket(sm); + + // Set responder name on petitioner instance + currPetition.getPetitioner().setLastPetitionGmName(currPetition.getResponder().getName()); + return true; + } + + public boolean cancelActivePetition(L2PcInstance player) { + for (Petition currPetition : getPendingPetitions().values()) { + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == player.getObjectId())) { + return (currPetition.endPetitionConsultation(PetitionState.PETITIONER_CANCEL)); + } + + if ((currPetition.getResponder() != null) && (currPetition.getResponder().getObjectId() == player.getObjectId())) { + return (currPetition.endPetitionConsultation(PetitionState.RESPONDER_CANCEL)); + } + } + + return false; + } + + public void checkPetitionMessages(L2PcInstance petitioner) { + if (petitioner != null) { + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == petitioner.getObjectId())) { + for (CreatureSay logMessage : currPetition.getLogMessages()) { + petitioner.sendPacket(logMessage); + } + + return; + } + } + } + } + + public boolean endActivePetition(L2PcInstance player) { + if (!player.isGM()) { + return false; + } + + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getResponder() != null) && (currPetition.getResponder().getObjectId() == player.getObjectId())) { + return (currPetition.endPetitionConsultation(PetitionState.COMPLETED)); + } + } + + return false; + } + + public Map getCompletedPetitions() { + return _completedPetitions; + } + + public Map getPendingPetitions() { + return _pendingPetitions; + } + + public int getPendingPetitionCount() { + return getPendingPetitions().size(); + } + + public int getPlayerTotalPetitionCount(L2PcInstance player) { + if (player == null) { + return 0; + } + + int petitionCount = 0; + + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == player.getObjectId())) { + petitionCount++; + } + } + + for (Petition currPetition : getCompletedPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == player.getObjectId())) { + petitionCount++; + } + } + + return petitionCount; + } + + public boolean isPetitionInProcess() { + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if (currPetition.getState() == PetitionState.IN_PROCESS) { + return true; + } + } + + return false; + } + + public boolean isPetitionInProcess(int petitionId) { + if (!isValidPetition(petitionId)) { + return false; + } + + final Petition currPetition = getPendingPetitions().get(petitionId); + return (currPetition.getState() == PetitionState.IN_PROCESS); + } + + public boolean isPlayerInConsultation(L2PcInstance player) { + if (player != null) { + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if (currPetition.getState() != PetitionState.IN_PROCESS) { + continue; + } + + if (((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == player.getObjectId())) || ((currPetition.getResponder() != null) && (currPetition.getResponder().getObjectId() == player.getObjectId()))) { + return true; + } + } + } + + return false; + } + + public boolean isPetitioningAllowed() { + return character().petitioningAllowed(); + } + + public boolean isPlayerPetitionPending(L2PcInstance petitioner) { + if (petitioner != null) { + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == petitioner.getObjectId())) { + return true; + } + } + } + + return false; + } + + private boolean isValidPetition(int petitionId) { + return getPendingPetitions().containsKey(petitionId); + } + + public boolean rejectPetition(L2PcInstance respondingAdmin, int petitionId) { + if (!isValidPetition(petitionId)) { + return false; + } + + final Petition currPetition = getPendingPetitions().get(petitionId); + + if (currPetition.getResponder() != null) { + return false; + } + + currPetition.setResponder(respondingAdmin); + return (currPetition.endPetitionConsultation(PetitionState.RESPONDER_REJECT)); + } + + public boolean sendActivePetitionMessage(L2PcInstance player, String messageText) { + // if (!isPlayerInConsultation(player)) + // return false; + + CreatureSay cs; + + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + if ((currPetition.getPetitioner() != null) && (currPetition.getPetitioner().getObjectId() == player.getObjectId())) { + cs = new CreatureSay(player.getObjectId(), Say2.PETITION_PLAYER, player.getName(), messageText); + currPetition.addLogMessage(cs); + + currPetition.sendResponderPacket(cs); + currPetition.sendPetitionerPacket(cs); + return true; + } + + if ((currPetition.getResponder() != null) && (currPetition.getResponder().getObjectId() == player.getObjectId())) { + cs = new CreatureSay(player.getObjectId(), Say2.PETITION_GM, player.getName(), messageText); + currPetition.addLogMessage(cs); + + currPetition.sendResponderPacket(cs); + currPetition.sendPetitionerPacket(cs); + return true; + } + } + + return false; + } + + public void sendPendingPetitionList(L2PcInstance activeChar) { + final StringBuilder htmlContent = StringUtil.startAppend(600 + (getPendingPetitionCount() * 300), "
    " + + "" + "" + + "
    Petition Menu

    " + "" + + ""); + + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + if (getPendingPetitionCount() == 0) { + htmlContent.append(""); + } else { + htmlContent.append(""); + } + + boolean color = true; + int petcount = 0; + for (Petition currPetition : getPendingPetitions().values()) { + if (currPetition == null) { + continue; + } + + StringUtil.append(htmlContent, ""); + color = !color; + petcount++; + if (petcount > 10) { + htmlContent.append(""); + break; + } + } + + htmlContent.append("
    " + + "

    There are no currently pending petitions.
    Current Petitions:
    "); + StringUtil.append(htmlContent, "", currPetition.getTypeAsString(), "
    ", dateFormat.format(new Date(currPetition.getSubmitTime()))); + StringUtil.append(htmlContent, "", currPetition.getPetitioner().getName(), "
    "); + if (currPetition.getState() != PetitionState.IN_PROCESS) { + StringUtil.append(htmlContent, "" + "" + + "
    "); + } else { + htmlContent.append("").append(currPetition.getResponder().getName()).append(""); + } + StringUtil.append(htmlContent, "
    ", currPetition.getTypeAsString(), "
    There is more pending petition...
    "); + + final NpcHtmlMessage htmlMsg = new NpcHtmlMessage(); + htmlMsg.setHtml(htmlContent.toString()); + activeChar.sendPacket(htmlMsg); + } + + public int submitPetition(L2PcInstance petitioner, String petitionText, int petitionType) { + // Create a new petition instance and add it to the list of pending petitions. + final Petition newPetition = new Petition(petitioner, petitionText, petitionType); + int newPetitionId = newPetition.getId(); + getPendingPetitions().put(newPetitionId, newPetition); + + // Notify all GMs that a new petition has been submitted. + final String msgContent = petitioner.getName() + " has submitted a new petition."; // (ID: " + newPetitionId + ")."; + AdminData.getInstance().broadcastToGMs(new CreatureSay(petitioner.getObjectId(), Say2.HERO_VOICE, "Petition System", msgContent)); + + return newPetitionId; + } + + public void viewPetition(L2PcInstance activeChar, int petitionId) { + if (!activeChar.isGM()) { + return; + } + + if (!isValidPetition(petitionId)) { + return; + } + + final Petition currPetition = getPendingPetitions().get(petitionId); + final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + final NpcHtmlMessage html = new NpcHtmlMessage(); + html.setFile(activeChar.getHtmlPrefix(), "data/html/admin/petition.htm"); + html.replace("%petition%", String.valueOf(currPetition.getId())); + html.replace("%time%", dateFormat.format(new Date(currPetition.getSubmitTime()))); + html.replace("%type%", currPetition.getTypeAsString()); + html.replace("%petitioner%", currPetition.getPetitioner().getName()); + html.replace("%online%", (currPetition.getPetitioner().isOnline() ? "00FF00" : "999999")); + html.replace("%text%", currPetition.getContent()); + + activeChar.sendPacket(html); + } + + /** + * Gets the single instance of {@code PetitionManager}. + * @return single instance of {@code PetitionManager} + */ + public static PetitionManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final PetitionManager _instance = new PetitionManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/PunishmentManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/PunishmentManager.java new file mode 100644 index 00000000..20dc13e7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/PunishmentManager.java @@ -0,0 +1,120 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.holders.PunishmentHolder; +import com.l2jserver.gameserver.model.punishment.PunishmentAffect; +import com.l2jserver.gameserver.model.punishment.PunishmentTask; +import com.l2jserver.gameserver.model.punishment.PunishmentType; + +/** + * Punishment Manager. + * @author UnAfraid + */ +public final class PunishmentManager { + + private static final Logger LOG = LoggerFactory.getLogger(PunishmentManager.class); + + private final Map _tasks = new ConcurrentHashMap<>(); + + protected PunishmentManager() { + load(); + } + + private void load() { + // Initiate task holders. + for (PunishmentAffect affect : PunishmentAffect.values()) { + _tasks.put(affect, new PunishmentHolder()); + } + + int initiated = 0; + int expired = 0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var st = con.createStatement(); + var rs = st.executeQuery("SELECT * FROM punishments")) { + while (rs.next()) { + final int id = rs.getInt("id"); + final String key = rs.getString("key"); + final PunishmentAffect affect = PunishmentAffect.getByName(rs.getString("affect")); + final PunishmentType type = PunishmentType.getByName(rs.getString("type")); + final long expirationTime = rs.getLong("expiration"); + final String reason = rs.getString("reason"); + final String punishedBy = rs.getString("punishedBy"); + if ((type != null) && (affect != null)) { + if ((expirationTime > 0) && (System.currentTimeMillis() > expirationTime)) // expired task. + { + expired++; + } else { + initiated++; + _tasks.get(affect).addPunishment(new PunishmentTask(id, key, affect, type, expirationTime, reason, punishedBy, true)); + } + } + } + } catch (Exception ex) { + LOG.warn("There has been an error while loading punishments!", ex); + } + + LOG.info("Loaded {} active and {} expired punishments.", initiated, expired); + } + + public void startPunishment(PunishmentTask task) { + _tasks.get(task.getAffect()).addPunishment(task); + } + + public void stopPunishment(Object key, PunishmentAffect affect, PunishmentType type) { + final PunishmentTask task = getPunishment(key, affect, type); + if (task != null) { + _tasks.get(affect).stopPunishment(task); + } + } + + public boolean hasPunishment(Object key, PunishmentAffect affect, PunishmentType type) { + final PunishmentHolder holder = _tasks.get(affect); + return holder.hasPunishment(String.valueOf(key), type); + } + + public long getPunishmentExpiration(Object key, PunishmentAffect affect, PunishmentType type) { + final PunishmentTask p = getPunishment(key, affect, type); + return p != null ? p.getExpirationTime() : 0; + } + + private PunishmentTask getPunishment(Object key, PunishmentAffect affect, PunishmentType type) { + return _tasks.get(affect).getPunishment(String.valueOf(key), type); + } + + /** + * Gets the single instance of {@code PunishmentManager}. + * @return single instance of {@code PunishmentManager} + */ + public static PunishmentManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final PunishmentManager _instance = new PunishmentManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/QuestManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/QuestManager.java new file mode 100644 index 00000000..2a9d95a7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/QuestManager.java @@ -0,0 +1,244 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.server; + +import java.io.File; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Util; +import com.l2jserver.gameserver.model.quest.Quest; +import com.l2jserver.gameserver.scripting.ScriptEngineManager; +import com.l2jserver.gameserver.scripting.ScriptManager; + +/** + * Quests and scripts manager. + * @author Zoey76 + */ +public final class QuestManager extends ScriptManager { + + private static final Logger LOG = LoggerFactory.getLogger(QuestManager.class); + + /** Map containing all the quests. */ + private final Map _quests = new ConcurrentHashMap<>(); + /** Map containing all the scripts. */ + private final Map _scripts = new ConcurrentHashMap<>(); + + protected QuestManager() { + // Prevent initialization. + } + + public boolean reload(String questFolder) { + final Quest q = getQuest(questFolder); + if (q == null) { + return false; + } + return q.reload(); + } + + /** + * Reloads a the quest by ID. + * @param questId the ID of the quest to be reloaded + * @return {@code true} if reload was successful, {@code false} otherwise + */ + public boolean reload(int questId) { + final Quest q = getQuest(questId); + if (q == null) { + return false; + } + return q.reload(); + } + + /** + * Reload all quests and scripts.
    + * Unload all quests and scripts and load scripts.cfg. + */ + public void reloadAllScripts() { + LOG.info("Reloading all server scripts."); + + // Unload quests. + for (Quest quest : _quests.values()) { + if (quest != null) { + quest.unload(false); + } + } + + _quests.clear(); + + // Unload scripts. + for (Quest script : _scripts.values()) { + if (script != null) { + script.unload(false); + } + } + _scripts.clear(); + + try { + ScriptEngineManager.getInstance().executeScriptList(new File(server().getDatapackRoot(), "data/scripts.cfg")); + } catch (Exception ex) { + LOG.warn("Failed loading scripts.cfg, no script going to be loaded!", ex); + } + + LOG.info("Loaded {} quests.", _quests.size()); + LOG.info("Loaded {} scripts.", _scripts.size()); + } + + /** + * Calls {@link Quest#saveGlobalData()} in all quests and scripts. + */ + public void save() { + // Save quests. + for (Quest quest : _quests.values()) { + quest.saveGlobalData(); + } + + // Save scripts. + for (Quest script : _scripts.values()) { + script.saveGlobalData(); + } + } + + /** + * Gets a quest by name.
    + * For backwards compatibility, verifies scripts with the given name if the quest is not found. + * @param name the quest name + * @return the quest + */ + public Quest getQuest(String name) { + if (_quests.containsKey(name)) { + return _quests.get(name); + } + return _scripts.get(name); + } + + /** + * Gets a quest by ID. + * @param questId the ID of the quest to get + * @return if found, the quest, {@code null} otherwise + */ + public Quest getQuest(int questId) { + for (Quest q : _quests.values()) { + if (q.getId() == questId) { + return q; + } + } + return null; + } + + /** + * Adds a new quest. + * @param quest the quest to be added + */ + public void addQuest(Quest quest) { + if (quest == null) { + throw new IllegalArgumentException("Quest argument cannot be null"); + } + + // FIXME: unloading the old quest at this point is a tad too late. + // the new quest has already initialized itself and read the data, starting + // an unpredictable number of tasks with that data. The old quest will now + // save data which will never be read. + // However, requesting the newQuest to re-read the data is not necessarily a + // good option, since the newQuest may have already started timers, spawned NPCs + // or taken any other action which it might re-take by re-reading the data. + // the current solution properly closes the running tasks of the old quest but + // ignores the data; perhaps the least of all evils... + final Quest old = _quests.put(quest.getName(), quest); + if (old != null) { + old.unload(); + LOG.info("Replaced quest {} with a new version.", old); + + } + + if (general().showQuestsLoadInLogs()) { + final String questName = quest.getName().contains("_") ? quest.getName().substring(quest.getName().indexOf('_') + 1) : quest.getName(); + LOG.info("Loaded quest {}.", Util.splitWords(questName)); + } + } + + /** + * Removes a script. + * @param script the script to remove + * @return {@code true} if the script was removed, {@code false} otherwise + */ + public boolean removeScript(Quest script) { + if (_quests.containsKey(script.getName())) { + _quests.remove(script.getName()); + return true; + } else if (_scripts.containsKey(script.getName())) { + _scripts.remove(script.getName()); + return true; + } + return false; + } + + public Map getQuests() { + return _quests; + } + + @Override + public boolean unload(Quest ms) { + ms.saveGlobalData(); + return removeScript(ms); + } + + @Override + public String getScriptManagerName() { + return getClass().getSimpleName(); + } + + /** + * Gets all the registered scripts. + * @return all the scripts + */ + @Override + public Map getScripts() { + return _scripts; + } + + /** + * Adds a script. + * @param script the script to be added + */ + public void addScript(Quest script) { + final Quest old = _scripts.put(script.getClass().getSimpleName(), script); + if (old != null) { + old.unload(); + LOG.info("Replaced script {} with a new version.", old); + } + + if (general().showQuestsLoadInLogs()) { + LOG.info("Loaded script {}.", Util.splitWords(script.getClass().getSimpleName())); + } + } + + public static QuestManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final QuestManager INSTANCE = new QuestManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossPointsManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossPointsManager.java new file mode 100644 index 00000000..9a2b7fd9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossPointsManager.java @@ -0,0 +1,144 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author Kerberos + * @authir JIV + * @version 8/24/10 + */ +public class RaidBossPointsManager { + + private static final Logger LOG = LoggerFactory.getLogger(RaidBossPointsManager.class); + + private final Map> _list = new ConcurrentHashMap<>(); + + public RaidBossPointsManager() { + init(); + } + + private void init() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT `charId`,`boss_id`,`points` FROM `character_raid_points`")) { + while (rs.next()) { + final var charId = rs.getInt("charId"); + final var bossId = rs.getInt("boss_id"); + final var points = rs.getInt("points"); + _list.computeIfAbsent(charId, _ -> new ConcurrentHashMap<>()).put(bossId, points); + } + LOG.info("Loaded {} characters raid points.", _list.size()); + } catch (Exception ex) { + LOG.warn("Couldn't load character raid points!", ex); + } + } + + public final void updatePointsInDB(L2PcInstance player, int raidId, int points) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO character_raid_points (`charId`,`boss_id`,`points`) VALUES (?,?,?)")) { + ps.setInt(1, player.getObjectId()); + ps.setInt(2, raidId); + ps.setInt(3, points); + ps.executeUpdate(); + } catch (Exception ex) { + LOG.warn("Could not update character raid points for player {}!", player, ex); + } + } + + public final void addPoints(L2PcInstance player, int bossId, int points) { + final var tmpPoint = _list.computeIfAbsent(player.getObjectId(), _ -> new ConcurrentHashMap<>()); + updatePointsInDB(player, bossId, tmpPoint.merge(bossId, points, Integer::sum)); + } + + public final int getPointsByOwnerId(int ownerId) { + final var tmpPoint = _list.get(ownerId); + var totalPoints = 0; + + if ((tmpPoint == null) || tmpPoint.isEmpty()) { + return 0; + } + + for (int points : tmpPoint.values()) { + totalPoints += points; + } + return totalPoints; + } + + public final Map getList(L2PcInstance player) { + return _list.get(player.getObjectId()); + } + + public final void cleanUp() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement()) { + s.executeUpdate("DELETE from character_raid_points WHERE charId > 0"); + _list.clear(); + } catch (Exception ex) { + LOG.warn("Could not clean character raid points!", ex); + } + } + + public final int calculateRanking(int playerObjId) { + final var rank = getRankList(); + if (rank.containsKey(playerObjId)) { + return rank.get(playerObjId); + } + return 0; + } + + public Map getRankList() { + final var tmpPoints = new HashMap(); + for (var ownerId : _list.keySet()) { + final var totalPoints = getPointsByOwnerId(ownerId); + if (totalPoints != 0) { + tmpPoints.put(ownerId, totalPoints); + } + } + + final var list = new ArrayList<>(tmpPoints.entrySet()); + list.sort(Comparator.comparing(Entry::getValue).reversed()); + var ranking = 1; + final var tmpRanking = new HashMap(); + for (var entry : list) { + tmpRanking.put(entry.getKey(), ranking++); + } + return tmpRanking; + } + + public static RaidBossPointsManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final RaidBossPointsManager _instance = new RaidBossPointsManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossSpawnManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossSpawnManager.java new file mode 100644 index 00000000..aba072d4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/RaidBossSpawnManager.java @@ -0,0 +1,458 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.npc; + +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.config.Configuration; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2RaidBossInstance; + +/** + * Raid Boss spawn manager. + * @author godson + */ +public class RaidBossSpawnManager { + + private static final Logger LOG = LoggerFactory.getLogger(RaidBossSpawnManager.class); + + private static final int EILHALDER_VON_HELLMANN = 25328; + + protected static final Map _bosses = new ConcurrentHashMap<>(); + + protected static final Map _spawns = new ConcurrentHashMap<>(); + + protected static final Map _storedInfo = new ConcurrentHashMap<>(); + + protected static final Map> _schedules = new ConcurrentHashMap<>(); + + public enum StatusEnum { + ALIVE, + DEAD, + UNDEFINED + } + + protected RaidBossSpawnManager() { + load(); + } + + public void load() { + LOG.info("Spawning raid bosses..."); + + _bosses.clear(); + _spawns.clear(); + _storedInfo.clear(); + _schedules.clear(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM raidboss_spawnlist ORDER BY boss_id")) { + while (rs.next()) { + final L2Spawn spawnDat = new L2Spawn(rs.getInt("boss_id")); + spawnDat.setX(rs.getInt("loc_x")); + spawnDat.setY(rs.getInt("loc_y")); + spawnDat.setZ(rs.getInt("loc_z")); + spawnDat.setAmount(rs.getInt("amount")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(rs.getInt("respawn_delay"), rs.getInt("respawn_random")); + + addNewSpawn(spawnDat, rs.getLong("respawn_time"), rs.getDouble("currentHP"), rs.getDouble("currentMP"), false); + } + + LOG.info("Loaded {} bosses.", _bosses.size()); + LOG.info("Scheduled {} boss instances.", _schedules.size()); + } catch (Exception ex) { + LOG.warn("There has been an error while initializing raid boss spawn manager!", ex); + } + } + + private static class SpawnSchedule implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(SpawnSchedule.class); + + private final int bossId; + + public SpawnSchedule(int npcId) { + bossId = npcId; + } + + @Override + public void run() { + L2RaidBossInstance raidBoss; + if (bossId == EILHALDER_VON_HELLMANN) { + raidBoss = DayNightSpawnManager.getInstance().handleBoss(_spawns.get(bossId)); + } else { + raidBoss = (L2RaidBossInstance) _spawns.get(bossId).doSpawn(); + } + + if (raidBoss != null) { + raidBoss.setRaidStatus(StatusEnum.ALIVE); + + final StatsSet info = new StatsSet(); + info.set("currentHP", raidBoss.getCurrentHp()); + info.set("currentMP", raidBoss.getCurrentMp()); + info.set("respawnTime", 0L); + + _storedInfo.put(bossId, info); + + LOG.info("Spawning Raid Boss {}.", raidBoss.getName()); + + _bosses.put(bossId, raidBoss); + } + + _schedules.remove(bossId); + } + } + + /** + * Update status. + * @param boss the boss + * @param isBossDead the is boss dead + */ + public void updateStatus(L2RaidBossInstance boss, boolean isBossDead) { + final StatsSet info = _storedInfo.get(boss.getId()); + if (info == null) { + return; + } + + if (isBossDead) { + boss.setRaidStatus(StatusEnum.DEAD); + + final int respawnMinDelay = (int) (boss.getSpawn().getRespawnMinDelay() * npc().getRaidMinRespawnMultiplier()); + final int respawnMaxDelay = (int) (boss.getSpawn().getRespawnMaxDelay() * npc().getRaidMaxRespawnMultiplier()); + final int respawnDelay = Rnd.get(respawnMinDelay, respawnMaxDelay); + final long respawnTime = Calendar.getInstance().getTimeInMillis() + respawnDelay; + + info.set("currentHP", boss.getMaxHp()); + info.set("currentMP", boss.getMaxMp()); + info.set("respawnTime", respawnTime); + + if (!_schedules.containsKey(boss.getId()) && ((respawnMinDelay > 0) || (respawnMaxDelay > 0))) { + final Calendar time = Calendar.getInstance(); + time.setTimeInMillis(respawnTime); + LOG.info("Updated {} respawn time to {}.", boss.getName(), time.getTime()); + + _schedules.put(boss.getId(), ThreadPoolManager.getInstance().scheduleGeneral(new SpawnSchedule(boss.getId()), respawnDelay)); + updateDb(); + } + } else { + boss.setRaidStatus(StatusEnum.ALIVE); + + info.set("currentHP", boss.getCurrentHp()); + info.set("currentMP", boss.getCurrentMp()); + info.set("respawnTime", 0L); + } + _storedInfo.put(boss.getId(), info); + } + + /** + * Adds the new spawn. + * @param spawnDat the spawn dat + * @param respawnTime the respawn time + * @param currentHP the current hp + * @param currentMP the current mp + * @param storeInDb the store in db + */ + public void addNewSpawn(L2Spawn spawnDat, long respawnTime, double currentHP, double currentMP, boolean storeInDb) { + if (spawnDat == null) { + return; + } + if (_spawns.containsKey(spawnDat.getId())) { + return; + } + + final int bossId = spawnDat.getId(); + final long time = Calendar.getInstance().getTimeInMillis(); + + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + + if ((respawnTime == 0L) || (time > respawnTime)) { + L2RaidBossInstance raidBoss; + if (bossId == EILHALDER_VON_HELLMANN) { + raidBoss = DayNightSpawnManager.getInstance().handleBoss(spawnDat); + } else { + raidBoss = (L2RaidBossInstance) spawnDat.doSpawn(); + } + + if (raidBoss != null) { + raidBoss.setCurrentHp(currentHP); + raidBoss.setCurrentMp(currentMP); + raidBoss.setRaidStatus(StatusEnum.ALIVE); + + _bosses.put(bossId, raidBoss); + + final StatsSet info = new StatsSet(); + info.set("currentHP", currentHP); + info.set("currentMP", currentMP); + info.set("respawnTime", 0L); + + _storedInfo.put(bossId, info); + } + } else { + final long spawnTime = respawnTime - Calendar.getInstance().getTimeInMillis(); + _schedules.put(bossId, ThreadPoolManager.getInstance().scheduleGeneral(new SpawnSchedule(bossId), spawnTime)); + } + + _spawns.put(bossId, spawnDat); + + if (storeInDb) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO raidboss_spawnlist (boss_id,amount,loc_x,loc_y,loc_z,heading,respawn_time,currentHp,currentMp) VALUES(?,?,?,?,?,?,?,?,?)")) { + ps.setInt(1, spawnDat.getId()); + ps.setInt(2, spawnDat.getAmount()); + ps.setInt(3, spawnDat.getX()); + ps.setInt(4, spawnDat.getY()); + ps.setInt(5, spawnDat.getZ()); + ps.setInt(6, spawnDat.getHeading()); + ps.setLong(7, respawnTime); + ps.setDouble(8, currentHP); + ps.setDouble(9, currentMP); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Could not store raid boss Id {} in the DB!", bossId, ex); + } + } + } + + /** + * Delete spawn. + * @param spawnDat the spawn dat + * @param updateDb the update db + */ + public void deleteSpawn(L2Spawn spawnDat, boolean updateDb) { + if (spawnDat == null) { + return; + } + + final int bossId = spawnDat.getId(); + if (!_spawns.containsKey(bossId)) { + return; + } + + SpawnTable.getInstance().deleteSpawn(spawnDat, false); + _spawns.remove(bossId); + + _bosses.remove(bossId); + + final ScheduledFuture f = _schedules.remove(bossId); + if (f != null) { + f.cancel(true); + } + + _storedInfo.remove(bossId); + + if (updateDb) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM raidboss_spawnlist WHERE boss_id=?")) { + ps.setInt(1, bossId); + ps.execute(); + } catch (Exception ex) { + LOG.warn("Could not remove raid boss Id {} from DB!", bossId, ex); + } + } + } + + private void updateDb() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE raidboss_spawnlist SET respawn_time = ?, currentHP = ?, currentMP = ? WHERE boss_id = ?")) { + for (Integer bossId : _storedInfo.keySet()) { + if (bossId == null) { + continue; + } + + L2RaidBossInstance boss = _bosses.get(bossId); + + if (boss == null) { + continue; + } + + if (boss.getRaidStatus().equals(StatusEnum.ALIVE)) { + updateStatus(boss, false); + } + + StatsSet info = _storedInfo.get(bossId); + + if (info == null) { + continue; + } + + try { + // TODO(Zoey76): Change this to use batch. + ps.setLong(1, info.getLong("respawnTime")); + ps.setDouble(2, info.getDouble("currentHP")); + ps.setDouble(3, info.getDouble("currentMP")); + ps.setInt(4, bossId); + ps.executeUpdate(); + ps.clearParameters(); + } catch (Exception ex) { + LOG.warn("Could not update raid boss spawn list table!", ex); + } + } + } catch (Exception ex) { + LOG.warn("SQL error while updating Raid Boss spawn to database!", ex); + } + } + + /** + * Gets the all raid boss status. + * @return the all raid boss status + */ + public String[] getAllRaidBossStatus() { + final String[] msg = new String[Math.max(_bosses.size(), 1)]; + if (_bosses.isEmpty()) { + msg[0] = "None"; + return msg; + } + + int index = 0; + for (var boss : _bosses.values()) { + msg[index++] = boss.getName() + ": " + boss.getRaidStatus().name(); + } + return msg; + } + + /** + * Gets the raid boss status. + * @param bossId the boss id + * @return the raid boss status + */ + public String getRaidBossStatus(int bossId) { + String msg = "RaidBoss Status..." + Configuration.EOL; + + if (_bosses == null) { + msg += "None"; + return msg; + } + + if (_bosses.containsKey(bossId)) { + final L2RaidBossInstance boss = _bosses.get(bossId); + + msg += boss.getName() + ": " + boss.getRaidStatus().name(); + } + + return msg; + } + + /** + * Gets the raid boss status id. + * @param bossId the boss id + * @return the raid boss status id + */ + public StatusEnum getRaidBossStatusId(int bossId) { + if (_bosses.containsKey(bossId)) { + return _bosses.get(bossId).getRaidStatus(); + } else if (_schedules.containsKey(bossId)) { + return StatusEnum.DEAD; + } else { + return StatusEnum.UNDEFINED; + } + } + + /** + * Notify spawn night boss. + * @param raidBoss the raid boss + */ + public void notifySpawnNightBoss(L2RaidBossInstance raidBoss) { + final StatsSet info = new StatsSet(); + info.set("currentHP", raidBoss.getCurrentHp()); + info.set("currentMP", raidBoss.getCurrentMp()); + info.set("respawnTime", 0L); + + raidBoss.setRaidStatus(StatusEnum.ALIVE); + + _storedInfo.put(raidBoss.getId(), info); + + LOG.info("Spawning Night Raid Boss {}.", raidBoss.getName()); + + _bosses.put(raidBoss.getId(), raidBoss); + } + + /** + * Checks if the boss is defined. + * @param bossId the boss id + * @return {@code true} if is defined + */ + public boolean isDefined(int bossId) { + return _spawns.containsKey(bossId); + } + + /** + * Gets the bosses. + * @return the bosses + */ + public Map getBosses() { + return _bosses; + } + + /** + * Gets the spawns. + * @return the spawns + */ + public Map getSpawns() { + return _spawns; + } + + /** + * Gets the stored info. + * @return the stored info + */ + public Map getStoredInfo() { + return _storedInfo; + } + + /** + * Saves and clears the raid bosses status, including all schedules. + */ + public void cleanUp() { + updateDb(); + + _bosses.clear(); + + if (_schedules != null) { + for (Integer bossId : _schedules.keySet()) { + ScheduledFuture f = _schedules.get(bossId); + f.cancel(true); + } + _schedules.clear(); + } + + _storedInfo.clear(); + _spawns.clear(); + } + + public static RaidBossSpawnManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final RaidBossSpawnManager INSTANCE = new RaidBossSpawnManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeGuardManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeGuardManager.java new file mode 100644 index 00000000..cf702165 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeGuardManager.java @@ -0,0 +1,187 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; + +public final class SiegeGuardManager { + private static final Logger LOG = LoggerFactory.getLogger(SiegeGuardManager.class); + + private final Castle _castle; + + private final List _siegeGuardSpawn = new ArrayList<>(); + + public SiegeGuardManager(Castle castle) { + _castle = castle; + } + + public void addSiegeGuard(L2PcInstance activeChar, int npcId) { + if (activeChar == null) { + return; + } + addSiegeGuard(activeChar.getX(), activeChar.getY(), activeChar.getZ(), activeChar.getHeading(), npcId); + } + + private void addSiegeGuard(int x, int y, int z, int heading, int npcId) { + saveSiegeGuard(x, y, z, heading, npcId, 0); + } + + public void hireMerc(L2PcInstance activeChar, int npcId) { + if (activeChar == null) { + return; + } + hireMerc(activeChar.getX(), activeChar.getY(), activeChar.getZ(), activeChar.getHeading(), npcId); + } + + public void hireMerc(int x, int y, int z, int heading, int npcId) { + saveSiegeGuard(x, y, z, heading, npcId, 1); + } + + /** + * Remove a single mercenary, identified by the npcId and location. Presumably, this is used when a castle lord picks up a previously dropped ticket + */ + public void removeMerc(int npcId, int x, int y, int z) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM castle_siege_guards WHERE npcId = ? AND x = ? AND y = ? AND z = ? AND isHired = 1")) { + ps.setInt(1, npcId); + ps.setInt(2, x); + ps.setInt(3, y); + ps.setInt(4, z); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error deleting hired siege guard at {},{},{}: {}", x, y, z, e.getMessage(), e); + } + } + + /** + * Remove mercs. + */ + public void removeMercs() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM castle_siege_guards WHERE castleId = ? AND isHired = 1")) { + ps.setInt(1, getCastle().getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error deleting hired siege guard for castle {}: {}", getCastle().getName(), e.getMessage(), e); + } + } + + /** + * Spawn guards. + */ + public void spawnSiegeGuard() { + try { + int hiredCount = 0, hiredMax = MercTicketManager.getInstance().getMaxAllowedMerc(_castle.getResidenceId()); + boolean isHired = getCastle().getOwnerId() > 0; + loadSiegeGuard(); + for (L2Spawn spawn : _siegeGuardSpawn) { + if (spawn != null) { + spawn.init(); + if (isHired) { + spawn.stopRespawn(); + if (++hiredCount > hiredMax) { + return; + } + } + } + } + } catch (Exception e) { + LOG.error("Error spawning siege guards for castle {}", getCastle().getName(), e); + } + } + + /** + * Unspawn guards. + */ + public void unspawnSiegeGuard() { + for (L2Spawn spawn : _siegeGuardSpawn) { + if ((spawn != null) && (spawn.getLastSpawn() != null)) { + spawn.stopRespawn(); + spawn.getLastSpawn().doDie(spawn.getLastSpawn()); + } + } + + _siegeGuardSpawn.clear(); + } + + /** + * Load guards. + */ + private void loadSiegeGuard() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM castle_siege_guards WHERE castleId = ? AND isHired = ?")) { + ps.setInt(1, getCastle().getResidenceId()); + if (getCastle().getOwnerId() > 0) { + ps.setInt(2, 1); + } else { + ps.setInt(2, 0); + } + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final L2Spawn spawn = new L2Spawn(rs.getInt("npcId")); + spawn.setAmount(1); + spawn.setX(rs.getInt("x")); + spawn.setY(rs.getInt("y")); + spawn.setZ(rs.getInt("z")); + spawn.setHeading(rs.getInt("heading")); + spawn.setRespawnDelay(rs.getInt("respawnDelay")); + spawn.setLocationId(0); + + _siegeGuardSpawn.add(spawn); + } + } + } catch (Exception e) { + LOG.warn("Error loading siege guard for castle {}: {}", getCastle().getName(), e.getMessage(), e); + } + } + + private void saveSiegeGuard(int x, int y, int z, int heading, int npcId, int isHire) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO castle_siege_guards (castleId, npcId, x, y, z, heading, respawnDelay, isHired) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { + ps.setInt(1, getCastle().getResidenceId()); + ps.setInt(2, npcId); + ps.setInt(3, x); + ps.setInt(4, y); + ps.setInt(5, z); + ps.setInt(6, heading); + ps.setInt(7, (isHire == 1 ? 0 : 600)); + ps.setInt(8, isHire); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error adding siege guard for castle {}: {}", getCastle().getName(), e.getMessage(), e); + } + } + + public Castle getCastle() { + return _castle; + } + + public List getSiegeGuardSpawn() { + return _siegeGuardSpawn; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeManager.java new file mode 100644 index 00000000..8a8432af --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/SiegeManager.java @@ -0,0 +1,250 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.siege; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.TowerSpawn; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Siege; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.skills.Skill; + +public final class SiegeManager { + + private static final Logger LOG = LoggerFactory.getLogger(SiegeManager.class); + + private final Map> _controlTowers = new HashMap<>(); + + private final Map> _flameTowers = new HashMap<>(); + + protected SiegeManager() { + load(); + } + + public void addSiegeSkills(L2PcInstance character) { + for (Skill sk : SkillData.getInstance().getSiegeSkills(character.isNoble(), character.getClan().getCastleId() > 0)) { + character.addSkill(sk, false); + } + } + + /** + * @param clan The L2Clan of the player + * @param castleId + * @return true if the clan is registered or owner of a castle + */ + public boolean checkIsRegistered(L2Clan clan, int castleId) { + if (clan == null) { + return false; + } + + if (clan.getCastleId() > 0) { + return true; + } + + boolean register = false; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT clan_id FROM siege_clans where clan_id=? and castle_id=?")) { + ps.setInt(1, clan.getId()); + ps.setInt(2, castleId); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + register = true; + } + } + } catch (Exception ex) { + LOG.warn("There has been an error verifying if the clan is registered to the siege!", ex); + } + return register; + } + + public void removeSiegeSkills(L2PcInstance character) { + for (Skill sk : SkillData.getInstance().getSiegeSkills(character.isNoble(), character.getClan().getCastleId() > 0)) { + character.removeSkill(sk); + } + } + + private void load() { + // Gludio + _controlTowers.put(1, List.of(siege().getGludioControlTower1(), // + siege().getGludioControlTower2(), // + siege().getGludioControlTower3())); + MercTicketManager.MERCS_MAX_PER_CASTLE[0] = siege().getGludioMaxMercenaries(); + _flameTowers.put(1, List.of(siege().getGludioFlameTower1(), // + siege().getGludioFlameTower1())); + // Dion + _controlTowers.put(2, List.of(siege().getDionControlTower1(), // + siege().getDionControlTower2(), // + siege().getDionControlTower3())); + _flameTowers.put(2, List.of(siege().getDionFlameTower1(), // + siege().getDionFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[1] = siege().getDionMaxMercenaries(); + // Giran + _controlTowers.put(3, List.of(siege().getGiranControlTower1(), // + siege().getGiranControlTower2(), // + siege().getGiranControlTower3())); + _flameTowers.put(3, List.of(siege().getGiranFlameTower1(), // + siege().getGiranFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[2] = siege().getGiranMaxMercenaries(); + // Oren + _controlTowers.put(4, List.of(siege().getOrenControlTower1(), // + siege().getOrenControlTower2(), // + siege().getOrenControlTower3())); + _flameTowers.put(4, List.of(siege().getOrenFlameTower1(), // + siege().getOrenFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[3] = siege().getOrenMaxMercenaries(); + // Aden + _controlTowers.put(5, List.of(siege().getAdenControlTower1(), // + siege().getAdenControlTower2(), // + siege().getAdenControlTower3())); + _flameTowers.put(5, List.of(siege().getAdenFlameTower1(), // + siege().getAdenFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[4] = siege().getAdenMaxMercenaries(); + // Innadril + _controlTowers.put(6, List.of(siege().getInnadrilControlTower1(), // + siege().getInnadrilControlTower2(), // + siege().getInnadrilControlTower3())); + _flameTowers.put(6, List.of(siege().getInnadrilFlameTower1(), // + siege().getInnadrilFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[5] = siege().getInnadrilMaxMercenaries(); + // Goddard + _controlTowers.put(7, List.of(siege().getGoddardControlTower1(), // + siege().getGoddardControlTower2(), // + siege().getGoddardControlTower3())); + _flameTowers.put(7, List.of(siege().getGoddardFlameTower1(), // + siege().getGoddardFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[6] = siege().getGoddardMaxMercenaries(); + // Rune + _controlTowers.put(8, List.of(siege().getRuneControlTower1(), // + siege().getRuneControlTower2(), // + siege().getRuneControlTower3())); + _flameTowers.put(8, List.of(siege().getRuneFlameTower1(), // + siege().getRuneFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[7] = siege().getRuneMaxMercenaries(); + // Schuttgart + _controlTowers.put(9, List.of(siege().getSchuttgartControlTower1(), // + siege().getSchuttgartControlTower2(), // + siege().getSchuttgartControlTower3())); + _flameTowers.put(9, List.of(siege().getSchuttgartFlameTower1(), // + siege().getSchuttgartFlameTower1())); + MercTicketManager.MERCS_MAX_PER_CASTLE[8] = siege().getSchuttgartMaxMercenaries(); + + for (Castle castle : CastleManager.getInstance().getCastles()) { + if (castle.getOwnerId() != 0) { + loadTrapUpgrade(castle.getResidenceId()); + } + } + } + + public List getControlTowers(int castleId) { + return _controlTowers.get(castleId); + } + + public List getFlameTowers(int castleId) { + return _flameTowers.get(castleId); + } + + public int getAttackerMaxClans() { + return siege().getAttackerMaxClans(); + } + + public int getAttackerRespawnDelay() { + return siege().getAttackerRespawn(); + } + + public int getDefenderMaxClans() { + return siege().getDefenderMaxClans(); + } + + public int getFlagMaxCount() { + return siege().getMaxFlags(); + } + + public Siege getSiege(ILocational loc) { + return getSiege(loc.getX(), loc.getY(), loc.getZ()); + } + + public Siege getSiege(L2Object activeObject) { + return getSiege(activeObject.getX(), activeObject.getY(), activeObject.getZ()); + } + + public Siege getSiege(int x, int y, int z) { + for (Castle castle : CastleManager.getInstance().getCastles()) { + if (castle.getSiege().checkIfInZone(x, y, z)) { + return castle.getSiege(); + } + } + return null; + } + + public int getSiegeClanMinLevel() { + return siege().getClanMinLevel(); + } + + public int getSiegeLength() { + return siege().getSiegeLength(); + } + + public int getBloodAllianceReward() { + return siege().getBloodAllianceReward(); + } + + public List getSieges() { + List sieges = new ArrayList<>(); + for (Castle castle : CastleManager.getInstance().getCastles()) { + sieges.add(castle.getSiege()); + } + return sieges; + } + + private void loadTrapUpgrade(int castleId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM castle_trapupgrade WHERE castleId=?")) { + ps.setInt(1, castleId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _flameTowers.get(castleId).get(rs.getInt("towerIndex")).setUpgradeLevel(rs.getInt("level")); + } + } + } catch (Exception ex) { + LOG.warn("There has been an error loading trap upgrade!", ex); + } + } + + public static SiegeManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final SiegeManager _instance = new SiegeManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/TerritoryWarManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/TerritoryWarManager.java new file mode 100644 index 00000000..30c56011 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/TerritoryWarManager.java @@ -0,0 +1,1389 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.territoryWar; +import static java.util.concurrent.TimeUnit.MINUTES; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2SiegeClan; +import com.l2jserver.gameserver.model.L2SkillLearn; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TerritoryWard; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2SiegeFlagInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.Siegable; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.quest.Quest; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; +import com.l2jserver.gameserver.util.Util; + +public final class TerritoryWarManager implements Siegable { + private static final Logger LOG = LoggerFactory.getLogger(TerritoryWarManager.class); + + private static final String DELETE = "DELETE FROM territory_registrations WHERE castleId = ? and registeredId = ?"; + + private static final String INSERT = "INSERT INTO territory_registrations (castleId, registeredId) values (?, ?)"; + + public static final Map TERRITORY_ITEM_IDS = new HashMap<>(); + + private static final String qn = "TerritoryWarSuperClass"; + + public static String GLOBAL_VARIABLE = "nextTWStartDate"; + + // Territory War settings + private final Map> _registeredClans = new ConcurrentHashMap<>(); + private final Map> _registeredMercenaries = new ConcurrentHashMap<>(); + private final Map _territoryList = new ConcurrentHashMap<>(); + private final List _disguisedPlayers = new CopyOnWriteArrayList<>(); + private final List _territoryWards = new CopyOnWriteArrayList<>(); + private final Map _clanFlags = new ConcurrentHashMap<>(); + private final Map _participantPoints = new ConcurrentHashMap<>(); + private final Calendar _startTWDate = Calendar.getInstance(); + private boolean _isRegistrationOver = true; + private boolean _isTWChannelOpen = false; + private boolean _isTWInProgress = false; + private ScheduledFuture _scheduledStartTWTask = null; + private ScheduledFuture _scheduledEndTWTask = null; + private ScheduledFuture _scheduledRewardOnlineTask = null; + + static { + TERRITORY_ITEM_IDS.put(81, 13757); + TERRITORY_ITEM_IDS.put(82, 13758); + TERRITORY_ITEM_IDS.put(83, 13759); + TERRITORY_ITEM_IDS.put(84, 13760); + TERRITORY_ITEM_IDS.put(85, 13761); + TERRITORY_ITEM_IDS.put(86, 13762); + TERRITORY_ITEM_IDS.put(87, 13763); + TERRITORY_ITEM_IDS.put(88, 13764); + TERRITORY_ITEM_IDS.put(89, 13765); + } + + private TerritoryWarManager() { + load(); + } + + public int getRegisteredTerritoryId(L2PcInstance player) { + if ((player == null) || !_isTWChannelOpen || (player.getLevel() < territoryWar().getPlayerMinLevel())) { + return 0; + } + if (player.getClan() != null) { + if (player.getClan().getCastleId() > 0) { + return player.getClan().getCastleId() + 80; + } + for (int cId : _registeredClans.keySet()) { + if (_registeredClans.get(cId).contains(player.getClan())) { + return cId + 80; + } + } + } + for (int cId : _registeredMercenaries.keySet()) { + if (_registeredMercenaries.get(cId).contains(player.getObjectId())) { + return cId + 80; + } + } + return 0; + } + + public boolean isAllyField(L2PcInstance player, int fieldId) { + if ((player == null) || (player.getSiegeSide() == 0)) { + return false; + } else if ((player.getSiegeSide() - 80) == fieldId) { + return true; + } else if ((fieldId > 100) && _territoryList.containsKey((player.getSiegeSide() - 80)) && (_territoryList.get((player.getSiegeSide() - 80)).getFortId() == fieldId)) { + return true; + } + return false; + + } + + /** + * @param castleId id of the castle to check registration for + * @param clan The L2Clan of the player + * @return true if the clan is registered + */ + public boolean checkIsRegistered(int castleId, L2Clan clan) { + if (clan == null) { + return false; + } + + if (clan.getCastleId() > 0) { + return ((castleId == -1) || (clan.getCastleId() == castleId)); + } + + if (castleId == -1) { + for (int cId : _registeredClans.keySet()) { + if (_registeredClans.get(cId).contains(clan)) { + return true; + } + } + return false; + } + return _registeredClans.get(castleId).contains(clan); + } + + /** + * @param castleId + * @param objId + * @return true if the player is registered + */ + public boolean checkIsRegistered(int castleId, int objId) { + if (castleId == -1) { + for (int cId : _registeredMercenaries.keySet()) { + if (_registeredMercenaries.get(cId).contains(objId)) { + return true; + } + } + return false; + } + return _registeredMercenaries.get(castleId).contains(objId); + } + + public Territory getTerritory(int castleId) { + return _territoryList.get(castleId); + } + + public List getAllTerritories() { + List ret = new LinkedList<>(); + for (Territory t : _territoryList.values()) { + if (t.getOwnerClan() != null) { + ret.add(t); + } + } + return ret; + } + + public List getRegisteredClans(int castleId) { + return _registeredClans.get(castleId); + } + + public void addDisguisedPlayer(int playerObjId) { + _disguisedPlayers.add(playerObjId); + } + + public boolean isDisguised(int playerObjId) { + return _disguisedPlayers.contains(playerObjId); + } + + public List getRegisteredMercenaries(int castleId) { + return _registeredMercenaries.get(castleId); + } + + public long getTWStartTimeInMillis() { + return _startTWDate.getTimeInMillis(); + } + + public Calendar getTWStart() { + return _startTWDate; + } + + public void setTWStartTimeInMillis(long time) { + _startTWDate.setTimeInMillis(time); + if (_isTWInProgress) { + if (_scheduledEndTWTask != null) { + _scheduledEndTWTask.cancel(false); + } + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), 1000); + } else { + if (_scheduledStartTWTask != null) { + _scheduledStartTWTask.cancel(false); + } + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), 1000); + } + } + + public boolean isTWChannelOpen() { + return _isTWChannelOpen; + } + + public void registerClan(int castleId, L2Clan clan) { + if ((clan == null) || ((_registeredClans.get(castleId) != null) && _registeredClans.get(castleId).contains(clan))) { + return; + } + + _registeredClans.putIfAbsent(castleId, new CopyOnWriteArrayList<>()); + _registeredClans.get(castleId).add(clan); + changeRegistration(castleId, clan.getId(), false); + } + + public void registerMerc(int castleId, L2PcInstance player) { + if ((player == null) || (player.getLevel() < territoryWar().getPlayerMinLevel()) || ((_registeredMercenaries.get(castleId) != null) && _registeredMercenaries.get(castleId).contains(player.getObjectId()))) { + return; + } + + _registeredMercenaries.putIfAbsent(castleId, new CopyOnWriteArrayList<>()); + _registeredMercenaries.get(castleId).add(player.getObjectId()); + changeRegistration(castleId, player.getObjectId(), false); + } + + public void removeClan(int castleId, L2Clan clan) { + if (clan == null) { + return; + } + + if ((_registeredClans.get(castleId) != null) && _registeredClans.get(castleId).contains(clan)) { + _registeredClans.get(castleId).remove(clan); + changeRegistration(castleId, clan.getId(), true); + } + } + + public void removeMerc(int castleId, L2PcInstance player) { + if (player == null) { + return; + } + + if ((_registeredMercenaries.get(castleId) != null) && _registeredMercenaries.get(castleId).contains(player.getObjectId())) { + _registeredMercenaries.get(castleId).remove((Integer) player.getObjectId()); + changeRegistration(castleId, player.getObjectId(), true); + } + } + + public boolean getIsRegistrationOver() { + return _isRegistrationOver; + } + + public boolean isTWInProgress() { + return _isTWInProgress; + } + + public void territoryCatapultDestroyed(int castleId) { + if (_territoryList.get(castleId) != null) { + _territoryList.get(castleId).changeNPCsSpawn(2, false); + } + for (L2DoorInstance door : CastleManager.getInstance().getCastleById(castleId).getDoors()) { + door.openMe(); + } + } + + public L2Npc addTerritoryWard(int territoryId, int newOwnerId, int oldOwnerId, boolean broadcastMessage) { + L2Npc ret = null; + Territory terNew = _territoryList.get(newOwnerId); + if (terNew != null) { + TerritoryNPCSpawn ward = terNew.getFreeWardSpawnPlace(); + if (ward != null) { + ward._npcId = territoryId; + ret = spawnNPC(36491 + territoryId, ward.getLocation()); + ward.setNPC(ret); + if (!isTWInProgress() && !territoryWar().spawnWardsWhenTWIsNotInProgress()) { + ret.decayMe(); + } + if ((terNew.getOwnerClan() != null) && terNew.getOwnedWardIds().contains(newOwnerId + 80)) { + for (int wardId : terNew.getOwnedWardIds()) { + final List residentialSkills = SkillTreesData.getInstance().getAvailableResidentialSkills(wardId); + for (L2SkillLearn s : residentialSkills) { + final Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + if (sk != null) { + for (L2PcInstance member : terNew.getOwnerClan().getOnlineMembers(0)) { + if (!member.isInOlympiadMode()) { + member.addSkill(sk, false); + } + } + } + } + } + } + } + if (_territoryList.containsKey(oldOwnerId)) { + Territory terOld = _territoryList.get(oldOwnerId); + terOld.removeWard(territoryId); + updateTerritoryData(terOld); + updateTerritoryData(terNew); + if (broadcastMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.CLAN_S1_HAS_SUCCEDED_IN_CAPTURING_S2_TERRITORY_WARD); + sm.addString(terNew.getOwnerClan().getName()); + sm.addCastleId(territoryId); + announceToParticipants(sm, 135000, 13500); + } + if (terOld.getOwnerClan() != null) { + final List territorySkills = SkillTreesData.getInstance().getAvailableResidentialSkills(territoryId); + for (L2SkillLearn s : territorySkills) { + final Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + if (sk != null) { + for (L2PcInstance member : terOld.getOwnerClan().getOnlineMembers(0)) { + member.removeSkill(sk, false); + } + } + } + + if (!terOld.getOwnedWardIds().isEmpty() && !terOld.getOwnedWardIds().contains(oldOwnerId + 80)) { + for (int wardId : terOld.getOwnedWardIds()) { + final List wardSkills = SkillTreesData.getInstance().getAvailableResidentialSkills(wardId); + for (L2SkillLearn s : wardSkills) { + final Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + if (sk != null) { + for (L2PcInstance member : terOld.getOwnerClan().getOnlineMembers(0)) { + member.removeSkill(sk, false); + } + } + } + } + } + } + } + } else { + LOG.warn("Missing territory for new Ward owner: {}; {}", newOwnerId, territoryId); + } + return ret; + } + + public L2SiegeFlagInstance getHQForClan(L2Clan clan) { + if (clan.getCastleId() > 0) { + return _territoryList.get(clan.getCastleId()).getHQ(); + } + return null; + } + + public L2SiegeFlagInstance getHQForTerritory(int territoryId) { + return _territoryList.get(territoryId - 80).getHQ(); + } + + public void setHQForClan(L2Clan clan, L2SiegeFlagInstance hq) { + if (clan.getCastleId() > 0) { + _territoryList.get(clan.getCastleId()).setHQ(hq); + } + } + + public void addClanFlag(L2Clan clan, L2SiegeFlagInstance flag) { + _clanFlags.put(clan, flag); + } + + public boolean isClanHasFlag(L2Clan clan) { + return _clanFlags.containsKey(clan); + } + + public L2SiegeFlagInstance getFlagForClan(L2Clan clan) { + return _clanFlags.get(clan); + } + + public void removeClanFlag(L2Clan clan) { + _clanFlags.remove(clan); + } + + public List getAllTerritoryWards() { + return _territoryWards; + } + + public TerritoryWard getTerritoryWardForOwner(int castleId) { + for (TerritoryWard twWard : _territoryWards) { + if (twWard.getTerritoryId() == castleId) { + return twWard; + } + } + return null; + } + + public TerritoryWard getTerritoryWard(int territoryId) { + for (TerritoryWard twWard : _territoryWards) { + if (twWard.getTerritoryId() == territoryId) { + return twWard; + } + } + return null; + } + + public TerritoryWard getTerritoryWard(L2PcInstance player) { + for (TerritoryWard twWard : _territoryWards) { + if (twWard.playerId == player.getObjectId()) { + return twWard; + } + } + return null; + } + + public void dropCombatFlag(L2PcInstance player, boolean isKilled, boolean isSpawnBack) { + for (TerritoryWard twWard : _territoryWards) { + if (twWard.playerId == player.getObjectId()) { + twWard.dropIt(); + if (isTWInProgress()) { + if (isKilled) { + twWard.spawnMe(); + } else if (isSpawnBack) { + twWard.spawnBack(); + } else { + for (TerritoryNPCSpawn wardSpawn : _territoryList.get(twWard.getOwnerCastleId()).getOwnedWard()) { + if (wardSpawn.getId() == twWard.getTerritoryId()) { + wardSpawn.setNPC(wardSpawn.getNpc().getSpawn().doSpawn()); + twWard.unSpawnMe(); + twWard.setNpc(wardSpawn.getNpc()); + } + } + } + } + if (isKilled) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_CHAR_THAT_ACQUIRED_S1_WARD_HAS_BEEN_KILLED); + sm.addString(twWard.getNpc().getName().replace(" Ward", "")); + announceToParticipants(sm, 0, 0); + } + } + } + } + + public void giveTWQuestPoint(L2PcInstance player) { + _participantPoints.putIfAbsent(player.getObjectId(), new Integer[] { + player.getSiegeSide(), + 0, + 0, + 0, + 0, + 0, + 0 + }); + _participantPoints.get(player.getObjectId())[2]++; + } + + public void giveTWPoint(L2PcInstance killer, int victimSide, int type) { + if (victimSide == 0) { + return; + } + if ((killer.getParty() != null) && (type < 5)) { + for (L2PcInstance pl : killer.getParty().getMembers()) { + if ((pl.getSiegeSide() == victimSide) || (pl.getSiegeSide() == 0) || !Util.checkIfInRange(2000, killer, pl, false)) { + continue; + } + + _participantPoints.putIfAbsent(pl.getObjectId(), new Integer[] { + pl.getSiegeSide(), + 0, + 0, + 0, + 0, + 0, + 0 + }); + _participantPoints.get(pl.getObjectId())[type]++; + } + } else { + if ((killer.getSiegeSide() == victimSide) || (killer.getSiegeSide() == 0)) { + return; + } + + _participantPoints.putIfAbsent(killer.getObjectId(), new Integer[] { + killer.getSiegeSide(), + 0, + 0, + 0, + 0, + 0, + 0 + }); + _participantPoints.get(killer.getObjectId())[type]++; + } + } + + public int[] calcReward(L2PcInstance player) { + if (_participantPoints.containsKey(player.getObjectId())) { + int[] reward = new int[2]; + Integer[] temp = _participantPoints.get(player.getObjectId()); + reward[0] = temp[0]; + // badges for being online. if char was not online at least 10 mins + // than he cant get rewards(also this will handle that player already get his/her rewards) + if (temp[6] < 10) { + return reward; + } + reward[1] += (temp[6] > 70 ? 7 : (int) (temp[6] * 0.1)); + // badges for player Quests + reward[1] += temp[2] * 7; + // badges for player Kills + if (temp[1] < 50) { + reward[1] += temp[1] * 0.1; + } else if (temp[1] < 120) { + reward[1] += (5 + ((temp[1] - 50) / 14)); + } else { + reward[1] += 10; + } + // badges for territory npcs + reward[1] += temp[3]; + // badges for territory catapults + reward[1] += temp[4] * 2; + // badges for territory Wards + reward[1] += (temp[5] > 0 ? 5 : 0); + // badges for territory quest done + reward[1] += Math.min(_territoryList.get(temp[0] - 80).getQuestDone()[0], 10); + reward[1] += _territoryList.get(temp[0] - 80).getQuestDone()[1]; + reward[1] += _territoryList.get(temp[0] - 80).getOwnedWardIds().size(); + return reward; + } + return new int[] { + 0, + 0 + }; + } + + public void debugReward(L2PcInstance player) { + player.sendMessage("Registered TerrId: " + player.getSiegeSide()); + if (_participantPoints.containsKey(player.getObjectId())) { + Integer[] temp = _participantPoints.get(player.getObjectId()); + player.sendMessage("TerrId: " + temp[0]); + player.sendMessage("PcKill: " + temp[1]); + player.sendMessage("PcQuests: " + temp[2]); + player.sendMessage("npcKill: " + temp[3]); + player.sendMessage("CatatKill: " + temp[4]); + player.sendMessage("WardKill: " + temp[5]); + player.sendMessage("onlineTime: " + temp[6]); + } else { + player.sendMessage("No points for you!"); + } + if (_territoryList.containsKey(player.getSiegeSide() - 80)) { + player.sendMessage("Your Territory's jobs:"); + player.sendMessage("npcKill: " + _territoryList.get(player.getSiegeSide() - 80).getQuestDone()[0]); + player.sendMessage("WardCaptured: " + _territoryList.get(player.getSiegeSide() - 80).getQuestDone()[1]); + } + } + + public void resetReward(L2PcInstance player) { + if (_participantPoints.containsKey(player.getObjectId())) { + _participantPoints.get(player.getObjectId())[6] = 0; + } + } + + public L2Npc spawnNPC(int npcId, Location loc) { + try { + final L2Spawn spawnDat = new L2Spawn(npcId); + spawnDat.setAmount(1); + spawnDat.setX(loc.getX()); + spawnDat.setY(loc.getY()); + spawnDat.setZ(loc.getZ()); + spawnDat.setHeading(loc.getHeading()); + spawnDat.stopRespawn(); + return spawnDat.spawnOne(false); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + return null; + } + + private void changeRegistration(int castleId, int objId, boolean delete) { + final String query = delete ? DELETE : INSERT; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(query)) { + ps.setInt(1, castleId); + ps.setInt(2, objId); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: Territory War registration: {}", e.getMessage(), e); + } + } + + private void updateTerritoryData(Territory ter) { + StringBuilder wardList = new StringBuilder(); + for (int i : ter.getOwnedWardIds()) { + wardList.append(i).append(";"); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE territories SET ownedWardIds=? WHERE territoryId=?")) { + ps.setString(1, wardList.toString()); + ps.setInt(2, ter.getTerritoryId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: Territory Data update: {}", e.getMessage(), e); + } + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM territory_spawnlist")) { + while (rs.next()) { + int castleId = rs.getInt("castleId"); + int npcId = rs.getInt("npcId"); + Location loc = new Location(rs.getInt("x"), rs.getInt("y"), rs.getInt("z"), rs.getInt("heading")); + int spawnType = rs.getInt("spawnType"); + if (!_territoryList.containsKey(castleId)) { + _territoryList.put(castleId, new Territory(castleId)); + } + switch (spawnType) { + case 0: // town npcs + case 1: // fortress npcs + case 2: // castle npcs + _territoryList.get(castleId).getSpawnList().add(new TerritoryNPCSpawn(castleId, loc, npcId, spawnType, null)); + break; + case 3: // ward spawns + _territoryList.get(castleId).addWardSpawnPlace(loc); + break; + default: + LOG.warn("Unknown npc type for {}", rs.getInt("id")); + } + } + } catch (Exception e) { + LOG.warn("SpawnList error: {}", e.getMessage(), e); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM territories")) { + while (rs.next()) { + int castleId = rs.getInt("castleId"); + int fortId = rs.getInt("fortId"); + String ownedWardIds = rs.getString("OwnedWardIds"); + + Territory t = _territoryList.get(castleId); + if (t != null) { + t._fortId = fortId; + if (CastleManager.getInstance().getCastleById(castleId).getOwnerId() > 0) { + t.setOwnerClan(ClanTable.getInstance().getClan(CastleManager.getInstance().getCastleById(castleId).getOwnerId())); + t.changeNPCsSpawn(0, true); + } + + if (!ownedWardIds.isEmpty()) { + for (String wardId : ownedWardIds.split(";")) { + if (Integer.parseInt(wardId) > 0) { + addTerritoryWard(Integer.parseInt(wardId), castleId, 0, false); + } + } + } + } + } + } catch (Exception e) { + LOG.warn("Territory list error(): {}", e.getMessage(), e); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM territory_registrations")) { + while (rs.next()) { + int castleId = rs.getInt("castleId"); + int registeredId = rs.getInt("registeredId"); + if (ClanTable.getInstance().getClan(registeredId) != null) { + if (_registeredClans.get(castleId) == null) { + _registeredClans.putIfAbsent(castleId, new CopyOnWriteArrayList<>()); + } + _registeredClans.get(castleId).add(ClanTable.getInstance().getClan(registeredId)); + } else { + _registeredMercenaries.computeIfAbsent(castleId, _ -> new CopyOnWriteArrayList<>()); + _registeredMercenaries.get(castleId).add(registeredId); + } + } + } catch (Exception e) { + LOG.warn("Registration list error: {}", e.getMessage(), e); + } + } + + public void startTerritoryWar() { + List activeTerritoryList = new LinkedList<>(); + for (Territory t : _territoryList.values()) { + Castle castle = CastleManager.getInstance().getCastleById(t.getCastleId()); + if (castle != null) { + if (castle.getOwnerId() > 0) { + activeTerritoryList.add(t); + } + } else { + LOG.warn("Castle missing! CastleId: {}", t.getCastleId()); + } + } + + if (activeTerritoryList.size() < 2) { + return; + } + + _isTWInProgress = true; + if (!updatePlayerTWStateFlags(false)) { + return; + } + + // teleportPlayer(Siege.TeleportWhoType.Attacker, MapRegionTable.TeleportWhereType.Town); // Teleport to the closest town + for (Territory t : activeTerritoryList) { + Castle castle = CastleManager.getInstance().getCastleById(t.getCastleId()); + Fort fort = FortManager.getInstance().getFortById(t.getFortId()); + // spawnControlTower(t.getCastleId()); // Spawn control tower + if (castle != null) { + t.changeNPCsSpawn(2, true); + castle.spawnDoor(); // Spawn door + castle.getZone().setSiegeInstance(this); + castle.getZone().setIsActive(true); + castle.getZone().updateZoneStatusForCharactersInside(); + } else { + LOG.warn("Castle missing! CastleId: {}", t.getCastleId()); + } + if (fort != null) { + t.changeNPCsSpawn(1, true); + fort.resetDoors(); // Spawn door + fort.getZone().setSiegeInstance(this); + fort.getZone().setIsActive(true); + fort.getZone().updateZoneStatusForCharactersInside(); + } else { + LOG.warn("Fort missing! FortId: {}", t.getFortId()); + } + for (TerritoryNPCSpawn ward : t.getOwnedWard()) { + if ((ward.getNpc() != null) && (t.getOwnerClan() != null)) { + if (!ward.getNpc().isVisible()) { + ward.setNPC(ward.getNpc().getSpawn().doSpawn()); + } + _territoryWards.add(new TerritoryWard(ward.getId(), ward.getLocation().getX(), ward.getLocation().getY(), ward.getLocation().getZ(), 0, ward.getId() + 13479, t.getCastleId(), ward.getNpc())); + } + } + t.getQuestDone()[0] = 0; // killed npc + t.getQuestDone()[1] = 0; // captured wards + } + _participantPoints.clear(); + + if (territoryWar().returnWardsWhenTWStarts()) { + for (TerritoryWard ward : _territoryWards) { + if (ward.getOwnerCastleId() != (ward.getTerritoryId() - 80)) { + ward.unSpawnMe(); + ward.setNpc(addTerritoryWard(ward.getTerritoryId(), ward.getTerritoryId() - 80, ward.getOwnerCastleId(), false)); + ward.setOwnerCastleId(ward.getTerritoryId() - 80); + } + } + } + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_HAS_BEGUN); + Broadcast.toAllOnlinePlayers(sm); + } + + public void endTerritoryWar() { + _isTWInProgress = false; + List activeTerritoryList = new LinkedList<>(); + for (Territory t : _territoryList.values()) { + Castle castle = CastleManager.getInstance().getCastleById(t.getCastleId()); + if (castle != null) { + if (castle.getOwnerId() > 0) { + activeTerritoryList.add(t); + } + } else { + LOG.warn("Castle missing! CastleId: {}", t.getCastleId()); + } + } + + if (activeTerritoryList.size() < 2) { + return; + } + + if (!updatePlayerTWStateFlags(true)) { + return; + } + + for (TerritoryWard twWard : _territoryWards) { + twWard.unSpawnMe(); + } + _territoryWards.clear(); + + // teleportPlayer(Siege.TeleportWhoType.Attacker, MapRegionTable.TeleportWhereType.Town); // Teleport to the closest town + for (Territory t : activeTerritoryList) { + Castle castle = CastleManager.getInstance().getCastleById(t.getCastleId()); + Fort fort = FortManager.getInstance().getFortById(t.getFortId()); + + if (castle != null) { + castle.spawnDoor(); + t.changeNPCsSpawn(2, false); + castle.getZone().setIsActive(false); + castle.getZone().updateZoneStatusForCharactersInside(); + castle.getZone().setSiegeInstance(null); + } else { + LOG.warn("Castle missing! CastleId: {}", t.getCastleId()); + } + + if (fort != null) { + t.changeNPCsSpawn(1, false); + fort.getZone().setIsActive(false); + fort.getZone().updateZoneStatusForCharactersInside(); + fort.getZone().setSiegeInstance(null); + } else { + LOG.warn("Fort missing! FortId: {}", t.getFortId()); + } + + if (t.getHQ() != null) { + t.getHQ().deleteMe(); + } + + for (TerritoryNPCSpawn ward : t.getOwnedWard()) { + if (ward.getNpc() != null) { + if (!ward.getNpc().isVisible() && territoryWar().spawnWardsWhenTWIsNotInProgress()) { + ward.setNPC(ward.getNpc().getSpawn().doSpawn()); + } else if (ward.getNpc().isVisible() && !territoryWar().spawnWardsWhenTWIsNotInProgress()) { + ward.getNpc().decayMe(); + } + } + } + } + for (L2SiegeFlagInstance flag : _clanFlags.values()) { + flag.deleteMe(); + } + _clanFlags.clear(); + + for (Integer castleId : _registeredClans.keySet()) { + for (L2Clan clan : _registeredClans.get(castleId)) { + changeRegistration(castleId, clan.getId(), true); + } + } + for (Integer castleId : _registeredMercenaries.keySet()) { + for (Integer pl_objId : _registeredMercenaries.get(castleId)) { + changeRegistration(castleId, pl_objId, true); + } + } + // change next TW date + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_HAS_ENDED); + Broadcast.toAllOnlinePlayers(sm); + } + + public boolean updatePlayerTWStateFlags(boolean clear) { + Quest twQuest = QuestManager.getInstance().getQuest(qn); + if (twQuest == null) { + LOG.warn("Missing main Quest!"); + return false; + } + for (int castleId : _registeredClans.keySet()) { + for (L2Clan clan : _registeredClans.get(castleId)) { + for (L2PcInstance player : clan.getOnlineMembers(0)) { + if (clear) { + player.setSiegeState((byte) 0); + if (!_isTWChannelOpen) { + player.setSiegeSide(0); + } + } else { + if ((player.getLevel() < territoryWar().getPlayerMinLevel()) || (player.getClassId().level() < 2)) { + continue; + } + if (_isTWInProgress) { + player.setSiegeState((byte) 1); + } + player.setSiegeSide(80 + castleId); + } + player.broadcastUserInfo(); + } + } + } + for (int castleId : _registeredMercenaries.keySet()) { + for (int objId : _registeredMercenaries.get(castleId)) { + L2PcInstance player = L2World.getInstance().getPlayer(objId); + if (player == null) { + continue; + } + if (clear) { + player.setSiegeState((byte) 0); + if (!_isTWChannelOpen) { + player.setSiegeSide(0); + } + } else { + if (_isTWInProgress) { + player.setSiegeState((byte) 1); + } + player.setSiegeSide(80 + castleId); + } + player.broadcastUserInfo(); + } + } + for (Territory terr : _territoryList.values()) { + if (terr.getOwnerClan() != null) { + for (L2PcInstance player : terr.getOwnerClan().getOnlineMembers(0)) { + if (player == null) { + continue; + } + if (clear) { + player.setSiegeState((byte) 0); + if (!_isTWChannelOpen) { + player.setSiegeSide(0); + } + } else { + if ((player.getLevel() < territoryWar().getPlayerMinLevel()) || (player.getClassId().level() < 2)) { + continue; + } + if (_isTWInProgress) { + player.setSiegeState((byte) 1); + } + player.setSiegeSide(80 + terr.getCastleId()); + } + player.broadcastUserInfo(); + } + } + } + twQuest.setOnEnterWorld(_isTWInProgress); + return true; + } + + protected class RewardOnlineParticipants implements Runnable { + @Override + public void run() { + if (isTWInProgress()) { + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + if ((player != null) && (player.getSiegeSide() > 0)) { + giveTWPoint(player, 1000, 6); + } + } + } else { + _scheduledRewardOnlineTask.cancel(false); + } + } + } + + protected class ScheduleStartTWTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(ScheduleStartTWTask.class); + + @Override + public void run() { + _scheduledStartTWTask.cancel(false); + try { + long timeRemaining = _startTWDate.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + if (timeRemaining > 7200000) { + _isRegistrationOver = false; + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining - 7200000); // Prepare task for 2h before TW start to end registration + } else if (timeRemaining > 1200000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_TERRITORY_WAR_REGISTERING_PERIOD_ENDED); + Broadcast.toAllOnlinePlayers(sm); + _isRegistrationOver = true; + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining - 1200000); // Prepare task for 20 mins left before TW start. + } else if (timeRemaining > 600000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_BEGINS_IN_20_MINUTES); + Broadcast.toAllOnlinePlayers(sm); + _isTWChannelOpen = true; + _isRegistrationOver = true; + updatePlayerTWStateFlags(false); + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining - 600000); // Prepare task for 10 mins left before TW start. + } else if (timeRemaining > 300000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_BEGINS_IN_10_MINUTES); + Broadcast.toAllOnlinePlayers(sm); + _isTWChannelOpen = true; + _isRegistrationOver = true; + updatePlayerTWStateFlags(false); + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining - 300000); // Prepare task for 5 mins left before TW start. + } else if (timeRemaining > 60000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_BEGINS_IN_5_MINUTES); + Broadcast.toAllOnlinePlayers(sm); + _isTWChannelOpen = true; + _isRegistrationOver = true; + updatePlayerTWStateFlags(false); + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining - 60000); // Prepare task for 1 min left before TW start. + } else if (timeRemaining > 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.TERRITORY_WAR_BEGINS_IN_1_MINUTE); + Broadcast.toAllOnlinePlayers(sm); + _isTWChannelOpen = true; + _isRegistrationOver = true; + updatePlayerTWStateFlags(false); + _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), timeRemaining); // Prepare task for TW start. + } else if ((timeRemaining + MINUTES.toMillis(territoryWar().getWarLength())) > 0) { + _isTWChannelOpen = true; + _isRegistrationOver = true; + startTerritoryWar(); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), 1000); // Prepare task for TW end. + _scheduledRewardOnlineTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new RewardOnlineParticipants(), 60000, 60000); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + private class ScheduleEndTWTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(ScheduleEndTWTask.class); + + protected ScheduleEndTWTask() { + } + + @Override + public void run() { + try { + _scheduledEndTWTask.cancel(false); + long timeRemaining = (_startTWDate.getTimeInMillis() + MINUTES.toMillis(territoryWar().getWarLength())) - Calendar.getInstance().getTimeInMillis(); + if (timeRemaining > 3600000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_TERRITORY_WAR_WILL_END_IN_S1_HOURS); + sm.addInt(2); + announceToParticipants(sm, 0, 0); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), timeRemaining - 3600000); // Prepare task for 1 hr left. + } else if (timeRemaining > 600000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_TERRITORY_WAR_WILL_END_IN_S1_MINUTES); + sm.addInt((int) (timeRemaining / 60000)); + announceToParticipants(sm, 0, 0); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), timeRemaining - 600000); // Prepare task for 10 minute left. + } else if (timeRemaining > 300000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_TERRITORY_WAR_WILL_END_IN_S1_MINUTES); + sm.addInt((int) (timeRemaining / 60000)); + announceToParticipants(sm, 0, 0); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), timeRemaining - 300000); // Prepare task for 5 minute left. + } else if (timeRemaining > 10000) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_TERRITORY_WAR_WILL_END_IN_S1_MINUTES); + sm.addInt((int) (timeRemaining / 60000)); + announceToParticipants(sm, 0, 0); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), timeRemaining - 10000); // Prepare task for 10 seconds count down + } else if (timeRemaining > 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SECONDS_TO_THE_END_OF_TERRITORY_WAR); + sm.addInt((int) (timeRemaining / 1000)); + announceToParticipants(sm, 0, 0); + _scheduledEndTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndTWTask(), timeRemaining); // Prepare task for second count down + } else { + endTerritoryWar(); + // _scheduledStartTWTask = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartTWTask(), 1000); + ThreadPoolManager.getInstance().scheduleGeneral(new closeTerritoryChannelTask(), 600000); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + private class closeTerritoryChannelTask implements Runnable { + public closeTerritoryChannelTask() { + } + + @Override + public void run() { + _isTWChannelOpen = false; + _disguisedPlayers.clear(); + updatePlayerTWStateFlags(true); + } + } + + public void announceToParticipants(L2GameServerPacket sm, int exp, int sp) { + // broadcast to clan members + for (Territory ter : _territoryList.values()) { + if (ter.getOwnerClan() != null) { + for (L2PcInstance member : ter.getOwnerClan().getOnlineMembers(0)) { + member.sendPacket(sm); + if ((exp > 0) || (sp > 0)) { + member.addExpAndSp(exp, sp); + } + } + } + } + for (List list : _registeredClans.values()) { + for (L2Clan c : list) { + for (L2PcInstance member : c.getOnlineMembers(0)) { + member.sendPacket(sm); + if ((exp > 0) || (sp > 0)) { + member.addExpAndSp(exp, sp); + } + } + } + } + // broadcast to mercenaries + for (List list : _registeredMercenaries.values()) { + for (int objId : list) { + L2PcInstance player = L2World.getInstance().getPlayer(objId); + if ((player != null) && ((player.getClan() == null) || !checkIsRegistered(-1, player.getClan()))) { + player.sendPacket(sm); + if ((exp > 0) || (sp > 0)) { + player.addExpAndSp(exp, sp); + } + } + } + } + } + + public static class TerritoryNPCSpawn implements IIdentifiable { + private final Location _location; + protected int _npcId; + private final int _castleId; + private final int _type; + private L2Npc _npc; + + public TerritoryNPCSpawn(int castle_id, Location loc, int npc_id, int type, L2Npc npc) { + _castleId = castle_id; + _location = loc; + _npcId = npc_id; + _type = type; + _npc = npc; + } + + public int getCastleId() { + return _castleId; + } + + /** + * Gets the NPC ID. + * @return the NPC ID + */ + @Override + public int getId() { + return _npcId; + } + + public int getType() { + return _type; + } + + public void setNPC(L2Npc npc) { + if (_npc != null) { + _npc.deleteMe(); + } + _npc = npc; + } + + public L2Npc getNpc() { + return _npc; + } + + public Location getLocation() { + return _location; + } + } + + public class Territory { + private static final Logger LOG = LoggerFactory.getLogger(Territory.class); + + private final int _territoryId; + private final int _castleId; // territory Castle + protected int _fortId; // territory Fortress + private L2Clan _ownerClan; + private final List _spawnList = new LinkedList<>(); + private final TerritoryNPCSpawn[] _territoryWardSpawnPlaces; + private boolean _isInProgress = false; + private L2SiegeFlagInstance _territoryHQ = null; + private final int[] _questDone; + + public Territory(int castleId) { + _castleId = castleId; + _territoryId = castleId + 80; + _territoryWardSpawnPlaces = new TerritoryNPCSpawn[9]; + _questDone = new int[2]; + } + + protected void addWardSpawnPlace(Location loc) { + for (int i = 0; i < _territoryWardSpawnPlaces.length; i++) { + if (_territoryWardSpawnPlaces[i] == null) { + _territoryWardSpawnPlaces[i] = new TerritoryNPCSpawn(_castleId, loc, 0, 4, null); + return; + } + } + } + + protected TerritoryNPCSpawn getFreeWardSpawnPlace() { + for (TerritoryNPCSpawn _territoryWardSpawnPlace : _territoryWardSpawnPlaces) { + if ((_territoryWardSpawnPlace != null) && (_territoryWardSpawnPlace.getNpc() == null)) { + return _territoryWardSpawnPlace; + } + } + LOG.warn("No free Ward spawn found for territory: {}", _territoryId); + for (int i = 0; i < _territoryWardSpawnPlaces.length; i++) { + if (_territoryWardSpawnPlaces[i] == null) { + LOG.warn("Territory ward spawn place {} is null!", i); + } else if (_territoryWardSpawnPlaces[i].getNpc() != null) { + LOG.warn("Territory ward spawn place {} has npc name: {}", i, _territoryWardSpawnPlaces[i].getNpc().getName()); + } else { + LOG.warn("Territory ward spawn place {} is empty!", i); + } + } + return null; + } + + public List getSpawnList() { + return _spawnList; + } + + protected void changeNPCsSpawn(int type, boolean isSpawn) { + if ((type < 0) || (type > 3)) { + LOG.warn("Wrong type({}) for NPCs spawn change!", type); + return; + } + for (TerritoryNPCSpawn twSpawn : _spawnList) { + if (twSpawn.getType() != type) { + continue; + } + if (isSpawn) { + twSpawn.setNPC(spawnNPC(twSpawn.getId(), twSpawn.getLocation())); + } else { + L2Npc npc = twSpawn.getNpc(); + if ((npc != null) && !npc.isDead()) { + npc.deleteMe(); + } + twSpawn.setNPC(null); + } + } + } + + protected void removeWard(int wardId) { + for (TerritoryNPCSpawn wardSpawn : _territoryWardSpawnPlaces) { + if (wardSpawn.getId() == wardId) { + wardSpawn.getNpc().deleteMe(); + wardSpawn.setNPC(null); + wardSpawn._npcId = 0; + return; + } + } + LOG.warn("Can't delete wardId: {} for territory: {}", wardId, _territoryId); + } + + public int getTerritoryId() { + return _territoryId; + } + + public int getCastleId() { + return _castleId; + } + + public int getFortId() { + return _fortId; + } + + public L2Clan getOwnerClan() { + return _ownerClan; + } + + public void setOwnerClan(L2Clan newOwner) { + _ownerClan = newOwner; + } + + public void setHQ(L2SiegeFlagInstance hq) { + _territoryHQ = hq; + } + + public L2SiegeFlagInstance getHQ() { + return _territoryHQ; + } + + public TerritoryNPCSpawn[] getOwnedWard() { + return _territoryWardSpawnPlaces; + } + + public int[] getQuestDone() { + return _questDone; + } + + public List getOwnedWardIds() { + List ret = new LinkedList<>(); + for (TerritoryNPCSpawn wardSpawn : _territoryWardSpawnPlaces) { + if (wardSpawn.getId() > 0) { + ret.add(wardSpawn.getId()); + } + } + return ret; + } + + public boolean isInProgress() { + return _isInProgress; + } + + public void setIsInProgress(boolean val) { + _isInProgress = val; + } + } + + @Override + public void startSiege() { + throw new UnsupportedOperationException(); + } + + @Override + public void endSiege() { + throw new UnsupportedOperationException(); + } + + @Override + public L2SiegeClan getAttackerClan(int clanId) { + throw new UnsupportedOperationException(); + } + + @Override + public L2SiegeClan getAttackerClan(L2Clan clan) { + throw new UnsupportedOperationException(); + } + + @Override + public List getAttackerClans() { + throw new UnsupportedOperationException(); + } + + @Override + public List getAttackersInZone() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean checkIsAttacker(L2Clan clan) { + throw new UnsupportedOperationException(); + } + + @Override + public L2SiegeClan getDefenderClan(int clanId) { + throw new UnsupportedOperationException(); + } + + @Override + public L2SiegeClan getDefenderClan(L2Clan clan) { + throw new UnsupportedOperationException(); + } + + @Override + public List getDefenderClans() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean checkIsDefender(L2Clan clan) { + throw new UnsupportedOperationException(); + } + + @Override + public List getFlag(L2Clan clan) { + throw new UnsupportedOperationException(); + } + + @Override + public Calendar getSiegeDate() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean giveFame() { + return true; + } + + @Override + public int getFameFrequency() { + return (int) character().getCastleZoneFameTaskFrequency(); + } + + @Override + public int getFameAmount() { + return character().getCastleZoneFameAcquirePoints(); + } + + @Override + public void updateSiege() { + + } + + public void declareLord(int territoryId, L2PcInstance player) { + // TODO(Zoey76): Implement. + } + + public static TerritoryWarManager getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final TerritoryWarManager _instance = new TerritoryWarManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/TownManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/TownManager.java new file mode 100644 index 00000000..f3cf8d2d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/TownManager.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.zone.L2ZoneType; +import com.l2jserver.gameserver.model.zone.type.L2TownZone; + +public final class TownManager { + public static int getTownCastle(int townId) { + return switch (townId) { + case 912 -> 1; + case 916 -> 2; + case 918 -> 3; + case 922 -> 4; + case 924 -> 5; + case 926 -> 6; + case 1538 -> 7; + case 1537 -> 8; + case 1714 -> 9; + default -> 0; + }; + } + + public static boolean townHasCastleInSiege(int townId) { + int castleIndex = getTownCastle(townId); + + if (castleIndex > 0) { + Castle castle = CastleManager.getInstance().getCastles().get(CastleManager.getInstance().getCastleIndex(castleIndex)); + if (castle != null) { + return castle.getSiege().isInProgress(); + } + } + return false; + } + + public static boolean townHasCastleInSiege(int x, int y) { + return townHasCastleInSiege(MapRegionManager.getInstance().getMapRegionLocId(x, y)); + } + + public static L2TownZone getTown(int townId) { + for (L2TownZone temp : ZoneManager.getInstance().getAllZones(L2TownZone.class)) { + if (temp.getTownId() == townId) { + return temp; + } + } + return null; + } + + /** + * Returns the town at that position (if any) + * @param x + * @param y + * @param z + * @return + */ + public static L2TownZone getTown(int x, int y, int z) { + for (L2ZoneType temp : ZoneManager.getInstance().getZones(x, y, z)) { + if (temp instanceof L2TownZone) { + return (L2TownZone) temp; + } + } + return null; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/WalkingManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/WalkingManager.java new file mode 100644 index 00000000..9105b404 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/WalkingManager.java @@ -0,0 +1,399 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.instancemanager.tasks.StartMovingTask; +import com.l2jserver.gameserver.model.L2NpcWalkerNode; +import com.l2jserver.gameserver.model.L2WalkRoute; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.WalkInfo; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; +import com.l2jserver.gameserver.model.actor.tasks.npc.walker.ArrivedTask; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcMoveNodeArrived; +import com.l2jserver.gameserver.model.holders.NpcRoutesHolder; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.NpcSay; +import com.l2jserver.gameserver.util.Broadcast; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class manages walking monsters. + * @author GKR + */ +public final class WalkingManager implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(WalkingManager.class); + + // Repeat style: + // -1 - no repeat + // 0 - go back + // 1 - go to first point (circle style) + // 2 - teleport to first point (conveyor style) + // 3 - random walking between points. + public static final byte NO_REPEAT = -1; + public static final byte REPEAT_GO_BACK = 0; + public static final byte REPEAT_GO_FIRST = 1; + public static final byte REPEAT_TELE_FIRST = 2; + public static final byte REPEAT_RANDOM = 3; + + private final Map _routes = new HashMap<>(); // all available routes + private final Map _activeRoutes = new HashMap<>(); // each record represents NPC, moving by predefined route from _routes, and moving progress + private final Map _routesToAttach = new HashMap<>(); // each record represents NPC and all available routes for it + + protected WalkingManager() { + load(); + } + + @Override + public void load() { + parseDatapackFile("data/Routes.xml"); + LOG.info("Loaded {} walking routes.", _routes.size()); + } + + @Override + public void parseDocument(Document doc) { + Node n = doc.getFirstChild(); + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if (d.getNodeName().equals("route")) { + final String routeName = parseString(d.getAttributes(), "name"); + boolean repeat = parseBoolean(d.getAttributes(), "repeat"); + final String repeatStyle = d.getAttributes().getNamedItem("repeatStyle").getNodeValue().toLowerCase(); + + final byte repeatType; + switch (repeatStyle) { + case "back" -> repeatType = REPEAT_GO_BACK; + case "cycle" -> repeatType = REPEAT_GO_FIRST; + case "conveyor" -> repeatType = REPEAT_TELE_FIRST; + case "random" -> repeatType = REPEAT_RANDOM; + default -> repeatType = NO_REPEAT; + } + + final List list = new ArrayList<>(); + for (Node r = d.getFirstChild(); r != null; r = r.getNextSibling()) { + if (r.getNodeName().equals("point")) { + NamedNodeMap attrs = r.getAttributes(); + int x = parseInteger(attrs, "X"); + int y = parseInteger(attrs, "Y"); + int z = parseInteger(attrs, "Z"); + int delay = parseInteger(attrs, "delay"); + boolean run = parseBoolean(attrs, "run"); + NpcStringId npcString = null; + String chatString = null; + + Node node = attrs.getNamedItem("string"); + if (node != null) { + chatString = node.getNodeValue(); + } else { + node = attrs.getNamedItem("npcString"); + if (node != null) { + npcString = NpcStringId.getNpcStringId(node.getNodeValue()); + if (npcString == null) { + LOG.warn("Unknown NPC String {} for route {}!", node.getNodeValue(), routeName); + continue; + } + } else { + node = attrs.getNamedItem("npcStringId"); + if (node != null) { + npcString = NpcStringId.getNpcStringId(Integer.parseInt(node.getNodeValue())); + } + } + } + list.add(new L2NpcWalkerNode(x, y, z, delay, run, npcString, chatString)); + } + + else if (r.getNodeName().equals("target")) { + NamedNodeMap attrs = r.getAttributes(); + try { + int npcId = Integer.parseInt(attrs.getNamedItem("id").getNodeValue()); + int x = Integer.parseInt(attrs.getNamedItem("spawnX").getNodeValue()); + int y = Integer.parseInt(attrs.getNamedItem("spawnY").getNodeValue()); + int z = Integer.parseInt(attrs.getNamedItem("spawnZ").getNodeValue()); + + NpcRoutesHolder holder = _routesToAttach.containsKey(npcId) ? _routesToAttach.get(npcId) : new NpcRoutesHolder(); + holder.addRoute(routeName, new Location(x, y, z)); + _routesToAttach.put(npcId, holder); + } catch (Exception e) { + LOG.warn("Error in target definition for route {}!", routeName); + } + } + } + _routes.put(routeName, new L2WalkRoute(routeName, list, repeat, false, repeatType)); + } + } + } + + /** + * @param npc NPC to check + * @return {@code true} if given NPC, or its leader is controlled by Walking Manager and moves currently. + */ + public boolean isOnWalk(L2Npc npc) { + L2MonsterInstance monster = null; + + if (npc.isMonster()) { + if (((L2MonsterInstance) npc).getLeader() == null) { + monster = (L2MonsterInstance) npc; + } else { + monster = ((L2MonsterInstance) npc).getLeader(); + } + } + + if (((monster != null) && !isRegistered(monster)) || !isRegistered(npc)) { + return false; + } + + final WalkInfo walk = monster != null ? _activeRoutes.get(monster.getObjectId()) : _activeRoutes.get(npc.getObjectId()); + return !walk.isStoppedByAttack() && !walk.isSuspended(); + } + + public L2WalkRoute getRoute(String route) { + return _routes.get(route); + } + + /** + * @param npc NPC to check + * @return {@code true} if given NPC controlled by Walking Manager. + */ + public boolean isRegistered(L2Npc npc) { + return _activeRoutes.containsKey(npc.getObjectId()); + } + + /** + * @param npc + * @return name of route + */ + public String getRouteName(L2Npc npc) { + return _activeRoutes.containsKey(npc.getObjectId()) ? _activeRoutes.get(npc.getObjectId()).getRoute().getName() : ""; + } + + /** + * Start to move given NPC by given route + * @param npc NPC to move + * @param routeName name of route to move by + */ + public void startMoving(final L2Npc npc, final String routeName) { + if (_routes.containsKey(routeName) && (npc != null) && !npc.isDead()) // check, if these route and NPC present + { + if (!_activeRoutes.containsKey(npc.getObjectId())) // new walk task + { + // only if not already moved / not engaged in battle... should not happens if called on spawn + if ((npc.getAI().getIntention() == CtrlIntention.AI_INTENTION_ACTIVE) || (npc.getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE)) { + final WalkInfo walk = new WalkInfo(routeName); + + if (npc.isDebug()) { + walk.setLastAction(System.currentTimeMillis()); + } + + L2NpcWalkerNode node = walk.getCurrentNode(); + + // adjust next waypoint, if NPC spawns at first waypoint + if ((npc.getX() == node.getX()) && (npc.getY() == node.getY())) { + walk.calculateNextNode(npc); + node = walk.getCurrentNode(); + npc.sendDebugMessage("Route '" + routeName + "': spawn point is same with first waypoint, adjusted to next"); + } + + if (!npc.isInsideRadius(node, 3000, true, false)) { + final String message = "Route '" + routeName + "': NPC (id=" + npc.getId() + ", x=" + npc.getX() + ", y=" + npc.getY() + ", z=" + npc.getZ() + ") is too far from starting point (node x=" + node.getX() + ", y=" + node.getY() + ", z=" + node.getZ() + ", range=" + + npc.calculateDistance(node, true, true) + "), walking will not start!"; + LOG.warn(message); + npc.sendDebugMessage(message); + return; + } + + npc.sendDebugMessage("Starting to move at route '" + routeName + "'"); + npc.setIsRunning(node.runToLocation()); + npc.getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, node); + walk.setWalkCheckTask(ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new StartMovingTask(npc, routeName), 60000, 60000)); // start walk check task, for resuming walk after fight + + npc.getKnownList().startTrackingTask(); + + _activeRoutes.put(npc.getObjectId(), walk); // register route + } else { + npc.sendDebugMessage("Failed to start moving along route '" + routeName + "', scheduled"); + ThreadPoolManager.getInstance().scheduleGeneral(new StartMovingTask(npc, routeName), 60000); + } + } else + // walk was stopped due to some reason (arrived to node, script action, fight or something else), resume it + { + if (_activeRoutes.containsKey(npc.getObjectId()) && ((npc.getAI().getIntention() == CtrlIntention.AI_INTENTION_ACTIVE) || (npc.getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE))) { + final WalkInfo walk = _activeRoutes.get(npc.getObjectId()); + if (walk == null) { + return; + } + + // Prevent call simultaneously from scheduled task and onArrived() or temporarily stop walking for resuming in future + if (walk.isBlocked() || walk.isSuspended()) { + npc.sendDebugMessage("Failed to continue moving along route '" + routeName + "' (operation is blocked)"); + return; + } + + walk.setBlocked(true); + final L2NpcWalkerNode node = walk.getCurrentNode(); + npc.sendDebugMessage("Route '" + routeName + "', continuing to node " + walk.getCurrentNodeId()); + npc.setIsRunning(node.runToLocation()); + npc.getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, node); + walk.setBlocked(false); + walk.setStoppedByAttack(false); + } else { + npc.sendDebugMessage("Failed to continue moving along route '" + routeName + "' (wrong AI state - " + npc.getAI().getIntention() + ")"); + } + } + } + } + + /** + * Cancel NPC moving permanently + * @param npc NPC to cancel + */ + public synchronized void cancelMoving(L2Npc npc) { + final WalkInfo walk = _activeRoutes.remove(npc.getObjectId()); + if (walk != null) { + walk.getWalkCheckTask().cancel(true); + npc.getKnownList().stopTrackingTask(); + } + } + + /** + * Resumes previously stopped moving + * @param npc NPC to resume + */ + public void resumeMoving(final L2Npc npc) { + final WalkInfo walk = _activeRoutes.get(npc.getObjectId()); + if (walk != null) { + walk.setSuspended(false); + walk.setStoppedByAttack(false); + startMoving(npc, walk.getRoute().getName()); + } + } + + /** + * Pause NPC moving until it will be resumed + * @param npc NPC to pause moving + * @param suspend {@code true} if moving was temporarily suspended for some reasons of AI-controlling script + * @param stoppedByAttack {@code true} if moving was suspended because of NPC was attacked or desired to attack + */ + public void stopMoving(L2Npc npc, boolean suspend, boolean stoppedByAttack) { + L2MonsterInstance monster = null; + + if (npc.isMonster()) { + if (((L2MonsterInstance) npc).getLeader() == null) { + monster = (L2MonsterInstance) npc; + } else { + monster = ((L2MonsterInstance) npc).getLeader(); + } + } + + if (((monster != null) && !isRegistered(monster)) || !isRegistered(npc)) { + return; + } + + final WalkInfo walk = monster != null ? _activeRoutes.get(monster.getObjectId()) : _activeRoutes.get(npc.getObjectId()); + + walk.setSuspended(suspend); + walk.setStoppedByAttack(stoppedByAttack); + + if (monster != null) { + monster.stopMove(null); + monster.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } else { + npc.stopMove(null); + npc.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + } + + /** + * Manage "node arriving"-related tasks: schedule move to next node; send ON_NODE_ARRIVED event to Quest script + * @param npc NPC to manage + */ + public void onArrived(final L2Npc npc) { + if (_activeRoutes.containsKey(npc.getObjectId())) { + // Notify quest + EventDispatcher.getInstance().notifyEventAsync(new NpcMoveNodeArrived(npc), npc); + + final WalkInfo walk = _activeRoutes.get(npc.getObjectId()); + + // Opposite should not happen... but happens sometime + if ((walk.getCurrentNodeId() >= 0) && (walk.getCurrentNodeId() < walk.getRoute().getNodesCount())) { + final L2NpcWalkerNode node = walk.getRoute().getNodeList().get(walk.getCurrentNodeId()); + if (npc.isInsideRadius(node, 10, false, false)) { + npc.sendDebugMessage("Route '" + walk.getRoute().getName() + "', arrived to node " + walk.getCurrentNodeId()); + npc.sendDebugMessage("Done in " + ((System.currentTimeMillis() - walk.getLastAction()) / 1000) + " s"); + walk.calculateNextNode(npc); + walk.setBlocked(true); // prevents to be ran from walk check task, if there is delay in this node. + + if (node.getNpcString() != null) { + Broadcast.toKnownPlayers(npc, new NpcSay(npc, Say2.NPC_ALL, node.getNpcString())); + } else if (!node.getChatText().isEmpty()) { + Broadcast.toKnownPlayers(npc, new NpcSay(npc, Say2.NPC_ALL, node.getChatText())); + } + + if (npc.isDebug()) { + walk.setLastAction(System.currentTimeMillis()); + } + ThreadPoolManager.getInstance().scheduleGeneral(new ArrivedTask(npc, walk), 100 + (node.getDelay() * 1000L)); + } + } + } + } + + /** + * Manage "on death"-related tasks: permanently cancel moving of died NPC + * @param npc NPC to manage + */ + public void onDeath(L2Npc npc) { + cancelMoving(npc); + } + + /** + * Manage "on spawn"-related tasks: start NPC moving, if there is route attached to its spawn point + * @param npc NPC to manage + */ + public void onSpawn(L2Npc npc) { + if (_routesToAttach.containsKey(npc.getId())) { + final String routeName = _routesToAttach.get(npc.getId()).getRouteName(npc); + if (!routeName.isEmpty()) { + startMoving(npc, routeName); + } + } + } + + public static WalkingManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final WalkingManager INSTANCE = new WalkingManager(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/ZoneManager.java b/src/main/java/com/l2jserver/gameserver/instancemanager/ZoneManager.java new file mode 100644 index 00000000..94108944 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/ZoneManager.java @@ -0,0 +1,631 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.zone.AbstractZoneSettings; +import com.l2jserver.gameserver.model.zone.L2ZoneForm; +import com.l2jserver.gameserver.model.zone.L2ZoneRespawn; +import com.l2jserver.gameserver.model.zone.L2ZoneType; +import com.l2jserver.gameserver.model.zone.form.ZoneCuboid; +import com.l2jserver.gameserver.model.zone.form.ZoneCylinder; +import com.l2jserver.gameserver.model.zone.form.ZoneNPoly; +import com.l2jserver.gameserver.model.zone.type.L2ArenaZone; +import com.l2jserver.gameserver.model.zone.type.L2OlympiadStadiumZone; +import com.l2jserver.gameserver.model.zone.type.L2RespawnZone; +import com.l2jserver.gameserver.model.zone.type.NpcSpawnTerritory; +import com.l2jserver.gameserver.util.IXmlReader; + +/** + * This class manages the zones + * @author durgus + */ +public final class ZoneManager implements IXmlReader { + + private static final Logger LOG = LoggerFactory.getLogger(ZoneManager.class); + + private static final Map _settings = new HashMap<>(); + + private final Map, Map> _classZones = new HashMap<>(); + + private final Map _spawnTerritories = new HashMap<>(); + + private int _lastDynamicId = 300000; + + private List _debugItems; + + protected ZoneManager() { + load(); + } + + public void reload() { + // Get the world regions + int count = 0; + final L2WorldRegion[][] worldRegions = L2World.getInstance().getWorldRegions(); + + // Backup old zone settings + for (Map map : _classZones.values()) { + for (L2ZoneType zone : map.values()) { + if (zone.getSettings() != null) { + _settings.put(zone.getName(), zone.getSettings()); + } + } + } + + // Clear zones + for (L2WorldRegion[] worldRegion : worldRegions) { + for (L2WorldRegion element : worldRegion) { + element.getZones().clear(); + count++; + } + } + + GrandBossManager.getInstance().getZones().clear(); + LOG.info("Removed zones in {} regions.", count); + + // Load the zones + load(); + + // Re-validate all characters in zones + for (L2Object obj : L2World.getInstance().getVisibleObjects()) { + if (obj instanceof L2Character) { + ((L2Character) obj).revalidateZone(true); + } + } + _settings.clear(); + } + + @Override + public void parseDocument(Document doc, File f) { + // Get the world regions + final L2WorldRegion[][] worldRegions = L2World.getInstance().getWorldRegions(); + NamedNodeMap attrs; + Node attribute; + String zoneName; + int[][] coords; + int zoneId, minZ, maxZ; + String zoneType, zoneShape; + final List rs = new ArrayList<>(); + + for (Node n = doc.getFirstChild(); n != null; n = n.getNextSibling()) { + if ("list".equalsIgnoreCase(n.getNodeName())) { + attrs = n.getAttributes(); + attribute = attrs.getNamedItem("enabled"); + if ((attribute != null) && !Boolean.parseBoolean(attribute.getNodeValue())) { + continue; + } + + for (Node d = n.getFirstChild(); d != null; d = d.getNextSibling()) { + if ("zone".equalsIgnoreCase(d.getNodeName())) { + attrs = d.getAttributes(); + + attribute = attrs.getNamedItem("type"); + if (attribute != null) { + zoneType = attribute.getNodeValue(); + } else { + LOG.warn("Missing type for zone in file {}!", f); + continue; + } + + attribute = attrs.getNamedItem("id"); + if (attribute != null) { + zoneId = Integer.parseInt(attribute.getNodeValue()); + } else { + zoneId = zoneType.equalsIgnoreCase("NpcSpawnTerritory") ? 0 : _lastDynamicId++; + } + + attribute = attrs.getNamedItem("name"); + if (attribute != null) { + zoneName = attribute.getNodeValue(); + } else { + zoneName = null; + } + + // Check zone name for NpcSpawnTerritory. Must exist and to be unique + if (zoneType.equalsIgnoreCase("NpcSpawnTerritory")) { + if (zoneName == null) { + LOG.warn("Missing name for NpcSpawnTerritory in file: {}, skipping zone!", f); + continue; + } else if (_spawnTerritories.containsKey(zoneName)) { + LOG.warn("Name {} already used for another zone, check file {}, skipping zone!", zoneName, f); + continue; + } + } + + minZ = parseInteger(attrs, "minZ"); + maxZ = parseInteger(attrs, "maxZ"); + + zoneType = parseString(attrs, "type"); + zoneShape = parseString(attrs, "shape"); + + // Get the zone shape from xml + L2ZoneForm zoneForm = null; + try { + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("node".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + int[] point = new int[2]; + point[0] = parseInteger(attrs, "X"); + point[1] = parseInteger(attrs, "Y"); + rs.add(point); + } + } + + coords = rs.toArray(new int[rs.size()][2]); + rs.clear(); + + if (coords.length == 0) { + LOG.warn("Missing data for zone {} XML file {}!", zoneId, f); + continue; + } + + // Create this zone. Parsing for cuboids is a bit different than for other polygons cuboids need exactly 2 points to be defined. + // Other polygons need at least 3 (one per vertex) + if (zoneShape.equalsIgnoreCase("Cuboid")) { + if (coords.length == 2) { + zoneForm = new ZoneCuboid(coords[0][0], coords[1][0], coords[0][1], coords[1][1], minZ, maxZ); + } else { + LOG.warn("Missing cuboid vertex in SQL data for zone {} in file {}!", zoneId, f); + continue; + } + } else if (zoneShape.equalsIgnoreCase("NPoly")) { + // nPoly needs to have at least 3 vertices + if (coords.length > 2) { + final int[] aX = new int[coords.length]; + final int[] aY = new int[coords.length]; + for (int i = 0; i < coords.length; i++) { + aX[i] = coords[i][0]; + aY[i] = coords[i][1]; + } + zoneForm = new ZoneNPoly(aX, aY, minZ, maxZ); + } else { + LOG.warn("Bad data for zone {} in file {}!", zoneId, f); + continue; + } + } else if (zoneShape.equalsIgnoreCase("Cylinder")) { + // A Cylinder zone requires a center point + // at x,y and a radius + attrs = d.getAttributes(); + final int zoneRad = Integer.parseInt(attrs.getNamedItem("rad").getNodeValue()); + if ((coords.length == 1) && (zoneRad > 0)) { + zoneForm = new ZoneCylinder(coords[0][0], coords[0][1], minZ, maxZ, zoneRad); + } else { + LOG.warn("Bad data for zone {} in file {}!", zoneId, f); + continue; + } + } else { + LOG.warn("Unknown shape: {} for zone {} in file {}!", zoneShape, zoneId, f); + continue; + } + } catch (Exception e) { + LOG.warn("Failed to load zone {} coordinates!", zoneId, e); + } + + // No further parameters needed, if NpcSpawnTerritory is loading + if (zoneType.equalsIgnoreCase("NpcSpawnTerritory")) { + _spawnTerritories.put(zoneName, new NpcSpawnTerritory(zoneName, zoneForm)); + continue; + } + + // Create the zone + Class newZone; + Constructor zoneConstructor; + L2ZoneType temp; + try { + newZone = Class.forName("com.l2jserver.gameserver.model.zone.type.L2" + zoneType); + zoneConstructor = newZone.getConstructor(int.class); + temp = (L2ZoneType) zoneConstructor.newInstance(zoneId); + temp.setZone(zoneForm); + } catch (Exception e) { + LOG.warn("No such zone type {} in file {}!", zoneType, f); + continue; + } + + // Check for additional parameters + for (Node cd = d.getFirstChild(); cd != null; cd = cd.getNextSibling()) { + if ("stat".equalsIgnoreCase(cd.getNodeName())) { + attrs = cd.getAttributes(); + String name = attrs.getNamedItem("name").getNodeValue(); + String val = attrs.getNamedItem("val").getNodeValue(); + + temp.setParameter(name, val); + } else if ("spawn".equalsIgnoreCase(cd.getNodeName()) && (temp instanceof L2ZoneRespawn)) { + attrs = cd.getAttributes(); + int spawnX = Integer.parseInt(attrs.getNamedItem("X").getNodeValue()); + int spawnY = Integer.parseInt(attrs.getNamedItem("Y").getNodeValue()); + int spawnZ = Integer.parseInt(attrs.getNamedItem("Z").getNodeValue()); + Node val = attrs.getNamedItem("type"); + ((L2ZoneRespawn) temp).parseLoc(spawnX, spawnY, spawnZ, val == null ? null : val.getNodeValue()); + } else if ("race".equalsIgnoreCase(cd.getNodeName()) && (temp instanceof L2RespawnZone)) { + attrs = cd.getAttributes(); + String race = attrs.getNamedItem("name").getNodeValue(); + String point = attrs.getNamedItem("point").getNodeValue(); + + ((L2RespawnZone) temp).addRaceRespawnPoint(race, point); + } + } + + if (checkId(zoneId)) { + LOG.debug("Caution: Zone ({}) from file {} overrides previous definition.", zoneId, f); + } + + if ((zoneName != null) && !zoneName.isEmpty()) { + temp.setName(zoneName); + } + + addZone(zoneId, temp); + + // Register the zone into any world region it intersects with... + final var boundingBox = temp.getZone().getBoundingBox(); + final var bottomLeftRegion = L2World.getInstance().getRegion((int) boundingBox.getMinX(), (int) boundingBox.getMinY()); + final var topRightRegion = L2World.getInstance().getRegion((int) boundingBox.getMaxX(), (int) boundingBox.getMaxY()); + + int ax, ay, bx, by; + for (int x = bottomLeftRegion.getTileX(); x <= topRightRegion.getTileX(); x++) { + for (int y = bottomLeftRegion.getTileY(); y <= topRightRegion.getTileY(); y++) { + ax = (x - L2World.OFFSET_X) << L2World.SHIFT_BY; + bx = ((x + 1) - L2World.OFFSET_X) << L2World.SHIFT_BY; + ay = (y - L2World.OFFSET_Y) << L2World.SHIFT_BY; + by = ((y + 1) - L2World.OFFSET_Y) << L2World.SHIFT_BY; + + if (temp.getZone().intersectsRectangle(ax, bx, ay, by)) { + worldRegions[x][y].addZone(temp); + } + } + } + } + } + } + } + } + + @Override + public void load() { + final var start = System.currentTimeMillis(); + _classZones.clear(); + _spawnTerritories.clear(); + parseDatapackDirectory("data/zones", false); + final var endZones = System.currentTimeMillis(); + LOG.info("Loaded {} zone classes and {} zones in {}ms.", _classZones.size(), getSize(), endZones - start); + parseDatapackDirectory("data/zones/npcSpawnTerritories", false); + LOG.info("Loaded {} NPC spawn territories in {}ms.", _spawnTerritories.size(), System.currentTimeMillis() - endZones); + } + + /** + * Gets the size. + * @return the size + */ + public int getSize() { + int i = 0; + for (Map map : _classZones.values()) { + i += map.size(); + } + return i; + } + + /** + * Check id. + * @param id the id + * @return true, if successful + */ + public boolean checkId(int id) { + for (Map map : _classZones.values()) { + if (map.containsKey(id)) { + return true; + } + } + return false; + } + + /** + * Add new zone. + * @param the generic type + * @param id the id + * @param zone the zone + */ + @SuppressWarnings("unchecked") + public void addZone(Integer id, T zone) { + Map map = (Map) _classZones.get(zone.getClass()); + if (map == null) { + map = new HashMap<>(); + map.put(id, zone); + _classZones.put(zone.getClass(), map); + } else { + map.put(id, zone); + } + } + + /** + * Returns all zones registered with the ZoneManager. To minimize iteration processing retrieve zones from L2WorldRegion for a specific location instead. + * @return zones + * @see #getAllZones(Class) + */ + @Deprecated + public Collection getAllZones() { + final List zones = new ArrayList<>(); + for (Map map : _classZones.values()) { + zones.addAll(map.values()); + } + return zones; + } + + /** + * Return all zones by class type. + * @param the generic type + * @param zoneType Zone class + * @return Collection of zones + */ + @SuppressWarnings("unchecked") + public Collection getAllZones(Class zoneType) { + return (Collection) _classZones.get(zoneType).values(); + } + + /** + * Get zone by ID. + * @param id the id + * @return the zone by id + * @see #getZoneById(int, Class) + */ + public L2ZoneType getZoneById(int id) { + for (Map map : _classZones.values()) { + if (map.containsKey(id)) { + return map.get(id); + } + } + return null; + } + + /** + * Get zone by ID and zone class. + * @param the generic type + * @param id the id + * @param zoneType the zone type + * @return zone + */ + @SuppressWarnings("unchecked") + public T getZoneById(int id, Class zoneType) { + return (T) _classZones.get(zoneType).get(id); + } + + /** + * Returns all zones from where the object is located. + * @param object the object + * @return zones + */ + public List getZones(L2Object object) { + return getZones(object.getX(), object.getY(), object.getZ()); + } + + /** + * Gets the zone. + * @param the generic type + * @param object the object + * @param type the type + * @return zone from where the object is located by type + */ + public T getZone(L2Object object, Class type) { + if (object == null) { + return null; + } + return getZone(object.getX(), object.getY(), object.getZ(), type); + } + + /** + * Returns all zones from given coordinates (plane). + * @param x the x + * @param y the y + * @return zones + */ + public List getZones(int x, int y) { + final L2WorldRegion region = L2World.getInstance().getRegion(x, y); + final List temp = new ArrayList<>(); + for (L2ZoneType zone : region.getZones()) { + if (zone.isInsideZone(x, y)) { + temp.add(zone); + } + } + return temp; + } + + /** + * Returns all zones from given coordinates. + * @param x the x + * @param y the y + * @param z the z + * @return zones + */ + public List getZones(int x, int y, int z) { + final L2WorldRegion region = L2World.getInstance().getRegion(x, y); + final List temp = new ArrayList<>(); + for (L2ZoneType zone : region.getZones()) { + if (zone.isInsideZone(x, y, z)) { + temp.add(zone); + } + } + return temp; + } + + /** + * Gets the zone. + * @param the generic type + * @param x the x + * @param y the y + * @param z the z + * @param type the type + * @return zone from given coordinates + */ + @SuppressWarnings("unchecked") + public T getZone(int x, int y, int z, Class type) { + final L2WorldRegion region = L2World.getInstance().getRegion(x, y); + for (L2ZoneType zone : region.getZones()) { + if (zone.isInsideZone(x, y, z) && type.isInstance(zone)) { + return (T) zone; + } + } + return null; + } + + /** + * Get spawm territory by name + * @param name name of territory to search + * @return link to zone form + */ + public NpcSpawnTerritory getSpawnTerritory(String name) { + return _spawnTerritories.get(name); + } + + /** + * Returns all spawm territories from where the object is located + * @param object + * @return zones + */ + public List getSpawnTerritories(L2Object object) { + List temp = new ArrayList<>(); + for (NpcSpawnTerritory territory : _spawnTerritories.values()) { + if (territory.isInsideZone(object.getX(), object.getY(), object.getZ())) { + temp.add(territory); + } + } + + return temp; + } + + /** + * Gets the arena. + * @param character the character + * @return the arena + */ + public L2ArenaZone getArena(L2Character character) { + if (character == null) { + return null; + } + + for (L2ZoneType temp : ZoneManager.getInstance().getZones(character.getX(), character.getY(), character.getZ())) { + if ((temp instanceof L2ArenaZone) && temp.isCharacterInZone(character)) { + return ((L2ArenaZone) temp); + } + } + + return null; + } + + /** + * Gets the olympiad stadium. + * @param character the character + * @return the olympiad stadium + */ + public L2OlympiadStadiumZone getOlympiadStadium(L2Character character) { + if (character == null) { + return null; + } + + for (L2ZoneType temp : ZoneManager.getInstance().getZones(character.getX(), character.getY(), character.getZ())) { + if ((temp instanceof L2OlympiadStadiumZone) && temp.isCharacterInZone(character)) { + return ((L2OlympiadStadiumZone) temp); + } + } + return null; + } + + /** + * For testing purposes only. + * @param the generic type + * @param obj the obj + * @param type the type + * @return the closest zone + */ + @SuppressWarnings("unchecked") + public T getClosestZone(L2Object obj, Class type) { + T zone = getZone(obj, type); + if (zone == null) { + double closestdis = Double.MAX_VALUE; + for (T temp : (Collection) _classZones.get(type).values()) { + double distance = temp.getDistanceToZone(obj); + if (distance < closestdis) { + closestdis = distance; + zone = temp; + } + } + } + return zone; + } + + /** + * General storage for debug items used for visualizing zones. + * @return list of items + */ + public List getDebugItems() { + if (_debugItems == null) { + _debugItems = new ArrayList<>(); + } + return _debugItems; + } + + /** + * Remove all debug items from l2world. + */ + public void clearDebugItems() { + if (_debugItems != null) { + final Iterator it = _debugItems.iterator(); + while (it.hasNext()) { + final L2ItemInstance item = it.next(); + if (item != null) { + item.decayMe(); + } + it.remove(); + } + } + } + + /** + * Gets the settings. + * @param name the name + * @return the settings + */ + public static AbstractZoneSettings getSettings(String name) { + return _settings.get(name); + } + + public static ZoneManager getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final ZoneManager INSTANCE = new ZoneManager(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/games/Lottery.java b/src/main/java/com/l2jserver/gameserver/instancemanager/games/Lottery.java new file mode 100644 index 00000000..d0d6b3f5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/games/Lottery.java @@ -0,0 +1,468 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.games; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Calendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; + +public class Lottery { + protected static final Logger LOG = LoggerFactory.getLogger(Lottery.class); + + public static final long SECOND = 1000; + public static final long MINUTE = 60000; + + private static final String INSERT_LOTTERY = "INSERT INTO games(id, idnr, enddate, prize, newprize) VALUES (?, ?, ?, ?, ?)"; + private static final String UPDATE_PRICE = "UPDATE games SET prize=?, newprize=? WHERE id = 1 AND idnr = ?"; + private static final String UPDATE_LOTTERY = "UPDATE games SET finished=1, prize=?, newprize=?, number1=?, number2=?, prize1=?, prize2=?, prize3=? WHERE id=1 AND idnr=?"; + private static final String SELECT_LAST_LOTTERY = "SELECT idnr, prize, newprize, enddate, finished FROM games WHERE id = 1 ORDER BY idnr DESC LIMIT 1"; + private static final String SELECT_LOTTERY_ITEM = "SELECT enchant_level, custom_type2 FROM items WHERE item_id = 4442 AND custom_type1 = ?"; + private static final String SELECT_LOTTERY_TICKET = "SELECT number1, number2, prize1, prize2, prize3 FROM games WHERE id = 1 and idnr = ?"; + + private int _number; + private long _prize; + private boolean _isSellingTickets; + private boolean _isStarted; + private long _enddate; + + protected Lottery() { + _number = 1; + _prize = general().getLotteryPrize(); + _isSellingTickets = false; + _isStarted = false; + _enddate = System.currentTimeMillis(); + + if (general().allowLottery()) { + (new StartLottery()).run(); + } + } + + public static Lottery getInstance() { + return SingletonHolder._instance; + } + + public int getId() { + return _number; + } + + public long getPrize() { + return _prize; + } + + public long getEndDate() { + return _enddate; + } + + public void increasePrize(long count) { + _prize += count; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_PRICE)) { + ps.setLong(1, getPrize()); + ps.setLong(2, getPrize()); + ps.setInt(3, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not increase current lottery prize: {}", e.getMessage(), e); + } + } + + public boolean isSellableTickets() { + return _isSellingTickets; + } + + public boolean isStarted() { + return _isStarted; + } + + private class StartLottery implements Runnable { + protected StartLottery() { + // Do nothing + } + + @Override + public void run() { + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery(SELECT_LAST_LOTTERY)) { + if (rs.next()) { + _number = rs.getInt("idnr"); + + if (rs.getInt("finished") == 1) { + _number++; + _prize = rs.getLong("newprize"); + } else { + _prize = rs.getLong("prize"); + _enddate = rs.getLong("enddate"); + + if (_enddate <= (System.currentTimeMillis() + (2 * MINUTE))) { + (new FinishLottery()).run(); + return; + } + + if (_enddate > System.currentTimeMillis()) { + _isStarted = true; + ThreadPoolManager.getInstance().scheduleGeneral(new FinishLottery(), _enddate - System.currentTimeMillis()); + + if (_enddate > (System.currentTimeMillis() + (12 * MINUTE))) { + _isSellingTickets = true; + ThreadPoolManager.getInstance().scheduleGeneral(new StopSellingTickets(), _enddate - System.currentTimeMillis() - (10 * MINUTE)); + } + return; + } + } + } + } catch (Exception e) { + LOG.warn("Could not restore lottery data: {}", e.getMessage(), e); + } + + if (general().debug()) { + LOG.info("Starting ticket sell for lottery #{}.", getId()); + } + _isSellingTickets = true; + _isStarted = true; + + Broadcast.toAllOnlinePlayers("Lottery tickets are now available for Lucky Lottery #" + getId() + "."); + Calendar finishtime = Calendar.getInstance(); + finishtime.setTimeInMillis(_enddate); + finishtime.set(Calendar.MINUTE, 0); + finishtime.set(Calendar.SECOND, 0); + + if (finishtime.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) { + finishtime.set(Calendar.HOUR_OF_DAY, 19); + _enddate = finishtime.getTimeInMillis(); + _enddate += 604800000; + } else { + finishtime.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY); + finishtime.set(Calendar.HOUR_OF_DAY, 19); + _enddate = finishtime.getTimeInMillis(); + } + + ThreadPoolManager.getInstance().scheduleGeneral(new StopSellingTickets(), _enddate - System.currentTimeMillis() - (10 * MINUTE)); + ThreadPoolManager.getInstance().scheduleGeneral(new FinishLottery(), _enddate - System.currentTimeMillis()); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_LOTTERY)) { + ps.setInt(1, 1); + ps.setInt(2, getId()); + ps.setLong(3, getEndDate()); + ps.setLong(4, getPrize()); + ps.setLong(5, getPrize()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not store new lottery data: {}", e.getMessage(), e); + } + } + } + + private class StopSellingTickets implements Runnable { + protected StopSellingTickets() { + // Do nothing + } + + @Override + public void run() { + if (general().debug()) { + LOG.info("Stopping ticket sell for lottery #{}.", getId()); + } + _isSellingTickets = false; + + Broadcast.toAllOnlinePlayers(SystemMessage.getSystemMessage(SystemMessageId.LOTTERY_TICKET_SALES_TEMP_SUSPENDED)); + } + } + + private class FinishLottery implements Runnable { + protected FinishLottery() { + // Do nothing + } + + @Override + public void run() { + if (general().debug()) { + LOG.info("Ending lottery #{}.", getId()); + } + + int[] luckynums = new int[5]; + int luckynum = 0; + + for (int i = 0; i < 5; i++) { + boolean found = true; + + while (found) { + luckynum = Rnd.get(20) + 1; + found = false; + + for (int j = 0; j < i; j++) { + if (luckynums[j] == luckynum) { + found = true; + break; + } + } + } + + luckynums[i] = luckynum; + } + + if (general().debug()) { + LOG.info("The lucky numbers are {}, {}, {}, {}, {}.", luckynums[0], luckynums[1], luckynums[2], luckynums[3], luckynums[4]); + } + + int enchant = 0; + int type2 = 0; + + for (int i = 0; i < 5; i++) { + if (luckynums[i] < 17) { + enchant += (int) Math.pow(2, luckynums[i] - 1); + } else { + type2 += (int) Math.pow(2, luckynums[i] - 17); + } + } + + if (general().debug()) { + LOG.info("Encoded lucky numbers are {}, {}", enchant, type2); + } + + int count1 = 0; + int count2 = 0; + int count3 = 0; + int count4 = 0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_LOTTERY_ITEM)) { + ps.setInt(1, getId()); + try (var rset = ps.executeQuery()) { + while (rset.next()) { + int curenchant = rset.getInt("enchant_level") & enchant; + int curtype2 = rset.getInt("custom_type2") & type2; + + if ((curenchant == 0) && (curtype2 == 0)) { + continue; + } + + int count = 0; + + for (int i = 1; i <= 16; i++) { + int val = curenchant / 2; + + if (val != Math.round((double) curenchant / 2)) { + count++; + } + + int val2 = curtype2 / 2; + + if (val2 != ((double) curtype2 / 2)) { + count++; + } + + curenchant = val; + curtype2 = val2; + } + + if (count == 5) { + count1++; + } else if (count == 4) { + count2++; + } else if (count == 3) { + count3++; + } else if (count > 0) { + count4++; + } + } + } + } catch (Exception e) { + LOG.warn("Could not restore lottery data: {}", e.getMessage(), e); + } + + long prize4 = count4 * general().getLottery2and1NumberPrize(); + long prize1 = 0; + long prize2 = 0; + long prize3 = 0; + + if (count1 > 0) { + prize1 = (long) (((getPrize() - prize4) * general().getLottery5NumberRate()) / count1); + } + + if (count2 > 0) { + prize2 = (long) (((getPrize() - prize4) * general().getLottery4NumberRate()) / count2); + } + + if (count3 > 0) { + prize3 = (long) (((getPrize() - prize4) * general().getLottery3NumberRate()) / count3); + } + + if (general().debug()) { + LOG.info("{} players with all FIVE numbers each win {}.", count1, prize1); + LOG.info("{} players with FOUR numbers each win {}.", count2, prize2); + LOG.info("{} players with THREE numbers each win {}.", count3, prize3); + LOG.info("{} players with ONE or TWO numbers each win {}.", count4, prize4); + } + + long newprize = getPrize() - (prize1 + prize2 + prize3 + prize4); + if (general().debug()) { + LOG.info("Jackpot for next lottery is {}.", newprize); + } + + SystemMessage sm; + if (count1 > 0) { + // There are winners. + sm = SystemMessage.getSystemMessage(SystemMessageId.AMOUNT_FOR_WINNER_S1_IS_S2_ADENA_WE_HAVE_S3_PRIZE_WINNER); + sm.addInt(getId()); + sm.addLong(getPrize()); + sm.addLong(count1); + Broadcast.toAllOnlinePlayers(sm); + } else { + // There are no winners. + sm = SystemMessage.getSystemMessage(SystemMessageId.AMOUNT_FOR_LOTTERY_S1_IS_S2_ADENA_NO_WINNER); + sm.addInt(getId()); + sm.addLong(getPrize()); + Broadcast.toAllOnlinePlayers(sm); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_LOTTERY)) { + ps.setLong(1, getPrize()); + ps.setLong(2, newprize); + ps.setInt(3, enchant); + ps.setInt(4, type2); + ps.setLong(5, prize1); + ps.setLong(6, prize2); + ps.setLong(7, prize3); + ps.setInt(8, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not store finished lottery data: {}", e.getMessage(), e); + } + + ThreadPoolManager.getInstance().scheduleGeneral(new StartLottery(), MINUTE); + _number++; + + _isStarted = false; + } + } + + public int[] decodeNumbers(int enchant, int type2) { + int[] res = new int[5]; + int id = 0; + int nr = 1; + + while (enchant > 0) { + int val = enchant / 2; + if (val != Math.round((double) enchant / 2)) { + res[id++] = nr; + } + enchant /= 2; + nr++; + } + + nr = 17; + + while (type2 > 0) { + int val = type2 / 2; + if (val != ((double) type2 / 2)) { + res[id++] = nr; + } + type2 /= 2; + nr++; + } + + return res; + } + + public long[] checkTicket(L2ItemInstance item) { + return checkTicket(item.getCustomType1(), item.getEnchantLevel(), item.getCustomType2()); + } + + private long[] checkTicket(int id, int enchant, int type2) { + long[] res = { + 0, + 0 + }; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_LOTTERY_TICKET)) { + ps.setInt(1, id); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + int curenchant = rs.getInt("number1") & enchant; + int curtype2 = rs.getInt("number2") & type2; + + if ((curenchant == 0) && (curtype2 == 0)) { + return res; + } + + int count = 0; + + for (int i = 1; i <= 16; i++) { + int val = curenchant / 2; + if (val != Math.round((double) curenchant / 2)) { + count++; + } + int val2 = curtype2 / 2; + if (val2 != ((double) curtype2 / 2)) { + count++; + } + curenchant = val; + curtype2 = val2; + } + + switch (count) { + case 0: + break; + case 5: + res[0] = 1; + res[1] = rs.getLong("prize1"); + break; + case 4: + res[0] = 2; + res[1] = rs.getLong("prize2"); + break; + case 3: + res[0] = 3; + res[1] = rs.getLong("prize3"); + break; + default: + res[0] = 4; + res[1] = general().getLottery2and1NumberPrize(); + } + + if (general().debug()) { + LOG.warn("count: {}, id: {}, enchant: {}, type2: {}", count, id, enchant, type2); + } + } + } + } catch (Exception e) { + LOG.warn("Could not check lottery ticket #{}: {}", id, e.getMessage(), e); + } + return res; + } + + private static class SingletonHolder { + protected static final Lottery _instance = new Lottery(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeAttackTimeTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeAttackTimeTask.java new file mode 100644 index 00000000..2d4e0466 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeAttackTimeTask.java @@ -0,0 +1,87 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Calendar; +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; + +/** + * Four Sepulchers change attack time task. + * @author xban1x + */ +public final class FourSepulchersChangeAttackTimeTask implements Runnable { + @Override + public void run() { + final FourSepulchersManager manager = FourSepulchersManager.getInstance(); + manager.setIsEntryTime(false); + manager.setIsWarmUpTime(false); + manager.setIsAttackTime(true); + manager.setIsCoolDownTime(false); + + manager.locationShadowSpawns(); + + manager.spawnMysteriousBox(31921); + manager.spawnMysteriousBox(31922); + manager.spawnMysteriousBox(31923); + manager.spawnMysteriousBox(31924); + + if (!manager.isFirstTimeRun()) { + manager.setWarmUpTimeEnd(Calendar.getInstance().getTimeInMillis()); + } + + long interval; + // say task + if (manager.isFirstTimeRun()) { + for (double min = Calendar.getInstance().get(Calendar.MINUTE); min < manager.getCycleMin(); min++) { + // looking for next shout time.... + if ((min % 5) == 0)// check if min can be divided by 5 + { + final Calendar inter = Calendar.getInstance(); + inter.set(Calendar.MINUTE, (int) min); + ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersManagerSayTask(), inter.getTimeInMillis() - Calendar.getInstance().getTimeInMillis()); + break; + } + } + } else { + ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersManagerSayTask(), 5 * 60400); + } + // searching time when attack time will be ended: + // counting difference between time when attack time ends and + // current time + // and then launching change time task + if (manager.isFirstTimeRun()) { + interval = manager.getAttackTimeEnd() - Calendar.getInstance().getTimeInMillis(); + } else { + interval = general().getTimeOfAttack(); + } + + manager.setChangeCoolDownTimeTask(ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeCoolDownTimeTask(), interval)); + final ScheduledFuture changeAttackTimeTask = manager.getChangeAttackTimeTask(); + + if (changeAttackTimeTask != null) { + changeAttackTimeTask.cancel(true); + manager.setChangeAttackTimeTask(null); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeCoolDownTimeTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeCoolDownTimeTask.java new file mode 100644 index 00000000..d0b414f8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeCoolDownTimeTask.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import java.util.Calendar; +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; + +/** + * Four Sepulchers change cool down time task. + * @author xban1x + */ +public final class FourSepulchersChangeCoolDownTimeTask implements Runnable { + @Override + public void run() { + final FourSepulchersManager manager = FourSepulchersManager.getInstance(); + manager.setIsEntryTime(false); + manager.setIsWarmUpTime(false); + manager.setIsAttackTime(false); + manager.setIsCoolDownTime(true); + + manager.clean(); + + final Calendar time = Calendar.getInstance(); + // one hour = 55th min to 55 min of next hour, so we check for this, + // also check for first launch + if (!manager.isFirstTimeRun() && (Calendar.getInstance().get(Calendar.MINUTE) > manager.getCycleMin())) { + time.set(Calendar.HOUR, Calendar.getInstance().get(Calendar.HOUR) + 1); + } + time.set(Calendar.MINUTE, manager.getCycleMin()); + if (manager.isFirstTimeRun()) { + manager.setIsFirstTimeRun(false); + } + + long interval = time.getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + + manager.setChangeEntryTimeTask(ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeEntryTimeTask(), interval)); + final ScheduledFuture changeCoolDownTimeTask = manager.getChangeCoolDownTimeTask(); + + if (changeCoolDownTimeTask != null) { + changeCoolDownTimeTask.cancel(true); + manager.setChangeCoolDownTimeTask(null); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeEntryTimeTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeEntryTimeTask.java new file mode 100644 index 00000000..01cd4bf0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeEntryTimeTask.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Calendar; +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; + +/** + * Four Sepulchers change entry time task. + * @author xban1x + */ +public final class FourSepulchersChangeEntryTimeTask implements Runnable { + @Override + public void run() { + final FourSepulchersManager manager = FourSepulchersManager.getInstance(); + manager.setIsEntryTime(true); + manager.setIsWarmUpTime(false); + manager.setIsAttackTime(false); + manager.setIsCoolDownTime(false); + + long interval; + // if this is first launch - search time whFourSepulchersManager_inEntryTime = true;naFourSepulchersManager_inEntryTime = true;maen entry time will be + // ended: + // counting difference between time when entry time ends and current time + // and then launching change time task + if (manager.isFirstTimeRun()) { + interval = manager.getEntrytTimeEnd() - Calendar.getInstance().getTimeInMillis(); + } else { + interval = general().getTimeOfEntry(); // else use stupid method + } + + // launching saying process... + ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersManagerSayTask(), 0); + manager.setChangeWarmUpTimeTask(ThreadPoolManager.getInstance().scheduleEffect(new FourSepulchersChangeWarmUpTimeTask(), interval)); + final ScheduledFuture changeEntryTimeTask = manager.getChangeEntryTimeTask(); + + if (changeEntryTimeTask != null) { + changeEntryTimeTask.cancel(true); + manager.setChangeEntryTimeTask(null); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeWarmUpTimeTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeWarmUpTimeTask.java new file mode 100644 index 00000000..a55d7ae5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersChangeWarmUpTimeTask.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Calendar; +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; + +/** + * Four Sepulchers change warm up time task. + * @author xban1x + */ +public final class FourSepulchersChangeWarmUpTimeTask implements Runnable { + @Override + public void run() { + final FourSepulchersManager manager = FourSepulchersManager.getInstance(); + manager.setIsEntryTime(true); + manager.setIsWarmUpTime(false); + manager.setIsAttackTime(false); + manager.setIsCoolDownTime(false); + + long interval; + // searching time when warmup time will be ended: + // counting difference between time when warmup time ends and + // current time and then launching change time task + if (manager.isFirstTimeRun()) { + interval = manager.getWarmUpTimeEnd() - Calendar.getInstance().getTimeInMillis(); + } else { + interval = general().getTimeOfWarmUp(); + } + + manager.setChangeAttackTimeTask(ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersChangeAttackTimeTask(), interval)); + final ScheduledFuture changeWarmUpTimeTask = manager.getChangeWarmUpTimeTask(); + + if (changeWarmUpTimeTask != null) { + changeWarmUpTimeTask.cancel(true); + manager.setChangeWarmUpTimeTask(null); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersManagerSayTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersManagerSayTask.java new file mode 100644 index 00000000..03e3cf14 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/FourSepulchersManagerSayTask.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Calendar; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; + +/** + * Four Sepulchers manager say task. + * @author xban1x + */ +public final class FourSepulchersManagerSayTask implements Runnable { + @Override + public void run() { + if (FourSepulchersManager.getInstance().isAttackTime()) { + final Calendar tmp = Calendar.getInstance(); + tmp.setTimeInMillis(Calendar.getInstance().getTimeInMillis() - FourSepulchersManager.getInstance().getWarmUpTimeEnd()); + if ((tmp.get(Calendar.MINUTE) + 5) < general().getTimeOfAttack()) { + // byte because minute cannot be more than 59 + FourSepulchersManager.getInstance().managerSay((byte) tmp.get(Calendar.MINUTE)); + ThreadPoolManager.getInstance().scheduleGeneral(new FourSepulchersManagerSayTask(), 5 * 60000); + } + // attack time ending chat + else if ((tmp.get(Calendar.MINUTE) + 5) >= general().getTimeOfAttack()) { + FourSepulchersManager.getInstance().managerSay((byte) 90); // sending a unique id :D + } + } else if (FourSepulchersManager.getInstance().isEntryTime()) { + FourSepulchersManager.getInstance().managerSay((byte) 0); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/GrandBossManagerStoreTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/GrandBossManagerStoreTask.java new file mode 100644 index 00000000..c25fdcc6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/GrandBossManagerStoreTask.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import com.l2jserver.gameserver.instancemanager.GrandBossManager; + +/** + * @author xban1x + */ +public class GrandBossManagerStoreTask implements Runnable { + @Override + public void run() { + GrandBossManager.getInstance().storeMe(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/MessageDeletionTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/MessageDeletionTask.java new file mode 100644 index 00000000..32324edb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/MessageDeletionTask.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.instancemanager.MailManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Message; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Message deletion task. + * @author xban1x + */ +public final class MessageDeletionTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(MessageDeletionTask.class); + + private final int _msgId; + + public MessageDeletionTask(int msgId) { + _msgId = msgId; + } + + @Override + public void run() { + final Message msg = MailManager.getInstance().getMessage(_msgId); + if (msg == null) { + return; + } + + if (msg.hasAttachments()) { + try { + final L2PcInstance sender = L2World.getInstance().getPlayer(msg.getSenderId()); + if (sender != null) { + msg.getAttachments().returnToWh(sender.getWarehouse()); + sender.sendPacket(SystemMessageId.MAIL_RETURNED); + } else { + msg.getAttachments().returnToWh(null); + } + + msg.getAttachments().deleteMe(); + msg.removeAttachments(); + + final L2PcInstance receiver = L2World.getInstance().getPlayer(msg.getReceiverId()); + if (receiver != null) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.MAIL_RETURNED); + // sm.addString(msg.getReceiverName()); + receiver.sendPacket(sm); + } + } catch (Exception e) { + LOG.warn("Error returning items:{}", e.getMessage(), e); + } + } + MailManager.getInstance().deleteMessageInDb(msg.getId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/PenaltyRemoveTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/PenaltyRemoveTask.java new file mode 100644 index 00000000..eb3962c6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/PenaltyRemoveTask.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import com.l2jserver.gameserver.instancemanager.HandysBlockCheckerManager; + +/** + * Handys Block Checker penalty remove. + * @author xban1x + */ +public final class PenaltyRemoveTask implements Runnable { + private final int _objectId; + + public PenaltyRemoveTask(int id) { + _objectId = id; + } + + @Override + public void run() { + HandysBlockCheckerManager.getInstance().removePenalty(_objectId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/StartMovingTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/StartMovingTask.java new file mode 100644 index 00000000..a5f47722 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/StartMovingTask.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.actor.L2Npc; + +/** + * Task which starts npc movement. + * @author xban1x + */ +public final class StartMovingTask implements Runnable { + private final L2Npc _npc; + private final String _routeName; + + public StartMovingTask(L2Npc npc, String routeName) { + _npc = npc; + _routeName = routeName; + } + + @Override + public void run() { + if (_npc != null) { + WalkingManager.getInstance().startMoving(_npc, _routeName); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/UpdateSoDStateTask.java b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/UpdateSoDStateTask.java new file mode 100644 index 00000000..b6293cd6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/instancemanager/tasks/UpdateSoDStateTask.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.instancemanager.tasks; + +import com.l2jserver.gameserver.instancemanager.GraciaSeedsManager; + +/** + * Task which updates Seed of Destruction state. + * @author xban1x + */ +public final class UpdateSoDStateTask implements Runnable { + @Override + public void run() { + final GraciaSeedsManager manager = GraciaSeedsManager.getInstance(); + manager.setSoDState(5, true, true); + manager.updateSodState(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/AbsorberInfo.java b/src/main/java/com/l2jserver/gameserver/model/AbsorberInfo.java new file mode 100644 index 00000000..d8e95915 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/AbsorberInfo.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.interfaces.IUniqueId; + +/** + * @author xban1x + */ +public final class AbsorberInfo implements IUniqueId { + private int _objectId; + private double _absorbedHp; + + public AbsorberInfo(int objectId, double pAbsorbedHp) { + _objectId = objectId; + _absorbedHp = pAbsorbedHp; + } + + public double getAbsorbedHp() { + return _absorbedHp; + } + + public void setAbsorbedHp(double absorbedHp) { + _absorbedHp = absorbedHp; + } + + @Override + public int getObjectId() { + return _objectId; + } + + public void setObjectId(int objectId) { + _objectId = objectId; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof AbsorberInfo) { + return (((AbsorberInfo) obj).getObjectId() == _objectId); + } + + return false; + } + + @Override + public int hashCode() { + return _objectId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/AbstractPlayerGroup.java b/src/main/java/com/l2jserver/gameserver/model/AbstractPlayerGroup.java new file mode 100644 index 00000000..557655fa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/AbstractPlayerGroup.java @@ -0,0 +1,160 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * @author Battlecruiser + */ +public abstract class AbstractPlayerGroup { + /** + * @return a list of all members of this group + */ + public abstract List getMembers(); + + /** + * @return a list of object IDs of the members of this group + */ + public List getMembersObjectId() { + final List ids = new ArrayList<>(); + forEachMember(m -> { + ids.add(m.getObjectId()); + return true; + }); + return ids; + } + + /** + * @return the leader of this group + */ + public abstract L2PcInstance getLeader(); + + /** + * Change the leader of this group to the specified player. + * @param leader the player to set as the new leader of this group + */ + public abstract void setLeader(L2PcInstance leader); + + /** + * @return the leader's object ID + */ + public int getLeaderObjectId() { + return getLeader().getObjectId(); + } + + /** + * Check if a given player is the leader of this group. + * @param player the player to check + * @return {@code true} if the specified player is the leader of this group, {@code false} otherwise + */ + public boolean isLeader(L2PcInstance player) { + return (getLeaderObjectId() == player.getObjectId()); + } + + /** + * @return the count of all players in this group + */ + public int getMemberCount() { + return getMembers().size(); + } + + /** + * @return the level of this group + */ + public abstract int getLevel(); + + /** + * Broadcast a packet to every member of this group. + * @param packet the packet to broadcast + */ + public void broadcastPacket(final L2GameServerPacket packet) { + forEachMember(m -> { + if (m != null) { + m.sendPacket(packet); + } + return true; + }); + } + + /** + * Broadcast a system message to this group. + * @param message the system message to broadcast + */ + public void broadcastMessage(SystemMessageId message) { + broadcastPacket(SystemMessage.getSystemMessage(message)); + } + + /** + * Broadcast a text message to this group. + * @param text to broadcast + */ + public void broadcastString(String text) { + broadcastPacket(SystemMessage.sendString(text)); + } + + public void broadcastCreatureSay(final CreatureSay msg, final L2PcInstance broadcaster) { + forEachMember(m -> { + if ((m != null) && !BlockList.isBlocked(m, broadcaster)) { + m.sendPacket(msg); + } + return true; + }); + } + + /** + * Check if this group contains a given player. + * @param player the player to check + * @return {@code true} if this group contains the specified player, {@code false} otherwise + */ + public boolean containsPlayer(L2PcInstance player) { + return getMembers().contains(player); + } + + /** + * @return a random member of this group + */ + public L2PcInstance getRandomPlayer() { + return getMembers().get(Rnd.get(getMemberCount())); + } + + /** + * Iterates over the group and executes procedure on each member + * @param procedure the procedure to be executed on each member.
    + * If executing the procedure on a member returns {@code true}, the loop continues to the next member, otherwise it breaks the loop + * @return {@code true} if the procedure executed correctly, {@code false} if the loop was broken prematurely + */ + public boolean forEachMember(Function procedure) { + for (L2PcInstance player : getMembers()) { + if (!procedure.apply(player)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ActionKey.java b/src/main/java/com/l2jserver/gameserver/model/ActionKey.java new file mode 100644 index 00000000..294bad62 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ActionKey.java @@ -0,0 +1,103 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * Action Key DTO. + * @author mrTJO + * @author Zoey76 + */ +public class ActionKey { + private final int _cat; + private int _cmd = 0; + private int _key = 0; + private int _toggleKey1 = 0; + private int _toggleKey2 = 0; + private int _show = 1; + + public ActionKey(int cat) { + _cat = cat; + } + + /** + * L2ActionKey Initialization + * @param cat Category ID + * @param cmd Command ID + * @param key User Defined Primary Key + * @param tgKey1 1st Toggled Key (eg. Alt, Ctrl or Shift) + * @param tgKey2 2nd Toggled Key (eg. Alt, Ctrl or Shift) + * @param show Show Action in UI + */ + public ActionKey(int cat, int cmd, int key, int tgKey1, int tgKey2, int show) { + _cat = cat; + _cmd = cmd; + _key = key; + _toggleKey1 = tgKey1; + _toggleKey2 = tgKey2; + _show = show; + } + + public int getCategory() { + return _cat; + } + + public int getCommandId() { + return _cmd; + } + + public void setCommandId(int cmd) { + _cmd = cmd; + } + + public int getKeyId() { + return _key; + } + + public void setKeyId(int key) { + _key = key; + } + + public int getToggleKey1() { + return _toggleKey1; + } + + public void setToggleKey1(int tKey1) { + _toggleKey1 = tKey1; + } + + public int getToggleKey2() { + return _toggleKey2; + } + + public void setToggleKey2(int tKey2) { + _toggleKey2 = tKey2; + } + + public int getShowStatus() { + return _show; + } + + public void setShowStatus(int show) { + _show = show; + } + + public String getSqlSaveString(int playerId, int order) { + return "(" + playerId + ", " + _cat + ", " + order + ", " + _cmd + "," + _key + ", " + _toggleKey1 + ", " + _toggleKey2 + ", " + _show + ")"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/AggroInfo.java b/src/main/java/com/l2jserver/gameserver/model/AggroInfo.java new file mode 100644 index 00000000..29f57558 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/AggroInfo.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * @author xban1x + */ +public final class AggroInfo { + private final L2Character _attacker; + private long _hate = 0; + private int _damage = 0; + + public AggroInfo(L2Character pAttacker) { + _attacker = pAttacker; + } + + public L2Character getAttacker() { + return _attacker; + } + + public long getHate() { + return _hate; + } + + public long checkHate(L2Character owner) { + if (_attacker.isAlikeDead() || !_attacker.isVisible() || !owner.getKnownList().knowsObject(_attacker)) { + _hate = 0; + } + + return _hate; + } + + public void addHate(long value) { + _hate = Math.min(_hate + value, 999999999); + } + + public void stopHate() { + _hate = 0; + } + + public int getDamage() { + return _damage; + } + + public void addDamage(int value) { + _damage = (int) Math.min(_damage + (long) value, 999999999); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof AggroInfo) { + return (((AggroInfo) obj).getAttacker() == _attacker); + } + + return false; + } + + @Override + public int hashCode() { + return _attacker.getObjectId(); + } + + @Override + public String toString() { + return "AggroInfo [attacker=" + _attacker + ", hate=" + _hate + ", damage=" + _damage + "]"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/AirShipTeleportList.java b/src/main/java/com/l2jserver/gameserver/model/AirShipTeleportList.java new file mode 100644 index 00000000..7878fc79 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/AirShipTeleportList.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * Holds a list of all AirShip teleports. + * @author xban1x + */ +public final class AirShipTeleportList { + private final int _location; + private final int[] _fuel; + private final VehiclePathPoint[][] _routes; + + public AirShipTeleportList(int loc, int[] f, VehiclePathPoint[][] r) { + _location = loc; + _fuel = f; + _routes = r; + } + + public int getLocation() { + return _location; + } + + public int[] getFuel() { + return _fuel; + } + + public VehiclePathPoint[][] getRoute() { + return _routes; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ArenaParticipantsHolder.java b/src/main/java/com/l2jserver/gameserver/model/ArenaParticipantsHolder.java new file mode 100644 index 00000000..71cb6445 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ArenaParticipantsHolder.java @@ -0,0 +1,143 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.instancemanager.HandysBlockCheckerManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.BlockCheckerEngine; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * @author xban1x + */ +public final class ArenaParticipantsHolder { + private final int _arena; + private final List _redPlayers; + private final List _bluePlayers; + private final BlockCheckerEngine _engine; + + public ArenaParticipantsHolder(int arena) { + _arena = arena; + _redPlayers = new ArrayList<>(6); + _bluePlayers = new ArrayList<>(6); + _engine = new BlockCheckerEngine(this, _arena); + } + + public List getRedPlayers() { + return _redPlayers; + } + + public List getBluePlayers() { + return _bluePlayers; + } + + public List getAllPlayers() { + List all = new ArrayList<>(12); + all.addAll(_redPlayers); + all.addAll(_bluePlayers); + return all; + } + + public void addPlayer(L2PcInstance player, int team) { + if (team == 0) { + _redPlayers.add(player); + } else { + _bluePlayers.add(player); + } + } + + public void removePlayer(L2PcInstance player, int team) { + if (team == 0) { + _redPlayers.remove(player); + } else { + _bluePlayers.remove(player); + } + } + + public int getPlayerTeam(L2PcInstance player) { + if (_redPlayers.contains(player)) { + return 0; + } else if (_bluePlayers.contains(player)) { + return 1; + } else { + return -1; + } + } + + public int getRedTeamSize() { + return _redPlayers.size(); + } + + public int getBlueTeamSize() { + return _bluePlayers.size(); + } + + public void broadCastPacketToTeam(L2GameServerPacket packet) { + for (L2PcInstance p : _redPlayers) { + p.sendPacket(packet); + } + for (L2PcInstance p : _bluePlayers) { + p.sendPacket(packet); + } + } + + public void clearPlayers() { + _redPlayers.clear(); + _bluePlayers.clear(); + } + + public BlockCheckerEngine getEvent() { + return _engine; + } + + public void updateEvent() { + _engine.updatePlayersOnStart(this); + } + + public void checkAndShuffle() { + final int redSize = _redPlayers.size(); + final int blueSize = _bluePlayers.size(); + if (redSize > (blueSize + 1)) { + broadCastPacketToTeam(SystemMessage.getSystemMessage(SystemMessageId.TEAM_ADJUSTED_BECAUSE_WRONG_POPULATION_RATIO)); + final int needed = redSize - (blueSize + 1); + for (int i = 0; i < (needed + 1); i++) { + final L2PcInstance plr = _redPlayers.get(i); + if (plr == null) { + continue; + } + HandysBlockCheckerManager.getInstance().changePlayerToTeam(plr, _arena, 1); + } + } else if (blueSize > (redSize + 1)) { + broadCastPacketToTeam(SystemMessage.getSystemMessage(SystemMessageId.TEAM_ADJUSTED_BECAUSE_WRONG_POPULATION_RATIO)); + final int needed = blueSize - (redSize + 1); + for (int i = 0; i < (needed + 1); i++) { + final L2PcInstance plr = _bluePlayers.get(i); + if (plr == null) { + continue; + } + HandysBlockCheckerManager.getInstance().changePlayerToTeam(plr, _arena, 0); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/AutoSpawnHandler.java b/src/main/java/com/l2jserver/gameserver/model/AutoSpawnHandler.java new file mode 100644 index 00000000..d3c42dcb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/AutoSpawnHandler.java @@ -0,0 +1,627 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * Auto Spawn handler.
    + * Allows spawning of a NPC object based on a timer.
    + * (From the official idea used for the Merchant and Blacksmith of Mammon)
    + * General Usage: - Call registerSpawn() with the parameters listed below.
    + * int npcId int[][] spawnPoints or specify NULL to add points later.
    + * int initialDelay (If < 0 = default value) int respawnDelay (If < 0 = default value)
    + * int despawnDelay (If < 0 = default value or if = 0, function disabled)
    + * spawnPoints is a standard two-dimensional int array containing X,Y and Z coordinates.
    + * The default respawn/despawn delays are currently every hour (as for Mammon on official servers).
    + * The resulting AutoSpawnInstance object represents the newly added spawn index.
    + * The internal methods of this object can be used to adjust random spawning, for instance a call to setRandomSpawn(1, true); would set the spawn at index 1 to be randomly rather than sequentially-based.
    + * Also they can be used to specify the number of NPC instances to spawn using setSpawnCount(), and broadcast a message to all users using setBroadcast().
    + * Random Spawning = OFF by default Broadcasting = OFF by default + * @author Tempy + */ +public class AutoSpawnHandler { + + private static final Logger LOG = LoggerFactory.getLogger(AutoSpawnHandler.class); + + private static final int DEFAULT_INITIAL_SPAWN = 30000; // 30 seconds after registration + + private static final int DEFAULT_RESPAWN = 3600000; // 1 hour in milliseconds + + private static final int DEFAULT_DESPAWN = 3600000; // 1 hour in milliseconds + + protected Map _registeredSpawns = new ConcurrentHashMap<>(); + + protected Map> _runningSpawns = new ConcurrentHashMap<>(); + + protected boolean _activeState = true; + + protected AutoSpawnHandler() { + restoreSpawnData(); + } + + public void reload() { + // stop all timers + for (ScheduledFuture sf : _runningSpawns.values()) { + if (sf != null) { + sf.cancel(true); + } + } + // unregister all registered spawns + for (AutoSpawnInstance asi : _registeredSpawns.values()) { + if (asi != null) { + this.removeSpawn(asi); + } + } + + // create clean list + _registeredSpawns.clear(); + _runningSpawns.clear(); + + // load + restoreSpawnData(); + } + + private void restoreSpawnData() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var s = con.createStatement(); + var rs = s.executeQuery("SELECT * FROM random_spawn ORDER BY groupId"); + var ps = con.prepareStatement("SELECT * FROM random_spawn_loc WHERE groupId=?")) { + // Restore spawn group data, then the location data. + while (rs.next()) { + // Register random spawn group, set various options on the + // created spawn instance. + AutoSpawnInstance spawnInst = registerSpawn(rs.getInt("npcId"), rs.getInt("initialDelay"), rs.getInt("respawnDelay"), rs.getInt("despawnDelay")); + + spawnInst.setSpawnCount(rs.getInt("count")); + spawnInst.setBroadcast(rs.getBoolean("broadcastSpawn")); + spawnInst.setRandomSpawn(rs.getBoolean("randomSpawn")); + + // Restore the spawn locations for this spawn group/instance. + ps.setInt(1, rs.getInt("groupId")); + try (var rs2 = ps.executeQuery()) { + ps.clearParameters(); + + while (rs2.next()) { + // Add each location to the spawn group/instance. + spawnInst.addSpawnLocation(rs2.getInt("x"), rs2.getInt("y"), rs2.getInt("z"), rs2.getInt("heading")); + } + } + } + LOG.info("Loaded {} handlers.", _registeredSpawns.size()); + } catch (Exception ex) { + LOG.warn("Could not restore spawn data!", ex); + } + } + + /** + * Registers a spawn with the given parameters with the spawner, and marks it as active.
    + * Returns a AutoSpawnInstance containing info about the spawn. + * @param npcId + * @param spawnPoints + * @param initialDelay (If < 0 = default value) + * @param respawnDelay (If < 0 = default value) + * @param despawnDelay (If < 0 = default value or if = 0, function disabled) + * @return AutoSpawnInstance spawnInst + */ + public AutoSpawnInstance registerSpawn(int npcId, int[][] spawnPoints, int initialDelay, int respawnDelay, int despawnDelay) { + if (initialDelay < 0) { + initialDelay = DEFAULT_INITIAL_SPAWN; + } + + if (respawnDelay < 0) { + respawnDelay = DEFAULT_RESPAWN; + } + + if (despawnDelay < 0) { + despawnDelay = DEFAULT_DESPAWN; + } + + AutoSpawnInstance newSpawn = new AutoSpawnInstance(npcId, initialDelay, respawnDelay, despawnDelay); + + if (spawnPoints != null) { + for (int[] spawnPoint : spawnPoints) { + newSpawn.addSpawnLocation(spawnPoint); + } + } + + int objectId = IdFactory.getInstance().getNextId(); + newSpawn._objectId = objectId; + _registeredSpawns.put(objectId, newSpawn); + + setSpawnActive(newSpawn, true); + return newSpawn; + } + + /** + * Registers a spawn with the given parameters with the spawner, and marks it as active.
    + * Returns a AutoSpawnInstance containing info about the spawn.
    + * Warning: Spawn locations must be specified separately using addSpawnLocation(). + * @param npcId + * @param initialDelay (If < 0 = default value) + * @param respawnDelay (If < 0 = default value) + * @param despawnDelay (If < 0 = default value or if = 0, function disabled) + * @return AutoSpawnInstance spawnInst + */ + public AutoSpawnInstance registerSpawn(int npcId, int initialDelay, int respawnDelay, int despawnDelay) { + return registerSpawn(npcId, null, initialDelay, respawnDelay, despawnDelay); + } + + /** + * Remove a registered spawn from the list, specified by the given spawn instance. + * @param spawnInst + * @return boolean removedSuccessfully + */ + public boolean removeSpawn(AutoSpawnInstance spawnInst) { + if (!isSpawnRegistered(spawnInst)) { + return false; + } + + try { + // Try to remove from the list of registered spawns if it exists. + _registeredSpawns.remove(spawnInst.getId()); + + // Cancel the currently associated running scheduled task. + ScheduledFuture respawnTask = _runningSpawns.remove(spawnInst._objectId); + respawnTask.cancel(false); + } catch (Exception ex) { + LOG.warn("Could not auto spawn for NPC Id {} (Object Id={})!", spawnInst._npcId, spawnInst._objectId, ex); + return false; + } + + return true; + } + + /** + * Remove a registered spawn from the list, specified by the given spawn object ID. + * @param objectId + */ + public void removeSpawn(int objectId) { + removeSpawn(_registeredSpawns.get(objectId)); + } + + /** + * Sets the active state of the specified spawn. + * @param spawnInst + * @param isActive + */ + public void setSpawnActive(AutoSpawnInstance spawnInst, boolean isActive) { + if (spawnInst == null) { + return; + } + + int objectId = spawnInst._objectId; + + if (isSpawnRegistered(objectId)) { + ScheduledFuture spawnTask; + + if (isActive) { + AutoSpawner rs = new AutoSpawner(objectId); + + if (spawnInst._desDelay > 0) { + spawnTask = ThreadPoolManager.getInstance().scheduleEffectAtFixedRate(rs, spawnInst._initDelay, spawnInst._resDelay); + } else { + spawnTask = ThreadPoolManager.getInstance().scheduleEffect(rs, spawnInst._initDelay); + } + + _runningSpawns.put(objectId, spawnTask); + } else { + AutoDespawner rd = new AutoDespawner(objectId); + spawnTask = _runningSpawns.remove(objectId); + + if (spawnTask != null) { + spawnTask.cancel(false); + } + + ThreadPoolManager.getInstance().scheduleEffect(rd, 0); + } + + spawnInst.setSpawnActive(isActive); + } + } + + /** + * Sets the active state of all auto spawn instances to that specified, and cancels the scheduled spawn task if necessary. + * @param isActive + */ + public void setAllActive(boolean isActive) { + if (_activeState == isActive) { + return; + } + + for (AutoSpawnInstance spawnInst : _registeredSpawns.values()) { + setSpawnActive(spawnInst, isActive); + } + + _activeState = isActive; + } + + /** + * Returns the number of milliseconds until the next occurrence of the given spawn. + * @param spawnInst + * @return + */ + public final long getTimeToNextSpawn(AutoSpawnInstance spawnInst) { + int objectId = spawnInst.getObjectId(); + + if (!isSpawnRegistered(objectId)) { + return -1; + } + + return (_runningSpawns.containsKey(objectId)) ? _runningSpawns.get(objectId).getDelay(TimeUnit.MILLISECONDS) : 0; + } + + /** + * Attempts to return the AutoSpawnInstance associated with the given NPC or Object ID type.
    + * Note: If isObjectId == false, returns first instance for the specified NPC ID. + * @param id + * @param isObjectId + * @return AutoSpawnInstance spawnInst + */ + public final AutoSpawnInstance getAutoSpawnInstance(int id, boolean isObjectId) { + if (isObjectId) { + if (isSpawnRegistered(id)) { + return _registeredSpawns.get(id); + } + } else { + for (AutoSpawnInstance spawnInst : _registeredSpawns.values()) { + if (spawnInst.getId() == id) { + return spawnInst; + } + } + } + return null; + } + + public List getAutoSpawnInstances(int npcId) { + final List result = new LinkedList<>(); + for (AutoSpawnInstance spawnInst : _registeredSpawns.values()) { + if (spawnInst.getId() == npcId) { + result.add(spawnInst); + } + } + return result; + } + + /** + * Tests if the specified object ID is assigned to an auto spawn. + * @param objectId + * @return isAssigned + */ + public final boolean isSpawnRegistered(int objectId) { + return _registeredSpawns.containsKey(objectId); + } + + /** + * Tests if the specified spawn instance is assigned to an auto spawn. + * @param spawnInst + * @return boolean isAssigned + */ + public final boolean isSpawnRegistered(AutoSpawnInstance spawnInst) { + return _registeredSpawns.containsValue(spawnInst); + } + + /** + * AutoSpawner class
    + * This handles the main spawn task for an auto spawn instance, and initializes a despawner if required. + * @author Tempy + */ + private class AutoSpawner implements Runnable { + private final int _objectId; + + protected AutoSpawner(int objectId) { + _objectId = objectId; + } + + @Override + public void run() { + try { + // Retrieve the required spawn instance for this spawn task. + AutoSpawnInstance spawnInst = _registeredSpawns.get(_objectId); + + // If the spawn is not scheduled to be active, cancel the spawn + // task. + if (!spawnInst.isSpawnActive()) { + return; + } + + Location[] locationList = spawnInst.getLocationList(); + + // If there are no set co-ordinates, cancel the spawn task. + if (locationList.length == 0) { + LOG.warn("No location co-ords specified for spawn instance (Object Id={}).", _objectId); + return; + } + + int locationCount = locationList.length; + int locationIndex = Rnd.nextInt(locationCount); + + // If random spawning is disabled, the spawn at the next set of co-ordinates after the last. + // If the index is greater than the number of possible spawns, reset the counter to zero. + if (!spawnInst.isRandomSpawn()) { + locationIndex = spawnInst._lastLocIndex + 1; + + if (locationIndex == locationCount) { + locationIndex = 0; + } + + spawnInst._lastLocIndex = locationIndex; + } + + // Set the X, Y and Z co-ordinates, where this spawn will take place. + final int x = locationList[locationIndex].getX(); + final int y = locationList[locationIndex].getY(); + final int z = locationList[locationIndex].getZ(); + final int heading = locationList[locationIndex].getHeading(); + + final L2Spawn newSpawn = new L2Spawn(spawnInst.getId()); + newSpawn.setX(x); + newSpawn.setY(y); + newSpawn.setZ(z); + if (heading != -1) { + newSpawn.setHeading(heading); + } + newSpawn.setAmount(spawnInst.getSpawnCount()); + if (spawnInst._desDelay == 0) { + newSpawn.setRespawnDelay(spawnInst._resDelay); + } + + // Add the new spawn information to the spawn table, but do not store it. + SpawnTable.getInstance().addNewSpawn(newSpawn, false); + L2Npc npcInst = null; + + if (spawnInst._spawnCount == 1) { + npcInst = newSpawn.doSpawn(); + npcInst.setXYZ(npcInst.getX(), npcInst.getY(), npcInst.getZ()); + spawnInst.addNpcInstance(npcInst); + } else { + for (int i = 0; i < spawnInst._spawnCount; i++) { + npcInst = newSpawn.doSpawn(); + + // To prevent spawning of more than one NPC in the exact same spot, move it slightly by a small random offset. + npcInst.setXYZ(npcInst.getX() + Rnd.nextInt(50), npcInst.getY() + Rnd.nextInt(50), npcInst.getZ()); + + // Add the NPC instance to the list of managed instances. + spawnInst.addNpcInstance(npcInst); + } + } + + if (npcInst != null) { + String nearestTown = MapRegionManager.getInstance().getClosestTownName(npcInst); + + // Announce to all players that the spawn has taken place, with the nearest town location. + if (spawnInst.isBroadcasting()) { + Broadcast.toAllOnlinePlayers("The " + npcInst.getName() + " has spawned near " + nearestTown + "!"); + } + } + + // If there is no despawn time, do not create a despawn task. + if (spawnInst.getDespawnDelay() > 0) { + AutoDespawner rd = new AutoDespawner(_objectId); + ThreadPoolManager.getInstance().scheduleAi(rd, spawnInst.getDespawnDelay() - 1000); + } + } catch (Exception ex) { + LOG.warn("An error occurred while initializing spawn instance (Object Id={})!", _objectId, ex); + } + } + } + + /** + * AutoDespawner Class
    + * Simply used as a secondary class for despawning an auto spawn instance. + * @author Tempy + */ + private class AutoDespawner implements Runnable { + private final int _objectId; + + protected AutoDespawner(int objectId) { + _objectId = objectId; + } + + @Override + public void run() { + try { + AutoSpawnInstance spawnInst = _registeredSpawns.get(_objectId); + if (spawnInst == null) { + LOG.warn("No spawn registered for object Id={}!", _objectId); + return; + } + + for (L2Npc npcInst : spawnInst.getNPCInstanceList()) { + npcInst.deleteMe(); + SpawnTable.getInstance().deleteSpawn(npcInst.getSpawn(), false); + spawnInst.removeNpcInstance(npcInst); + } + } catch (Exception ex) { + LOG.warn("An error occurred while despawning spawn (Object Id={})!", _objectId, ex); + } + } + } + + /** + * AutoSpawnInstance Class
    + * Stores information about a registered auto spawn. + * @author Tempy + */ + public static class AutoSpawnInstance implements IIdentifiable { + protected int _objectId; + + protected int _spawnIndex; + + protected int _npcId; + + protected int _initDelay; + + protected int _resDelay; + + protected int _desDelay; + + protected int _spawnCount = 1; + + protected int _lastLocIndex = -1; + + private final Queue _npcList = new ConcurrentLinkedQueue<>(); + + private final List _locList = new CopyOnWriteArrayList<>(); + + private boolean _spawnActive; + + private boolean _randomSpawn = false; + + private boolean _broadcastAnnouncement = false; + + protected AutoSpawnInstance(int npcId, int initDelay, int respawnDelay, int despawnDelay) { + _npcId = npcId; + _initDelay = initDelay; + _resDelay = respawnDelay; + _desDelay = despawnDelay; + } + + protected void setSpawnActive(boolean activeValue) { + _spawnActive = activeValue; + } + + protected boolean addNpcInstance(L2Npc npcInst) { + return _npcList.add(npcInst); + } + + protected boolean removeNpcInstance(L2Npc npcInst) { + return _npcList.remove(npcInst); + } + + public int getObjectId() { + return _objectId; + } + + public int getInitialDelay() { + return _initDelay; + } + + public int getRespawnDelay() { + return _resDelay; + } + + public int getDespawnDelay() { + return _desDelay; + } + + /** + * Gets the NPC ID. + * @return the NPC ID + */ + @Override + public int getId() { + return _npcId; + } + + public int getSpawnCount() { + return _spawnCount; + } + + public Location[] getLocationList() { + return _locList.toArray(new Location[_locList.size()]); + } + + public Queue getNPCInstanceList() { + return _npcList; + } + + public List getSpawns() { + final List npcSpawns = new ArrayList<>(); + for (L2Npc npcInst : _npcList) { + npcSpawns.add(npcInst.getSpawn()); + } + return npcSpawns; + } + + public void setSpawnCount(int spawnCount) { + _spawnCount = spawnCount; + } + + public void setRandomSpawn(boolean randValue) { + _randomSpawn = randValue; + } + + public void setBroadcast(boolean broadcastValue) { + _broadcastAnnouncement = broadcastValue; + } + + public boolean isSpawnActive() { + return _spawnActive; + } + + public boolean isRandomSpawn() { + return _randomSpawn; + } + + public boolean isBroadcasting() { + return _broadcastAnnouncement; + } + + public boolean addSpawnLocation(int x, int y, int z, int heading) { + return _locList.add(new Location(x, y, z, heading)); + } + + public boolean addSpawnLocation(int[] spawnLoc) { + if (spawnLoc.length != 3) { + return false; + } + + return addSpawnLocation(spawnLoc[0], spawnLoc[1], spawnLoc[2], -1); + } + + public Location removeSpawnLocation(int locIndex) { + try { + return _locList.remove(locIndex); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + } + + public static AutoSpawnHandler getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final AutoSpawnHandler INSTANCE = new AutoSpawnHandler(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/BlockList.java b/src/main/java/com/l2jserver/gameserver/model/BlockList.java new file mode 100644 index 00000000..f8eb93ed --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/BlockList.java @@ -0,0 +1,242 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.network.SystemMessageId.BLOCK_LIST_HEADER; +import static com.l2jserver.gameserver.network.SystemMessageId.FRIEND_LIST_FOOTER; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_ALREADY_IN_FRIENDS_LIST; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_HAS_ADDED_YOU_TO_IGNORE_LIST; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_WAS_ADDED_TO_YOUR_IGNORE_LIST; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_WAS_REMOVED_FROM_YOUR_IGNORE_LIST; +import static com.l2jserver.gameserver.network.SystemMessageId.TARGET_IS_INCORRECT; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_CAN_ONLY_ENTER_UP_TO_128_NAMES_IN_YOUR_BLOCK_LIST; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +// TODO(Zoey76): Rewrite this class in an OOP style. +public class BlockList { + private static final Logger LOG = LoggerFactory.getLogger(BlockList.class); + + private static final Map> OFFLINE_LIST = new ConcurrentHashMap<>(); + + private final L2PcInstance _owner; + + private List _blockList; + + public BlockList(L2PcInstance owner) { + _owner = owner; + _blockList = OFFLINE_LIST.get(owner.getObjectId()); + if (_blockList == null) { + _blockList = loadList(_owner.getObjectId()); + } + } + + private void addToBlockList(int target) { + _blockList.add(target); + persistInDB(target); + } + + private void removeFromBlockList(int target) { + _blockList.remove(Integer.valueOf(target)); + removeFromDB(target); + } + + public void playerLogout() { + OFFLINE_LIST.put(_owner.getObjectId(), _blockList); + } + + private static List loadList(int ObjId) { + List list = new ArrayList<>(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT friendId FROM character_friends WHERE charId=? AND relation=1")) { + ps.setInt(1, ObjId); + try (var rs = ps.executeQuery()) { + int friendId; + while (rs.next()) { + friendId = rs.getInt("friendId"); + if (friendId == ObjId) { + continue; + } + list.add(friendId); + } + } + } catch (Exception e) { + LOG.warn("Error found in {} FriendList while loading BlockList: {}", ObjId, e.getMessage(), e); + } + return list; + } + + private void removeFromDB(int targetId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM character_friends WHERE charId=? AND friendId=? AND relation=1")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, targetId); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not remove blocked player: {}", e.getMessage(), e); + } + } + + private void persistInDB(int targetId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO character_friends (charId, friendId, relation) VALUES (?, ?, 1)")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, targetId); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not add blocked player: {}", e.getMessage(), e); + } + } + + public boolean isInBlockList(L2PcInstance target) { + return isInBlockList(target.getObjectId()); + } + + public boolean isInBlockList(int targetId) { + return _blockList.contains(targetId); + } + + private boolean isBlockAll() { + return _owner.getMessageRefusal(); + } + + public static boolean isBlocked(L2PcInstance listOwner, L2PcInstance target) { + BlockList blockList = listOwner.getBlockList(); + return blockList.isBlockAll() || blockList.isInBlockList(target); + } + + public static boolean isBlocked(L2PcInstance listOwner, int targetId) { + BlockList blockList = listOwner.getBlockList(); + return blockList.isBlockAll() || blockList.isInBlockList(targetId); + } + + private void setBlockAll(boolean state) { + _owner.setMessageRefusal(state); + } + + private List getBlockList() { + return _blockList; + } + + public static void addToBlockList(L2PcInstance listOwner, int targetId) { + if (listOwner == null) { + return; + } + + if (listOwner.getBlockList().getBlockList().size() >= character().getBlockListLimit()) { + listOwner.sendPacket(YOU_CAN_ONLY_ENTER_UP_TO_128_NAMES_IN_YOUR_BLOCK_LIST); + return; + } + + String charName = CharNameTable.getInstance().getNameById(targetId); + if (listOwner.isFriend(targetId)) { + final var sm = SystemMessage.getSystemMessage(S1_ALREADY_IN_FRIENDS_LIST); + sm.addString(charName); + listOwner.sendPacket(sm); + return; + } + + if (listOwner.getBlockList().getBlockList().contains(targetId)) { + listOwner.sendMessage("Already in ignore list."); + return; + } + + listOwner.getBlockList().addToBlockList(targetId); + + var sm = SystemMessage.getSystemMessage(S1_WAS_ADDED_TO_YOUR_IGNORE_LIST); + sm.addString(charName); + listOwner.sendPacket(sm); + + L2PcInstance player = L2World.getInstance().getPlayer(targetId); + + if (player != null) { + sm = SystemMessage.getSystemMessage(S1_HAS_ADDED_YOU_TO_IGNORE_LIST); + sm.addString(listOwner.getName()); + player.sendPacket(sm); + } + } + + public static void removeFromBlockList(L2PcInstance listOwner, int targetId) { + if (listOwner == null) { + return; + } + + String charName = CharNameTable.getInstance().getNameById(targetId); + + if (!listOwner.getBlockList().getBlockList().contains(targetId)) { + listOwner.sendPacket(TARGET_IS_INCORRECT); + return; + } + + listOwner.getBlockList().removeFromBlockList(targetId); + + final var sm = SystemMessage.getSystemMessage(S1_WAS_REMOVED_FROM_YOUR_IGNORE_LIST); + sm.addString(charName); + listOwner.sendPacket(sm); + } + + public static boolean isInBlockList(L2PcInstance listOwner, L2PcInstance target) { + return listOwner.getBlockList().isInBlockList(target); + } + + public boolean isBlockAll(L2PcInstance listOwner) { + return listOwner.getBlockList().isBlockAll(); + } + + public static void setBlockAll(L2PcInstance listOwner, boolean newValue) { + listOwner.getBlockList().setBlockAll(newValue); + } + + public static void sendListToOwner(L2PcInstance listOwner) { + int i = 1; + listOwner.sendPacket(BLOCK_LIST_HEADER); + for (int playerId : listOwner.getBlockList().getBlockList()) { + listOwner.sendMessage((i++) + ". " + CharNameTable.getInstance().getNameById(playerId)); + } + listOwner.sendPacket(FRIEND_LIST_FOOTER); + } + + /** + * @param ownerId object id of owner block list + * @param targetId object id of potential blocked player + * @return true if blocked + */ + public static boolean isInBlockList(int ownerId, int targetId) { + L2PcInstance player = L2World.getInstance().getPlayer(ownerId); + if (player != null) { + return BlockList.isBlocked(player, targetId); + } + if (!OFFLINE_LIST.containsKey(ownerId)) { + OFFLINE_LIST.put(ownerId, loadList(ownerId)); + } + return OFFLINE_LIST.get(ownerId).contains(targetId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/CharEffectList.java b/src/main/java/com/l2jserver/gameserver/model/CharEffectList.java new file mode 100644 index 00000000..1807136a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/CharEffectList.java @@ -0,0 +1,1424 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.effects.AbstractEffect; +import com.l2jserver.gameserver.model.effects.EffectFlag; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameManager; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameTask; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.AbnormalStatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.ExOlympiadSpelledInfo; +import com.l2jserver.gameserver.network.serverpackets.PartySpelled; +import com.l2jserver.gameserver.network.serverpackets.ShortBuffStatusUpdate; + +/** + * Effect lists.
    + * Holds all the buff infos that are affecting a creature.
    + * Manages the logic that controls whether a buff is added, remove, replaced or set inactive.
    + * Uses maps with skill ID as key and buff info DTO as value to avoid iterations.
    + * Uses Double-Checked Locking to avoid useless initialization and synchronization issues and overhead.
    + * Methods may resemble List interface, although it doesn't implement such interface. + * @author Zoey76 + */ +public final class CharEffectList { + private static final Logger LOG = LoggerFactory.getLogger(CharEffectList.class); + + /** Queue containing all effects from buffs for this effect list. */ + private volatile Queue _buffs; + /** Queue containing all triggered skills for this effect list. */ + private volatile Queue _triggered; + /** Queue containing all dances/songs for this effect list. */ + private volatile Queue _dances; + /** Queue containing all toggle for this effect list. */ + private volatile Queue _toggles; + /** Queue containing all debuffs for this effect list. */ + private volatile Queue _debuffs; + /** Queue containing all passives for this effect list. They bypass most of the actions and they are not included in most operations. */ + private volatile Queue _passives; + /** Map containing the all stacked effect in progress for each abnormal type. */ + private volatile Map _stackedEffects; + /** Set containing all abnormal types that shouldn't be added to this creature effect list. */ + private volatile Set _blockedBuffSlots = null; + /** Short buff skill ID. */ + private BuffInfo _shortBuff = null; + /** If {@code true} this effect list has buffs removed on any action. */ + private volatile boolean _hasBuffsRemovedOnAnyAction = false; + /** If {@code true} this effect list has buffs removed on damage. */ + private volatile boolean _hasBuffsRemovedOnDamage = false; + /** If {@code true} this effect list has debuffs removed on damage. */ + private volatile boolean _hasDebuffsRemovedOnDamage = false; + /** Effect flags. */ + private int _effectFlags; + /** If {@code true} only party icons need to be updated. */ + private boolean _partyOnly = false; + /** The owner of this effect list. */ + private final L2Character _owner; + /** Hidden buffs count, prevents iterations. */ + private final AtomicInteger _hiddenBuffs = new AtomicInteger(); + + /** + * Constructor for effect list. + * @param owner the creature that owns this effect list + */ + public CharEffectList(L2Character owner) { + _owner = owner; + } + + /** + * Gets buff skills. + * @return the buff skills + */ + public Queue getBuffs() { + if (_buffs == null) { + synchronized (this) { + if (_buffs == null) { + _buffs = new ConcurrentLinkedQueue<>(); + } + } + } + return _buffs; + } + + /** + * Gets triggered skill skills. + * @return the triggered skill skills + */ + public Queue getTriggered() { + if (_triggered == null) { + synchronized (this) { + if (_triggered == null) { + _triggered = new ConcurrentLinkedQueue<>(); + } + } + } + return _triggered; + } + + /** + * Gets dance/song skills. + * @return the dance/song skills + */ + public Queue getDances() { + if (_dances == null) { + synchronized (this) { + if (_dances == null) { + _dances = new ConcurrentLinkedQueue<>(); + } + } + } + return _dances; + } + + /** + * Gets toggle skills. + * @return the toggle skills + */ + public Queue getToggles() { + if (_toggles == null) { + synchronized (this) { + if (_toggles == null) { + _toggles = new ConcurrentLinkedQueue<>(); + } + } + } + return _toggles; + } + + /** + * Gets debuff skills. + * @return the debuff skills + */ + public Queue getDebuffs() { + if (_debuffs == null) { + synchronized (this) { + if (_debuffs == null) { + _debuffs = new ConcurrentLinkedQueue<>(); + } + } + } + return _debuffs; + } + + /** + * Gets passive skills. + * @return the passive skills + */ + public Queue getPassives() { + if (_passives == null) { + synchronized (this) { + if (_passives == null) { + _passives = new ConcurrentLinkedQueue<>(); + } + } + } + return _passives; + } + + /** + * Gets all the effects on this effect list. + * @return all the effects on this effect list + */ + public List getEffects() { + if (isEmpty()) { + return Collections.emptyList(); + } + + final List buffs = new ArrayList<>(); + if (hasBuffs()) { + buffs.addAll(getBuffs()); + } + + if (hasTriggered()) { + buffs.addAll(getTriggered()); + } + + if (hasDances()) { + buffs.addAll(getDances()); + } + + if (hasToggles()) { + buffs.addAll(getToggles()); + } + + if (hasDebuffs()) { + buffs.addAll(getDebuffs()); + } + return buffs; + } + + /** + * Gets the effect list where the skill effects should be. + * @param skill the skill + * @return the effect list + */ + private Queue getEffectList(Skill skill) { + if (skill == null) { + return null; + } + + final Queue effects; + if (skill.isPassive()) { + effects = getPassives(); + } else if (skill.isDebuff()) { + effects = getDebuffs(); + } else if (skill.isTrigger()) { + effects = getTriggered(); + } else if (skill.isDance()) { + effects = getDances(); + } else if (skill.isToggle()) { + effects = getToggles(); + } else { + effects = getBuffs(); + } + return effects; + } + + /** + * Gets the first effect for the given effect type.
    + * Prevents initialization.
    + * TODO: Remove this method after all the effect types gets replaced by abnormal skill types. + * @param type the effect type + * @return the first effect matching the given effect type + */ + public BuffInfo getFirstEffect(L2EffectType type) { + if (hasBuffs()) { + for (BuffInfo info : getBuffs()) { + if (info != null) { + for (AbstractEffect effect : info.getEffects()) { + if ((effect != null) && (effect.getEffectType() == type)) { + return info; + } + } + } + } + } + + if (hasTriggered()) { + for (BuffInfo info : getTriggered()) { + if (info != null) { + for (AbstractEffect effect : info.getEffects()) { + if ((effect != null) && (effect.getEffectType() == type)) { + return info; + } + } + } + } + } + + if (hasDances()) { + for (BuffInfo info : getDances()) { + if (info != null) { + for (AbstractEffect effect : info.getEffects()) { + if ((effect != null) && (effect.getEffectType() == type)) { + return info; + } + } + } + } + } + + if (hasToggles()) { + for (BuffInfo info : getToggles()) { + if (info != null) { + for (AbstractEffect effect : info.getEffects()) { + if ((effect != null) && (effect.getEffectType() == type)) { + return info; + } + } + } + } + } + + if (hasDebuffs()) { + for (BuffInfo info : getDebuffs()) { + if (info != null) { + for (AbstractEffect effect : info.getEffects()) { + if ((effect != null) && (effect.getEffectType() == type)) { + return info; + } + } + } + } + } + return null; + } + + /** + * Verifies if this effect list contains the given skill ID.
    + * Prevents initialization. + * @param skillId the skill ID to verify + * @return {@code true} if the skill ID is present in the effect list, {@code false} otherwise + */ + public boolean isAffectedBySkill(int skillId) { + return getBuffInfoBySkillId(skillId) != null; + } + + /** + * Gets the buff info by skill ID.
    + * Prevents initialization. + * @param skillId the skill ID + * @return the buff info + */ + public BuffInfo getBuffInfoBySkillId(int skillId) { + BuffInfo info = null; + if (hasBuffs()) { + info = getBuffs().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + + if (hasTriggered() && (info == null)) { + info = getTriggered().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + + if (hasDances() && (info == null)) { + info = getDances().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + + if (hasToggles() && (info == null)) { + info = getToggles().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + + if (hasDebuffs() && (info == null)) { + info = getDebuffs().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + + if (hasPassives() && (info == null)) { + info = getPassives().stream().filter(b -> b.getSkill().getId() == skillId).findFirst().orElse(null); + } + return info; + } + + /** + * Gets a buff info by abnormal type.
    + * It's O(1) for every buff in this effect list. + * @param type the abnormal skill type + * @return the buff info if it's present, {@code null} otherwise + */ + public BuffInfo getBuffInfoByAbnormalType(AbnormalType type) { + return (_stackedEffects != null) ? _stackedEffects.get(type) : null; + } + + /** + * Adds abnormal types to the blocked buff slot set. + * @param blockedBuffSlots the blocked buff slot set to add + */ + public void addBlockedBuffSlots(Set blockedBuffSlots) { + if (_blockedBuffSlots == null) { + synchronized (this) { + if (_blockedBuffSlots == null) { + _blockedBuffSlots = ConcurrentHashMap.newKeySet(blockedBuffSlots.size()); + } + } + } + _blockedBuffSlots.addAll(blockedBuffSlots); + } + + /** + * Removes abnormal types from the blocked buff slot set. + * @param blockedBuffSlots the blocked buff slot set to remove + * @return {@code true} if the blocked buff slots set has been modified, {@code false} otherwise + */ + public boolean removeBlockedBuffSlots(Set blockedBuffSlots) { + if (_blockedBuffSlots != null) { + return _blockedBuffSlots.removeAll(blockedBuffSlots); + } + return false; + } + + /** + * Gets all the blocked abnormal types for this creature effect list. + * @return the current blocked buff slots set + */ + public Set getAllBlockedBuffSlots() { + return _blockedBuffSlots; + } + + /** + * Gets the Short Buff info. + * @return the short buff info + */ + public BuffInfo getShortBuff() { + return _shortBuff; + } + + /** + * Sets the Short Buff data and sends an update if the effected is a player. + * @param info the buff info + */ + public void shortBuffStatusUpdate(BuffInfo info) { + if (_owner.isPlayer()) { + _shortBuff = info; + if (info == null) { + _owner.sendPacket(ShortBuffStatusUpdate.RESET_SHORT_BUFF); + } else { + _owner.sendPacket(new ShortBuffStatusUpdate(info.getSkill().getId(), info.getSkill().getLevel(), info.getTime())); + } + } + } + + /** + * Checks if the given skill stacks with an existing one. + * @param skill the skill to verify + * @return {@code true} if this effect stacks with the given skill, {@code false} otherwise + */ + private boolean doesStack(Skill skill) { + final AbnormalType type = skill.getAbnormalType(); + if (type.isNone() || isEmpty()) { + return false; + } + + final Queue effects = getEffectList(skill); + for (BuffInfo info : effects) { + if ((info != null) && (info.getSkill().getAbnormalType() == type)) { + return true; + } + } + return false; + } + + /** + * Gets the buffs count without including the hidden buffs (after getting an Herb buff).
    + * Prevents initialization. + * @return the number of buffs in this creature effect list + */ + public int getBuffCount() { + return hasBuffs() ? getBuffs().size() - _hiddenBuffs.get() - (getShortBuff() != null ? 1 : 0) : 0; + } + + /** + * Gets the Songs/Dances count.
    + * Prevents initialization. + * @return the number of Songs/Dances in this creature effect list + */ + public int getDanceCount() { + return hasDances() ? getDances().size() : 0; + } + + /** + * Gets the triggered buffs count.
    + * Prevents initialization. + * @return the number of triggered buffs in this creature effect list + */ + public int getTriggeredBuffCount() { + return hasTriggered() ? getTriggered().size() : 0; + } + + /** + * Gets the hidden buff count. + * @return the number of hidden buffs + */ + public int getHiddenBuffsCount() { + return _hiddenBuffs.get(); + } + + /** + * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. + * @param info the buff info + */ + protected void stopAndRemove(BuffInfo info) { + stopAndRemove(true, info, getEffectList(info.getSkill())); + } + + /** + * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. + * @param info the buff info + * @param effects the effect list + */ + protected void stopAndRemove(BuffInfo info, Queue effects) { + stopAndRemove(true, info, effects); + } + + /** + * Auxiliary method to stop all effects from a buff info and remove it from an effect list and stacked effects. + * @param removed {@code true} if the effect is removed, {@code false} otherwise + * @param info the buff info + * @param buffs the buff list + */ + private void stopAndRemove(boolean removed, BuffInfo info, Queue buffs) { + if (info == null) { + return; + } + + // Removes the buff from the given effect list. + buffs.remove(info); + // Stop the buff effects. + info.stopAllEffects(removed); + // If it's a hidden buff that ends, then decrease hidden buff count. + if (!info.isInUse()) { + _hiddenBuffs.decrementAndGet(); + } + // Removes the buff from the stack. + else if (_stackedEffects != null) { + _stackedEffects.remove(info.getSkill().getAbnormalType()); + } + + // If it's an herb that ends, check if there are hidden buffs. + if (info.getSkill().isAbnormalInstant() && hasBuffs()) { + for (BuffInfo buff : getBuffs()) { + if ((buff != null) && (buff.getSkill().getAbnormalType() == info.getSkill().getAbnormalType()) && !buff.isInUse()) { + // Sets the buff in use again. + buff.setInUse(true); + // Adds the stats. + buff.addStats(); + // Adds the buff to the stack. + if (_stackedEffects != null) { + _stackedEffects.put(buff.getSkill().getAbnormalType(), buff); + } + // If it's a hidden buff that gets activated, then decrease hidden buff count. + _hiddenBuffs.decrementAndGet(); + break; + } + } + } + + if (!removed) { + info.getSkill().applyEffectScope(EffectScope.END, info, true, false); + } + } + + /** + * Exits all effects in this effect list.
    + * Stops all the effects, clear the effect lists and updates the effect flags and icons. + */ + public void stopAllEffects() { + // Stop buffs. + stopAllBuffs(false, true); + // Stop dances and songs. + stopAllDances(false); + // Stop toggles. + stopAllToggles(false); + // Stop debuffs. + stopAllDebuffs(false); + + if (_stackedEffects != null) { + _stackedEffects.clear(); + } + + // Update effect flags and icons. + updateEffectList(true); + } + + /** + * Stops all effects in this effect list except those that last through death. + */ + public void stopAllEffectsExceptThoseThatLastThroughDeath() { + boolean update = false; + if (hasBuffs()) { + getBuffs().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getBuffs())); + update = true; + } + + if (hasTriggered()) { + getTriggered().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getTriggered())); + update = true; + } + + if (hasDebuffs()) { + getDebuffs().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getDebuffs())); + update = true; + } + + if (hasDances()) { + getDances().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getDances())); + update = true; + } + + if (hasToggles()) { + getToggles().stream().filter(info -> !info.getSkill().isStayAfterDeath()).forEach(info -> stopAndRemove(info, getToggles())); + update = true; + } + + // Update effect flags and icons. + updateEffectList(update); + } + + /** + * Stop all effects that doesn't stay on sub-class change. + */ + public void stopAllEffectsNotStayOnSubclassChange() { + boolean update = false; + if (hasBuffs()) { + getBuffs().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getBuffs())); + update = true; + } + + if (hasTriggered()) { + getTriggered().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getTriggered())); + update = true; + } + + if (hasDebuffs()) { + getDebuffs().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getDebuffs())); + update = true; + } + + if (hasDances()) { + getDances().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getDances())); + update = true; + } + + if (hasToggles()) { + getToggles().stream().filter(info -> !info.getSkill().isStayOnSubclassChange()).forEach(info -> stopAndRemove(info, getToggles())); + update = true; + } + + // Update effect flags and icons. + updateEffectList(update); + } + + /** + * Stops all the active buffs. + * @param update set to true to update the effect flags and icons + * @param triggered if {@code true} stops triggered skills buffs + */ + public void stopAllBuffs(boolean update, boolean triggered) { + if (hasBuffs()) { + getBuffs().forEach(b -> stopAndRemove(b, getBuffs())); + } + + if (triggered && hasTriggered()) { + getTriggered().forEach(b -> stopAndRemove(b, getTriggered())); + } + + // Update effect flags and icons. + updateEffectList(update); + } + + /** + * Stops all active toggle skills.
    + * Performs an update. + */ + public void stopAllToggles() { + stopAllToggles(true); + } + + /** + * Stops all active toggle skills. + * @param update set to true to update the effect flags and icons + */ + public void stopAllToggles(boolean update) { + if (hasToggles()) { + getToggles().forEach(b -> stopAndRemove(b, getToggles())); + // Update effect flags and icons. + updateEffectList(update); + } + } + + /** + * Stops all active dances/songs skills. + * @param update set to true to update the effect flags and icons + */ + public void stopAllDances(boolean update) { + if (hasDances()) { + getDances().forEach(b -> stopAndRemove(b, getDances())); + // Update effect flags and icons. + updateEffectList(update); + } + } + + /** + * Stops all active dances/songs skills. + * @param update set to true to update the effect flags and icons + */ + public void stopAllDebuffs(boolean update) { + if (hasDebuffs()) { + getDebuffs().forEach(b -> stopAndRemove(b, getDebuffs())); + // Update effect flags and icons. + updateEffectList(update); + } + } + + /** + * Exit all effects having a specified type.
    + * TODO: Remove after all effect types are replaced by abnormal skill types. + * @param type the type of the effect to stop + */ + public void stopEffects(L2EffectType type) { + boolean update = false; + final Consumer action = info -> { + if (info.getEffects().stream().anyMatch(effect -> (effect != null) && (effect.getEffectType() == type))) { + stopAndRemove(info); + } + }; + + if (hasBuffs()) { + getBuffs().stream().filter(Objects::nonNull).forEach(action); + update = true; + } + + if (hasTriggered()) { + getTriggered().stream().filter(Objects::nonNull).forEach(action); + update = true; + } + + if (hasDances()) { + getDances().stream().filter(Objects::nonNull).forEach(action); + update = true; + } + + if (hasToggles()) { + getToggles().stream().filter(Objects::nonNull).forEach(action); + update = true; + } + + if (hasDebuffs()) { + getDebuffs().stream().filter(Objects::nonNull).forEach(action); + update = true; + } + + // Update effect flags and icons. + updateEffectList(update); + } + + /** + * Exits all effects created by a specific skill ID.
    + * Removes the effects from the effect list.
    + * Removes the stats from the creature.
    + * Updates the effect flags and icons.
    + * Presents two overloads:
    + * {@link #stopSkillEffects(boolean, Skill)}
    + * {@link #stopSkillEffects(boolean, AbnormalType)} + * @param removed {@code true} if the effect is removed, {@code false} otherwise + * @param skillId the skill ID + */ + public void stopSkillEffects(boolean removed, int skillId) { + final BuffInfo info = getBuffInfoBySkillId(skillId); + if (info != null) { + remove(removed, info); + } + } + + /** + * Exits all effects created by a specific skill.
    + * Removes the effects from the effect list.
    + * Removes the stats from the creature.
    + * Updates the effect flags and icons.
    + * Presents two overloads:
    + * {@link #stopSkillEffects(boolean, int)}
    + * {@link #stopSkillEffects(boolean, AbnormalType)} + * @param removed {@code true} if the effect is removed, {@code false} otherwise + * @param skill the skill + */ + public void stopSkillEffects(boolean removed, Skill skill) { + if (skill != null) { + stopSkillEffects(removed, skill.getId()); + } + } + + /** + * Exits all effects created by a specific skill abnormal type.
    + * It's O(1) for every effect in this effect list except passive effects.
    + * Presents two overloads:
    + * {@link #stopSkillEffects(boolean, int)}
    + * {@link #stopSkillEffects(boolean, Skill)} + * @param removed {@code true} if the effect is removed, {@code false} otherwise + * @param type the skill abnormal type + * @return {@code true} if there was a buff info with the given abnormal type + */ + public boolean stopSkillEffects(boolean removed, AbnormalType type) { + if (_stackedEffects != null) { + final BuffInfo old = _stackedEffects.remove(type); + if (old != null) { + stopSkillEffects(removed, old.getSkill()); + return true; + } + } + return false; + } + + /** + * Exits all buffs effects of the skills with "removedOnAnyAction" set.
    + * Called on any action except movement (attack, cast). + */ + public void stopEffectsOnAction() { + if (_hasBuffsRemovedOnAnyAction) { + boolean update = false; + if (hasBuffs()) { + getBuffs().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getBuffs())); + update = true; + } + + if (hasTriggered()) { + getTriggered().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getTriggered())); + update = true; + } + + if (hasDebuffs()) { + getDebuffs().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getDebuffs())); + update = true; + } + + if (hasDances()) { + getDances().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getDances())); + update = true; + } + + if (hasToggles()) { + getToggles().stream().filter(info -> info.getSkill().isRemovedOnAnyActionExceptMove()).forEach(info -> stopAndRemove(info, getToggles())); + update = true; + } + + // Update effect flags and icons. + updateEffectList(update); + } + } + + public void stopEffectsOnDamage(boolean awake) { + if (awake) { + boolean update = false; + if (_hasBuffsRemovedOnDamage) { + if (hasBuffs()) { + getBuffs().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getBuffs())); + update = true; + } + + if (hasTriggered()) { + getTriggered().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getTriggered())); + update = true; + } + + if (hasDances()) { + getDances().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getDances())); + update = true; + } + + if (hasToggles()) { + getToggles().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getToggles())); + update = true; + } + } + + if (_hasDebuffsRemovedOnDamage) { + if (hasDebuffs()) { + getDebuffs().stream().filter(Objects::nonNull).filter(info -> info.getSkill().isRemovedOnDamage()).forEach(info -> stopAndRemove(info, getDebuffs())); + update = true; + } + } + // Update effect flags and icons. + updateEffectList(update); + } + } + + /** + * @param partyOnly + */ + public void updateEffectIcons(boolean partyOnly) { + if (partyOnly) { + _partyOnly = true; + } + // Update effect flags and icons. + updateEffectList(true); + } + + /** + * Verify if this effect list is empty.
    + * Prevents initialization. + * @return {@code true} if this effect list contains any skills + */ + public boolean isEmpty() { + return !hasBuffs() && !hasTriggered() && !hasDances() && !hasDebuffs() && !hasToggles(); + } + + /** + * Verify if this effect list has buffs skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_buffs} is not {@code null} and is not empty + */ + public boolean hasBuffs() { + return (_buffs != null) && !_buffs.isEmpty(); + } + + /** + * Verify if this effect list has triggered skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_triggered} is not {@code null} and is not empty + */ + public boolean hasTriggered() { + return (_triggered != null) && !_triggered.isEmpty(); + } + + /** + * Verify if this effect list has dance/song skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_dances} is not {@code null} and is not empty + */ + public boolean hasDances() { + return (_dances != null) && !_dances.isEmpty(); + } + + /** + * Verify if this effect list has toggle skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_toggles} is not {@code null} and is not empty + */ + public boolean hasToggles() { + return (_toggles != null) && !_toggles.isEmpty(); + } + + /** + * Verify if this effect list has debuffs skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_debuffs} is not {@code null} and is not empty + */ + public boolean hasDebuffs() { + return (_debuffs != null) && !_debuffs.isEmpty(); + } + + /** + * Verify if this effect list has passive skills.
    + * Prevents initialization. + * @return {@code true} if {@link #_passives} is not {@code null} and is not empty + */ + public boolean hasPassives() { + return (_passives != null) && !_passives.isEmpty(); + } + + /** + * Executes a procedure for all effects.
    + * Prevents initialization. + * @param function the function to execute + * @param dances if {@code true} dances/songs will be included + */ + public void forEach(Function function, boolean dances) { + boolean update = false; + if (hasBuffs()) { + for (BuffInfo info : getBuffs()) { + update |= function.apply(info); + } + } + + if (hasTriggered()) { + for (BuffInfo info : getTriggered()) { + update |= function.apply(info); + } + } + + if (dances && hasDances()) { + for (BuffInfo info : getDances()) { + update |= function.apply(info); + } + } + + if (hasToggles()) { + for (BuffInfo info : getToggles()) { + update |= function.apply(info); + } + } + + if (hasDebuffs()) { + for (BuffInfo info : getDebuffs()) { + update |= function.apply(info); + } + } + // Update effect flags and icons. + updateEffectList(update); + } + + /** + * Removes a set of effects from this effect list. + * @param removed {@code true} if the effect is removed, {@code false} otherwise + * @param info the effects to remove + */ + public void remove(boolean removed, BuffInfo info) { + if (info == null) { + return; + } + + // Remove the effect from creature effects. + stopAndRemove(removed, info, getEffectList(info.getSkill())); + // Update effect flags and icons. + updateEffectList(true); + } + + /** + * Adds a set of effects to this effect list. + * @param info the buff info + */ + public void add(BuffInfo info) { + if (info == null) { + return; + } + + // Support for blocked buff slots. + final Skill skill = info.getSkill(); + if ((_blockedBuffSlots != null) && _blockedBuffSlots.contains(skill.getAbnormalType())) { + return; + } + + // Passive effects are treated specially + if (skill.isPassive()) { + // Passive effects don't need stack type! + if (!skill.getAbnormalType().isNone()) { + LOG.warn("Passive {} with abnormal type: {}!", skill, skill.getAbnormalType()); + } + + // Check for passive skill conditions. + if (!skill.checkCondition(info.getEffector(), info.getEffected(), false)) { + return; + } + + // Puts the effects in the list. + getPassives().stream().filter(b -> b.getSkill().getId() == skill.getId()).forEach(b -> { + // Removes the old stats from the creature if the skill was present. + b.setInUse(false); + b.removeStats(); + getPassives().remove(b); + }); + + getPassives().add(info); + + // Initialize effects. + info.initializeEffects(); + return; + } + + // Prevent adding and initializing buffs/effects on dead creatures. + if (info.getEffected().isDead() && (info.getEffector() != info.getEffector())) { + return; + } + + // The old effect is removed using Map#remove(key) instead of Map#put(key, value) (that would be the wisest choice), + // Because order matters and put method would insert in the same place it was before, instead of, at the end of the effect list + // Where new buff should be placed + if (skill.getAbnormalType().isNone()) { + stopSkillEffects(true, skill); + } + // Verify stacked skills. + else { + if (_stackedEffects == null) { + synchronized (this) { + if (_stackedEffects == null) { + _stackedEffects = new ConcurrentHashMap<>(); + } + } + } + + if (_stackedEffects.containsKey(skill.getAbnormalType())) { + BuffInfo stackedInfo = _stackedEffects.get(skill.getAbnormalType()); + // Skills are only replaced if the incoming buff has greater or equal abnormal level. + if ((stackedInfo != null) && (skill.getAbnormalLvl() >= stackedInfo.getSkill().getAbnormalLvl())) { + // If it is an herb, set as not in use the lesser buff. + // Effect will be present in the effect list. + // Effects stats are removed and onActionTime() is not called. + // But finish task continues to run, and ticks as well. + if (skill.isAbnormalInstant()) { + if (stackedInfo.getSkill().isAbnormalInstant()) { + stopSkillEffects(true, skill.getAbnormalType()); + stackedInfo = _stackedEffects.get(skill.getAbnormalType()); + } + + if (stackedInfo != null) { + stackedInfo.setInUse(false); + // Remove stats + stackedInfo.removeStats(); + _hiddenBuffs.incrementAndGet(); + } + } + // Remove buff that will stack with the abnormal type. + else { + if (stackedInfo.getSkill().isAbnormalInstant()) { + stopSkillEffects(true, skill.getAbnormalType()); + } + stopSkillEffects(true, skill.getAbnormalType()); + } + } + // If the new buff is a lesser buff, then don't add it. + else { + return; + } + } + _stackedEffects.put(skill.getAbnormalType(), info); + } + + // Select the map that holds the effects related to this skill. + final Queue effects = getEffectList(skill); + // Remove first buff when buff list is full. + if (!skill.isDebuff() && !skill.isToggle() && !skill.is7Signs() && !doesStack(skill)) { + int buffsToRemove = -1; + if (skill.isDance()) { + buffsToRemove = getDanceCount() - character().getMaxDanceAmount(); + } else if (skill.isTrigger()) { + buffsToRemove = getTriggeredBuffCount() - character().getMaxTriggeredBuffAmount(); + } else if (!skill.isHealingPotionSkill()) { + buffsToRemove = getBuffCount() - _owner.getStat().getMaxBuffCount(); + } + + for (BuffInfo bi : effects) { + if (buffsToRemove < 0) { + break; + } + + if (!bi.isInUse()) { + continue; + } + + if (bi.getSkill().getAbnormalType() == AbnormalType.SUMMON_CONDITION) { + continue; + } + + stopAndRemove(bi, effects); + + buffsToRemove--; + } + } + + // After removing old buff (same ID) or stacked buff (same abnormal type), + // Add the buff to the end of the effect list. + effects.add(info); + // Initialize effects. + info.initializeEffects(); + // Update effect flags and icons. + updateEffectList(true); + } + + /** + * Update effect icons.
    + * Prevents initialization. + */ + private void updateEffectIcons() { + if (_owner == null) { + return; + } + + updateEffectFlags(); + + if (!_owner.isPlayable()) { + return; + } + + AbnormalStatusUpdate asu = null; + PartySpelled ps = null; + PartySpelled psSummon = null; + ExOlympiadSpelledInfo os = null; + boolean isSummon = false; + + if (_owner.isPlayer()) { + if (_partyOnly) { + _partyOnly = false; + } else { + asu = new AbnormalStatusUpdate(); + } + + if (_owner.isInParty()) { + ps = new PartySpelled(_owner); + } + + if (_owner.getActingPlayer().isInOlympiadMode() && _owner.getActingPlayer().isOlympiadStart()) { + os = new ExOlympiadSpelledInfo(_owner.getActingPlayer()); + } + } else if (_owner.isSummon()) { + isSummon = true; + ps = new PartySpelled(_owner); + psSummon = new PartySpelled(_owner); + } + + // Buffs. + if (hasBuffs()) { + for (BuffInfo info : getBuffs()) { + if (info.getSkill().isHealingPotionSkill()) { + shortBuffStatusUpdate(info); + } else { + addIcon(info, asu, ps, psSummon, os, isSummon); + } + } + } + + // Triggered buffs. + if (hasTriggered()) { + for (BuffInfo info : getTriggered()) { + addIcon(info, asu, ps, psSummon, os, isSummon); + } + } + + // Songs and dances. + if (hasDances()) { + for (BuffInfo info : getDances()) { + addIcon(info, asu, ps, psSummon, os, isSummon); + } + } + + // Songs and dances. + if (hasToggles()) { + for (BuffInfo info : getToggles()) { + addIcon(info, asu, ps, psSummon, os, isSummon); + } + } + + // Debuffs. + if (hasDebuffs()) { + for (BuffInfo info : getDebuffs()) { + addIcon(info, asu, ps, psSummon, os, isSummon); + } + } + + if (asu != null) { + _owner.sendPacket(asu); + } + + if (ps != null) { + if (_owner.isSummon()) { + final L2PcInstance summonOwner = ((L2Summon) _owner).getOwner(); + if (summonOwner != null) { + if (summonOwner.isInParty()) { + summonOwner.getParty().broadcastToPartyMembers(summonOwner, psSummon); // send to all member except summonOwner + } + summonOwner.sendPacket(ps); + } + } else if (_owner.isPlayer() && _owner.isInParty()) { + _owner.getParty().broadcastPacket(ps); + } + } + + if (os != null) { + final OlympiadGameTask game = OlympiadGameManager.getInstance().getOlympiadTask(_owner.getActingPlayer().getOlympiadGameId()); + if ((game != null) && game.isBattleStarted()) { + game.getZone().broadcastPacketToObservers(os); + } + } + } + + private void addIcon(BuffInfo info, AbnormalStatusUpdate asu, PartySpelled ps, PartySpelled psSummon, ExOlympiadSpelledInfo os, boolean isSummon) { + // Avoid null and not in use buffs. + if ((info == null) || !info.isInUse()) { + return; + } + + final Skill skill = info.getSkill(); + if (asu != null) { + asu.addSkill(info); + } + + if ((ps != null) && (isSummon || !skill.isToggle())) { + ps.addSkill(info); + } + + if ((psSummon != null) && !skill.isToggle()) { + psSummon.addSkill(info); + } + + if (os != null) { + os.addSkill(info); + } + } + + /** + * Wrapper to update abnormal icons and effect flags. + * @param update if {@code true} performs an update + */ + private void updateEffectList(boolean update) { + if (update) { + updateEffectIcons(); + computeEffectFlags(); + } + } + + /** + * Updates effect flags.
    + * TODO: Rework it to update in real time (add/remove/stop/activate/deactivate operations) and avoid iterations. + */ + private void updateEffectFlags() { + if (hasBuffs()) { + for (BuffInfo info : getBuffs()) { + if (info == null) { + continue; + } + + if (info.getSkill().isRemovedOnAnyActionExceptMove()) { + _hasBuffsRemovedOnAnyAction = true; + } + + if (info.getSkill().isRemovedOnDamage()) { + _hasBuffsRemovedOnDamage = true; + } + } + } + + if (hasTriggered()) { + for (BuffInfo info : getTriggered()) { + if (info == null) { + continue; + } + + if (info.getSkill().isRemovedOnAnyActionExceptMove()) { + _hasBuffsRemovedOnAnyAction = true; + } + + if (info.getSkill().isRemovedOnDamage()) { + _hasBuffsRemovedOnDamage = true; + } + } + } + + if (hasToggles()) { + for (BuffInfo info : getToggles()) { + if (info == null) { + continue; + } + + if (info.getSkill().isRemovedOnAnyActionExceptMove()) { + _hasBuffsRemovedOnAnyAction = true; + } + + if (info.getSkill().isRemovedOnDamage()) { + _hasBuffsRemovedOnDamage = true; + } + } + } + + if (hasDebuffs()) { + for (BuffInfo info : getDebuffs()) { + if ((info != null) && info.getSkill().isRemovedOnDamage()) { + _hasDebuffsRemovedOnDamage = true; + } + } + } + } + + /** + * Recalculate effect bits flag.
    + * TODO: Rework to update in real time and avoid iterations. + */ + private void computeEffectFlags() { + int flags = 0; + if (hasBuffs()) { + for (BuffInfo info : getBuffs()) { + if (info != null) { + for (AbstractEffect e : info.getEffects()) { + flags |= e.getEffectFlags(); + } + } + } + } + + if (hasTriggered()) { + for (BuffInfo info : getTriggered()) { + if (info != null) { + for (AbstractEffect e : info.getEffects()) { + flags |= e.getEffectFlags(); + } + } + } + } + + if (hasDebuffs()) { + for (BuffInfo info : getDebuffs()) { + if (info != null) { + for (AbstractEffect e : info.getEffects()) { + flags |= e.getEffectFlags(); + } + } + } + } + + if (hasDances()) { + for (BuffInfo info : getDances()) { + if (info != null) { + for (AbstractEffect e : info.getEffects()) { + flags |= e.getEffectFlags(); + } + } + } + } + + if (hasToggles()) { + for (BuffInfo info : getToggles()) { + if (info != null) { + for (AbstractEffect e : info.getEffects()) { + flags |= e.getEffectFlags(); + } + } + } + } + _effectFlags = flags; + } + + /** + * Check if target is affected with special buff + * @param flag of special buff + * @return boolean true if affected + */ + public boolean isAffected(EffectFlag flag) { + return (_effectFlags & flag.getMask()) != 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/CharSelectInfoPackage.java b/src/main/java/com/l2jserver/gameserver/model/CharSelectInfoPackage.java new file mode 100644 index 00000000..d4099cea --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/CharSelectInfoPackage.java @@ -0,0 +1,345 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.itemcontainer.PcInventory; + +/** + * Used to Store data sent to Client for Character.
    + * Selection screen. + * @since 2005/03/27 15:29:33 + */ +public class CharSelectInfoPackage { + private String _name; + private int _objectId = 0; + private long _exp = 0; + private int _sp = 0; + private int _clanId = 0; + private int _race = 0; + private int _classId = 0; + private int _baseClassId = 0; + private long _deleteTimer = 0L; + private long _lastAccess = 0L; + private int _face = 0; + private int _hairStyle = 0; + private int _hairColor = 0; + private int _sex = 0; + private int _level = 1; + private int _maxCp = 0; + private double _currentCp = 0; + private int _maxHp = 0; + private double _currentHp = 0; + private int _maxMp = 0; + private double _currentMp = 0; + private final int[][] _paperdoll; + private int _karma = 0; + private int _pkKills = 0; + private int _pvpKills = 0; + private int _augmentationId = 0; + private int _x = 0; + private int _y = 0; + private int _z = 0; + private String _htmlPrefix = null; + private int _vitalityPoints = 0; + private int _accessLevel = 0; + + /** + * Constructor for CharSelectInfoPackage. + * @param objectId character object Id. + * @param name the character's name. + */ + public CharSelectInfoPackage(int objectId, String name) { + setObjectId(objectId); + _name = name; + _paperdoll = PcInventory.restoreVisibleInventory(objectId); + } + + /** + * @return the character object Id. + */ + public int getObjectId() { + return _objectId; + } + + public void setObjectId(int objectId) { + _objectId = objectId; + } + + /** + * @return the character's access level. + */ + public int getAccessLevel() { + return _accessLevel; + } + + /** + * @param level the character's access level to be set. + */ + public void setAccessLevel(int level) { + _accessLevel = level; + } + + public int getClanId() { + return _clanId; + } + + public void setClanId(int clanId) { + _clanId = clanId; + } + + public int getClassId() { + return _classId; + } + + public int getBaseClassId() { + return _baseClassId; + } + + public void setClassId(int classId) { + _classId = classId; + } + + public void setBaseClassId(int baseClassId) { + _baseClassId = baseClassId; + } + + public double getCurrentCp() { + return _currentCp; + } + + public void setCurrentCp(double currentCp) { + _currentCp = currentCp; + } + + public double getCurrentHp() { + return _currentHp; + } + + public void setCurrentHp(double currentHp) { + _currentHp = currentHp; + } + + public double getCurrentMp() { + return _currentMp; + } + + public void setCurrentMp(double currentMp) { + _currentMp = currentMp; + } + + public long getDeleteTimer() { + return _deleteTimer; + } + + public void setDeleteTimer(long deleteTimer) { + _deleteTimer = deleteTimer; + } + + public long getLastAccess() { + return _lastAccess; + } + + public void setLastAccess(long lastAccess) { + _lastAccess = lastAccess; + } + + public long getExp() { + return _exp; + } + + public void setExp(long exp) { + _exp = exp; + } + + public int getFace() { + return _face; + } + + public void setFace(int face) { + _face = face; + } + + public int getHairColor() { + return _hairColor; + } + + public void setHairColor(int hairColor) { + _hairColor = hairColor; + } + + public int getHairStyle() { + return _hairStyle; + } + + public void setHairStyle(int hairStyle) { + _hairStyle = hairStyle; + } + + public int getPaperdollObjectId(int slot) { + return _paperdoll[slot][0]; + } + + public int getPaperdollItemId(int slot) { + return _paperdoll[slot][1]; + } + + public int getLevel() { + return _level; + } + + public void setLevel(int level) { + _level = level; + } + + public int getMaxCp() { + return _maxCp; + } + + public void setMaxCp(int maxCp) { + _maxCp = maxCp; + } + + public int getMaxHp() { + return _maxHp; + } + + public void setMaxHp(int maxHp) { + _maxHp = maxHp; + } + + public int getMaxMp() { + return _maxMp; + } + + public void setMaxMp(int maxMp) { + _maxMp = maxMp; + } + + public String getName() { + return _name; + } + + public void setName(String name) { + _name = name; + } + + public int getRace() { + return _race; + } + + public void setRace(int race) { + _race = race; + } + + public int getSex() { + return _sex; + } + + public void setSex(int sex) { + _sex = sex; + } + + public int getSp() { + return _sp; + } + + public void setSp(int sp) { + _sp = sp; + } + + public int getEnchantEffect() { + if (_paperdoll[Inventory.PAPERDOLL_RHAND][2] > 0) { + return _paperdoll[Inventory.PAPERDOLL_RHAND][2]; + } + return _paperdoll[Inventory.PAPERDOLL_RHAND][2]; + } + + public void setKarma(int k) { + _karma = k; + } + + public int getKarma() { + return _karma; + } + + public void setAugmentationId(int augmentationId) { + _augmentationId = augmentationId; + } + + public int getAugmentationId() { + return _augmentationId; + } + + public void setPkKills(int PkKills) { + _pkKills = PkKills; + } + + public int getPkKills() { + return _pkKills; + } + + public void setPvPKills(int PvPKills) { + _pvpKills = PvPKills; + } + + public int getPvPKills() { + return _pvpKills; + } + + public int getX() { + return _x; + } + + public int getY() { + return _y; + } + + public int getZ() { + return _z; + } + + public void setX(int x) { + _x = x; + } + + public void setY(int y) { + _y = y; + } + + public void setZ(int z) { + _z = z; + } + + public String getHtmlPrefix() { + return _htmlPrefix; + } + + public void setHtmlPrefix(String s) { + _htmlPrefix = s; + } + + public void setVitalityPoints(int points) { + _vitalityPoints = points; + } + + public int getVitalityPoints() { + return _vitalityPoints; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ClanInfo.java b/src/main/java/com/l2jserver/gameserver/model/ClanInfo.java new file mode 100644 index 00000000..aa015604 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ClanInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author UnAfraid + */ +public class ClanInfo { + private final L2Clan _clan; + private final int _total; + private final int _online; + + public ClanInfo(final L2Clan clan) { + _clan = clan; + _total = clan.getMembersCount(); + _online = clan.getOnlineMembersCount(); + } + + public L2Clan getClan() { + return _clan; + } + + public int getTotal() { + return _total; + } + + public int getOnline() { + return _online; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ClanPrivilege.java b/src/main/java/com/l2jserver/gameserver/model/ClanPrivilege.java new file mode 100644 index 00000000..a723af7a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ClanPrivilege.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * This enum is used for clan privileges.
    + * The ordinal of each entry is the bit index in the privilege bitmask. + * @author HorridoJoho + */ +public enum ClanPrivilege { + /** dummy entry */ + DUMMY, + /** Privilege to join clan */ + CL_JOIN_CLAN, + /** Privilege to give a title */ + CL_GIVE_TITLE, + /** Privilege to view warehouse content */ + CL_VIEW_WAREHOUSE, + /** Privilege to manage clan ranks */ + CL_MANAGE_RANKS, + CL_PLEDGE_WAR, + CL_DISMISS, + /** Privilege to register clan crest */ + CL_REGISTER_CREST, + CL_APPRENTICE, + CL_TROOPS_FAME, + CL_SUMMON_AIRSHIP, + /** Privilege to open a door */ + CH_OPEN_DOOR, + CH_OTHER_RIGHTS, + CH_AUCTION, + CH_DISMISS, + CH_SET_FUNCTIONS, + CS_OPEN_DOOR, + CS_MANOR_ADMIN, + CS_MANAGE_SIEGE, + CS_USE_FUNCTIONS, + CS_DISMISS, + CS_TAXES, + CS_MERCENARIES, + CS_SET_FUNCTIONS; + + public int getBitmask() { + return 1 << ordinal(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/CombatFlag.java b/src/main/java/com/l2jserver/gameserver/model/CombatFlag.java new file mode 100644 index 00000000..21ddbebb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/CombatFlag.java @@ -0,0 +1,112 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class CombatFlag { + // private static final Logger _log = Logger.getLogger(CombatFlag.class.getName()); + + private L2PcInstance _player = null; + private int _playerId = 0; + private L2ItemInstance _item = null; + private L2ItemInstance _itemInstance; + private final Location _location; + private final int _itemId; + + public CombatFlag(int fortId, int x, int y, int z, int heading, int item_id) { + _location = new Location(x, y, z, heading); + _itemId = item_id; + } + + public synchronized void spawnMe() { + // Init the dropped L2ItemInstance and add it in the world as a visible object at the position where mob was last + _itemInstance = ItemTable.getInstance().createItem("Combat", _itemId, 1, null, null); + _itemInstance.dropMe(null, _location.getX(), _location.getY(), _location.getZ()); + } + + public synchronized void unSpawnMe() { + if (_player != null) { + dropIt(); + } + if (_itemInstance != null) { + _itemInstance.decayMe(); + } + } + + public boolean activate(L2PcInstance player, L2ItemInstance item) { + if (player.isMounted()) { + player.sendPacket(SystemMessageId.CANNOT_EQUIP_ITEM_DUE_TO_BAD_CONDITION); + return false; + } + + // Player holding it data + _player = player; + _playerId = _player.getObjectId(); + _itemInstance = null; + + // Equip with the weapon + _item = item; + _player.getInventory().equipItem(_item); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_EQUIPPED); + sm.addItemName(_item); + _player.sendPacket(sm); + + // Refresh inventory + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(_item); + _player.sendPacket(iu); + } else { + _player.sendPacket(new ItemList(_player, false)); + } + // Refresh player stats + _player.broadcastUserInfo(); + _player.setCombatFlagEquipped(true); + return true; + } + + public void dropIt() { + // Reset player stats + _player.setCombatFlagEquipped(false); + int slot = _player.getInventory().getSlotFromItem(_item); + _player.getInventory().unEquipItemInBodySlot(slot); + _player.destroyItem("CombatFlag", _item, null, true); + _item = null; + _player.broadcastUserInfo(); + _player = null; + _playerId = 0; + } + + public int getPlayerObjectId() { + return _playerId; + } + + public L2ItemInstance getCombatFlagInstance() { + return _itemInstance; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/CropProcure.java b/src/main/java/com/l2jserver/gameserver/model/CropProcure.java new file mode 100644 index 00000000..f6f18b17 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/CropProcure.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author malyelfik + */ +public final class CropProcure extends SeedProduction { + private final int _rewardType; + + public CropProcure(int id, long amount, int type, long startAmount, long price) { + super(id, amount, price, startAmount); + _rewardType = type; + } + + public int getReward() { + return _rewardType; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/CursedWeapon.java b/src/main/java/com/l2jserver/gameserver/model/CursedWeapon.java new file mode 100644 index 00000000..c12374cc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/CursedWeapon.java @@ -0,0 +1,626 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.xml.impl.TransformData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.model.L2Party.messageType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.INamable; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.Earthquake; +import com.l2jserver.gameserver.network.serverpackets.ExRedSky; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.Broadcast; + +public class CursedWeapon implements INamable { + private static final Logger LOG = LoggerFactory.getLogger(CursedWeapon.class); + + // _name is the name of the cursed weapon associated with its ID. + private final String _name; + // _itemId is the Item ID of the cursed weapon. + private final int _itemId; + // _skillId is the skills ID. + private final int _skillId; + private final int _skillMaxLevel; + private int _dropRate; + private int _duration; + private int _durationLost; + private int _disappearChance; + private int _stageKills; + + // this should be false unless if the cursed weapon is dropped, in that case it would be true. + private boolean _isDropped = false; + // this sets the cursed weapon status to true only if a player has the cursed weapon, otherwise this should be false. + private boolean _isActivated = false; + private ScheduledFuture _removeTask; + + private int _nbKills = 0; + private long _endTime = 0; + + private int _playerId = 0; + protected L2PcInstance _player = null; + private L2ItemInstance _item = null; + private int _playerKarma = 0; + private int _playerPkKills = 0; + private int transformationId = 0; + + public CursedWeapon(int itemId, int skillId, String name) { + _name = name; + _itemId = itemId; + _skillId = skillId; + _skillMaxLevel = SkillData.getInstance().getMaxLevel(_skillId); + } + + public void endOfLife() { + if (_isActivated) { + if ((_player != null) && _player.isOnline()) { + // Remove from player + LOG.info("{} being removed online.", _name); + + _player.abortAttack(); + + _player.setKarma(_playerKarma); + _player.setPkKills(_playerPkKills); + _player.setCursedWeaponEquippedId(0); + removeSkill(); + + // Remove + _player.getInventory().unEquipItemInBodySlot(L2Item.SLOT_LR_HAND); + _player.storeMe(); + + // Destroy + L2ItemInstance removedItem = _player.getInventory().destroyItemByItemId("", _itemId, 1, _player, null); + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + if (removedItem.getCount() == 0) { + iu.addRemovedItem(removedItem); + } else { + iu.addModifiedItem(removedItem); + } + + _player.sendPacket(iu); + } else { + _player.sendPacket(new ItemList(_player, true)); + } + + _player.broadcastUserInfo(); + } else { + // Remove from Db + LOG.info("{} being removed offline.", _name); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var del = con.prepareStatement("DELETE FROM items WHERE owner_id=? AND item_id=?"); + var ps = con.prepareStatement("UPDATE characters SET karma=?, pkkills=? WHERE charId=?")) { + // Delete the item + del.setInt(1, _playerId); + del.setInt(2, _itemId); + if (del.executeUpdate() != 1) { + LOG.warn("Error while deleting itemId {} from userId {}", _itemId, _playerId); + } + + // Restore the karma + ps.setInt(1, _playerKarma); + ps.setInt(2, _playerPkKills); + ps.setInt(3, _playerId); + if (ps.executeUpdate() != 1) { + LOG.warn("Error while updating karma & pkkills for userId {}", _playerId); + } + } catch (Exception e) { + LOG.warn("Could not delete : {}", e.getMessage(), e); + } + } + } else { + // either this cursed weapon is in the inventory of someone who has another cursed weapon equipped, + // OR this cursed weapon is on the ground. + if ((_player != null) && (_player.getInventory().getItemByItemId(_itemId) != null)) { + // Destroy + L2ItemInstance removedItem = _player.getInventory().destroyItemByItemId("", _itemId, 1, _player, null); + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + if (removedItem.getCount() == 0) { + iu.addRemovedItem(removedItem); + } else { + iu.addModifiedItem(removedItem); + } + + _player.sendPacket(iu); + } else { + _player.sendPacket(new ItemList(_player, true)); + } + + _player.broadcastUserInfo(); + } + // is dropped on the ground + else if (_item != null) { + _item.decayMe(); + L2World.getInstance().removeObject(_item); + LOG.info("{} item has been removed from World.", _name); + } + } + + // Delete infos from table if any + CursedWeaponsManager.removeFromDb(_itemId); + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_HAS_DISAPPEARED); + sm.addItemName(_itemId); + CursedWeaponsManager.announce(sm); + + // Reset state + cancelTask(); + _isActivated = false; + _isDropped = false; + _endTime = 0; + _player = null; + _playerId = 0; + _playerKarma = 0; + _playerPkKills = 0; + _item = null; + _nbKills = 0; + } + + private void cancelTask() { + if (_removeTask != null) { + _removeTask.cancel(true); + _removeTask = null; + } + } + + private class RemoveTask implements Runnable { + protected RemoveTask() { + } + + @Override + public void run() { + if (System.currentTimeMillis() >= getEndTime()) { + endOfLife(); + } + } + } + + private void dropIt(L2Attackable attackable, L2PcInstance player) { + dropIt(attackable, player, null, true); + } + + private void dropIt(L2Attackable attackable, L2PcInstance player, L2Character killer, boolean fromMonster) { + _isActivated = false; + + if (fromMonster) { + _item = attackable.dropItem(player, _itemId, 1); + _item.setDropTime(0); // Prevent item from being removed by ItemsAutoDestroy + + // RedSky and Earthquake + ExRedSky packet = new ExRedSky(10); + Earthquake eq = new Earthquake(player.getX(), player.getY(), player.getZ(), 14, 3); + Broadcast.toAllOnlinePlayers(packet); + Broadcast.toAllOnlinePlayers(eq); + } else { + _item = _player.getInventory().getItemByItemId(_itemId); + _player.dropItem("DieDrop", _item, killer, true); + _player.setKarma(_playerKarma); + _player.setPkKills(_playerPkKills); + _player.setCursedWeaponEquippedId(0); + removeSkill(); + _player.abortAttack(); + // L2ItemInstance item = _player.getInventory().getItemByItemId(_itemId); + // _player.getInventory().dropItem("DieDrop", item, _player, null); + // _player.getInventory().getItemByItemId(_itemId).dropMe(_player, _player.getX(), _player.getY(), _player.getZ()); + } + _isDropped = true; + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_WAS_DROPPED_IN_THE_S1_REGION); + if (player != null) { + sm.addZoneName(player.getX(), player.getY(), player.getZ()); // Region Name + } else if (_player != null) { + sm.addZoneName(_player.getX(), _player.getY(), _player.getZ()); // Region Name + } else { + sm.addZoneName(killer.getX(), killer.getY(), killer.getZ()); // Region Name + } + sm.addItemName(_itemId); + CursedWeaponsManager.announce(sm); // in the Hot Spring region + } + + public void cursedOnLogin() { + doTransform(); + giveSkill(); + + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.S2_OWNER_HAS_LOGGED_INTO_THE_S1_REGION); + msg.addZoneName(_player.getX(), _player.getY(), _player.getZ()); + msg.addItemName(_player.getCursedWeaponEquippedId()); + CursedWeaponsManager.announce(msg); + + CursedWeapon cw = CursedWeaponsManager.getInstance().getCursedWeapon(_player.getCursedWeaponEquippedId()); + SystemMessage msg2 = SystemMessage.getSystemMessage(SystemMessageId.S2_MINUTE_OF_USAGE_TIME_ARE_LEFT_FOR_S1); + int timeLeft = (int) (cw.getTimeLeft() / 60000); + msg2.addItemName(_player.getCursedWeaponEquippedId()); + msg2.addInt(timeLeft); + _player.sendPacket(msg2); + } + + /** + * Yesod:
    + * Rebind the passive skill belonging to the CursedWeapon. Invoke this method if the weapon owner switches to a subclass. + */ + public void giveSkill() { + int level = 1 + (_nbKills / _stageKills); + if (level > _skillMaxLevel) { + level = _skillMaxLevel; + } + + final Skill skill = SkillData.getInstance().getSkill(_skillId, level); + _player.addSkill(skill, false); + + // Void Burst, Void Flow + _player.addSkill(CommonSkill.VOID_BURST.getSkill(), false); + _player.addTransformSkill(CommonSkill.VOID_BURST.getSkill()); + _player.addSkill(CommonSkill.VOID_FLOW.getSkill(), false); + _player.addTransformSkill(CommonSkill.VOID_FLOW.getSkill()); + _player.sendSkillList(); + } + + public void doTransform() { + if (_itemId == 8689) { + transformationId = 302; + } else if (_itemId == 8190) { + transformationId = 301; + } + + if (_player.isTransformed() || _player.isInStance()) { + _player.stopTransformation(true); + + ThreadPoolManager.getInstance().scheduleGeneral(() -> TransformData.getInstance().transformPlayer(transformationId, _player), 500); + } else { + TransformData.getInstance().transformPlayer(transformationId, _player); + } + } + + public void removeSkill() { + _player.removeSkill(_skillId); + _player.removeSkill(CommonSkill.VOID_BURST.getSkill().getId()); + _player.removeSkill(CommonSkill.VOID_FLOW.getSkill().getId()); + _player.untransform(); + _player.sendSkillList(); + } + + public void reActivate() { + _isActivated = true; + if ((_endTime - System.currentTimeMillis()) <= 0) { + endOfLife(); + } else { + _removeTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new RemoveTask(), _durationLost * 12000L, _durationLost * 12000L); + } + + } + + public boolean checkDrop(L2Attackable attackable, L2PcInstance player) { + if (Rnd.get(100000) < _dropRate) { + // Drop the item + dropIt(attackable, player); + + // Start the Life Task + _endTime = System.currentTimeMillis() + (_duration * 60000L); + _removeTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new RemoveTask(), _durationLost * 12000L, _durationLost * 12000L); + + return true; + } + + return false; + } + + public void activate(L2PcInstance player, L2ItemInstance item) { + // If the player is mounted, attempt to unmount first. + // Only allow picking up the cursed weapon if unmounting is successful. + if (player.isMounted() && !player.dismount()) { + // TODO: Verify the following system message, may still be custom. + player.sendPacket(SystemMessageId.FAILED_TO_PICKUP_S1); + player.dropItem("InvDrop", item, null, true); + return; + } + + _isActivated = true; + + // Player holding it data + _player = player; + _playerId = _player.getObjectId(); + _playerKarma = _player.getKarma(); + _playerPkKills = _player.getPkKills(); + saveData(); + + // Change player stats + _player.setCursedWeaponEquippedId(_itemId); + _player.setKarma(9999999); + _player.setPkKills(0); + if (_player.isInParty()) { + _player.getParty().removePartyMember(_player, messageType.Expelled); + } + + // Disable All Skills + // Do Transform + doTransform(); + // Add skill + giveSkill(); + + // Equip with the weapon + _item = item; + // L2ItemInstance[] items = + _player.getInventory().equipItem(_item); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_EQUIPPED); + sm.addItemName(_item); + _player.sendPacket(sm); + + // Fully heal player + _player.setCurrentHpMp(_player.getMaxHp(), _player.getMaxMp()); + _player.setCurrentCp(_player.getMaxCp()); + + // Refresh inventory + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(_item); + // iu.addItems(Arrays.asList(items)); + _player.sendPacket(iu); + } else { + _player.sendPacket(new ItemList(_player, false)); + } + + // Refresh player stats + _player.broadcastUserInfo(); + + SocialAction atk = new SocialAction(_player.getObjectId(), 17); + + _player.broadcastPacket(atk); + + sm = SystemMessage.getSystemMessage(SystemMessageId.THE_OWNER_OF_S2_HAS_APPEARED_IN_THE_S1_REGION); + sm.addZoneName(_player.getX(), _player.getY(), _player.getZ()); // Region Name + sm.addItemName(_item); + CursedWeaponsManager.announce(sm); + } + + public void saveData() { + if (general().debug()) { + LOG.info("CursedWeapon: Saving data to disk."); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var del = con.prepareStatement("DELETE FROM cursed_weapons WHERE itemId = ?"); + var ps = con.prepareStatement("INSERT INTO cursed_weapons (itemId, charId, playerKarma, playerPkKills, nbKills, endTime) VALUES (?, ?, ?, ?, ?, ?)")) { + // Delete previous data + del.setInt(1, _itemId); + del.executeUpdate(); + + if (_isActivated) { + ps.setInt(1, _itemId); + ps.setInt(2, _playerId); + ps.setInt(3, _playerKarma); + ps.setInt(4, _playerPkKills); + ps.setInt(5, _nbKills); + ps.setLong(6, _endTime); + ps.executeUpdate(); + } + } catch (Exception e) { + LOG.error("Failed to save data.", e); + } + } + + public void dropIt(L2Character killer) { + if (Rnd.get(100) <= _disappearChance) { + // Remove it + endOfLife(); + } else { + // Unequip & Drop + dropIt(null, null, killer, false); + // Reset player stats + _player.setKarma(_playerKarma); + _player.setPkKills(_playerPkKills); + _player.setCursedWeaponEquippedId(0); + removeSkill(); + + _player.abortAttack(); + + _player.broadcastUserInfo(); + } + } + + public void increaseKills() { + _nbKills++; + + if ((_player != null) && _player.isOnline()) { + _player.setPkKills(_nbKills); + _player.sendPacket(new UserInfo(_player)); + + if (((_nbKills % _stageKills) == 0) && (_nbKills <= (_stageKills * (_skillMaxLevel - 1)))) { + giveSkill(); + } + } + // Reduce time-to-live + _endTime -= _durationLost * 60000L; + saveData(); + } + + public void setDisappearChance(int disappearChance) { + _disappearChance = disappearChance; + } + + public void setDropRate(int dropRate) { + _dropRate = dropRate; + } + + public void setDuration(int duration) { + _duration = duration; + } + + public void setDurationLost(int durationLost) { + _durationLost = durationLost; + } + + public void setStageKills(int stageKills) { + _stageKills = stageKills; + } + + public void setNbKills(int nbKills) { + _nbKills = nbKills; + } + + public void setPlayerId(int playerId) { + _playerId = playerId; + } + + public void setPlayerKarma(int playerKarma) { + _playerKarma = playerKarma; + } + + public void setPlayerPkKills(int playerPkKills) { + _playerPkKills = playerPkKills; + } + + public void setActivated(boolean isActivated) { + _isActivated = isActivated; + } + + public void setDropped(boolean isDropped) { + _isDropped = isDropped; + } + + public void setEndTime(long endTime) { + _endTime = endTime; + } + + public void setPlayer(L2PcInstance player) { + _player = player; + } + + public void setItem(L2ItemInstance item) { + _item = item; + } + + public boolean isActivated() { + return _isActivated; + } + + public boolean isDropped() { + return _isDropped; + } + + public long getEndTime() { + return _endTime; + } + + @Override + public String getName() { + return _name; + } + + public int getItemId() { + return _itemId; + } + + public int getSkillId() { + return _skillId; + } + + public int getPlayerId() { + return _playerId; + } + + public L2PcInstance getPlayer() { + return _player; + } + + public int getPlayerKarma() { + return _playerKarma; + } + + public int getPlayerPkKills() { + return _playerPkKills; + } + + public int getNbKills() { + return _nbKills; + } + + public int getStageKills() { + return _stageKills; + } + + public boolean isActive() { + return _isActivated || _isDropped; + } + + public int getLevel() { + if (_nbKills > (_stageKills * _skillMaxLevel)) { + return _skillMaxLevel; + } + return (_nbKills / _stageKills); + } + + public long getTimeLeft() { + return _endTime - System.currentTimeMillis(); + } + + public void goTo(L2PcInstance player) { + if (player == null) { + return; + } + + if (_isActivated && (_player != null)) { + // Go to player holding the weapon + player.teleToLocation(_player.getLocation(), true); + } else if (_isDropped && (_item != null)) { + // Go to item on the ground + player.teleToLocation(_item.getLocation(), true); + } else { + player.sendMessage(_name + " isn't in the World."); + } + } + + public Location getWorldPosition() { + if (_isActivated && (_player != null)) { + return _player.getLocation(); + } + + if (_isDropped && (_item != null)) { + return _item.getLocation(); + } + + return null; + } + + public long getDuration() { + return _duration; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/DamageDoneInfo.java b/src/main/java/com/l2jserver/gameserver/model/DamageDoneInfo.java new file mode 100644 index 00000000..b5fd8782 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/DamageDoneInfo.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author xban1x + */ +public final class DamageDoneInfo { + private final L2PcInstance _attacker; + private int _damage = 0; + + public DamageDoneInfo(L2PcInstance attacker) { + _attacker = attacker; + } + + public L2PcInstance getAttacker() { + return _attacker; + } + + public void addDamage(int damage) { + _damage += damage; + } + + public int getDamage() { + return _damage; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj instanceof DamageDoneInfo) { + return (((DamageDoneInfo) obj).getAttacker() == _attacker); + } + + return false; + } + + @Override + public int hashCode() { + return _attacker.getObjectId(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/DimensionalRiftRoom.java b/src/main/java/com/l2jserver/gameserver/model/DimensionalRiftRoom.java new file mode 100644 index 00000000..fcc8a050 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/DimensionalRiftRoom.java @@ -0,0 +1,131 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.awt.Polygon; +import java.awt.Shape; +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.commons.util.Rnd; + +/** + * Dimensional Rift Room. + * @author xban1x + */ +public final class DimensionalRiftRoom { + private final byte _type; + private final byte _room; + private final int _xMin; + private final int _xMax; + private final int _yMin; + private final int _yMax; + private final int _zMin; + private final int _zMax; + private final Location _teleportCoords; + private final Shape _s; + private final boolean _isBossRoom; + private final List _roomSpawns = new ArrayList<>(); + private L2Party _party = null; + + public DimensionalRiftRoom(byte type, byte room, int xMin, int xMax, int yMin, int yMax, int zMin, int zMax, int xT, int yT, int zT, boolean isBossRoom) { + _type = type; + _room = room; + _xMin = (xMin + 128); + _xMax = (xMax - 128); + _yMin = (yMin + 128); + _yMax = (yMax - 128); + _zMin = zMin; + _zMax = zMax; + _teleportCoords = new Location(xT, yT, zT); + _isBossRoom = isBossRoom; + _s = new Polygon(new int[] { + xMin, + xMax, + xMax, + xMin + }, new int[] { + yMin, + yMin, + yMax, + yMax + }, 4); + } + + public byte getType() { + return _type; + } + + public byte getRoom() { + return _room; + } + + public int getRandomX() { + return Rnd.get(_xMin, _xMax); + } + + public int getRandomY() { + return Rnd.get(_yMin, _yMax); + } + + public Location getTeleportCoordinates() { + return _teleportCoords; + } + + public boolean checkIfInZone(int x, int y, int z) { + return _s.contains(x, y) && (z >= _zMin) && (z <= _zMax); + } + + public boolean isBossRoom() { + return _isBossRoom; + } + + public List getSpawns() { + return _roomSpawns; + } + + public void spawn() { + for (L2Spawn spawn : _roomSpawns) { + spawn.doSpawn(); + spawn.startRespawn(); + } + } + + public DimensionalRiftRoom unspawn() { + for (L2Spawn spawn : _roomSpawns) { + spawn.stopRespawn(); + if (spawn.getLastSpawn() != null) { + spawn.getLastSpawn().deleteMe(); + } + } + return this; + } + + /** + * Sets the party inside. + * @param party + */ + public void setParty(L2Party party) { + _party = party; + } + + public L2Party getParty() { + return _party; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/DropProtection.java b/src/main/java/com/l2jserver/gameserver/model/DropProtection.java new file mode 100644 index 00000000..31c192a1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/DropProtection.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; + +/** + * @author DrHouse + */ +public class DropProtection implements Runnable { + private volatile boolean _isProtected = false; + private L2PcInstance _owner = null; + private ScheduledFuture _task = null; + + private static final long PROTECTED_MILLIS_TIME = 15000; + + @Override + public synchronized void run() { + _isProtected = false; + _owner = null; + _task = null; + } + + public boolean isProtected() { + return _isProtected; + } + + public L2PcInstance getOwner() { + return _owner; + } + + public synchronized boolean tryPickUp(L2PcInstance actor) { + if (!_isProtected) { + return true; + } + + if (_owner == actor) { + return true; + } + + if ((_owner.getParty() != null) && (_owner.getParty() == actor.getParty())) { + return true; + } + + /* + * if (_owner.getClan() != null && _owner.getClan() == actor.getClan()) return true; + */ + + return false; + } + + public boolean tryPickUp(L2PetInstance pet) { + return tryPickUp(pet.getOwner()); + } + + public synchronized void unprotect() { + if (_task != null) { + _task.cancel(false); + } + _isProtected = false; + _owner = null; + _task = null; + } + + public synchronized void protect(L2PcInstance player) { + unprotect(); + + _isProtected = true; + _owner = player; + + if (_owner == null) { + throw new NullPointerException("Trying to protect dropped item to null owner"); + } + + _task = ThreadPoolManager.getInstance().scheduleGeneral(this, PROTECTED_MILLIS_TIME); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Elementals.java b/src/main/java/com/l2jserver/gameserver/model/Elementals.java new file mode 100644 index 00000000..cf37bda1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Elementals.java @@ -0,0 +1,301 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.functions.FuncAdd; + +public final class Elementals { + private static final Map TABLE = new HashMap<>(); + + static { + for (ElementalItems item : ElementalItems.values()) { + TABLE.put(item._itemId, item); + } + } + + public static final byte NONE = -1; + public static final byte FIRE = 0; + public static final byte WATER = 1; + public static final byte WIND = 2; + public static final byte EARTH = 3; + public static final byte HOLY = 4; + public static final byte DARK = 5; + + public static final int FIRST_WEAPON_BONUS = 20; + public static final int NEXT_WEAPON_BONUS = 5; + public static final int ARMOR_BONUS = 6; + + public static final int[] WEAPON_VALUES = { + 0, // Level 1 + 25, // Level 2 + 75, // Level 3 + 150, // Level 4 + 175, // Level 5 + 225, // Level 6 + 300, // Level 7 + 325, // Level 8 + 375, // Level 9 + 450, // Level 10 + 475, // Level 11 + 525, // Level 12 + 600, // Level 13 + Integer.MAX_VALUE + // TODO: Higher stones + }; + + public static final int[] ARMOR_VALUES = { + 0, // Level 1 + 12, // Level 2 + 30, // Level 3 + 60, // Level 4 + 72, // Level 5 + 90, // Level 6 + 120, // Level 7 + 132, // Level 8 + 150, // Level 9 + 180, // Level 10 + 192, // Level 11 + 210, // Level 12 + 240, // Level 13 + Integer.MAX_VALUE + // TODO: Higher stones + }; + + public enum ElementalItemType { + Stone(3), + Roughore(3), + Crystal(6), + Jewel(9), + Energy(12); + + public int _maxLevel; + + ElementalItemType(int maxLvl) { + _maxLevel = maxLvl; + } + } + + public enum ElementalItems { + fireStone(FIRE, 9546, ElementalItemType.Stone), + waterStone(WATER, 9547, ElementalItemType.Stone), + windStone(WIND, 9549, ElementalItemType.Stone), + earthStone(EARTH, 9548, ElementalItemType.Stone), + divineStone(HOLY, 9551, ElementalItemType.Stone), + darkStone(DARK, 9550, ElementalItemType.Stone), + + fireRoughtore(FIRE, 10521, ElementalItemType.Roughore), + waterRoughtore(WATER, 10522, ElementalItemType.Roughore), + windRoughtore(WIND, 10524, ElementalItemType.Roughore), + earthRoughtore(EARTH, 10523, ElementalItemType.Roughore), + divineRoughtore(HOLY, 10526, ElementalItemType.Roughore), + darkRoughtore(DARK, 10525, ElementalItemType.Roughore), + + fireCrystal(FIRE, 9552, ElementalItemType.Crystal), + waterCrystal(WATER, 9553, ElementalItemType.Crystal), + windCrystal(WIND, 9555, ElementalItemType.Crystal), + earthCrystal(EARTH, 9554, ElementalItemType.Crystal), + divineCrystal(HOLY, 9557, ElementalItemType.Crystal), + darkCrystal(DARK, 9556, ElementalItemType.Crystal), + + fireJewel(FIRE, 9558, ElementalItemType.Jewel), + waterJewel(WATER, 9559, ElementalItemType.Jewel), + windJewel(WIND, 9561, ElementalItemType.Jewel), + earthJewel(EARTH, 9560, ElementalItemType.Jewel), + divineJewel(HOLY, 9563, ElementalItemType.Jewel), + darkJewel(DARK, 9562, ElementalItemType.Jewel), + + // not yet supported by client (Freya pts) + fireEnergy(FIRE, 9564, ElementalItemType.Energy), + waterEnergy(WATER, 9565, ElementalItemType.Energy), + windEnergy(WIND, 9567, ElementalItemType.Energy), + earthEnergy(EARTH, 9566, ElementalItemType.Energy), + divineEnergy(HOLY, 9569, ElementalItemType.Energy), + darkEnergy(DARK, 9568, ElementalItemType.Energy); + + public byte _element; + public int _itemId; + public ElementalItemType _type; + + ElementalItems(byte element, int itemId, ElementalItemType type) { + _element = element; + _itemId = itemId; + _type = type; + } + } + + public static byte getItemElement(int itemId) { + ElementalItems item = TABLE.get(itemId); + if (item != null) { + return item._element; + } + return NONE; + } + + public static ElementalItems getItemElemental(int itemId) { + return TABLE.get(itemId); + } + + public static int getMaxElementLevel(int itemId) { + ElementalItems item = TABLE.get(itemId); + if (item != null) { + return item._type._maxLevel; + } + return -1; + } + + public static String getElementName(byte element) { + return switch (element) { + case FIRE -> "Fire"; + case WATER -> "Water"; + case WIND -> "Wind"; + case EARTH -> "Earth"; + case DARK -> "Dark"; + case HOLY -> "Holy"; + default -> "None"; + }; + } + + public static byte getElementId(String name) { + String tmp = name.toLowerCase(); + if (tmp.equals("fire")) { + return FIRE; + } + if (tmp.equals("water")) { + return WATER; + } + if (tmp.equals("wind")) { + return WIND; + } + if (tmp.equals("earth")) { + return EARTH; + } + if (tmp.equals("dark")) { + return DARK; + } + if (tmp.equals("holy")) { + return HOLY; + } + return NONE; + } + + public static byte getOppositeElement(byte element) { + return (byte) (((element % 2) == 0) ? (element + 1) : (element - 1)); + } + + public static class ElementalStatBoni { + private byte _elementalType; + private int _elementalValue; + private boolean _active; + + public ElementalStatBoni(byte type, int value) { + _elementalType = type; + _elementalValue = value; + _active = false; + } + + public void applyBonus(L2PcInstance player, boolean isArmor) { + // make sure the bonuses are not applied twice.. + if (_active) { + return; + } + + switch (_elementalType) { + case FIRE -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.FIRE_RES : Stats.FIRE_POWER, 0x40, this, _elementalValue, null)); + case WATER -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.WATER_RES : Stats.WATER_POWER, 0x40, this, _elementalValue, null)); + case WIND -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.WIND_RES : Stats.WIND_POWER, 0x40, this, _elementalValue, null)); + case EARTH -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.EARTH_RES : Stats.EARTH_POWER, 0x40, this, _elementalValue, null)); + case DARK -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.DARK_RES : Stats.DARK_POWER, 0x40, this, _elementalValue, null)); + case HOLY -> player.addStatFuncs(new FuncAdd(isArmor ? Stats.HOLY_RES : Stats.HOLY_POWER, 0x40, this, _elementalValue, null)); + } + + _active = true; + } + + public void removeBonus(L2PcInstance player) { + // make sure the bonuses are not removed twice + if (!_active) { + return; + } + + player.removeStatsOwner(this); + + _active = false; + } + + public void setValue(int val) { + _elementalValue = val; + } + + public void setElement(byte type) { + _elementalType = type; + } + } + + // non static: + private final ElementalStatBoni _boni; + private byte _element; + private int _value; + + public byte getElement() { + return _element; + } + + public void setElement(byte type) { + _element = type; + _boni.setElement(type); + } + + public int getValue() { + return _value; + } + + public void setValue(int val) { + _value = val; + _boni.setValue(val); + } + + @Override + public String toString() { + return getElementName(_element) + " +" + _value; + } + + public Elementals(byte type, int value) { + _element = type; + _value = value; + _boni = new ElementalStatBoni(_element, _value); + } + + public void applyBonus(L2PcInstance player, boolean isArmor) { + _boni.applyBonus(player, isArmor); + } + + public void removeBonus(L2PcInstance player) { + _boni.removeBonus(player); + } + + public void updateBonus(L2PcInstance player, boolean isArmor) { + _boni.removeBonus(player); + _boni.applyBonus(player, isArmor); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/FortSiegeSpawn.java b/src/main/java/com/l2jserver/gameserver/model/FortSiegeSpawn.java new file mode 100644 index 00000000..20855747 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/FortSiegeSpawn.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; + +/** + * Fort Siege Spawn. + * @author xban1x + */ +public final class FortSiegeSpawn extends Location implements IIdentifiable { + private final int _npcId; + private final int _fortId; + private final int _id; + + public FortSiegeSpawn(int fort_id, int x, int y, int z, int heading, int npc_id, int id) { + super(x, y, z, heading); + _fortId = fort_id; + _npcId = npc_id; + _id = id; + } + + public int getFortId() { + return _fortId; + } + + /** + * Gets the NPC ID. + * @return the NPC ID + */ + @Override + public int getId() { + return _npcId; + } + + public int getMessageId() { + return _id; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Hit.java b/src/main/java/com/l2jserver/gameserver/model/Hit.java new file mode 100644 index 00000000..56cfc732 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Hit.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author UnAfraid + */ +public class Hit { + private static final int HITFLAG_USESS = 0x10; + private static final int HITFLAG_CRIT = 0x20; + private static final int HITFLAG_SHLD = 0x40; + private static final int HITFLAG_MISS = 0x80; + + private final int _targetId; + private final int _damage; + private int _flags = 0; + + public Hit(L2Object target, int damage, boolean miss, boolean crit, byte shld, boolean soulshot, int ssGrade) { + _targetId = target.getObjectId(); + _damage = damage; + + if (soulshot) { + _flags |= HITFLAG_USESS | ssGrade; + } + + if (crit) { + _flags |= HITFLAG_CRIT; + } + + if (shld > 0) { + _flags |= HITFLAG_SHLD; + } + + if (miss) { + _flags |= HITFLAG_MISS; + } + } + + public int getTargetId() { + return _targetId; + } + + public int getDamage() { + return _damage; + } + + public int getFlags() { + return _flags; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ItemInfo.java b/src/main/java/com/l2jserver/gameserver/model/ItemInfo.java new file mode 100644 index 00000000..5c2e7170 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ItemInfo.java @@ -0,0 +1,247 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * Get all information from L2ItemInstance to generate ItemInfo. + */ +public class ItemInfo { + /** Identifier of the L2ItemInstance */ + private int _objectId; + + /** The L2Item template of the L2ItemInstance */ + private L2Item _item; + + /** The level of enchant on the L2ItemInstance */ + private int _enchant; + + /** The augmentation of the item */ + private int _augmentation; + + /** The quantity of L2ItemInstance */ + private long _count; + + /** The price of the L2ItemInstance */ + private int _price; + + /** The custom L2ItemInstance types (used loto, race tickets) */ + private int _type1; + private int _type2; + + /** If True the L2ItemInstance is equipped */ + private int _equipped; + + /** The action to do clientside (1=ADD, 2=MODIFY, 3=REMOVE) */ + private int _change; + + /** The mana of this item */ + private int _mana; + private int _time; + + private int _location; + + private int _elemAtkType = -2; + private int _elemAtkPower = 0; + private final int[] _elemDefAttr = { + 0, + 0, + 0, + 0, + 0, + 0 + }; + + private int[] _option; + + /** + * Get all information from L2ItemInstance to generate ItemInfo. + * @param item + */ + public ItemInfo(L2ItemInstance item) { + if (item == null) { + return; + } + + // Get the Identifier of the L2ItemInstance + _objectId = item.getObjectId(); + + // Get the L2Item of the L2ItemInstance + _item = item.getItem(); + + // Get the enchant level of the L2ItemInstance + _enchant = item.getEnchantLevel(); + + // Get the augmentation boni + if (item.isAugmented()) { + _augmentation = item.getAugmentation().getAugmentationId(); + } else { + _augmentation = 0; + } + + // Get the quantity of the L2ItemInstance + _count = item.getCount(); + + // Get custom item types (used loto, race tickets) + _type1 = item.getCustomType1(); + _type2 = item.getCustomType2(); + + // Verify if the L2ItemInstance is equipped + _equipped = item.isEquipped() ? 1 : 0; + + // Get the action to do clientside + switch (item.getLastChange()) { + case (L2ItemInstance.ADDED) -> _change = 1; + case (L2ItemInstance.MODIFIED) -> _change = 2; + case (L2ItemInstance.REMOVED) -> _change = 3; + } + + // Get shadow item mana + _mana = item.getMana(); + _time = item.isTimeLimitedItem() ? (int) (item.getRemainingTime() / 1000) : -9999; + _location = item.getLocationSlot(); + + _elemAtkType = item.getAttackElementType(); + _elemAtkPower = item.getAttackElementPower(); + for (byte i = 0; i < 6; i++) { + _elemDefAttr[i] = item.getElementDefAttr(i); + } + _option = item.getEnchantOptions(); + } + + public ItemInfo(L2ItemInstance item, int change) { + this(item); + _change = change; + } + + public ItemInfo(TradeItem item) { + if (item == null) { + return; + } + + // Get the Identifier of the L2ItemInstance + _objectId = item.getObjectId(); + + // Get the L2Item of the L2ItemInstance + _item = item.getItem(); + + // Get the enchant level of the L2ItemInstance + _enchant = item.getEnchant(); + + // Get the augmentation boni + _augmentation = 0; + + // Get the quantity of the L2ItemInstance + _count = item.getCount(); + + // Get custom item types (used loto, race tickets) + _type1 = item.getCustomType1(); + _type2 = item.getCustomType2(); + + // Verify if the L2ItemInstance is equipped + _equipped = 0; + + // Get the action to do clientside + _change = 0; + + // Get shadow item mana + _mana = -1; + _time = -9999; + + _location = item.getLocationSlot(); + + _elemAtkType = item.getAttackElementType(); + _elemAtkPower = item.getAttackElementPower(); + for (byte i = 0; i < 6; i++) { + _elemDefAttr[i] = item.getElementDefAttr(i); + } + + _option = item.getEnchantOptions(); + } + + public int getObjectId() { + return _objectId; + } + + public L2Item getItem() { + return _item; + } + + public int getEnchant() { + return _enchant; + } + + public int getAugmentationBonus() { + return _augmentation; + } + + public long getCount() { + return _count; + } + + public int getPrice() { + return _price; + } + + public int getCustomType1() { + return _type1; + } + + public int getCustomType2() { + return _type2; + } + + public int getEquipped() { + return _equipped; + } + + public int getChange() { + return _change; + } + + public int getMana() { + return _mana; + } + + public int getTime() { + return _time; + } + + public int getLocation() { + return _location; + } + + public int getAttackElementType() { + return _elemAtkType; + } + + public int getAttackElementPower() { + return _elemAtkPower; + } + + public int getElementDefAttr(byte i) { + return _elemDefAttr[i]; + } + + public int[] getEnchantOptions() { + return _option; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/ItemRequest.java b/src/main/java/com/l2jserver/gameserver/model/ItemRequest.java new file mode 100644 index 00000000..b50ba9e0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ItemRequest.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +public class ItemRequest { + int _objectId; + int _itemId; + long _count; + long _price; + + public ItemRequest(int objectId, long count, long price) { + _objectId = objectId; + _count = count; + _price = price; + } + + public ItemRequest(int objectId, int itemId, long count, long price) { + _objectId = objectId; + _itemId = itemId; + _count = count; + _price = price; + } + + public int getObjectId() { + return _objectId; + } + + public int getItemId() { + return _itemId; + } + + public void setCount(long count) { + _count = count; + } + + public long getCount() { + return _count; + } + + public long getPrice() { + return _price; + } + + @Override + public int hashCode() { + return _objectId; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof ItemRequest)) { + return false; + } + return (_objectId != ((ItemRequest) obj)._objectId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2AccessLevel.java b/src/main/java/com/l2jserver/gameserver/model/L2AccessLevel.java new file mode 100644 index 00000000..bddfed19 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2AccessLevel.java @@ -0,0 +1,199 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.data.xml.impl.AdminData; + +/** + * @author HorridoJoho + */ +public class L2AccessLevel { + /** The access level. */ + private final int _accessLevel; + /** The access level name. */ + private final String _name; + /** Child access levels. */ + private L2AccessLevel _childrenAccessLevel = null; + /** Child access levels. */ + private final int _child; + /** The name color for the access level. */ + private final int _nameColor; + /** The title color for the access level. */ + private final int _titleColor; + /** Flag to determine if the access level has GM access. */ + private final boolean _isGm; + /** Flag for peace zone attack */ + private final boolean _allowPeaceAttack; + /** Flag for fixed res */ + private final boolean _allowFixedRes; + /** Flag for transactions */ + private final boolean _allowTransaction; + /** Flag for AltG commands */ + private final boolean _allowAltG; + /** Flag to give damage */ + private final boolean _giveDamage; + /** Flag to take aggro */ + private final boolean _takeAggro; + /** Flag to gain exp in party */ + private final boolean _gainExp; + + public L2AccessLevel(StatsSet set) { + _accessLevel = set.getInt("level"); + _name = set.getString("name"); + _nameColor = Integer.decode("0x" + set.getString("nameColor", "FFFFFF")); + _titleColor = Integer.decode("0x" + set.getString("titleColor", "FFFFFF")); + _child = set.getInt("childAccess", 0); + _isGm = set.getBoolean("isGM", false); + _allowPeaceAttack = set.getBoolean("allowPeaceAttack", false); + _allowFixedRes = set.getBoolean("allowFixedRes", false); + _allowTransaction = set.getBoolean("allowTransaction", true); + _allowAltG = set.getBoolean("allowAltg", false); + _giveDamage = set.getBoolean("giveDamage", true); + _takeAggro = set.getBoolean("takeAggro", true); + _gainExp = set.getBoolean("gainExp", true); + } + + public L2AccessLevel() { + _accessLevel = 0; + _name = "User"; + _nameColor = Integer.decode("0xFFFFFF"); + _titleColor = Integer.decode("0xFFFFFF"); + _child = 0; + _isGm = false; + _allowPeaceAttack = false; + _allowFixedRes = false; + _allowTransaction = true; + _allowAltG = false; + _giveDamage = true; + _takeAggro = true; + _gainExp = true; + } + + /** + * Gets the access level. + * @return the access level + */ + public int getLevel() { + return _accessLevel; + } + + /** + * Gets the access level name. + * @return the access level name + */ + public String getName() { + return _name; + } + + /** + * Gets the name color of the access level. + * @return the name color for the access level + */ + public int getNameColor() { + return _nameColor; + } + + /** + * Gets the title color color of the access level. + * @return the title color for the access level + */ + public int getTitleColor() { + return _titleColor; + } + + /** + * Verifies if the access level has GM access or not. + * @return {@code true} if access level have GM access, otherwise {@code false} + */ + public boolean isGm() { + return _isGm; + } + + /** + * Verifies if the access level is allowed to attack in peace zone or not. + * @return {@code true} if the access level is allowed to attack in peace zone, otherwise {@code false} + */ + public boolean allowPeaceAttack() { + return _allowPeaceAttack; + } + + /** + * Verifies if the access level is allowed to use fixed resurrection or not. + * @return {@ode true} if the access level is allowed to use fixed resurrection, otherwise {@code false} + */ + public boolean allowFixedRes() { + return _allowFixedRes; + } + + /** + * Verifies if the access level is allowed to perform transactions or not. + * @return {@ode true} if access level is allowed to perform transactions, otherwise {@code false} + */ + public boolean allowTransaction() { + return _allowTransaction; + } + + /** + * Verifies if the access level is allowed to use AltG commands or not. + * @return {@ode true} if access level is allowed to use AltG commands, otherwise {@code false} + */ + public boolean allowAltG() { + return _allowAltG; + } + + /** + * Verifies if the access level can give damage or not. + * @return {@ode true} if the access level can give damage, otherwise {@code false} + */ + public boolean canGiveDamage() { + return _giveDamage; + } + + /** + * Verifies if the access level can take aggro or not. + * @return {@ode true} if the access level can take aggro, otherwise {@code false} + */ + public boolean canTakeAggro() { + return _takeAggro; + } + + /** + * Verifies if the access level can gain exp or not. + * @return {@ode true} if the access level can gain exp, otherwise {@code false} + */ + public boolean canGainExp() { + return _gainExp; + } + + /** + * Returns if the access level contains allowedAccess as child. + * @param accessLevel the parent access level + * @return {@ode true} if a child access level is equals to allowedAccess, otherwise {@code false} + */ + public boolean hasChildAccess(L2AccessLevel accessLevel) { + if (_childrenAccessLevel == null) { + if (_child <= 0) { + return false; + } + + _childrenAccessLevel = AdminData.getInstance().getAccessLevel(_child); + } + return ((_childrenAccessLevel.getLevel() == accessLevel.getLevel()) || _childrenAccessLevel.hasChildAccess(accessLevel)); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2AdminCommandAccessRight.java b/src/main/java/com/l2jserver/gameserver/model/L2AdminCommandAccessRight.java new file mode 100644 index 00000000..1fa84c46 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2AdminCommandAccessRight.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.data.xml.impl.AdminData; + +/** + * @author HorridoJoho + */ +public class L2AdminCommandAccessRight { + private final String _adminCommand; + private final int _accessLevel; + private final boolean _requireConfirm; + + public L2AdminCommandAccessRight(StatsSet set) { + _adminCommand = set.getString("command"); + _requireConfirm = set.getBoolean("confirmDlg", false); + _accessLevel = set.getInt("accessLevel", 7); + } + + public L2AdminCommandAccessRight(String command, boolean confirm, int level) { + _adminCommand = command; + _requireConfirm = confirm; + _accessLevel = level; + } + + /** + * @return the admin command the access right belongs to + */ + public String getAdminCommand() { + return _adminCommand; + } + + /** + * @param characterAccessLevel + * @return {@code true} if characterAccessLevel is allowed to use the admin command which belongs to this access right, {@code false} otherwise + */ + public boolean hasAccess(L2AccessLevel characterAccessLevel) { + L2AccessLevel accessLevel = AdminData.getInstance().getAccessLevel(_accessLevel); + return ((accessLevel.getLevel() == characterAccessLevel.getLevel()) || characterAccessLevel.hasChildAccess(accessLevel)); + } + + /** + * @return {@code true} if admin command requires confirmation before execution, {@code false} otherwise. + */ + public boolean getRequireConfirm() { + return _requireConfirm; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ArmorSet.java b/src/main/java/com/l2jserver/gameserver/model/L2ArmorSet.java new file mode 100644 index 00000000..1e6260aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ArmorSet.java @@ -0,0 +1,267 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * @author Luno + */ +public final class L2ArmorSet { + private int _chestId; + private final List _legs; + private final List _head; + private final List _gloves; + private final List _feet; + private final List _shield; + + private final List _skills; + private final List _shieldSkills; + private final List _enchant6Skill; + + private int _con; + private int _dex; + private int _str; + private int _men; + private int _wit; + private int _int; + + public L2ArmorSet() { + _legs = new ArrayList<>(); + _head = new ArrayList<>(); + _gloves = new ArrayList<>(); + _feet = new ArrayList<>(); + _shield = new ArrayList<>(); + + _skills = new ArrayList<>(); + _shieldSkills = new ArrayList<>(); + _enchant6Skill = new ArrayList<>(); + } + + public void addChest(int id) { + _chestId = id; + } + + public void addLegs(int id) { + _legs.add(id); + } + + public void addHead(int id) { + _head.add(id); + } + + public void addGloves(int id) { + _gloves.add(id); + } + + public void addFeet(int id) { + _feet.add(id); + } + + public void addShield(int id) { + _shield.add(id); + } + + public void addSkill(SkillHolder holder) { + _skills.add(holder); + } + + public void addShieldSkill(SkillHolder holder) { + _shieldSkills.add(holder); + } + + public void addEnchant6Skill(SkillHolder holder) { + _enchant6Skill.add(holder); + } + + public void addCon(int val) { + _con = val; + } + + public void addDex(int val) { + _dex = val; + } + + public void addStr(int val) { + _str = val; + } + + public void addMen(int val) { + _men = val; + } + + public void addWit(int val) { + _wit = val; + } + + public void addInt(int val) { + _int = val; + } + + /** + * Checks if player have equipped all items from set (not checking shield) + * @param player whose inventory is being checked + * @return True if player equips whole set + */ + public boolean containAll(L2PcInstance player) { + Inventory inv = player.getInventory(); + + L2ItemInstance legsItem = inv.getPaperdollItem(Inventory.PAPERDOLL_LEGS); + L2ItemInstance headItem = inv.getPaperdollItem(Inventory.PAPERDOLL_HEAD); + L2ItemInstance glovesItem = inv.getPaperdollItem(Inventory.PAPERDOLL_GLOVES); + L2ItemInstance feetItem = inv.getPaperdollItem(Inventory.PAPERDOLL_FEET); + + int legs = 0; + int head = 0; + int gloves = 0; + int feet = 0; + + if (legsItem != null) { + legs = legsItem.getId(); + } + if (headItem != null) { + head = headItem.getId(); + } + if (glovesItem != null) { + gloves = glovesItem.getId(); + } + if (feetItem != null) { + feet = feetItem.getId(); + } + + return containAll(_chestId, legs, head, gloves, feet); + } + + public boolean containAll(int chest, int legs, int head, int gloves, int feet) { + if ((_chestId != 0) && (_chestId != chest)) { + return false; + } + if (!_legs.isEmpty() && !_legs.contains(legs)) { + return false; + } + if (!_head.isEmpty() && !_head.contains(head)) { + return false; + } + if (!_gloves.isEmpty() && !_gloves.contains(gloves)) { + return false; + } + return _feet.isEmpty() || _feet.contains(feet); + } + + public boolean containItem(int slot, int itemId) { + return switch (slot) { + case Inventory.PAPERDOLL_CHEST -> _chestId == itemId; + case Inventory.PAPERDOLL_LEGS -> _legs.contains(itemId); + case Inventory.PAPERDOLL_HEAD -> _head.contains(itemId); + case Inventory.PAPERDOLL_GLOVES -> _gloves.contains(itemId); + case Inventory.PAPERDOLL_FEET -> _feet.contains(itemId); + default -> false; + }; + } + + public int getChestId() { + return _chestId; + } + + public List getSkills() { + return _skills; + } + + public boolean containShield(L2PcInstance player) { + Inventory inv = player.getInventory(); + + L2ItemInstance shieldItem = inv.getPaperdollItem(Inventory.PAPERDOLL_LHAND); + return ((shieldItem != null) && _shield.contains(shieldItem.getId())); + } + + public boolean containShield(int shieldId) { + return _shield.contains(shieldId); + } + + public List getShieldSkillId() { + return _shieldSkills; + } + + public List getEnchant6skillId() { + return _enchant6Skill; + } + + /** + * @param player + * @return true if all parts of set are enchanted to +6 or more + */ + public boolean isEnchanted6(L2PcInstance player) { + // Player don't have full set + if (!containAll(player)) { + return false; + } + + Inventory inv = player.getInventory(); + + L2ItemInstance chestItem = inv.getPaperdollItem(Inventory.PAPERDOLL_CHEST); + L2ItemInstance legsItem = inv.getPaperdollItem(Inventory.PAPERDOLL_LEGS); + L2ItemInstance headItem = inv.getPaperdollItem(Inventory.PAPERDOLL_HEAD); + L2ItemInstance glovesItem = inv.getPaperdollItem(Inventory.PAPERDOLL_GLOVES); + L2ItemInstance feetItem = inv.getPaperdollItem(Inventory.PAPERDOLL_FEET); + + if ((chestItem == null) || (chestItem.getEnchantLevel() < 6)) { + return false; + } + if (!_legs.isEmpty() && ((legsItem == null) || (legsItem.getEnchantLevel() < 6))) { + return false; + } + if (!_gloves.isEmpty() && ((glovesItem == null) || (glovesItem.getEnchantLevel() < 6))) { + return false; + } + if (!_head.isEmpty() && ((headItem == null) || (headItem.getEnchantLevel() < 6))) { + return false; + } + return _feet.isEmpty() || ((feetItem != null) && (feetItem.getEnchantLevel() >= 6)); + } + + public int getCON() { + return _con; + } + + public int getDEX() { + return _dex; + } + + public int getSTR() { + return _str; + } + + public int getMEN() { + return _men; + } + + public int getWIT() { + return _wit; + } + + public int getINT() { + return _int; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Augmentation.java b/src/main/java/com/l2jserver/gameserver/model/L2Augmentation.java new file mode 100644 index 00000000..ffd9683d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Augmentation.java @@ -0,0 +1,119 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.OptionData; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.options.Options; + +/** + * Used to store an augmentation and its bonuses. + * @author durgus + * @author UnAfraid + */ +public final class L2Augmentation { + private final int _effectsId; + private final AugmentationStatBoni _boni; + + public L2Augmentation(int effects) { + _effectsId = effects; + _boni = new AugmentationStatBoni(_effectsId); + } + + public static class AugmentationStatBoni { + private static final Logger LOG = LoggerFactory.getLogger(AugmentationStatBoni.class); + + private final List _options = new ArrayList<>(); + private boolean _active; + + public AugmentationStatBoni(int augmentationId) { + _active = false; + int[] stats = new int[2]; + stats[0] = 0x0000FFFF & augmentationId; + stats[1] = (augmentationId >> 16); + + for (int stat : stats) { + Options op = OptionData.getInstance().getOptions(stat); + if (op != null) { + _options.add(op); + } else { + LOG.warn("Couldn't find option: {}", stat); + } + } + } + + public void applyBonus(L2PcInstance player) { + // make sure the bonuses are not applied twice.. + if (_active) { + return; + } + + for (Options op : _options) { + op.apply(player); + } + + _active = true; + } + + public void removeBonus(L2PcInstance player) { + // make sure the bonuses are not removed twice + if (!_active) { + return; + } + + for (Options op : _options) { + op.remove(player); + } + + _active = false; + } + } + + public int getAttributes() { + return _effectsId; + } + + /** + * Get the augmentation "id" used in server packets. + * @return augmentationId + */ + public int getAugmentationId() { + return _effectsId; + } + + /** + * Applies the bonuses to the player. + */ + public void applyBonus(L2PcInstance player) { + _boni.applyBonus(player); + } + + /** + * Removes the augmentation bonuses from the player. + */ + public void removeBonus(L2PcInstance player) { + _boni.removeBonus(player); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Clan.java b/src/main/java/com/l2jserver/gameserver/model/L2Clan.java new file mode 100644 index 00000000..dbbc78f9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Clan.java @@ -0,0 +1,2353 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.clan; +import static com.l2jserver.gameserver.config.Configuration.fortress; +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.bbs.service.ForumsBBSManager; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.sql.impl.CrestTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager.Territory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanJoin; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanLeaderChange; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanLeave; +import com.l2jserver.gameserver.model.events.impl.character.player.clan.PlayerClanLevelUp; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.interfaces.INamable; +import com.l2jserver.gameserver.model.itemcontainer.ClanWarehouse; +import com.l2jserver.gameserver.model.itemcontainer.ItemContainer; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.ExSubPledgeSkillAdd; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.PledgeReceiveSubPledgeCreated; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowInfoUpdate; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListAll; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListDeleteAll; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListUpdate; +import com.l2jserver.gameserver.network.serverpackets.PledgeSkillList; +import com.l2jserver.gameserver.network.serverpackets.PledgeSkillList.SubPledgeSkill; +import com.l2jserver.gameserver.network.serverpackets.PledgeSkillListAdd; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.EnumIntBitmask; +import com.l2jserver.gameserver.util.Util; + +/** + * Clan. + * @author JIV + * @author janiii + * @author _DS_ + * @author Gigiikun + * @author MELERIX + * @author Zoey76 + * @author UnAfraid + * @author VlLight + * @author nBd + * @author Adry_85 + * @author xban1x + * @author Nos + * @author Zealar + * @author HorridoJoho + * @author Kita + */ +public class L2Clan implements IIdentifiable, INamable { + private static final Logger LOG = LoggerFactory.getLogger(L2Clan.class); + + // SQL queries + private static final String INSERT_CLAN_DATA = "INSERT INTO clan_data (clan_id,clan_name,clan_level,hasCastle,blood_alliance_count,blood_oath_count,ally_id,ally_name,leader_id,crest_id,crest_large_id,ally_crest_id,new_leader_id) values (?,?,?,?,?,?,?,?,?,?,?,?,?)"; + private static final String SELECT_CLAN_DATA = "SELECT * FROM clan_data where clan_id=?"; + + // Ally Penalty Types + /** Clan left ally */ + public static final int PENALTY_TYPE_CLAN_LEFT = 1; + /** Clan was dismissed from ally */ + public static final int PENALTY_TYPE_CLAN_DISMISSED = 2; + /** Leader clan dismiss clan from ally */ + public static final int PENALTY_TYPE_DISMISS_CLAN = 3; + /** Leader clan dissolve ally */ + public static final int PENALTY_TYPE_DISSOLVE_ALLY = 4; + // Sub-unit types + /** Clan subunit type of Academy */ + public static final int SUBUNIT_ACADEMY = -1; + /** Clan subunit type of Royal Guard A */ + public static final int SUBUNIT_ROYAL1 = 100; + /** Clan subunit type of Royal Guard B */ + public static final int SUBUNIT_ROYAL2 = 200; + /** Clan subunit type of Order of Knights A-1 */ + public static final int SUBUNIT_KNIGHT1 = 1001; + /** Clan subunit type of Order of Knights A-2 */ + public static final int SUBUNIT_KNIGHT2 = 1002; + /** Clan subunit type of Order of Knights B-1 */ + public static final int SUBUNIT_KNIGHT3 = 2001; + /** Clan subunit type of Order of Knights B-2 */ + public static final int SUBUNIT_KNIGHT4 = 2002; + + private String _name; + private int _clanId; + private L2ClanMember _leader; + private final Map _members = new ConcurrentHashMap<>(); + + private String _allyName; + private int _allyId; + private int _level; + private int _castleId; + private int _fortId; + private int _hideoutId; + private int _hiredGuards; + private int _crestId; + private int _crestLargeId; + private int _allyCrestId; + private int _auctionBidAt = 0; + private long _allyPenaltyExpiryTime; + private int _allyPenaltyType; + private long _charPenaltyExpiryTime; + private long _dissolvingExpiryTime; + private int _bloodAllianceCount; + private int _bloodOathCount; + + private final ItemContainer _warehouse = new ClanWarehouse(this); + private final Set _atWarWith = ConcurrentHashMap.newKeySet(); + private final Set _atWarAttackers = ConcurrentHashMap.newKeySet(); + + /** Map(Integer, L2Skill) containing all skills of the L2Clan */ + private final Map _skills = new ConcurrentHashMap<>(); + private final Map _privs = new ConcurrentHashMap<>(); + private final Map _subPledges = new ConcurrentHashMap<>(); + private final Map _subPledgeSkills = new ConcurrentHashMap<>(); + + private int _reputationScore = 0; + private int _rank = 0; + + private String _notice; + private boolean _noticeEnabled = false; + private static final int MAX_NOTICE_LENGTH = 8192; + private int _newLeaderId; + + private final AtomicInteger _siegeKills = new AtomicInteger(); + private final AtomicInteger _siegeDeaths = new AtomicInteger(); + + /** + * Called if a clan is referenced only by id. In this case all other data needs to be fetched from db + * @param clanId A valid clan Id to create and restore + */ + public L2Clan(int clanId) { + _clanId = clanId; + initializePrivs(); + restore(); + getWarehouse().restore(); + } + + /** + * Called only if a new clan is created + * @param clanId A valid clan Id to create + * @param clanName A valid clan name + */ + public L2Clan(int clanId, String clanName) { + _clanId = clanId; + _name = clanName; + initializePrivs(); + } + + @Override + public int getId() { + return _clanId; + } + + public void setClanId(int clanId) { + _clanId = clanId; + } + + public int getLeaderId() { + return (_leader != null ? _leader.getObjectId() : 0); + } + + public L2ClanMember getLeader() { + return _leader; + } + + public void setLeader(L2ClanMember leader) { + _leader = leader; + _members.put(leader.getObjectId(), leader); + } + + public void setNewLeader(L2ClanMember member) { + final L2PcInstance newLeader = member.getPlayerInstance(); + final L2ClanMember exMember = getLeader(); + final L2PcInstance exLeader = exMember.getPlayerInstance(); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanLeaderChange(exMember, member, this)); + + if (exLeader != null) { + if (exLeader.isFlying()) { + exLeader.dismount(); + } + + if (getLevel() >= SiegeManager.getInstance().getSiegeClanMinLevel()) { + SiegeManager.getInstance().removeSiegeSkills(exLeader); + } + exLeader.getClanPrivileges().clear(); + exLeader.broadcastUserInfo(); + + } else { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE characters SET clan_privs = ? WHERE charId = ?")) { + ps.setInt(1, 0); + ps.setInt(2, getLeaderId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Couldn't update clan privs for old clan leader", e); + } + } + + setLeader(member); + if (getNewLeaderId() != 0) { + setNewLeaderId(0, true); + } + updateClanInDB(); + + if (exLeader != null) { + exLeader.setPledgeClass(L2ClanMember.calculatePledgeClass(exLeader)); + exLeader.broadcastUserInfo(); + exLeader.checkItemRestriction(); + } + + if (newLeader != null) { + newLeader.setPledgeClass(L2ClanMember.calculatePledgeClass(newLeader)); + newLeader.getClanPrivileges().setAll(); + + if (getLevel() >= SiegeManager.getInstance().getSiegeClanMinLevel()) { + SiegeManager.getInstance().addSiegeSkills(newLeader); + } + newLeader.broadcastUserInfo(); + } else { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE characters SET clan_privs = ? WHERE charId = ?")) { + ps.setInt(1, EnumIntBitmask.getAllBitmask(ClanPrivilege.class)); + ps.setInt(2, getLeaderId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Couldn't update clan privs for new clan leader", e); + } + } + + broadcastClanStatus(); + broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.CLAN_LEADER_PRIVILEGES_HAVE_BEEN_TRANSFERRED_TO_C1).addString(member.getName())); + + LOG.info("Leader of Clan: {} changed to: {} ex leader: {}", getName(), member.getName(), exMember.getName()); + } + + public String getLeaderName() { + if (_leader == null) { + LOG.warn("Clan {} without clan leader!", getName()); + return ""; + } + return _leader.getName(); + } + + @Override + public String getName() { + return _name; + } + + public void setName(String name) { + _name = name; + } + + /** + * Adds a clan member to the clan. + * @param member the clan member. + */ + private void addClanMember(L2ClanMember member) { + _members.put(member.getObjectId(), member); + } + + /** + * Adds a clan member to the clan.
    + * Using a different constructor, to make it easier to read. + * @param player the clan member + */ + public void addClanMember(L2PcInstance player) { + final L2ClanMember member = new L2ClanMember(this, player); + member.setPlayerInstance(player); + addClanMember(member); + + player.setClan(this); + player.setPledgeClass(L2ClanMember.calculatePledgeClass(player)); + player.sendPacket(new PledgeShowMemberListUpdate(player)); + player.sendPacket(new PledgeSkillList(this)); + + addSkillEffects(player); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanJoin(member, this)); + } + + /** + * Updates player status in clan. + * @param player the player to be updated. + */ + public void updateClanMember(L2PcInstance player) { + final L2ClanMember member = new L2ClanMember(player.getClan(), player); + if (player.isClanLeader()) { + setLeader(member); + } + + addClanMember(member); + } + + /** + * @param name the name of the required clan member. + * @return the clan member for a given name. + */ + public L2ClanMember getClanMember(String name) { + for (L2ClanMember temp : _members.values()) { + if (temp.getName().equals(name)) { + return temp; + } + } + return null; + } + + /** + * @param objectId the required clan member object Id. + * @return the clan member for a given {@code objectId}. + */ + public L2ClanMember getClanMember(int objectId) { + return _members.get(objectId); + } + + /** + * @param objectId the object Id of the member that will be removed. + * @param clanJoinExpiryTime time penalty to join a clan. + */ + public void removeClanMember(int objectId, long clanJoinExpiryTime) { + final L2ClanMember exMember = _members.remove(objectId); + if (exMember == null) { + LOG.warn("Member Object ID: {} not found in clan while trying to remove", objectId); + return; + } + final int subPledgeLeader = getLeaderSubPledge(objectId); + if (subPledgeLeader != 0) { + // Subunit leader withdraws, position becomes vacant and leader should appoint new via NPC + getSubPledge(subPledgeLeader).setLeaderId(0); + updateSubPledgeInDB(subPledgeLeader); + } + + if (exMember.getApprentice() != 0) { + final L2ClanMember apprentice = getClanMember(exMember.getApprentice()); + if (apprentice != null) { + if (apprentice.getPlayerInstance() != null) { + apprentice.getPlayerInstance().setSponsor(0); + } else { + apprentice.setApprenticeAndSponsor(0, 0); + } + + apprentice.saveApprenticeAndSponsor(0, 0); + } + } + if (exMember.getSponsor() != 0) { + final L2ClanMember sponsor = getClanMember(exMember.getSponsor()); + if (sponsor != null) { + if (sponsor.getPlayerInstance() != null) { + sponsor.getPlayerInstance().setApprentice(0); + } else { + sponsor.setApprenticeAndSponsor(0, 0); + } + + sponsor.saveApprenticeAndSponsor(0, 0); + } + } + exMember.saveApprenticeAndSponsor(0, 0); + if (character().removeCastleCirclets()) { + CastleManager.getInstance().removeCirclet(exMember, getCastleId()); + } + + final L2PcInstance player = exMember.getPlayerInstance(); + if (player != null) { + if (!player.isNoble()) { + player.setTitle(""); + } + player.setApprentice(0); + player.setSponsor(0); + + if (player.isClanLeader()) { + SiegeManager.getInstance().removeSiegeSkills(player); + player.setClanCreateExpiryTime(System.currentTimeMillis() + character().getDaysBeforeCreateAClan()); + } + // remove Clan skills from Player + removeSkillEffects(player); + + // remove Residential skills + if (player.getClan().getCastleId() > 0) { + CastleManager.getInstance().getCastleByOwner(player.getClan()).removeResidentialSkills(player); + } + if (player.getClan().getFortId() > 0) { + FortManager.getInstance().getFortByOwner(player.getClan()).removeResidentialSkills(player); + } + player.sendSkillList(); + + player.setClan(null); + + // players leaving from clan academy have no penalty + if (exMember.getPledgeType() != -1) { + player.setClanJoinExpiryTime(clanJoinExpiryTime); + } + + player.setPledgeClass(L2ClanMember.calculatePledgeClass(player)); + player.broadcastUserInfo(); + // disable clan tab + player.sendPacket(PledgeShowMemberListDeleteAll.STATIC_PACKET); + } else { + removeMemberInDatabase(exMember.getObjectId(), clanJoinExpiryTime, getLeaderId() == objectId ? System.currentTimeMillis() + character().getDaysBeforeCreateAClan() : 0); + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanLeave(exMember, this)); + } + + public L2ClanMember[] getMembers() { + return _members.values().toArray(new L2ClanMember[0]); + } + + public int getMembersCount() { + return _members.size(); + } + + public int getSubPledgeMembersCount(int subpl) { + int result = 0; + for (L2ClanMember temp : _members.values()) { + if (temp.getPledgeType() == subpl) { + result++; + } + } + return result; + } + + /** + * @param pledgeType the Id of the pledge type. + * @return the maximum number of members allowed for a given {@code pledgeType}. + */ + public int getMaxNrOfMembers(int pledgeType) { + int limit = 0; + + switch (pledgeType) { + case 0: + limit = switch (getLevel()) { + case 3 -> 30; + case 2 -> 20; + case 1 -> 15; + case 0 -> 10; + default -> 40; + }; + break; + case -1: + limit = 20; + break; + case 100: + case 200: + if (getLevel() == 11) { + limit = 30; + } else { + limit = 20; + } + break; + case 1001: + case 1002: + case 2001: + case 2002: + limit = switch (getLevel()) { + case 9, 10, 11 -> 25; + default -> 10; + }; + break; + default: + break; + } + return limit; + } + + /** + * @param exclude the object Id to exclude from list. + * @return all online members excluding the one with object id {code exclude}. + */ + public List getOnlineMembers(int exclude) { + final List onlineMembers = new ArrayList<>(); + for (L2ClanMember temp : _members.values()) { + if ((temp != null) && temp.isOnline() && (temp.getObjectId() != exclude)) { + onlineMembers.add(temp.getPlayerInstance()); + } + } + return onlineMembers; + } + + /** + * @return the online clan member count. + */ + public int getOnlineMembersCount() { + int count = 0; + for (L2ClanMember temp : _members.values()) { + if ((temp == null) || !temp.isOnline()) { + continue; + } + count++; + } + return count; + } + + /** + * @return the alliance Id. + */ + public int getAllyId() { + return _allyId; + } + + /** + * @return the alliance name. + */ + public String getAllyName() { + return _allyName; + } + + /** + * @param allyCrestId the alliance crest Id to be set. + */ + public void setAllyCrestId(int allyCrestId) { + _allyCrestId = allyCrestId; + } + + /** + * @return the alliance crest Id. + */ + public int getAllyCrestId() { + return _allyCrestId; + } + + /** + * Gets the clan level. + * @return the clan level + */ + public int getLevel() { + return _level; + } + + /** + * Sets the clan level. + * @param level the level to set + */ + public void setLevel(int level) { + _level = level; + } + + /** + * @return the castle Id for this clan if owns a castle, zero otherwise. + */ + public int getCastleId() { + return _castleId; + } + + /** + * @return the fort Id for this clan if owns a fort, zero otherwise. + */ + public int getFortId() { + return _fortId; + } + + /** + * @return the hideout Id for this clan if owns a hideout, zero otherwise. + */ + public int getHideoutId() { + return _hideoutId; + } + + /** + * @param crestId the Id of the clan crest to be set. + */ + public void setCrestId(int crestId) { + _crestId = crestId; + } + + /** + * @return Returns the clanCrestId. + */ + public int getCrestId() { + return _crestId; + } + + /** + * @param crestLargeId The id of pledge LargeCrest. + */ + public void setCrestLargeId(int crestLargeId) { + _crestLargeId = crestLargeId; + } + + /** + * @return Returns the clan CrestLargeId + */ + public int getCrestLargeId() { + return _crestLargeId; + } + + /** + * @param allyId The allyId to set. + */ + public void setAllyId(int allyId) { + _allyId = allyId; + } + + /** + * @param allyName The allyName to set. + */ + public void setAllyName(String allyName) { + _allyName = allyName; + } + + /** + * @param castleId the castle Id to set. + */ + public void setCastleId(int castleId) { + _castleId = castleId; + } + + /** + * @param fortId the fort Id to set. + */ + public void setFortId(int fortId) { + _fortId = fortId; + } + + /** + * @param hideoutId the hideout Id to set. + */ + public void setHideoutId(int hideoutId) { + _hideoutId = hideoutId; + } + + /** + * @param id the Id of the player to be verified. + * @return {code true} if the player belongs to the clan. + */ + public boolean isMember(int id) { + return ((id != 0) && _members.containsKey(id)); + } + + /** + * @return the Blood Alliance count for this clan + */ + public int getBloodAllianceCount() { + return _bloodAllianceCount; + } + + /** + * Increase Blood Alliance count by config predefined count and updates the database. + */ + public void increaseBloodAllianceCount() { + _bloodAllianceCount += SiegeManager.getInstance().getBloodAllianceReward(); + updateBloodAllianceCountInDB(); + } + + /** + * Reset the Blood Alliance count to zero and updates the database. + */ + public void resetBloodAllianceCount() { + _bloodAllianceCount = 0; + updateBloodAllianceCountInDB(); + } + + /** + * Store current Blood Alliances count in database. + */ + public void updateBloodAllianceCountInDB() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET blood_alliance_count=? WHERE clan_id=?")) { + ps.setInt(1, getBloodAllianceCount()); + ps.setInt(2, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception on updateBloodAllianceCountInDB(): {}", e.getMessage(), e); + } + } + + /** + * @return the Blood Oath count for this clan + */ + public int getBloodOathCount() { + return _bloodOathCount; + } + + /** + * Increase Blood Oath count by config predefined count and updates the database. + */ + public void increaseBloodOathCount() { + _bloodOathCount += fortress().getBloodOathCount(); + updateBloodOathCountInDB(); + } + + /** + * Reset the Blood Oath count to zero and updates the database. + */ + public void resetBloodOathCount() { + _bloodOathCount = 0; + updateBloodOathCountInDB(); + } + + /** + * Store current Blood Alliances count in database. + */ + public void updateBloodOathCountInDB() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET blood_oath_count=? WHERE clan_id=?")) { + ps.setInt(1, getBloodOathCount()); + ps.setInt(2, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception on updateBloodAllianceCountInDB(): {}", e.getMessage(), e); + } + } + + /** + * Store in database current clan's reputation. + */ + public void updateClanScoreInDB() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET reputation_score=? WHERE clan_id=?")) { + ps.setInt(1, getReputationScore()); + ps.setInt(2, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception on updateClanScoreInDb(): {}", e.getMessage(), e); + } + } + + /** + * Updates in database clan information: + *
      + *
    • Clan leader Id
    • + *
    • Alliance Id
    • + *
    • Alliance name
    • + *
    • Clan's reputation
    • + *
    • Alliance's penalty expiration time
    • + *
    • Alliance's penalty type
    • + *
    • Character's penalty expiration time
    • + *
    • Dissolving expiration time
    • + *
    • Clan's id
    • + *
    + */ + public void updateClanInDB() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET leader_id=?,ally_id=?,ally_name=?,reputation_score=?,ally_penalty_expiry_time=?,ally_penalty_type=?,char_penalty_expiry_time=?,dissolving_expiry_time=?,new_leader_id=? WHERE clan_id=?")) { + ps.setInt(1, getLeaderId()); + ps.setInt(2, getAllyId()); + ps.setString(3, getAllyName()); + ps.setInt(4, getReputationScore()); + ps.setLong(5, getAllyPenaltyExpiryTime()); + ps.setInt(6, getAllyPenaltyType()); + ps.setLong(7, getCharPenaltyExpiryTime()); + ps.setLong(8, getDissolvingExpiryTime()); + ps.setInt(9, getNewLeaderId()); + ps.setInt(10, getId()); + ps.execute(); + if (general().debug()) { + LOG.debug("New clan leader saved in db: {}", getId()); + } + } catch (Exception e) { + LOG.error("Error saving clan: {}", e.getMessage(), e); + } + } + + /** + * Stores in database clan information: + *
      + *
    • Clan Id
    • + *
    • Clan name
    • + *
    • Clan level
    • + *
    • Has castle
    • + *
    • Alliance Id
    • + *
    • Alliance name
    • + *
    • Clan leader Id
    • + *
    • Clan crest Id
    • + *
    • Clan large crest Id
    • + *
    • Alliance crest Id
    • + *
    + */ + public void store() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_CLAN_DATA)) { + ps.setInt(1, getId()); + ps.setString(2, getName()); + ps.setInt(3, getLevel()); + ps.setInt(4, getCastleId()); + ps.setInt(5, getBloodAllianceCount()); + ps.setInt(6, getBloodOathCount()); + ps.setInt(7, getAllyId()); + ps.setString(8, getAllyName()); + ps.setInt(9, getLeaderId()); + ps.setInt(10, getCrestId()); + ps.setInt(11, getCrestLargeId()); + ps.setInt(12, getAllyCrestId()); + ps.setInt(13, getNewLeaderId()); + ps.execute(); + if (general().debug()) { + LOG.debug("New clan saved in db: {}", getId()); + } + } catch (Exception e) { + LOG.error("Error saving new clan: {}", e.getMessage(), e); + } + } + + /** + * Removes a clan member from this clan. + * @param playerId the clan member object ID to be removed + * @param clanJoinExpiryTime the time penalty for the player to join a new clan + * @param clanCreateExpiryTime the time penalty for the player to create a new clan + */ + private void removeMemberInDatabase(int playerId, long clanJoinExpiryTime, long clanCreateExpiryTime) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("UPDATE characters SET clanid=0, title=?, clan_join_expiry_time=?, clan_create_expiry_time=?, clan_privs=0, wantspeace=0, subpledge=0, lvl_joined_academy=0, apprentice=0, sponsor=0 WHERE charId=?"); + var ps2 = con.prepareStatement("UPDATE characters SET apprentice=0 WHERE apprentice=?"); + var ps3 = con.prepareStatement("UPDATE characters SET sponsor=0 WHERE sponsor=?")) { + // Remove clan member. + ps1.setString(1, ""); + ps1.setLong(2, clanJoinExpiryTime); + ps1.setLong(3, clanCreateExpiryTime); + ps1.setInt(4, playerId); + ps1.execute(); + // Remove apprentice. + ps2.setInt(1, playerId); + ps2.execute(); + // Remove sponsor. + ps3.setInt(1, playerId); + ps3.execute(); + } catch (Exception e) { + LOG.error("Error removing clan member: {}", e.getMessage(), e); + } + } + + private void restore() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(SELECT_CLAN_DATA)) { + ps.setInt(1, getId()); + try (var clanData = ps.executeQuery()) { + if (clanData.next()) { + setName(clanData.getString("clan_name")); + setLevel(clanData.getInt("clan_level")); + setCastleId(clanData.getInt("hasCastle")); + _bloodAllianceCount = clanData.getInt("blood_alliance_count"); + _bloodOathCount = clanData.getInt("blood_oath_count"); + setAllyId(clanData.getInt("ally_id")); + setAllyName(clanData.getString("ally_name")); + setAllyPenaltyExpiryTime(clanData.getLong("ally_penalty_expiry_time"), clanData.getInt("ally_penalty_type")); + if (getAllyPenaltyExpiryTime() < System.currentTimeMillis()) { + setAllyPenaltyExpiryTime(0, 0); + } + setCharPenaltyExpiryTime(clanData.getLong("char_penalty_expiry_time")); + + if ((getCharPenaltyExpiryTime() + character().getDaysBeforeJoinAClan()) < System.currentTimeMillis()) { + setCharPenaltyExpiryTime(0); + } + setDissolvingExpiryTime(clanData.getLong("dissolving_expiry_time")); + + setCrestId(clanData.getInt("crest_id")); + setCrestLargeId(clanData.getInt("crest_large_id")); + setAllyCrestId(clanData.getInt("ally_crest_id")); + + setReputationScore(clanData.getInt("reputation_score"), false); + setAuctionBidAt(clanData.getInt("auction_bid_at"), false); + setNewLeaderId(clanData.getInt("new_leader_id"), false); + + final int leaderId = (clanData.getInt("leader_id")); + + ps.clearParameters(); + + try (var select = con.prepareStatement("SELECT char_name,level,classid,charId,title,power_grade,subpledge,apprentice,sponsor,sex,race FROM characters WHERE clanid=?")) { + select.setInt(1, getId()); + try (var clanMember = select.executeQuery()) { + while (clanMember.next()) { + final var member = new L2ClanMember(this, clanMember); + if (member.getObjectId() == leaderId) { + setLeader(member); + } else { + addClanMember(member); + } + } + } + } + } + } + + if (general().debug() && (getName() != null)) { + LOG.info("Restored clan data for \"" + getName() + "\" from database."); + } + + restoreSubPledges(); + restoreRankPrivs(); + restoreSkills(); + restoreNotice(); + } catch (Exception ex) { + LOG.error("Error restoring clan data for clan {}!", getId(), ex); + } + } + + private void restoreNotice() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT enabled,notice FROM clan_notices WHERE clan_id=?")) { + ps.setInt(1, getId()); + try (var noticeData = ps.executeQuery()) { + while (noticeData.next()) { + _noticeEnabled = noticeData.getBoolean("enabled"); + _notice = noticeData.getString("notice"); + } + } + } catch (Exception e) { + LOG.error("Error restoring clan notice: {}", e.getMessage(), e); + } + } + + private void storeNotice(String notice, boolean enabled) { + if (notice == null) { + notice = ""; + } + + if (notice.length() > MAX_NOTICE_LENGTH) { + notice = notice.substring(0, MAX_NOTICE_LENGTH - 1); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO clan_notices (clan_id,notice,enabled) values (?,?,?) ON DUPLICATE KEY UPDATE notice=?,enabled=?")) { + ps.setInt(1, getId()); + ps.setString(2, notice); + ps.setBoolean(3, enabled); + ps.setString(4, notice); + ps.setBoolean(5, enabled); + ps.execute(); + } catch (Exception e) { + LOG.warn("Error could not store clan notice: {}", e.getMessage(), e); + } + + _notice = notice; + _noticeEnabled = enabled; + } + + public void setNoticeEnabled(boolean enabled) { + storeNotice(_notice, enabled); + } + + public void setNotice(String notice) { + storeNotice(notice, _noticeEnabled); + } + + public boolean isNoticeEnabled() { + return _noticeEnabled; + } + + public String getNotice() { + if (_notice == null) { + return ""; + } + return _notice; + } + + private void restoreSkills() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT skill_id,skill_level,sub_pledge_id FROM clan_skills WHERE clan_id=?")) { + // Retrieve all skills of this L2PcInstance from the database + ps.setInt(1, getId()); + try (var rs = ps.executeQuery()) { + // Go though the recordset of this SQL query + while (rs.next()) { + int id = rs.getInt("skill_id"); + int level = rs.getInt("skill_level"); + // Create a L2Skill object for each record + Skill skill = SkillData.getInstance().getSkill(id, level); + // Add the L2Skill object to the L2Clan _skills + int subType = rs.getInt("sub_pledge_id"); + + if (subType == -2) { + _skills.put(skill.getId(), skill); + } else if (subType == 0) { + _subPledgeSkills.put(skill.getId(), skill); + } else { + SubPledge subunit = _subPledges.get(subType); + if (subunit != null) { + subunit.addNewSkill(skill); + } else { + LOG.info("Missing subpledge " + subType + " for clan " + this + ", skill skipped."); + } + } + } + } + } catch (Exception e) { + LOG.error("Error restoring clan skills: {}", e.getMessage(), e); + } + } + + public final Skill[] getAllSkills() { + return _skills.values().toArray(new Skill[0]); + } + + public Map getSkills() { + return _skills; + } + + /** + * Used to add a skill to skill list of this L2Clan + * @return the previous version of the skill if it was present, {@code null} otherwise + */ + public Skill addSkill(Skill newSkill) { + Skill oldSkill = null; + + if (newSkill != null) { + // Replace oldSkill by newSkill or Add the newSkill + oldSkill = _skills.put(newSkill.getId(), newSkill); + } + + return oldSkill; + } + + public Skill addNewSkill(Skill newSkill) { + return addNewSkill(newSkill, -2); + } + + /** + * Used to add a new skill to the list, send a packet to all online clan members, update their stats and store it in db + * @param newSkill + * @param subType + * @return + */ + public Skill addNewSkill(Skill newSkill, int subType) { + Skill oldSkill = null; + if (newSkill != null) { + + if (subType == -2) { + oldSkill = _skills.put(newSkill.getId(), newSkill); + } else if (subType == 0) { + oldSkill = _subPledgeSkills.put(newSkill.getId(), newSkill); + } else { + SubPledge subunit = getSubPledge(subType); + if (subunit != null) { + oldSkill = subunit.addNewSkill(newSkill); + } else { + LOG.warn("Subpledge {} does not exist for clan {}", subType, this); + return null; + } + } + + try (var con = ConnectionFactory.getInstance().getConnection()) { + if (oldSkill != null) { + try (var ps = con.prepareStatement("UPDATE clan_skills SET skill_level=? WHERE skill_id=? AND clan_id=?")) { + ps.setInt(1, newSkill.getLevel()); + ps.setInt(2, oldSkill.getId()); + ps.setInt(3, getId()); + ps.execute(); + } + } else { + try (var ps = con.prepareStatement("INSERT INTO clan_skills (clan_id,skill_id,skill_level,skill_name,sub_pledge_id) VALUES (?,?,?,?,?)")) { + ps.setInt(1, getId()); + ps.setInt(2, newSkill.getId()); + ps.setInt(3, newSkill.getLevel()); + ps.setString(4, newSkill.getName()); + ps.setInt(5, subType); + ps.execute(); + } + } + } catch (Exception e) { + LOG.warn("Error could not store clan skills: {}", e.getMessage(), e); + } + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.CLAN_SKILL_S1_ADDED); + sm.addSkillName(newSkill.getId()); + + for (L2ClanMember temp : _members.values()) { + if ((temp != null) && (temp.getPlayerInstance() != null) && temp.isOnline()) { + if (subType == -2) { + if (newSkill.getMinPledgeClass() <= temp.getPlayerInstance().getPledgeClass()) { + temp.getPlayerInstance().addSkill(newSkill, false); // Skill is not saved to player DB + temp.getPlayerInstance().sendPacket(new PledgeSkillListAdd(newSkill.getId(), newSkill.getLevel())); + temp.getPlayerInstance().sendPacket(sm); + temp.getPlayerInstance().sendSkillList(); + } + } else { + if (temp.getPledgeType() == subType) { + temp.getPlayerInstance().addSkill(newSkill, false); // Skill is not saved to player DB + temp.getPlayerInstance().sendPacket(new ExSubPledgeSkillAdd(subType, newSkill.getId(), newSkill.getLevel())); + temp.getPlayerInstance().sendPacket(sm); + temp.getPlayerInstance().sendSkillList(); + } + } + } + } + } + + return oldSkill; + } + + public void addSkillEffects() { + for (Skill skill : _skills.values()) { + for (L2ClanMember temp : _members.values()) { + try { + if ((temp != null) && temp.isOnline()) { + if (skill.getMinPledgeClass() <= temp.getPlayerInstance().getPledgeClass()) { + temp.getPlayerInstance().addSkill(skill, false); // Skill is not saved to player DB + } + } + } catch (NullPointerException e) { + LOG.warn(e.getMessage(), e); + } + } + } + } + + public void addSkillEffects(L2PcInstance player) { + if (player == null) { + return; + } + + for (Skill skill : _skills.values()) { + if (skill.getMinPledgeClass() <= player.getPledgeClass()) { + player.addSkill(skill, false); // Skill is not saved to player DB + } + } + + if (player.getPledgeType() == 0) { + for (Skill skill : _subPledgeSkills.values()) { + player.addSkill(skill, false); // Skill is not saved to player DB + } + } else { + SubPledge subunit = getSubPledge(player.getPledgeType()); + if (subunit == null) { + return; + } + for (Skill skill : subunit.getSkills()) { + player.addSkill(skill, false); // Skill is not saved to player DB + } + } + + if (_reputationScore < 0) { + skillsStatus(player, true); + } + } + + public void removeSkillEffects(L2PcInstance player) { + if (player == null) { + return; + } + + for (Skill skill : _skills.values()) { + player.removeSkill(skill, false); // Skill is not saved to player DB + } + + if (player.getPledgeType() == 0) { + for (Skill skill : _subPledgeSkills.values()) { + player.removeSkill(skill, false); // Skill is not saved to player DB + } + } else { + SubPledge subunit = getSubPledge(player.getPledgeType()); + if (subunit == null) { + return; + } + for (Skill skill : subunit.getSkills()) { + player.removeSkill(skill, false); // Skill is not saved to player DB + } + } + } + + public void skillsStatus(L2PcInstance player, boolean disable) { + if (player == null) { + return; + } + + for (Skill skill : _skills.values()) { + if (disable) { + player.disableSkill(skill, -1); + } else { + player.enableSkill(skill); + } + } + + if (player.getPledgeType() == 0) { + for (Skill skill : _subPledgeSkills.values()) { + if (disable) { + player.disableSkill(skill, -1); + } else { + player.enableSkill(skill); + } + } + } else { + final SubPledge subunit = getSubPledge(player.getPledgeType()); + if (subunit != null) { + for (Skill skill : subunit.getSkills()) { + if (disable) { + player.disableSkill(skill, -1); + } else { + player.enableSkill(skill); + } + } + } + } + } + + public void broadcastToOnlineAllyMembers(L2GameServerPacket packet) { + for (L2Clan clan : ClanTable.getInstance().getClanAllies(getAllyId())) { + clan.broadcastToOnlineMembers(packet); + } + } + + public void broadcastToOnlineMembers(L2GameServerPacket packet) { + for (L2ClanMember member : _members.values()) { + if ((member != null) && member.isOnline()) { + member.getPlayerInstance().sendPacket(packet); + } + } + } + + public void broadcastCSToOnlineMembers(CreatureSay packet, L2PcInstance broadcaster) { + for (L2ClanMember member : _members.values()) { + if ((member != null) && member.isOnline() && !BlockList.isBlocked(member.getPlayerInstance(), broadcaster)) { + member.getPlayerInstance().sendPacket(packet); + } + } + } + + public void broadcastToOtherOnlineMembers(L2GameServerPacket packet, L2PcInstance player) { + for (L2ClanMember member : _members.values()) { + if ((member != null) && member.isOnline() && (member.getPlayerInstance() != player)) { + member.getPlayerInstance().sendPacket(packet); + } + } + } + + @Override + public String toString() { + return getName() + "[" + getId() + "]"; + } + + public ItemContainer getWarehouse() { + return _warehouse; + } + + public boolean isAtWarWith(int id) { + return _atWarWith.contains(id); + } + + public boolean isAtWarWith(L2Clan clan) { + return _atWarWith.contains(clan.getId()); + } + + public boolean isAtWarAttacker(int id) { + return _atWarAttackers.contains(id); + } + + public void setEnemyClan(L2Clan clan) { + _atWarWith.add(clan.getId()); + } + + public void setEnemyClan(int id) { + _atWarWith.add(id); + } + + public void setAttackerClan(L2Clan clan) { + _atWarAttackers.add(clan.getId()); + } + + public void setAttackerClan(int clan) { + _atWarAttackers.add(clan); + } + + public void deleteEnemyClan(L2Clan clan) { + _atWarWith.remove(clan.getId()); + } + + public void deleteAttackerClan(L2Clan clan) { + _atWarAttackers.remove(clan.getId()); + } + + public int getHiredGuards() { + return _hiredGuards; + } + + public void incrementHiredGuards() { + _hiredGuards++; + } + + public boolean isAtWar() { + return !_atWarWith.isEmpty(); + } + + public Set getWarList() { + return _atWarWith; + } + + public Set getAttackerList() { + return _atWarAttackers; + } + + public void broadcastClanStatus() { + for (L2PcInstance member : getOnlineMembers(0)) { + member.sendPacket(PledgeShowMemberListDeleteAll.STATIC_PACKET); + member.sendPacket(new PledgeShowMemberListAll(this, member)); + } + } + + public static class SubPledge { + private final int _id; + private String _subPledgeName; + private int _leaderId; + private final Map _subPledgeSkills = new HashMap<>(); + + public SubPledge(int id, String name, int leaderId) { + _id = id; + _subPledgeName = name; + _leaderId = leaderId; + } + + public int getId() { + return _id; + } + + public String getName() { + return _subPledgeName; + } + + public void setName(String name) { + _subPledgeName = name; + } + + public int getLeaderId() { + return _leaderId; + } + + public void setLeaderId(int leaderId) { + _leaderId = leaderId; + } + + public Skill addNewSkill(Skill skill) { + return _subPledgeSkills.put(skill.getId(), skill); + } + + public Collection getSkills() { + return _subPledgeSkills.values(); + } + + public Skill getSkill(int id) { + return _subPledgeSkills.get(id); + } + } + + public static class RankPrivs { + private final int _rankId; + private final int _party;// TODO find out what this stuff means and implement it + private final EnumIntBitmask _rankPrivs; + + public RankPrivs(int rank, int party, int privs) { + _rankId = rank; + _party = party; + _rankPrivs = new EnumIntBitmask<>(ClanPrivilege.class, privs); + } + + public RankPrivs(int rank, int party, EnumIntBitmask rankPrivs) { + _rankId = rank; + _party = party; + _rankPrivs = rankPrivs; + } + + public int getRank() { + return _rankId; + } + + public int getParty() { + return _party; + } + + public EnumIntBitmask getPrivs() { + return _rankPrivs; + } + + public void setPrivs(int privs) { + _rankPrivs.setBitmask(privs); + } + } + + private void restoreSubPledges() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT sub_pledge_id,name,leader_id FROM clan_subpledges WHERE clan_id=?")) { + // Retrieve all subpledges of this clan from the database + ps.setInt(1, getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int id = rs.getInt("sub_pledge_id"); + String name = rs.getString("name"); + int leaderId = rs.getInt("leader_id"); + // Create a SubPledge object for each record + SubPledge pledge = new SubPledge(id, name, leaderId); + _subPledges.put(id, pledge); + } + } + } catch (Exception e) { + LOG.warn("Could not restore clan sub-units: {}", e.getMessage(), e); + } + } + + /** + * used to retrieve subPledge by type + */ + public final SubPledge getSubPledge(int pledgeType) { + return _subPledges.get(pledgeType); + } + + /** + * Used to retrieve subPledge by name + */ + public final SubPledge getSubPledge(String pledgeName) { + for (SubPledge sp : _subPledges.values()) { + if (sp.getName().equalsIgnoreCase(pledgeName)) { + return sp; + } + } + return null; + } + + /** + * Used to retrieve all subPledges + */ + public final SubPledge[] getAllSubPledges() { + return _subPledges.values().toArray(new SubPledge[0]); + } + + public SubPledge createSubPledge(L2PcInstance player, int pledgeType, int leaderId, String subPledgeName) { + SubPledge subPledge = null; + pledgeType = getAvailablePledgeTypes(pledgeType); + if (pledgeType == 0) { + if (pledgeType == SUBUNIT_ACADEMY) { + player.sendPacket(SystemMessageId.CLAN_HAS_ALREADY_ESTABLISHED_A_CLAN_ACADEMY); + } else { + player.sendMessage("You can't create any more sub-units of this type"); + } + return null; + } + if (_leader.getObjectId() == leaderId) { + player.sendMessage("Leader is not correct"); + return null; + } + + // Royal Guard 5000 points per each + // Order of Knights 10000 points per each + if ((pledgeType != -1) && (((getReputationScore() < clan().getCreateRoyalGuardCost()) && (pledgeType < SUBUNIT_KNIGHT1)) || // + ((getReputationScore() < clan().getCreateKnightUnitCost()) && (pledgeType > SUBUNIT_ROYAL2)))) { + player.sendPacket(SystemMessageId.THE_CLAN_REPUTATION_SCORE_IS_TOO_LOW); + return null; + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO clan_subpledges (clan_id,sub_pledge_id,name,leader_id) values (?,?,?,?)")) { + ps.setInt(1, getId()); + ps.setInt(2, pledgeType); + ps.setString(3, subPledgeName); + ps.setInt(4, pledgeType != -1 ? leaderId : 0); + ps.execute(); + + subPledge = new SubPledge(pledgeType, subPledgeName, leaderId); + _subPledges.put(pledgeType, subPledge); + + if (pledgeType != -1) { + // Royal Guard 5000 points per each + // Order of Knights 10000 points per each + if (pledgeType < SUBUNIT_KNIGHT1) { + setReputationScore(getReputationScore() - clan().getCreateRoyalGuardCost(), true); + } else { + setReputationScore(getReputationScore() - clan().getCreateKnightUnitCost(), true); + // TODO: clan lvl9 or more can reinforce knights cheaper if first knight unit already created, use clan().getReinforceKnightUnitCost() + } + } + + if (general().debug()) { + LOG.debug("New sub_clan saved in db: {}; {}", getId(), pledgeType); + } + } catch (Exception e) { + LOG.error("Error saving sub clan data: {}", e.getMessage(), e); + } + + broadcastToOnlineMembers(new PledgeShowInfoUpdate(_leader.getClan())); + broadcastToOnlineMembers(new PledgeReceiveSubPledgeCreated(subPledge, _leader.getClan())); + return subPledge; + } + + public int getAvailablePledgeTypes(int pledgeType) { + if (_subPledges.get(pledgeType) != null) { + switch (pledgeType) { + case SUBUNIT_ROYAL2, SUBUNIT_ACADEMY, SUBUNIT_KNIGHT4 -> pledgeType = 0; + case SUBUNIT_ROYAL1 -> pledgeType = getAvailablePledgeTypes(SUBUNIT_ROYAL2); + case SUBUNIT_KNIGHT1 -> pledgeType = getAvailablePledgeTypes(SUBUNIT_KNIGHT2); + case SUBUNIT_KNIGHT2 -> pledgeType = getAvailablePledgeTypes(SUBUNIT_KNIGHT3); + case SUBUNIT_KNIGHT3 -> pledgeType = getAvailablePledgeTypes(SUBUNIT_KNIGHT4); + } + } + return pledgeType; + } + + public void updateSubPledgeInDB(int pledgeType) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_subpledges SET leader_id=?, name=? WHERE clan_id=? AND sub_pledge_id=?")) { + ps.setInt(1, getSubPledge(pledgeType).getLeaderId()); + ps.setString(2, getSubPledge(pledgeType).getName()); + ps.setInt(3, getId()); + ps.setInt(4, pledgeType); + ps.execute(); + if (general().debug()) { + LOG.debug("Subpledge updated in db: {}", getId()); + } + } catch (Exception e) { + LOG.error("Error updating subpledge: {}", e.getMessage(), e); + } + } + + private void restoreRankPrivs() { + DAOFactory.getInstance().getClanDAO().getPrivileges(getId()).forEach((rank, privileges) -> _privs.get(rank).setPrivs(privileges)); + } + + private void initializePrivs() { + for (int i = 1; i < 10; i++) { + _privs.put(i, new RankPrivs(i, 0, new EnumIntBitmask<>(ClanPrivilege.class, false))); + } + } + + public EnumIntBitmask getRankPrivs(int rank) { + if (_privs.get(rank) != null) { + return _privs.get(rank).getPrivs(); + } + return new EnumIntBitmask<>(ClanPrivilege.class, false); + } + + public void setRankPrivs(int rank, int privs) { + final var rankPrivileges = _privs.get(rank); + if (rankPrivileges != null) { + rankPrivileges.setPrivs(privs); + + DAOFactory.getInstance().getClanDAO().storePrivileges(getId(), rank, privs); + + for (var cm : getMembers()) { + if (cm.isOnline() && (cm.getPlayerInstance() != null) && (cm.getPowerGrade() == rank)) { + cm.getPlayerInstance().getClanPrivileges().setBitmask(privs); + cm.getPlayerInstance().sendPacket(new UserInfo(cm.getPlayerInstance())); + cm.getPlayerInstance().sendPacket(new ExBrExtraUserInfo(cm.getPlayerInstance())); + } + } + broadcastClanStatus(); + } else { + _privs.put(rank, new RankPrivs(rank, 0, privs)); + + DAOFactory.getInstance().getClanDAO().storePrivileges(getId(), rank, privs); + } + } + + public final RankPrivs[] getAllRankPrivs() { + return _privs.values().toArray(new RankPrivs[0]); + } + + public int getLeaderSubPledge(int leaderId) { + int id = 0; + for (SubPledge sp : _subPledges.values()) { + if (sp.getLeaderId() == 0) { + continue; + } + if (sp.getLeaderId() == leaderId) { + id = sp.getId(); + } + } + return id; + } + + public synchronized void addReputationScore(int value, boolean save) { + setReputationScore(getReputationScore() + value, save); + } + + public synchronized void takeReputationScore(int value, boolean save) { + setReputationScore(getReputationScore() - value, save); + } + + private void setReputationScore(int value, boolean save) { + if ((_reputationScore >= 0) && (value < 0)) { + broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.REPUTATION_POINTS_0_OR_LOWER_CLAN_SKILLS_DEACTIVATED)); + for (L2ClanMember member : _members.values()) { + if (member.isOnline() && (member.getPlayerInstance() != null)) { + skillsStatus(member.getPlayerInstance(), true); + } + } + } else if ((_reputationScore < 0) && (value >= 0)) { + broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.CLAN_SKILLS_WILL_BE_ACTIVATED_SINCE_REPUTATION_IS_0_OR_HIGHER)); + for (L2ClanMember member : _members.values()) { + if (member.isOnline() && (member.getPlayerInstance() != null)) { + skillsStatus(member.getPlayerInstance(), false); + } + } + } + + _reputationScore = value; + if (_reputationScore > 100000000) { + _reputationScore = 100000000; + } + if (_reputationScore < -100000000) { + _reputationScore = -100000000; + } + broadcastToOnlineMembers(new PledgeShowInfoUpdate(this)); + if (save) { + updateClanScoreInDB(); + } + } + + public int getReputationScore() { + return _reputationScore; + } + + public void setRank(int rank) { + _rank = rank; + } + + public int getRank() { + return _rank; + } + + public int getAuctionBidAt() { + return _auctionBidAt; + } + + public void setAuctionBidAt(int id, boolean storeInDb) { + _auctionBidAt = id; + + if (storeInDb) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET auction_bid_at=? WHERE clan_id=?")) { + ps.setInt(1, id); + ps.setInt(2, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not store auction for clan: {}", e.getMessage(), e); + } + } + } + + /** + * @param activeChar the clan inviting player. + * @param target the invited player. + * @param pledgeType the pledge type to join. + * @return {core true} if activeChar and target meet various conditions to join a clan. + */ + public boolean checkClanJoinCondition(L2PcInstance activeChar, L2PcInstance target, int pledgeType) { + if (activeChar == null) { + return false; + } + if (!activeChar.hasClanPrivilege(ClanPrivilege.CL_JOIN_CLAN)) { + activeChar.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return false; + } + if (target == null) { + activeChar.sendPacket(SystemMessageId.YOU_HAVE_INVITED_THE_WRONG_TARGET); + return false; + } + if (activeChar.getObjectId() == target.getObjectId()) { + activeChar.sendPacket(SystemMessageId.CANNOT_INVITE_YOURSELF); + return false; + } + if (getCharPenaltyExpiryTime() > System.currentTimeMillis()) { + activeChar.sendPacket(SystemMessageId.YOU_MUST_WAIT_BEFORE_ACCEPTING_A_NEW_MEMBER); + return false; + } + if (target.getClanId() != 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_WORKING_WITH_ANOTHER_CLAN); + sm.addString(target.getName()); + activeChar.sendPacket(sm); + return false; + } + if (target.getClanJoinExpiryTime() > System.currentTimeMillis()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_MUST_WAIT_BEFORE_JOINING_ANOTHER_CLAN); + sm.addString(target.getName()); + activeChar.sendPacket(sm); + return false; + } + if (((target.getLevel() > 40) || (target.getClassId().level() >= 2)) && (pledgeType == -1)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DOESNOT_MEET_REQUIREMENTS_TO_JOIN_ACADEMY); + sm.addString(target.getName()); + activeChar.sendPacket(sm); + activeChar.sendPacket(SystemMessageId.ACADEMY_REQUIREMENTS); + return false; + } + if (getSubPledgeMembersCount(pledgeType) >= getMaxNrOfMembers(pledgeType)) { + if (pledgeType == 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CLAN_IS_FULL); + sm.addString(getName()); + activeChar.sendPacket(sm); + } else { + activeChar.sendPacket(SystemMessageId.SUBCLAN_IS_FULL); + } + return false; + } + return true; + } + + /** + * @param activeChar the clan inviting player. + * @param target the invited player. + * @return {core true} if activeChar and target meet various conditions to join a clan. + */ + public boolean checkAllyJoinCondition(L2PcInstance activeChar, L2PcInstance target) { + if (activeChar == null) { + return false; + } + if ((activeChar.getAllyId() == 0) || !activeChar.isClanLeader() || (activeChar.getClanId() != activeChar.getAllyId())) { + activeChar.sendPacket(SystemMessageId.FEATURE_ONLY_FOR_ALLIANCE_LEADER); + return false; + } + L2Clan leaderClan = activeChar.getClan(); + if (leaderClan.getAllyPenaltyExpiryTime() > System.currentTimeMillis()) { + if (leaderClan.getAllyPenaltyType() == PENALTY_TYPE_DISMISS_CLAN) { + activeChar.sendPacket(SystemMessageId.CANT_INVITE_CLAN_WITHIN_1_DAY); + return false; + } + } + if (target == null) { + activeChar.sendPacket(SystemMessageId.YOU_HAVE_INVITED_THE_WRONG_TARGET); + return false; + } + if (activeChar.getObjectId() == target.getObjectId()) { + activeChar.sendPacket(SystemMessageId.CANNOT_INVITE_YOURSELF); + return false; + } + if (target.getClan() == null) { + activeChar.sendPacket(SystemMessageId.TARGET_MUST_BE_IN_CLAN); + return false; + } + if (!target.isClanLeader()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_IS_NOT_A_CLAN_LEADER); + sm.addString(target.getName()); + activeChar.sendPacket(sm); + return false; + } + L2Clan targetClan = target.getClan(); + if (target.getAllyId() != 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CLAN_ALREADY_MEMBER_OF_S2_ALLIANCE); + sm.addString(targetClan.getName()); + sm.addString(targetClan.getAllyName()); + activeChar.sendPacket(sm); + return false; + } + if (targetClan.getAllyPenaltyExpiryTime() > System.currentTimeMillis()) { + if (targetClan.getAllyPenaltyType() == PENALTY_TYPE_CLAN_LEFT) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANT_ENTER_ALLIANCE_WITHIN_1_DAY); + sm.addString(target.getClan().getName()); + sm.addString(target.getClan().getAllyName()); + activeChar.sendPacket(sm); + return false; + } + if (targetClan.getAllyPenaltyType() == PENALTY_TYPE_CLAN_DISMISSED) { + activeChar.sendPacket(SystemMessageId.CANT_ENTER_ALLIANCE_WITHIN_1_DAY); + return false; + } + } + if (activeChar.isInsideZone(ZoneId.SIEGE) && target.isInsideZone(ZoneId.SIEGE)) { + activeChar.sendPacket(SystemMessageId.OPPOSING_CLAN_IS_PARTICIPATING_IN_SIEGE); + return false; + } + if (leaderClan.isAtWarWith(targetClan.getId())) { + activeChar.sendPacket(SystemMessageId.MAY_NOT_ALLY_CLAN_BATTLE); + return false; + } + + if (ClanTable.getInstance().getClanAllies(activeChar.getAllyId()).size() >= character().getMaxNumOfClansInAlly()) { + activeChar.sendPacket(SystemMessageId.YOU_HAVE_EXCEEDED_THE_LIMIT); + return false; + } + + return true; + } + + public long getAllyPenaltyExpiryTime() { + return _allyPenaltyExpiryTime; + } + + public int getAllyPenaltyType() { + return _allyPenaltyType; + } + + public void setAllyPenaltyExpiryTime(long expiryTime, int penaltyType) { + _allyPenaltyExpiryTime = expiryTime; + _allyPenaltyType = penaltyType; + } + + public long getCharPenaltyExpiryTime() { + return _charPenaltyExpiryTime; + } + + public void setCharPenaltyExpiryTime(long time) { + _charPenaltyExpiryTime = time; + } + + public long getDissolvingExpiryTime() { + return _dissolvingExpiryTime; + } + + public void setDissolvingExpiryTime(long time) { + _dissolvingExpiryTime = time; + } + + public void createAlly(L2PcInstance player, String allyName) { + if (null == player) { + return; + } + + if (general().debug()) { + LOG.debug("{}({}) requested ally creation from ", player.getObjectId(), player.getName()); + } + + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.ONLY_CLAN_LEADER_CREATE_ALLIANCE); + return; + } + if (getAllyId() != 0) { + player.sendPacket(SystemMessageId.ALREADY_JOINED_ALLIANCE); + return; + } + if (getLevel() < 5) { + player.sendPacket(SystemMessageId.TO_CREATE_AN_ALLY_YOU_CLAN_MUST_BE_LEVEL_5_OR_HIGHER); + return; + } + if (getAllyPenaltyExpiryTime() > System.currentTimeMillis()) { + if (getAllyPenaltyType() == PENALTY_TYPE_DISSOLVE_ALLY) { + player.sendPacket(SystemMessageId.CANT_CREATE_ALLIANCE_10_DAYS_DISOLUTION); + return; + } + } + if (getDissolvingExpiryTime() > System.currentTimeMillis()) { + player.sendPacket(SystemMessageId.YOU_MAY_NOT_CREATE_ALLY_WHILE_DISSOLVING); + return; + } + if (!Util.isAlphaNumeric(allyName)) { + player.sendPacket(SystemMessageId.INCORRECT_ALLIANCE_NAME); + return; + } + if ((allyName.length() > 16) || (allyName.length() < 2)) { + player.sendPacket(SystemMessageId.INCORRECT_ALLIANCE_NAME_LENGTH); + return; + } + if (ClanTable.getInstance().isAllyExists(allyName)) { + player.sendPacket(SystemMessageId.ALLIANCE_ALREADY_EXISTS); + return; + } + + setAllyId(getId()); + setAllyName(allyName.trim()); + setAllyPenaltyExpiryTime(0, 0); + updateClanInDB(); + + player.sendPacket(new UserInfo(player)); + player.sendPacket(new ExBrExtraUserInfo(player)); + + // TODO: Need correct message id + player.sendMessage("Alliance " + allyName + " has been created."); + } + + public void dissolveAlly(L2PcInstance player) { + if (getAllyId() == 0) { + player.sendPacket(SystemMessageId.NO_CURRENT_ALLIANCES); + return; + } + if (!player.isClanLeader() || (getId() != getAllyId())) { + player.sendPacket(SystemMessageId.FEATURE_ONLY_FOR_ALLIANCE_LEADER); + return; + } + if (player.isInsideZone(ZoneId.SIEGE)) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_ALLY_WHILE_IN_SIEGE); + return; + } + + broadcastToOnlineAllyMembers(SystemMessage.getSystemMessage(SystemMessageId.ALLIANCE_DISOLVED)); + + long currentTime = System.currentTimeMillis(); + for (L2Clan clan : ClanTable.getInstance().getClanAllies(getAllyId())) { + if (clan.getId() != getId()) { + clan.setAllyId(0); + clan.setAllyName(null); + clan.setAllyPenaltyExpiryTime(0, 0); + clan.updateClanInDB(); + } + } + + setAllyId(0); + setAllyName(null); + changeAllyCrest(0, false); + + setAllyPenaltyExpiryTime(currentTime + character().getDaysBeforeCreateNewAllyWhenDissolved(), PENALTY_TYPE_DISSOLVE_ALLY); + updateClanInDB(); + } + + public boolean levelUpClan(L2PcInstance player) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return false; + } + if (System.currentTimeMillis() < getDissolvingExpiryTime()) { + player.sendPacket(SystemMessageId.CANNOT_RISE_LEVEL_WHILE_DISSOLUTION_IN_PROGRESS); + return false; + } + + boolean increaseClanLevel = false; + + switch (getLevel()) { + case 0: { + // Upgrade to 1 + if ((player.getSp() >= 20000) && (player.getAdena() >= 650000)) { + if (player.reduceAdena("ClanLvl", 650000, player.getTarget(), true)) { + player.setSp(player.getSp() - 20000); + SystemMessage sp = SystemMessage.getSystemMessage(SystemMessageId.SP_DECREASED_S1); + sp.addInt(20000); + player.sendPacket(sp); + increaseClanLevel = true; + } + } + break; + } + case 1: { + // Upgrade to 2 + if ((player.getSp() >= 100000) && (player.getAdena() >= 2500000)) { + if (player.reduceAdena("ClanLvl", 2500000, player.getTarget(), true)) { + player.setSp(player.getSp() - 100000); + SystemMessage sp = SystemMessage.getSystemMessage(SystemMessageId.SP_DECREASED_S1); + sp.addInt(100000); + player.sendPacket(sp); + increaseClanLevel = true; + } + } + break; + } + case 2: { + // Upgrade to 3 + if ((player.getSp() >= 350000) && (player.getInventory().getItemByItemId(1419) != null)) { + // TODO unhardcode these item IDs + // itemId 1419 == Blood Mark + if (player.destroyItemByItemId("ClanLvl", 1419, 1, player.getTarget(), false)) { + player.setSp(player.getSp() - 350000); + SystemMessage sp = SystemMessage.getSystemMessage(SystemMessageId.SP_DECREASED_S1); + sp.addInt(350000); + player.sendPacket(sp); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(1419); + player.sendPacket(sm); + increaseClanLevel = true; + } + } + break; + } + case 3: { + // Upgrade to 4 + if ((player.getSp() >= 1000000) && (player.getInventory().getItemByItemId(3874) != null)) { + // itemId 3874 == Alliance Manifesto + if (player.destroyItemByItemId("ClanLvl", 3874, 1, player.getTarget(), false)) { + player.setSp(player.getSp() - 1000000); + SystemMessage sp = SystemMessage.getSystemMessage(SystemMessageId.SP_DECREASED_S1); + sp.addInt(1000000); + player.sendPacket(sp); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(3874); + player.sendPacket(sm); + increaseClanLevel = true; + } + } + break; + } + case 4: { + // Upgrade to 5 + if ((player.getSp() >= 2500000) && (player.getInventory().getItemByItemId(3870) != null)) { + // itemId 3870 == Seal of Aspiration + if (player.destroyItemByItemId("ClanLvl", 3870, 1, player.getTarget(), false)) { + player.setSp(player.getSp() - 2500000); + SystemMessage sp = SystemMessage.getSystemMessage(SystemMessageId.SP_DECREASED_S1); + sp.addInt(2500000); + player.sendPacket(sp); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(3870); + player.sendPacket(sm); + increaseClanLevel = true; + } + } + break; + } + case 5: + // Upgrade to 6 + if ((getReputationScore() >= clan().getClanLevel6Cost()) && (getMembersCount() >= clan().getClanLevel6Requirement())) { + setReputationScore(getReputationScore() - clan().getClanLevel6Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel6Cost()); + player.sendPacket(cr); + increaseClanLevel = true; + } + break; + + case 6: + // Upgrade to 7 + if ((getReputationScore() >= clan().getClanLevel7Cost()) && (getMembersCount() >= clan().getClanLevel7Requirement())) { + setReputationScore(getReputationScore() - clan().getClanLevel7Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel7Cost()); + player.sendPacket(cr); + increaseClanLevel = true; + } + break; + case 7: + // Upgrade to 8 + if ((getReputationScore() >= clan().getClanLevel8Cost()) && (getMembersCount() >= clan().getClanLevel8Requirement())) { + setReputationScore(getReputationScore() - clan().getClanLevel8Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel8Cost()); + player.sendPacket(cr); + increaseClanLevel = true; + } + break; + case 8: + // Upgrade to 9 + if ((getReputationScore() >= clan().getClanLevel9Cost()) && (player.getInventory().getItemByItemId(9910) != null) && (getMembersCount() >= clan().getClanLevel9Requirement())) { + // itemId 9910 == Blood Oath + if (player.destroyItemByItemId("ClanLvl", 9910, 150, player.getTarget(), false)) { + setReputationScore(getReputationScore() - clan().getClanLevel9Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel9Cost()); + player.sendPacket(cr); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(9910); + sm.addLong(150); + player.sendPacket(sm); + increaseClanLevel = true; + } + } + break; + case 9: + // Upgrade to 10 + if ((getReputationScore() >= clan().getClanLevel10Cost()) && (player.getInventory().getItemByItemId(9911) != null) && (getMembersCount() >= clan().getClanLevel10Requirement())) { + // itemId 9911 == Blood Alliance + if (player.destroyItemByItemId("ClanLvl", 9911, 5, player.getTarget(), false)) { + setReputationScore(getReputationScore() - clan().getClanLevel10Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel10Cost()); + player.sendPacket(cr); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(9911); + sm.addLong(5); + player.sendPacket(sm); + increaseClanLevel = true; + } + } + break; + case 10: + // Upgrade to 11 + boolean hasTerritory = false; + for (Territory terr : TerritoryWarManager.getInstance().getAllTerritories()) { + if (terr.getOwnerClan().getId() == getId()) { + hasTerritory = true; + break; + } + } + if (hasTerritory && (getReputationScore() >= clan().getClanLevel11Cost()) && (getMembersCount() >= clan().getClanLevel11Requirement())) { + setReputationScore(getReputationScore() - clan().getClanLevel11Cost(), true); + SystemMessage cr = SystemMessage.getSystemMessage(SystemMessageId.S1_DEDUCTED_FROM_CLAN_REP); + cr.addInt(clan().getClanLevel11Cost()); + player.sendPacket(cr); + increaseClanLevel = true; + } + break; + default: + return false; + } + + if (!increaseClanLevel) { + player.sendPacket(SystemMessageId.FAILED_TO_INCREASE_CLAN_LEVEL); + return false; + } + + // the player should know that he has less sp now :p + StatusUpdate su = new StatusUpdate(player); + su.addAttribute(StatusUpdate.SP, player.getSp()); + player.sendPacket(su); + + player.sendPacket(new ItemList(player, false)); + + changeLevel(getLevel() + 1); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerClanLevelUp(this)); + return true; + } + + public void changeLevel(int level) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET clan_level = ? WHERE clan_id = ?")) { + ps.setInt(1, level); + ps.setInt(2, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not increase clan level: {}", e.getMessage(), e); + } + + setLevel(level); + + ForumsBBSManager.getInstance().onClanLevel(this); + + if (getLeader().isOnline()) { + L2PcInstance leader = getLeader().getPlayerInstance(); + if (level > 4) { + SiegeManager.getInstance().addSiegeSkills(leader); + leader.sendPacket(SystemMessageId.CLAN_CAN_ACCUMULATE_CLAN_REPUTATION_POINTS); + } else { + SiegeManager.getInstance().removeSiegeSkills(leader); + } + } + + // notify all the members about it + broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.CLAN_LEVEL_INCREASED)); + broadcastToOnlineMembers(new PledgeShowInfoUpdate(this)); + } + + /** + * Change the clan crest. If crest id is 0, crest is removed. New crest id is saved to database. + * @param crestId if 0, crest is removed, else new crest id is set and saved to database + */ + public void changeClanCrest(int crestId) { + if (getCrestId() != 0) { + CrestTable.getInstance().removeCrest(getCrestId()); + } + + setCrestId(crestId); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET crest_id = ? WHERE clan_id = ?")) { + ps.setInt(1, crestId); + ps.setInt(2, getId()); + ps.executeUpdate(); + } catch (Exception e) { + LOG.warn("Could not update crest for clan {} [{}]: {}", getName(), getId(), e.getMessage(), e); + } + + for (L2PcInstance member : getOnlineMembers(0)) { + member.broadcastUserInfo(); + } + } + + /** + * Change the ally crest. If crest id is 0, crest is removed. New crest id is saved to database. + * @param crestId if 0, crest is removed, else new crest id is set and saved to database + * @param onlyThisClan + */ + public void changeAllyCrest(int crestId, boolean onlyThisClan) { + String sqlStatement = "UPDATE clan_data SET ally_crest_id = ? WHERE clan_id = ?"; + int allyId = getId(); + if (!onlyThisClan) { + if (getAllyCrestId() != 0) { + CrestTable.getInstance().removeCrest(getAllyCrestId()); + } + sqlStatement = "UPDATE clan_data SET ally_crest_id = ? WHERE ally_id = ?"; + allyId = getAllyId(); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(sqlStatement)) { + ps.setInt(1, crestId); + ps.setInt(2, allyId); + ps.executeUpdate(); + } catch (Exception e) { + LOG.warn("Could not update ally crest for ally/clan id {}: {}", allyId, e.getMessage(), e); + } + + if (onlyThisClan) { + setAllyCrestId(crestId); + for (L2PcInstance member : getOnlineMembers(0)) { + member.broadcastUserInfo(); + } + } else { + for (L2Clan clan : ClanTable.getInstance().getClanAllies(getAllyId())) { + clan.setAllyCrestId(crestId); + for (L2PcInstance member : clan.getOnlineMembers(0)) { + member.broadcastUserInfo(); + } + } + } + } + + /** + * Change the large crest. If crest id is 0, crest is removed. New crest id is saved to database. + * @param crestId if 0, crest is removed, else new crest id is set and saved to database + */ + public void changeLargeCrest(int crestId) { + if (getCrestLargeId() != 0) { + CrestTable.getInstance().removeCrest(getCrestLargeId()); + } + + setCrestLargeId(crestId); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE clan_data SET crest_large_id = ? WHERE clan_id = ?")) { + ps.setInt(1, crestId); + ps.setInt(2, getId()); + ps.executeUpdate(); + } catch (Exception e) { + LOG.warn("Could not update large crest for clan {} [{}]: {}", getName(), getId(), e.getMessage(), e); + } + + for (L2PcInstance member : getOnlineMembers(0)) { + member.broadcastUserInfo(); + } + } + + /** + * Check if this clan can learn the skill for the given skill ID, level. + * @return {@code true} if skill can be learned. + */ + public boolean isLearnableSubSkill(int skillId, int skillLevel) { + Skill current = _subPledgeSkills.get(skillId); + // is next level? + if ((current != null) && ((current.getLevel() + 1) == skillLevel)) { + return true; + } + // is first level? + if ((current == null) && (skillLevel == 1)) { + return true; + } + // other sub-pledges + for (SubPledge subunit : _subPledges.values()) { + // disable academy + if (subunit.getId() == -1) { + continue; + } + current = subunit.getSkill(skillId); + // is next level? + if ((current != null) && ((current.getLevel() + 1) == skillLevel)) { + return true; + } + // is first level? + if ((current == null) && (skillLevel == 1)) { + return true; + } + } + return false; + } + + public boolean isLearnableSubPledgeSkill(Skill skill, int subType) { + // academy + if (subType == -1) { + return false; + } + + int id = skill.getId(); + Skill current; + if (subType == 0) { + current = _subPledgeSkills.get(id); + } else { + current = _subPledges.get(subType).getSkill(id); + } + // is next level? + if ((current != null) && ((current.getLevel() + 1) == skill.getLevel())) { + return true; + } + // is first level? + return (current == null) && (skill.getLevel() == 1); + } + + public List getAllSubSkills() { + final List list = new LinkedList<>(); + for (Skill skill : _subPledgeSkills.values()) { + list.add(new SubPledgeSkill(0, skill.getId(), skill.getLevel())); + } + for (SubPledge subunit : _subPledges.values()) { + for (Skill skill : subunit.getSkills()) { + list.add(new SubPledgeSkill(subunit.getId(), skill.getId(), skill.getLevel())); + } + } + return list; + } + + public void setNewLeaderId(int objectId, boolean storeInDb) { + _newLeaderId = objectId; + if (storeInDb) { + updateClanInDB(); + } + } + + public int getNewLeaderId() { + return _newLeaderId; + } + + public L2PcInstance getNewLeader() { + return L2World.getInstance().getPlayer(_newLeaderId); + } + + public String getNewLeaderName() { + return CharNameTable.getInstance().getNameById(_newLeaderId); + } + + public int getSiegeKills() { + return _siegeKills.get(); + } + + public int getSiegeDeaths() { + return _siegeDeaths.get(); + } + + public int addSiegeKill() { + return _siegeKills.incrementAndGet(); + } + + public int addSiegeDeath() { + return _siegeDeaths.incrementAndGet(); + } + + public void clearSiegeKills() { + _siegeKills.set(0); + } + + public void clearSiegeDeaths() { + _siegeDeaths.set(0); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ClanMember.java b/src/main/java/com/l2jserver/gameserver/model/L2ClanMember.java new file mode 100644 index 00000000..04d32402 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ClanMember.java @@ -0,0 +1,591 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * This class holds the clan members data. + */ +public class L2ClanMember { + private static final Logger LOG = LoggerFactory.getLogger(L2ClanMember.class); + + private final L2Clan _clan; + private int _objectId; + private String _name; + private String _title; + private int _powerGrade; + private int _level; + private int _classId; + private boolean _sex; + private int _raceOrdinal; + private L2PcInstance _player; + private int _pledgeType; + private int _apprentice; + private int _sponsor; + + /** + * Used to restore a clan member from the database. + * @param clan the clan where the clan member belongs. + * @param clanMember the clan member result set + * @throws SQLException if the columnLabel is not valid or a database error occurs + */ + public L2ClanMember(L2Clan clan, ResultSet clanMember) throws SQLException { + if (clan == null) { + throw new IllegalArgumentException("Cannot create a Clan Member with a null clan."); + } + _clan = clan; + _name = clanMember.getString("char_name"); + _level = clanMember.getInt("level"); + _classId = clanMember.getInt("classid"); + _objectId = clanMember.getInt("charId"); + _pledgeType = clanMember.getInt("subpledge"); + _title = clanMember.getString("title"); + _powerGrade = clanMember.getInt("power_grade"); + _apprentice = clanMember.getInt("apprentice"); + _sponsor = clanMember.getInt("sponsor"); + _sex = clanMember.getInt("sex") != 0; + _raceOrdinal = clanMember.getInt("race"); + } + + /** + * Creates a clan member from a player instance. + * @param clan the clan where the player belongs + * @param player the player from which the clan member will be created + */ + public L2ClanMember(L2Clan clan, L2PcInstance player) { + if (clan == null) { + throw new IllegalArgumentException("Cannot create a Clan Member if player has a null clan."); + } + _player = player; + _clan = clan; + _name = player.getName(); + _level = player.getLevel(); + _classId = player.getClassId().getId(); + _objectId = player.getObjectId(); + _pledgeType = player.getPledgeType(); + _powerGrade = player.getPowerGrade(); + _title = player.getTitle(); + _sponsor = 0; + _apprentice = 0; + _sex = player.getAppearance().getSex(); + _raceOrdinal = player.getRace().ordinal(); + } + + /** + * Sets the player instance. + * @param player the new player instance + */ + public void setPlayerInstance(L2PcInstance player) { + if ((player == null) && (_player != null)) { + // this is here to keep the data when the player logs off + _name = _player.getName(); + _level = _player.getLevel(); + _classId = _player.getClassId().getId(); + _objectId = _player.getObjectId(); + _powerGrade = _player.getPowerGrade(); + _pledgeType = _player.getPledgeType(); + _title = _player.getTitle(); + _apprentice = _player.getApprentice(); + _sponsor = _player.getSponsor(); + _sex = _player.getAppearance().getSex(); + _raceOrdinal = _player.getRace().ordinal(); + } + + if (player != null) { + _clan.addSkillEffects(player); + if ((_clan.getLevel() > 3) && player.isClanLeader()) { + SiegeManager.getInstance().addSiegeSkills(player); + } + if (player.isClanLeader()) { + _clan.setLeader(this); + } + } + _player = player; + } + + /** + * Gets the player instance. + * @return the player instance + */ + public L2PcInstance getPlayerInstance() { + return _player; + } + + /** + * Verifies if the clan member is online. + * @return {@code true} if is online + */ + public boolean isOnline() { + if ((_player == null) || !_player.isOnline()) { + return false; + } + return !_player.isInOfflineMode(); + } + + /** + * Gets the class id. + * @return the classId + */ + public int getClassId() { + if (_player != null) { + return _player.getClassId().getId(); + } + return _classId; + } + + /** + * Gets the level. + * @return the level + */ + public int getLevel() { + if (_player != null) { + return _player.getLevel(); + } + return _level; + } + + /** + * Gets the name. + * @return the name + */ + public String getName() { + if (_player != null) { + return _player.getName(); + } + return _name; + } + + /** + * Gets the object id. + * @return Returns the objectId. + */ + public int getObjectId() { + if (_player != null) { + return _player.getObjectId(); + } + return _objectId; + } + + /** + * Gets the title. + * @return the title + */ + public String getTitle() { + if (_player != null) { + return _player.getTitle(); + } + return _title; + } + + /** + * Gets the pledge type. + * @return the pledge type + */ + public int getPledgeType() { + if (_player != null) { + return _player.getPledgeType(); + } + return _pledgeType; + } + + /** + * Sets the pledge type. + * @param pledgeType the new pledge type + */ + public void setPledgeType(int pledgeType) { + _pledgeType = pledgeType; + if (_player != null) { + _player.setPledgeType(pledgeType); + } else { + // db save if char not logged in + updatePledgeType(); + } + } + + /** + * Update pledge type. + */ + public void updatePledgeType() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE characters SET subpledge=? WHERE charId=?")) { + ps.setLong(1, _pledgeType); + ps.setInt(2, getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not update pledge type: {}", e.getMessage(), e); + } + } + + /** + * Gets the power grade. + * @return the power grade + */ + public int getPowerGrade() { + if (_player != null) { + return _player.getPowerGrade(); + } + return _powerGrade; + } + + /** + * Sets the power grade. + * @param powerGrade the new power grade + */ + public void setPowerGrade(int powerGrade) { + _powerGrade = powerGrade; + if (_player != null) { + _player.setPowerGrade(powerGrade); + } else { + // db save if char not logged in + updatePowerGrade(); + } + } + + /** + * Update the characters table of the database with power grade. + */ + public void updatePowerGrade() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE characters SET power_grade=? WHERE charId=?")) { + ps.setLong(1, _powerGrade); + ps.setInt(2, getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not update power grade: {}", e.getMessage(), e); + } + } + + /** + * Sets the apprentice and sponsor. + * @param apprenticeID the apprentice id + * @param sponsorID the sponsor id + */ + public void setApprenticeAndSponsor(int apprenticeID, int sponsorID) { + _apprentice = apprenticeID; + _sponsor = sponsorID; + } + + /** + * Gets the player's race ordinal. + * @return the race ordinal + */ + public int getRaceOrdinal() { + if (_player != null) { + return _player.getRace().ordinal(); + } + return _raceOrdinal; + } + + /** + * Gets the player's sex. + * @return the sex + */ + public boolean getSex() { + if (_player != null) { + return _player.getAppearance().getSex(); + } + return _sex; + } + + /** + * Gets the sponsor. + * @return the sponsor + */ + public int getSponsor() { + if (_player != null) { + return _player.getSponsor(); + } + return _sponsor; + } + + /** + * Gets the apprentice. + * @return the apprentice + */ + public int getApprentice() { + if (_player != null) { + return _player.getApprentice(); + } + return _apprentice; + } + + /** + * Gets the apprentice or sponsor name. + * @return the apprentice or sponsor name + */ + public String getApprenticeOrSponsorName() { + if (_player != null) { + _apprentice = _player.getApprentice(); + _sponsor = _player.getSponsor(); + } + + if (_apprentice != 0) { + L2ClanMember apprentice = _clan.getClanMember(_apprentice); + if (apprentice != null) { + return apprentice.getName(); + } + return "Error"; + } + if (_sponsor != 0) { + L2ClanMember sponsor = _clan.getClanMember(_sponsor); + if (sponsor != null) { + return sponsor.getName(); + } + return "Error"; + } + return ""; + } + + /** + * Gets the clan. + * @return the clan + */ + public L2Clan getClan() { + return _clan; + } + + /** + * Calculate pledge class. + * @param player the player + * @return the int + */ + public static int calculatePledgeClass(L2PcInstance player) { + int pledgeClass = 0; + if (player == null) { + return pledgeClass; + } + + L2Clan clan = player.getClan(); + if (clan != null) { + switch (clan.getLevel()) { + case 4: + if (player.isClanLeader()) { + pledgeClass = 3; + } + break; + case 5: + if (player.isClanLeader()) { + pledgeClass = 4; + } else { + pledgeClass = 2; + } + break; + case 6: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 2; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 5; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 4; + default -> 3; + }; + } + break; + } + break; + case 7: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 3; + break; + case 1001: + case 1002: + case 2001: + case 2002: + pledgeClass = 2; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 7; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 6; + case 1001, 1002, 2001, 2002 -> 5; + default -> 4; + }; + } + break; + } + break; + case 8: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 4; + break; + case 1001: + case 1002: + case 2001: + case 2002: + pledgeClass = 3; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 8; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 7; + case 1001, 1002, 2001, 2002 -> 6; + default -> 5; + }; + } + break; + } + break; + case 9: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 5; + break; + case 1001: + case 1002: + case 2001: + case 2002: + pledgeClass = 4; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 9; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 8; + case 1001, 1002, 2001, 2002 -> 7; + default -> 6; + }; + } + break; + } + break; + case 10: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 6; + break; + case 1001: + case 1002: + case 2001: + case 2002: + pledgeClass = 5; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 10; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 9; + case 1001, 1002, 2001, 2002 -> 8; + default -> 7; + }; + } + break; + } + break; + case 11: + switch (player.getPledgeType()) { + case -1: + pledgeClass = 1; + break; + case 100: + case 200: + pledgeClass = 7; + break; + case 1001: + case 1002: + case 2001: + case 2002: + pledgeClass = 6; + break; + case 0: + if (player.isClanLeader()) { + pledgeClass = 11; + } else { + pledgeClass = switch (clan.getLeaderSubPledge(player.getObjectId())) { + case 100, 200 -> 10; + case 1001, 1002, 2001, 2002 -> 9; + default -> 8; + }; + } + break; + } + break; + default: + pledgeClass = 1; + break; + } + } + + if (player.isNoble() && (pledgeClass < 5)) { + pledgeClass = 5; + } + + if (player.isHero() && (pledgeClass < 8)) { + pledgeClass = 8; + } + return pledgeClass; + } + + /** + * Save apprentice and sponsor. + * @param apprentice the apprentice + * @param sponsor the sponsor + */ + public void saveApprenticeAndSponsor(int apprentice, int sponsor) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE characters SET apprentice=?,sponsor=? WHERE charId=?")) { + ps.setInt(1, apprentice); + ps.setInt(2, sponsor); + ps.setInt(3, getObjectId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not save apprentice/sponsor: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2CommandChannel.java b/src/main/java/com/l2jserver/gameserver/model/L2CommandChannel.java new file mode 100644 index 00000000..5c912476 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2CommandChannel.java @@ -0,0 +1,214 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExCloseMPCC; +import com.l2jserver.gameserver.network.serverpackets.ExMPCCPartyInfoUpdate; +import com.l2jserver.gameserver.network.serverpackets.ExOpenMPCC; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * This class serves as a container for command channels. + * @author chris_00 + */ +public class L2CommandChannel extends AbstractPlayerGroup { + private final List _parties = new CopyOnWriteArrayList<>(); + private L2PcInstance _commandLeader; + private int _channelLvl; + + public L2CommandChannel(L2PcInstance leader) { + _commandLeader = leader; + L2Party party = leader.getParty(); + _parties.add(party); + _channelLvl = party.getLevel(); + party.setCommandChannel(this); + party.broadcastMessage(SystemMessageId.COMMAND_CHANNEL_FORMED); + party.broadcastPacket(ExOpenMPCC.STATIC_PACKET); + } + + /** + * Add a party to this command channel. + * @param party the party to add + */ + public void addParty(L2Party party) { + if (party == null) { + return; + } + // Update the command channel info for existing players + broadcastPacket(new ExMPCCPartyInfoUpdate(party, 1)); + + _parties.add(party); + if (party.getLevel() > _channelLvl) { + _channelLvl = party.getLevel(); + } + party.setCommandChannel(this); + party.broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.JOINED_COMMAND_CHANNEL)); + party.broadcastPacket(ExOpenMPCC.STATIC_PACKET); + } + + /** + * Remove a party from this command channel. + * @param party the party to remove + */ + public void removeParty(L2Party party) { + if (party == null) { + return; + } + + _parties.remove(party); + _channelLvl = 0; + for (L2Party pty : _parties) { + if (pty.getLevel() > _channelLvl) { + _channelLvl = pty.getLevel(); + } + } + party.setCommandChannel(null); + party.broadcastPacket(new ExCloseMPCC()); + if (_parties.size() < 2) { + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.COMMAND_CHANNEL_DISBANDED)); + disbandChannel(); + } else { + // Update the command channel info for existing players + broadcastPacket(new ExMPCCPartyInfoUpdate(party, 0)); + } + } + + public void disbandChannel() { + for (L2Party party : _parties) { + if (party != null) { + removeParty(party); + } + } + _parties.clear(); + } + + /** + * @return the total count of all members of this command channel + */ + @Override + public int getMemberCount() { + int count = 0; + for (L2Party party : _parties) { + if (party != null) { + count += party.getMemberCount(); + } + } + return count; + } + + public List getParties() { + return _parties; + } + + /** + * @return a list of all members in this command channel + */ + @Override + public List getMembers() { + final List members = new LinkedList<>(); + for (L2Party party : getParties()) { + members.addAll(party.getMembers()); + } + return members; + } + + /** + * @return the level of this command channel (equals the level of the highest-leveled character in this command channel) + */ + @Override + public int getLevel() { + return _channelLvl; + } + + @Override + public void setLeader(L2PcInstance leader) { + _commandLeader = leader; + if (leader.getLevel() > _channelLvl) { + _channelLvl = leader.getLevel(); + } + } + + /** + * @param obj + * @return true if proper condition for RaidWar + */ + public boolean meetRaidWarCondition(L2Object obj) { + if (!((obj instanceof L2Character) && ((L2Character) obj).isRaid())) { + return false; + } + return (getMemberCount() >= character().getRaidLootRightsCCSize()); + } + + /** + * @return the leader of this command channel + */ + @Override + public L2PcInstance getLeader() { + return _commandLeader; + } + + /** + * Check if a given player is in this command channel. + * @param player the player to check + * @return {@code true} if he does, {@code false} otherwise + */ + @Override + public boolean containsPlayer(L2PcInstance player) { + for (L2Party party : _parties) { + if (party.containsPlayer(player)) { + return true; + } + } + return false; + } + + /** + * Iterates over all command channel members without the need to allocate a new list + * @see com.l2jserver.gameserver.model.AbstractPlayerGroup#forEachMember(Function) + */ + @Override + public boolean forEachMember(Function function) { + for (L2Party party : _parties) { + if (!party.forEachMember(function)) { + return false; + } + } + return true; + } + + /** + * Check whether the leader of this command channel is the same as the leader of the specified command channel
    + * (which essentially means they're the same group). + * @param cc the other command channel to check against + * @return {@code true} if this command channel equals the specified command channel, {@code false} otherwise + */ + public boolean equals(L2CommandChannel cc) { + return (cc != null) && (getLeaderObjectId() == cc.getLeaderObjectId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ContactList.java b/src/main/java/com/l2jserver/gameserver/model/L2ContactList.java new file mode 100644 index 00000000..c8cdc1ee --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ContactList.java @@ -0,0 +1,153 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * TODO: System messages:
    + * ADD: 3223: The previous name is being registered. Please try again later.
    + * DEL 3219: $s1 was successfully deleted from your Contact List.
    + * DEL 3217: The name is not currently registered. + * @author UnAfraid + * @author mrTJO + */ +public class L2ContactList { + private static final Logger LOG = LoggerFactory.getLogger(L2ContactList.class); + + private final L2PcInstance activeChar; + + private final List _contacts = new CopyOnWriteArrayList<>(); + + private static final String QUERY_ADD = "INSERT INTO character_contacts (charId, contactId) VALUES (?, ?)"; + private static final String QUERY_REMOVE = "DELETE FROM character_contacts WHERE charId = ? and contactId = ?"; + private static final String QUERY_LOAD = "SELECT contactId FROM character_contacts WHERE charId = ?"; + + public L2ContactList(L2PcInstance player) { + activeChar = player; + restore(); + } + + public void restore() { + _contacts.clear(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(QUERY_LOAD)) { + ps.setInt(1, activeChar.getObjectId()); + try (var rs = ps.executeQuery()) { + int contactId; + String contactName; + while (rs.next()) { + contactId = rs.getInt(1); + contactName = CharNameTable.getInstance().getNameById(contactId); + if ((contactName == null) || contactName.equals(activeChar.getName()) || (contactId == activeChar.getObjectId())) { + continue; + } + + _contacts.add(contactName); + } + } + } catch (Exception e) { + LOG.warn("Error found in {}'s ContactsList: {}", activeChar.getName(), e.getMessage(), e); + } + } + + public boolean add(String name) { + int contactId = CharNameTable.getInstance().getIdByName(name); + if (_contacts.contains(name)) { + activeChar.sendPacket(SystemMessageId.NAME_ALREADY_EXIST_ON_CONTACT_LIST); + return false; + } else if (activeChar.getName().equals(name)) { + activeChar.sendPacket(SystemMessageId.CANNOT_ADD_YOUR_NAME_ON_CONTACT_LIST); + return false; + } else if (_contacts.size() >= 100) { + activeChar.sendPacket(SystemMessageId.CONTACT_LIST_LIMIT_REACHED); + return false; + } else if (contactId < 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.NAME_S1_NOT_EXIST_TRY_ANOTHER_NAME); + sm.addString(name); + activeChar.sendPacket(sm); + return false; + } else { + for (String contactName : _contacts) { + if (contactName.equalsIgnoreCase(name)) { + activeChar.sendPacket(SystemMessageId.NAME_ALREADY_EXIST_ON_CONTACT_LIST); + return false; + } + } + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(QUERY_ADD)) { + ps.setInt(1, activeChar.getObjectId()); + ps.setInt(2, contactId); + ps.execute(); + + _contacts.add(name); + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SUCCESSFULLY_ADDED_TO_CONTACT_LIST); + sm.addString(name); + activeChar.sendPacket(sm); + } catch (Exception e) { + LOG.warn("Error found in {}'s ContactsList: {}", activeChar.getName(), e.getMessage(), e); + } + return true; + } + + public void remove(String name) { + int contactId = CharNameTable.getInstance().getIdByName(name); + + if (!_contacts.contains(name)) { + activeChar.sendPacket(SystemMessageId.NAME_NOT_REGISTERED_ON_CONTACT_LIST); + return; + } else if (contactId < 1) { + // TODO: Message? + return; + } + + _contacts.remove(name); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(QUERY_REMOVE)) { + ps.setInt(1, activeChar.getObjectId()); + ps.setInt(2, contactId); + ps.execute(); + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SUCCESFULLY_DELETED_FROM_CONTACT_LIST); + sm.addString(name); + activeChar.sendPacket(sm); + } catch (Exception e) { + LOG.warn("Error found in {}'s ContactsList: {}", activeChar.getName(), e.getMessage(), e); + } + } + + public List getAllContacts() { + return _contacts; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Crest.java b/src/main/java/com/l2jserver/gameserver/model/L2Crest.java new file mode 100644 index 00000000..8f988262 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Crest.java @@ -0,0 +1,104 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.hexId; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.network.serverpackets.AllyCrest; +import com.l2jserver.gameserver.network.serverpackets.ExPledgeCrestLarge; +import com.l2jserver.gameserver.network.serverpackets.PledgeCrest; + +/** + * @author NosBit + */ +public final class L2Crest implements IIdentifiable { + public enum CrestType { + PLEDGE(1), + PLEDGE_LARGE(2), + ALLY(3); + + private final int _id; + + CrestType(int id) { + _id = id; + } + + public int getId() { + return _id; + } + + public static CrestType getById(int id) { + for (CrestType crestType : values()) { + if (crestType.getId() == id) { + return crestType; + } + } + return null; + } + } + + private final int _id; + private final byte[] _data; + private final CrestType _type; + + public L2Crest(int id, byte[] data, CrestType type) { + _id = id; + _data = data; + _type = type; + } + + @Override + public int getId() { + return _id; + } + + public byte[] getData() { + return _data; + } + + public CrestType getType() { + return _type; + } + + /** + * Gets the client path to crest for use in html and sends the crest to {@code L2PcInstance} + * @param activeChar the @{code L2PcInstance} where html is send to. + * @return the client path to crest + */ + public String getClientPath(L2PcInstance activeChar) { + String path = null; + switch (getType()) { + case PLEDGE -> { + activeChar.sendPacket(new PledgeCrest(getId(), getData())); + path = "Crest.crest_" + hexId().getServerID() + "_" + getId(); + } + case PLEDGE_LARGE -> { + activeChar.sendPacket(new ExPledgeCrestLarge(getId(), getData())); + path = "Crest.crest_" + hexId().getServerID() + "_" + getId() + "_l"; + } + case ALLY -> { + activeChar.sendPacket(new AllyCrest(getId(), getData())); + path = "Crest.crest_" + hexId().getServerID() + "_" + getId(); + } + } + return path; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillGroup.java b/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillGroup.java new file mode 100644 index 00000000..d716bf39 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillGroup.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public final class L2EnchantSkillGroup { + private final int _id; + private final List _enchantDetails = new ArrayList<>(); + + public L2EnchantSkillGroup(int id) { + _id = id; + } + + public void addEnchantDetail(EnchantSkillHolder detail) { + _enchantDetails.add(detail); + } + + public int getId() { + return _id; + } + + public List getEnchantGroupDetails() { + return _enchantDetails; + } + + public static class EnchantSkillHolder { + private final int _level; + private final int _adenaCost; + private final int _expCost; + private final int _spCost; + private final byte[] _rate; + + public EnchantSkillHolder(StatsSet set) { + _level = set.getInt("level"); + _adenaCost = set.getInt("adena"); + _expCost = set.getInt("exp"); + _spCost = set.getInt("sp"); + _rate = new byte[24]; + for (int i = 0; i < 24; i++) { + _rate[i] = set.getByte("chance" + (76 + i), (byte) 0); + } + } + + /** + * @return Returns the level. + */ + public int getLevel() { + return _level; + } + + /** + * @return Returns the spCost. + */ + public int getSpCost() { + return _spCost; + } + + public int getExpCost() { + return _expCost; + } + + public int getAdenaCost() { + return _adenaCost; + } + + public byte getRate(L2PcInstance ply) { + if (ply.getLevel() < 76) { + return 0; + } + return _rate[ply.getLevel() - 76]; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillLearn.java b/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillLearn.java new file mode 100644 index 00000000..5aee5bfd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2EnchantSkillLearn.java @@ -0,0 +1,97 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Set; +import java.util.TreeMap; + +import com.l2jserver.gameserver.data.xml.impl.EnchantSkillGroupsData; +import com.l2jserver.gameserver.model.L2EnchantSkillGroup.EnchantSkillHolder; + +public final class L2EnchantSkillLearn { + private final int _id; + private final int _baseLvl; + private final TreeMap _enchantRoutes = new TreeMap<>(); + + public L2EnchantSkillLearn(int id, int baseLvl) { + _id = id; + _baseLvl = baseLvl; + } + + public void addNewEnchantRoute(int route, int group) { + _enchantRoutes.put(route, group); + } + + public int getId() { + return _id; + } + + public int getBaseLevel() { + return _baseLvl; + } + + public static int getEnchantRoute(int level) { + return (int) Math.floor(level / 100.0); + } + + public static int getEnchantIndex(int level) { + return (level % 100) - 1; + } + + public L2EnchantSkillGroup getFirstRouteGroup() { + return EnchantSkillGroupsData.getInstance().getEnchantSkillGroupById(_enchantRoutes.firstEntry().getValue()); + } + + public Set getAllRoutes() { + return _enchantRoutes.keySet(); + } + + public int getMinSkillLevel(int level) { + if ((level % 100) == 1) { + return _baseLvl; + } + return level - 1; + } + + public boolean isMaxEnchant(int level) { + int route = getEnchantRoute(level); + if ((route < 1) || !_enchantRoutes.containsKey(route)) { + return false; + } + int index = getEnchantIndex(level); + + return (index + 1) >= EnchantSkillGroupsData.getInstance().getEnchantSkillGroupById(_enchantRoutes.get(route)).getEnchantGroupDetails().size(); + } + + public EnchantSkillHolder getEnchantSkillHolder(int level) { + int route = getEnchantRoute(level); + if ((route < 1) || !_enchantRoutes.containsKey(route)) { + return null; + } + int index = getEnchantIndex(level); + L2EnchantSkillGroup group = EnchantSkillGroupsData.getInstance().getEnchantSkillGroupById(_enchantRoutes.get(route)); + + if (index < 0) { + return group.getEnchantGroupDetails().get(0); + } else if (index >= group.getEnchantGroupDetails().size()) { + return group.getEnchantGroupDetails().get(EnchantSkillGroupsData.getInstance().getEnchantSkillGroupById(_enchantRoutes.get(route)).getEnchantGroupDetails().size() - 1); + } + return group.getEnchantGroupDetails().get(index); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProduct.java b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProduct.java new file mode 100644 index 00000000..960725e6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProduct.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author JIV + */ +public class L2ExtractableProduct { + private final int _id; + private final int _min; + private final int _max; + private final int _chance; + + /** + * Create Extractable product + * @param id crete item id + * @param min item count max + * @param max item count min + * @param chance chance for creating + */ + public L2ExtractableProduct(int id, int min, int max, double chance) { + _id = id; + _min = min; + _max = max; + _chance = (int) (chance * 1000); + } + + public int getId() { + return _id; + } + + public int getMin() { + return _min; + } + + public int getMax() { + return _max; + } + + public int getChance() { + return _chance; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProductItem.java b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProductItem.java new file mode 100644 index 00000000..bbad6f96 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableProductItem.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; + +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author Zoey76 + */ +public class L2ExtractableProductItem { + private final List _items; + private final double _chance; + + public L2ExtractableProductItem(List items, double chance) { + _items = items; + _chance = chance; + } + + public List getItems() { + return _items; + } + + public double getChance() { + return _chance; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ExtractableSkill.java b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableSkill.java new file mode 100644 index 00000000..c7a90eab --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ExtractableSkill.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; + +/** + * Extractable skill DTO. + * @author Zoey76 + */ +public class L2ExtractableSkill { + private final int _hash; + private final List _product; + + /** + * Instantiates a new extractable skill. + * @param hash the hash + * @param products the products + */ + public L2ExtractableSkill(int hash, List products) { + _hash = hash; + _product = products; + } + + /** + * Gets the skill hash. + * @return the skill hash + */ + public int getSkillHash() { + return _hash; + } + + /** + * Gets the product items. + * @return the product items + */ + public List getProductItems() { + return _product; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2GroupSpawn.java b/src/main/java/com/l2jserver/gameserver/model/L2GroupSpawn.java new file mode 100644 index 00000000..a1938f38 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2GroupSpawn.java @@ -0,0 +1,100 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.data.sql.impl.TerritoryTable; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2ControllableMobInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @author littlecrow + */ +public class L2GroupSpawn extends L2Spawn { + + private static final Logger LOG = LoggerFactory.getLogger(L2GroupSpawn.class); + + private final L2NpcTemplate _template; + + public L2GroupSpawn(L2NpcTemplate mobTemplate) throws SecurityException, ClassNotFoundException, NoSuchMethodException { + super(mobTemplate); + _template = mobTemplate; + + setAmount(1); + } + + public L2Npc doGroupSpawn() { + try { + if (_template.isType("L2Pet") || _template.isType("L2Minion")) { + return null; + } + + int newLocX = 0; + int newLocY = 0; + int newLocZ = 0; + + if ((getX() == 0) && (getY() == 0)) { + if (getLocationId() == 0) { + return null; + } + + final Location location = TerritoryTable.getInstance().getRandomPoint(getLocationId()); + if (location != null) { + newLocX = location.getX(); + newLocY = location.getY(); + newLocZ = location.getZ(); + } + } else { + newLocX = getX(); + newLocY = getY(); + newLocZ = getZ(); + } + + final var objectId = IdFactory.getInstance().getNextId(); + final var mob = new L2ControllableMobInstance(objectId, _template); + mob.setCurrentHpMp(mob.getMaxHp(), mob.getMaxMp()); + + if (getHeading() == -1) { + mob.setHeading(Rnd.nextInt(61794)); + } else { + mob.setHeading(getHeading()); + } + + mob.setSpawn(this); + mob.spawnMe(newLocX, newLocY, newLocZ); + mob.onSpawn(); + + if (general().debug()) { + LOG.trace("Spawned Mob Id: {} ,at: X: {} Y: {} Z: {}", _template.getId(), mob.getX(), mob.getY(), mob.getZ()); + } + return mob; + + } catch (Exception e) { + LOG.warn("NPC class not found: {}", e.getMessage(), e); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2ManufactureItem.java b/src/main/java/com/l2jserver/gameserver/model/L2ManufactureItem.java new file mode 100644 index 00000000..81579e28 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2ManufactureItem.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.data.xml.impl.RecipeData; + +public class L2ManufactureItem { + private final int _recipeId; + private final long _cost; + private final boolean _isDwarven; + + public L2ManufactureItem(int recipeId, long cost) { + _recipeId = recipeId; + _cost = cost; + _isDwarven = RecipeData.getInstance().getRecipeList(_recipeId).isDwarvenRecipe(); + } + + public int getRecipeId() { + return _recipeId; + } + + public long getCost() { + return _cost; + } + + public boolean isDwarven() { + return _isDwarven; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2MapRegion.java b/src/main/java/com/l2jserver/gameserver/model/L2MapRegion.java new file mode 100644 index 00000000..762f4887 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2MapRegion.java @@ -0,0 +1,186 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.enums.Race; + +/** + * @author Nyaran + */ +public class L2MapRegion { + private final String _name; + private final String _town; + private final int _locId; + private final int _castle; + private final int _bbs; + private List _maps = null; + + private List _spawnLocs = null; + private List _otherSpawnLocs = null; + private List _chaoticSpawnLocs = null; + private List _banishSpawnLocs = null; + + private final Map _bannedRace = new HashMap<>(); + + public L2MapRegion(String name, String town, int locId, int castle, int bbs) { + _name = name; + _town = town; + _locId = locId; + _castle = castle; + _bbs = bbs; + } + + public final String getName() { + return _name; + } + + public final String getTown() { + return _town; + } + + public final int getLocId() { + return _locId; + } + + public final int getCastle() { + return _castle; + } + + public final int getBbs() { + return _bbs; + } + + public final void addMap(int x, int y) { + if (_maps == null) { + _maps = new ArrayList<>(); + } + + _maps.add(new int[] { + x, + y + }); + } + + public final List getMaps() { + return _maps; + } + + public final boolean isZoneInRegion(int x, int y) { + if (_maps == null) { + return false; + } + + for (int[] map : _maps) { + if ((map[0] == x) && (map[1] == y)) { + return true; + } + } + return false; + } + + // Respawn + public final void addSpawn(int x, int y, int z) { + if (_spawnLocs == null) { + _spawnLocs = new ArrayList<>(); + } + + _spawnLocs.add(new Location(x, y, z)); + } + + public final void addOtherSpawn(int x, int y, int z) { + if (_otherSpawnLocs == null) { + _otherSpawnLocs = new ArrayList<>(); + } + + _otherSpawnLocs.add(new Location(x, y, z)); + } + + public final void addChaoticSpawn(int x, int y, int z) { + if (_chaoticSpawnLocs == null) { + _chaoticSpawnLocs = new ArrayList<>(); + } + + _chaoticSpawnLocs.add(new Location(x, y, z)); + } + + public final void addBanishSpawn(int x, int y, int z) { + if (_banishSpawnLocs == null) { + _banishSpawnLocs = new ArrayList<>(); + } + + _banishSpawnLocs.add(new Location(x, y, z)); + } + + public final List getSpawns() { + return _spawnLocs; + } + + public final Location getSpawnLoc() { + if (character().randomRespawnInTown()) { + return _spawnLocs.get(Rnd.get(_spawnLocs.size())); + } + return _spawnLocs.get(0); + } + + public final Location getOtherSpawnLoc() { + if (_otherSpawnLocs != null) { + if (character().randomRespawnInTown()) { + return _otherSpawnLocs.get(Rnd.get(_otherSpawnLocs.size())); + } + return _otherSpawnLocs.get(0); + } + return getSpawnLoc(); + } + + public final Location getChaoticSpawnLoc() { + if (_chaoticSpawnLocs != null) { + if (character().randomRespawnInTown()) { + return _chaoticSpawnLocs.get(Rnd.get(_chaoticSpawnLocs.size())); + } + return _chaoticSpawnLocs.get(0); + } + return getSpawnLoc(); + } + + public final Location getBanishSpawnLoc() { + if (_banishSpawnLocs != null) { + if (character().randomRespawnInTown()) { + return _banishSpawnLocs.get(Rnd.get(_banishSpawnLocs.size())); + } + return _banishSpawnLocs.get(0); + } + return getSpawnLoc(); + } + + public final void addBannedRace(String race, String point) { + _bannedRace.put(Race.valueOf(race), point); + } + + public final Map getBannedRace() { + return _bannedRace; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2NpcWalkerNode.java b/src/main/java/com/l2jserver/gameserver/model/L2NpcWalkerNode.java new file mode 100644 index 00000000..96cad377 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2NpcWalkerNode.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.network.NpcStringId; + +/** + * @author Rayan RPG + * @author JIV + * @since 927 + */ +public class L2NpcWalkerNode extends Location { + private final String _chatString; + private final NpcStringId _npcString; + private final int _delay; + private final boolean _runToLocation; + + public L2NpcWalkerNode(int moveX, int moveY, int moveZ, int delay, boolean runToLocation, NpcStringId npcString, String chatText) { + super(moveX, moveY, moveZ); + _delay = delay; + _runToLocation = runToLocation; + _npcString = npcString; + _chatString = ((chatText == null) ? "" : chatText); + } + + public int getDelay() { + return _delay; + } + + public boolean runToLocation() { + return _runToLocation; + } + + public NpcStringId getNpcString() { + return _npcString; + } + + public String getChatText() { + if (_npcString != null) { + throw new IllegalStateException("npcString is defined for walker route!"); + } + return _chatString; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Object.java b/src/main/java/com/l2jserver/gameserver/model/L2Object.java new file mode 100644 index 00000000..bef2d7aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Object.java @@ -0,0 +1,874 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.ShotType; +import com.l2jserver.gameserver.handler.ActionHandler; +import com.l2jserver.gameserver.handler.ActionShiftHandler; +import com.l2jserver.gameserver.handler.IActionHandler; +import com.l2jserver.gameserver.handler.IActionShiftHandler; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.knownlist.ObjectKnownList; +import com.l2jserver.gameserver.model.actor.poly.ObjectPoly; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.events.ListenersContainer; +import com.l2jserver.gameserver.model.interfaces.IDecayable; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.interfaces.INamable; +import com.l2jserver.gameserver.model.interfaces.IPositionable; +import com.l2jserver.gameserver.model.interfaces.ISpawnable; +import com.l2jserver.gameserver.model.interfaces.IUniqueId; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.DeleteObject; +import com.l2jserver.gameserver.network.serverpackets.ExSendUIEvent; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.util.Util; + +/** + * Base class for all interactive objects. + */ +public abstract class L2Object extends ListenersContainer implements IIdentifiable, INamable, ISpawnable, IUniqueId, IDecayable, IPositionable { + /** Name */ + private String _name; + /** Object ID */ + private int _objectId; + /** World Region */ + private L2WorldRegion _worldRegion; + /** Instance type */ + private InstanceType _instanceType = null; + private volatile Map _scripts; + /** X coordinate */ + private final AtomicInteger _x = new AtomicInteger(0); + /** Y coordinate */ + private final AtomicInteger _y = new AtomicInteger(0); + /** Z coordinate */ + private final AtomicInteger _z = new AtomicInteger(0); + /** Orientation */ + private final AtomicInteger _heading = new AtomicInteger(0); + /** Instance id of object. 0 - Global */ + private final AtomicInteger _instanceId = new AtomicInteger(0); + private boolean _isVisible; + private boolean _isInvisible; + private ObjectKnownList _knownList; + + public L2Object(int objectId) { + setInstanceType(InstanceType.L2Object); + _objectId = objectId; + initKnownList(); + } + + /** + * Gets the instance type of object. + * @return the instance type + */ + public final InstanceType getInstanceType() { + return _instanceType; + } + + /** + * Sets the instance type. + * @param newInstanceType the instance type to set + */ + protected final void setInstanceType(InstanceType newInstanceType) { + _instanceType = newInstanceType; + } + + /** + * Verifies if object is of any given instance types. + * @param instanceTypes the instance types to verify + * @return {@code true} if object is of any given instance types, {@code false} otherwise + */ + public final boolean isInstanceTypes(InstanceType... instanceTypes) { + return _instanceType.isTypes(instanceTypes); + } + + public final void onAction(L2PcInstance player) { + onAction(player, true); + } + + public void onAction(L2PcInstance player, boolean interact) { + IActionHandler handler = ActionHandler.getInstance().getHandler(getInstanceType()); + if (handler != null) { + handler.action(player, this, interact); + } + + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public void onActionShift(L2PcInstance player) { + IActionShiftHandler handler = ActionShiftHandler.getInstance().getHandler(getInstanceType()); + if (handler != null) { + handler.action(player, this, true); + } + + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public void onForcedAttack(L2PcInstance player) { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public void onSpawn() { + } + + @Override + public boolean decayMe() { + assert getWorldRegion() != null; + + L2WorldRegion reg = getWorldRegion(); + + synchronized (this) { + _isVisible = false; + setWorldRegion(null); + } + + // this can synchronize on others instances, so it's out of + // synchronized, to avoid deadlocks + // Remove the L2Object from the world + L2World.getInstance().removeVisibleObject(this, reg); + L2World.getInstance().removeObject(this); + + return true; + } + + public void refreshID() { + L2World.getInstance().removeObject(this); + IdFactory.getInstance().releaseId(getObjectId()); + _objectId = IdFactory.getInstance().getNextId(); + } + + @Override + public final boolean spawnMe() { + assert (getWorldRegion() == null) && (getLocation().getX() != 0) && (getLocation().getY() != 0) && (getLocation().getZ() != 0); + + synchronized (this) { + // Set the x,y,z position of the L2Object spawn and update its _worldregion + _isVisible = true; + setWorldRegion(L2World.getInstance().getRegion(getLocation())); + + // Add the L2Object spawn in the _allobjects of L2World + L2World.getInstance().storeObject(this); + + // Add the L2Object spawn to _visibleObjects and if necessary to _allplayers of its L2WorldRegion + getWorldRegion().addVisibleObject(this); + } + + // this can synchronize on others instances, so it's out of synchronized, to avoid deadlocks + // Add the L2Object spawn in the world as a visible object + L2World.getInstance().addVisibleObject(this, getWorldRegion()); + + onSpawn(); + + return true; + } + + public final void spawnMe(int x, int y, int z) { + assert getWorldRegion() == null; + + synchronized (this) { + // Set the x,y,z position of the L2Object spawn and update its _worldregion + _isVisible = true; + + if (x > L2World.MAP_MAX_X) { + x = L2World.MAP_MAX_X - 5000; + } + if (x < L2World.MAP_MIN_X) { + x = L2World.MAP_MIN_X + 5000; + } + if (y > L2World.MAP_MAX_Y) { + y = L2World.MAP_MAX_Y - 5000; + } + if (y < L2World.MAP_MIN_Y) { + y = L2World.MAP_MIN_Y + 5000; + } + + setXYZ(x, y, z); + setWorldRegion(L2World.getInstance().getRegion(getLocation())); + + // Add the L2Object spawn in the _allobjects of L2World + } + + L2World.getInstance().storeObject(this); + + // these can synchronize on others instances, so they're out of + // synchronized, to avoid deadlocks + + // Add the L2Object spawn to _visibleObjects and if necessary to _allplayers of its L2WorldRegion + getWorldRegion().addVisibleObject(this); + + // Add the L2Object spawn in the world as a visible object + L2World.getInstance().addVisibleObject(this, getWorldRegion()); + + onSpawn(); + } + + /** + * Verify if object can be attacked. + * @return {@code true} if object can be attacked, {@code false} otherwise + */ + public boolean canBeAttacked() { + return false; + } + + public abstract boolean isAutoAttackable(L2Character attacker); + + public final boolean isVisible() { + return getWorldRegion() != null; + } + + public final void setIsVisible(boolean value) { + _isVisible = value; + if (!_isVisible) { + setWorldRegion(null); + } + } + + public void toggleVisible() { + if (isVisible()) { + decayMe(); + } else { + spawnMe(); + } + } + + public ObjectKnownList getKnownList() { + return _knownList; + } + + public void initKnownList() { + _knownList = new ObjectKnownList(this); + } + + public final void setKnownList(ObjectKnownList value) { + _knownList = value; + } + + @Override + public String getName() { + return _name; + } + + public void setName(String value) { + _name = value; + } + + @Override + public final int getObjectId() { + return _objectId; + } + + public final ObjectPoly getPoly() { + final ObjectPoly poly = getScript(ObjectPoly.class); + return (poly == null) ? addScript(new ObjectPoly(this)) : poly; + } + + public abstract void sendInfo(L2PcInstance activeChar); + + public void sendPacket(L2GameServerPacket mov) { + } + + public void sendPacket(SystemMessageId id) { + } + + public L2PcInstance getActingPlayer() { + return null; + } + + /** + * Verify if object is instance of L2Attackable. + * @return {@code true} if object is instance of L2Attackable, {@code false} otherwise + */ + public boolean isAttackable() { + return false; + } + + /** + * Verify if object is instance of L2Character. + * @return {@code true} if object is instance of L2Character, {@code false} otherwise + */ + public boolean isCharacter() { + return false; + } + + /** + * Verify if object is instance of L2DoorInstance. + * @return {@code true} if object is instance of L2DoorInstance, {@code false} otherwise + */ + public boolean isDoor() { + return false; + } + + /** + * Verify if object is instance of L2MonsterInstance. + * @return {@code true} if object is instance of L2MonsterInstance, {@code false} otherwise + */ + public boolean isMonster() { + return false; + } + + /** + * Verify if object is instance of L2Npc. + * @return {@code true} if object is instance of L2Npc, {@code false} otherwise + */ + public boolean isNpc() { + return false; + } + + /** + * Verify if object is instance of L2PetInstance. + * @return {@code true} if object is instance of L2PetInstance, {@code false} otherwise + */ + public boolean isPet() { + return false; + } + + /** + * Verify if object is instance of L2PcInstance. + * @return {@code true} if object is instance of L2PcInstance, {@code false} otherwise + */ + public boolean isPlayer() { + return false; + } + + /** + * Verify if object is instance of L2Playable. + * @return {@code true} if object is instance of L2Playable, {@code false} otherwise + */ + public boolean isPlayable() { + return false; + } + + /** + * Verify if object is instance of L2ServitorInstance. + * @return {@code true} if object is instance of L2ServitorInstance, {@code false} otherwise + */ + public boolean isServitor() { + return false; + } + + /** + * Verify if object is instance of L2Summon. + * @return {@code true} if object is instance of L2Summon, {@code false} otherwise + */ + public boolean isSummon() { + return false; + } + + /** + * Verify if object is instance of L2TrapInstance. + * @return {@code true} if object is instance of L2TrapInstance, {@code false} otherwise + */ + public boolean isTrap() { + return false; + } + + /** + * Verify if object is instance of L2ItemInstance. + * @return {@code true} if object is instance of L2ItemInstance, {@code false} otherwise + */ + public boolean isItem() { + return false; + } + + /** + * Verifies if the object is a walker NPC. + * @return {@code true} if object is a walker NPC, {@code false} otherwise + */ + public boolean isWalker() { + return false; + } + + /** + * Verifies if this object is a vehicle. + * @return {@code true} if object is Vehicle, {@code false} otherwise + */ + public boolean isVehicle() { + return false; + } + + /** + * @return {@code true} if object Can be targeted + */ + public boolean isTargetable() { + return true; + } + + /** + * Check if the object is in the given zone Id. + * @param zone the zone Id to check + * @return {@code true} if the object is in that zone Id + */ + public boolean isInsideZone(ZoneId zone) { + return false; + } + + /** + * Check if current object has charged shot. + * @param type of the shot to be checked. + * @return {@code true} if the object has charged shot + */ + public boolean isChargedShot(ShotType type) { + return false; + } + + /** + * Charging shot into the current object. + * @param type of the shot to be charged. + * @param charged + */ + public void setChargedShot(ShotType type, boolean charged) { + } + + /** + * Try to recharge a shot. + * @param physical skill are using Soul shots. + * @param magical skill are using Spirit shots. + */ + public void rechargeShots(boolean physical, boolean magical) { + } + + /** + * @param + * @param script + * @return + */ + public final T addScript(T script) { + if (_scripts == null) { + synchronized (this) { + if (_scripts == null) { + _scripts = new ConcurrentHashMap<>(); + } + } + } + _scripts.put(script.getClass().getName(), script); + return script; + } + + /** + * @param + * @param script + * @return + */ + @SuppressWarnings("unchecked") + public final T removeScript(Class script) { + if (_scripts == null) { + return null; + } + return (T) _scripts.remove(script.getName()); + } + + /** + * @param + * @param script + * @return + */ + @SuppressWarnings("unchecked") + public final T getScript(Class script) { + if (_scripts == null) { + return null; + } + return (T) _scripts.get(script.getName()); + } + + public void removeStatusListener(L2Character object) { + + } + + protected void badCoords() { + if (isCharacter()) { + decayMe(); + } else if (isPlayer()) { + ((L2Character) this).teleToLocation(new Location(0, 0, 0), false); + ((L2Character) this).sendMessage("Error with your coords, Please ask a GM for help!"); + } + } + + public final void setXYZInvisible(int x, int y, int z) { + assert getWorldRegion() == null; + if (x > L2World.MAP_MAX_X) { + x = L2World.MAP_MAX_X - 5000; + } + if (x < L2World.MAP_MIN_X) { + x = L2World.MAP_MIN_X + 5000; + } + if (y > L2World.MAP_MAX_Y) { + y = L2World.MAP_MAX_Y - 5000; + } + if (y < L2World.MAP_MIN_Y) { + y = L2World.MAP_MIN_Y + 5000; + } + + setXYZ(x, y, z); + setIsVisible(false); + } + + public final void setLocationInvisible(ILocational loc) { + setXYZInvisible(loc.getX(), loc.getY(), loc.getZ()); + } + + public void updateWorldRegion() { + if (!isVisible()) { + return; + } + + L2WorldRegion newRegion = L2World.getInstance().getRegion(getLocation()); + if (newRegion != getWorldRegion()) { + getWorldRegion().removeVisibleObject(this); + + setWorldRegion(newRegion); + + // Add the L2Object spawn to _visibleObjects and if necessary to _allplayers of its L2WorldRegion + getWorldRegion().addVisibleObject(this); + } + } + + public final L2WorldRegion getWorldRegion() { + return _worldRegion; + } + + public void setWorldRegion(L2WorldRegion value) { + if ((getWorldRegion() != null) && isCharacter()) // confirm revalidation of old region's zones + { + if (value != null) { + getWorldRegion().revalidateZones((L2Character) this); // at world region change + } else { + getWorldRegion().removeFromZones((L2Character) this); // at world region change + } + } + + _worldRegion = value; + } + + /** + * Gets the X coordinate. + * @return the X coordinate + */ + @Override + public int getX() { + return _x.get(); + } + + /** + * Gets the Y coordinate. + * @return the Y coordinate + */ + @Override + public int getY() { + return _y.get(); + } + + /** + * Gets the Z coordinate. + * @return the Z coordinate + */ + @Override + public int getZ() { + return _z.get(); + } + + /** + * Gets the heading. + * @return the heading + */ + @Override + public int getHeading() { + return _heading.get(); + } + + /** + * Gets the instance ID. + * @return the instance ID + */ + @Override + public int getInstanceId() { + return _instanceId.get(); + } + + /** + * Gets the location object. + * @return the location object + */ + @Override + public Location getLocation() { + return new Location(getX(), getY(), getZ(), getHeading(), getInstanceId()); + } + + /** + * Sets the X coordinate + * @param newX the X coordinate + */ + @Override + public void setX(int newX) { + _x.set(newX); + } + + /** + * Sets the Y coordinate + * @param newY the Y coordinate + */ + @Override + public void setY(int newY) { + _y.set(newY); + } + + /** + * Sets the Z coordinate + * @param newZ the Z coordinate + */ + @Override + public void setZ(int newZ) { + _z.set(newZ); + } + + /** + * Sets the x, y, z coordinate. + * @param newX the X coordinate + * @param newY the Y coordinate + * @param newZ the Z coordinate + */ + @Override + public final void setXYZ(int newX, int newY, int newZ) { + assert getWorldRegion() != null; + + setX(newX); + setY(newY); + setZ(newZ); + + try { + if (L2World.getInstance().getRegion(getLocation()) != getWorldRegion()) { + updateWorldRegion(); + } + } catch (Exception e) { + badCoords(); + } + } + + /** + * Sets the x, y, z coordinate. + * @param loc the location object + */ + @Override + public void setXYZ(ILocational loc) { + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + } + + /** + * Sets heading of object. + * @param newHeading the new heading + */ + @Override + public void setHeading(int newHeading) { + _heading.set(newHeading); + } + + /** + * Sets the instance ID of object.
    + * 0 - Global
    + * TODO: Add listener here. + * @param instanceId the ID of the instance + */ + @Override + public void setInstanceId(int instanceId) { + if ((instanceId < 0) || (getInstanceId() == instanceId)) { + return; + } + + Instance oldI = InstanceManager.getInstance().getInstance(getInstanceId()); + Instance newI = InstanceManager.getInstance().getInstance(instanceId); + if (newI == null) { + return; + } + + if (isPlayer()) { + final L2PcInstance player = getActingPlayer(); + if ((getInstanceId() > 0) && (oldI != null)) { + oldI.removePlayer(getObjectId()); + if (oldI.isShowTimer()) { + sendInstanceUpdate(oldI, true); + } + } + if (instanceId > 0) { + newI.addPlayer(getObjectId()); + if (newI.isShowTimer()) { + sendInstanceUpdate(newI, false); + } + } + if (player.hasSummon()) { + player.getSummon().setInstanceId(instanceId); + } + } else if (isNpc()) { + final L2Npc npc = (L2Npc) this; + if ((getInstanceId() > 0) && (oldI != null)) { + oldI.removeNpc(npc); + } + if (instanceId > 0) { + newI.addNpc(npc); + } + } + + _instanceId.set(instanceId); + if (_isVisible && (_knownList != null)) { + // We don't want some ugly looking disappear/appear effects, so don't update + // the knownlist here, but players usually enter instancezones through teleporting + // and the teleport will do the revalidation for us. + if (!isPlayer()) { + decayMe(); + spawnMe(); + } + } + } + + /** + * Sets location of object. + * @param loc the location object + */ + @Override + public void setLocation(Location loc) { + _x.set(loc.getX()); + _y.set(loc.getY()); + _z.set(loc.getZ()); + _heading.set(loc.getHeading()); + _instanceId.set(loc.getInstanceId()); + } + + /** + * Calculates distance between this L2Object and given x, y , z. + * @param x the X coordinate + * @param y the Y coordinate + * @param z the Z coordinate + * @param includeZAxis if {@code true} Z axis will be included + * @param squared if {@code true} return will be squared + * @return distance between object and given x, y, z. + */ + public final double calculateDistance(int x, int y, int z, boolean includeZAxis, boolean squared) { + final double distance = Math.pow(x - getX(), 2) + Math.pow(y - getY(), 2) + (includeZAxis ? Math.pow(z - getZ(), 2) : 0); + return (squared) ? distance : Math.sqrt(distance); + } + + /** + * Calculates distance between this L2Object and given location. + * @param loc the location object + * @param includeZAxis if {@code true} Z axis will be included + * @param squared if {@code true} return will be squared + * @return distance between object and given location. + */ + public final double calculateDistance(ILocational loc, boolean includeZAxis, boolean squared) { + return calculateDistance(loc.getX(), loc.getY(), loc.getZ(), includeZAxis, squared); + } + + /** + * Calculates the angle in degrees from this object to the given object.
    + * The return value can be described as how much this object has to turn
    + * to have the given object directly in front of it. + * @param target the object to which to calculate the angle + * @return the angle this object has to turn to have the given object in front of it + */ + public final double calculateDirectionTo(ILocational target) { + int heading = Util.calculateHeadingFrom(this, target) - getHeading(); + if (heading < 0) { + heading = 65535 + heading; + } + return Util.convertHeadingToDegree(heading); + } + + /** + * Sends an instance update for player. + * @param instance the instance to update + * @param hide if {@code true} hide the player + */ + private void sendInstanceUpdate(Instance instance, boolean hide) { + final int startTime = (int) ((System.currentTimeMillis() - instance.getInstanceStartTime()) / 1000); + final int endTime = (int) ((instance.getInstanceEndTime() - instance.getInstanceStartTime()) / 1000); + if (instance.isTimerIncrease()) { + sendPacket(new ExSendUIEvent(getActingPlayer(), hide, true, startTime, endTime, instance.getTimerText())); + } else { + sendPacket(new ExSendUIEvent(getActingPlayer(), hide, false, endTime - startTime, 0, instance.getTimerText())); + } + } + + /** + * @return {@code true} if this object is invisible, {@code false} otherwise. + */ + public boolean isInvisible() { + return _isInvisible; + } + + /** + * Sets this object as invisible or not + * @param invis + */ + public void setInvisible(boolean invis) { + _isInvisible = invis; + if (invis) { + final DeleteObject deletePacket = new DeleteObject(this); + for (L2Object obj : getKnownList().getKnownObjects().values()) { + if ((obj != null) && obj.isPlayer()) { + final L2PcInstance player = obj.getActingPlayer(); + if (!isVisibleFor(player)) { + obj.sendPacket(deletePacket); + } + } + } + } + + // Broadcast information regarding the object to those which are suppose to see. + broadcastInfo(); + } + + /** + * @param character The character we are checking visibility for. + * @return {@code true} if character can see an invisible object if it's invisible, {@code false} otherwise. + */ + public boolean isVisibleFor(final L2Character character) { + return !isInvisible() || character.canOverrideCond(PcCondOverride.SEE_ALL_PLAYERS); + } + + /** + * Broadcasts describing info to known players. + */ + public void broadcastInfo() { + for (L2Object obj : getKnownList().getKnownObjects().values()) { + if ((obj != null) && obj.isPlayer() && isVisibleFor(obj.getActingPlayer())) { + sendInfo(obj.getActingPlayer()); + } + } + } + + @Override + public boolean equals(Object obj) { + return ((obj instanceof L2Object) && (((L2Object) obj).getObjectId() == getObjectId())); + } + + @Override + public String toString() { + return getName() + " [" + getObjectId() + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Party.java b/src/main/java/com/l2jserver/gameserver/model/L2Party.java new file mode 100644 index 00000000..d2b79ed1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Party.java @@ -0,0 +1,924 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.hunting; +import static com.l2jserver.gameserver.config.Configuration.rates; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.SevenSignsFestival; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.PartyDistributionType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; +import com.l2jserver.gameserver.model.entity.DimensionalRift; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExAskModifyPartyLooting; +import com.l2jserver.gameserver.network.serverpackets.ExCloseMPCC; +import com.l2jserver.gameserver.network.serverpackets.ExOpenMPCC; +import com.l2jserver.gameserver.network.serverpackets.ExPartyPetWindowAdd; +import com.l2jserver.gameserver.network.serverpackets.ExPartyPetWindowDelete; +import com.l2jserver.gameserver.network.serverpackets.ExSetPartyLooting; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.PartyMemberPosition; +import com.l2jserver.gameserver.network.serverpackets.PartySmallWindowAdd; +import com.l2jserver.gameserver.network.serverpackets.PartySmallWindowAll; +import com.l2jserver.gameserver.network.serverpackets.PartySmallWindowDelete; +import com.l2jserver.gameserver.network.serverpackets.PartySmallWindowDeleteAll; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * This class serves as a container for player parties. + * @author nuocnam + */ +public class L2Party extends AbstractPlayerGroup { + private static final Logger LOG = LoggerFactory.getLogger(L2Party.class); + + private static final int MAXIMUM_LEVEL_DIFFERENCE = 9; + + // @formatter:off + private static final double[] BONUS_EXP_SP = + { + 1.0, 1.10, 1.20, 1.30, 1.40, 1.50, 2.0, 2.10, 2.20 + }; + // @formatter:on + + private static final Duration PARTY_POSITION_BROADCAST_INTERVAL = Duration.ofSeconds(12); + private static final Duration PARTY_DISTRIBUTION_TYPE_REQUEST_TIMEOUT = Duration.ofSeconds(15); + + private final List _members = new CopyOnWriteArrayList<>(); + private boolean _pendingInvitation = false; + private long _pendingInviteTimeout; + private int _partyLvl; + private volatile PartyDistributionType _distributionType; + private volatile PartyDistributionType _changeRequestDistributionType; + private volatile Future _changeDistributionTypeRequestTask = null; + private volatile Set _changeDistributionTypeAnswers = null; + private int _itemLastLoot = 0; + private L2CommandChannel _commandChannel = null; + private DimensionalRift _dr; + private Future _positionBroadcastTask = null; + private PartyMemberPosition _positionPacket; + private boolean _disbanding = false; + + /** + * The message type send to the party members. + */ + public enum messageType { + Expelled, + Left, + None, + Disconnected + } + + /** + * Construct a new L2Party object with a single member - the leader. + * @param leader the leader of this party + * @param partyDistributionType the item distribution rule of this party + */ + public L2Party(L2PcInstance leader, PartyDistributionType partyDistributionType) { + _members.add(leader); + _partyLvl = leader.getLevel(); + _distributionType = partyDistributionType; + } + + /** + * Check if another player can start invitation process. + * @return {@code true} if this party waits for a response on an invitation, {@code false} otherwise + */ + public boolean getPendingInvitation() { + return _pendingInvitation; + } + + /** + * Set invitation process flag and store time for expiration.
    + * Happens when a player joins party or declines to join. + * @param val the pending invitation state to set + */ + public void setPendingInvitation(boolean val) { + _pendingInvitation = val; + _pendingInviteTimeout = GameTimeController.getInstance().getGameTicks() + (L2PcInstance.REQUEST_TIMEOUT * GameTimeController.TICKS_PER_SECOND); + } + + /** + * Check if a player invitation request is expired. + * @return {@code true} if time is expired, {@code false} otherwise + * @see com.l2jserver.gameserver.model.actor.instance.L2PcInstance#isRequestExpired() + */ + public boolean isInvitationRequestExpired() { + return (_pendingInviteTimeout <= GameTimeController.getInstance().getGameTicks()); + } + + /** + * Get a random member from this party. + * @param itemId the ID of the item for which the member must have inventory space + * @param target the object of which the member must be within a certain range (must not be null) + * @return a random member from this party or {@code null} if none of the members have inventory space for the specified item + */ + private L2PcInstance getCheckedRandomMember(int itemId, L2Character target) { + List availableMembers = new ArrayList<>(); + for (L2PcInstance member : getMembers()) { + if (member.getInventory().validateCapacityByItemId(itemId) && Util.checkIfInRange(character().getPartyRange2(), target, member, true)) { + availableMembers.add(member); + } + } + if (!availableMembers.isEmpty()) { + return availableMembers.get(Rnd.get(availableMembers.size())); + } + return null; + } + + /** + * get next item looter + */ + private L2PcInstance getCheckedNextLooter(int ItemId, L2Character target) { + for (int i = 0; i < getMemberCount(); i++) { + if (++_itemLastLoot >= getMemberCount()) { + _itemLastLoot = 0; + } + L2PcInstance member; + try { + member = getMembers().get(_itemLastLoot); + if (member.getInventory().validateCapacityByItemId(ItemId) && Util.checkIfInRange(character().getPartyRange2(), target, member, true)) { + return member; + } + } catch (Exception e) { + // continue, take another member if this just logged off + } + } + + return null; + } + + /** + * get next item looter + */ + private L2PcInstance getActualLooter(L2PcInstance player, int ItemId, boolean spoil, L2Character target) { + L2PcInstance looter = null; + + switch (_distributionType) { + case RANDOM: + if (!spoil) { + looter = getCheckedRandomMember(ItemId, target); + } + break; + case RANDOM_INCLUDING_SPOIL: + looter = getCheckedRandomMember(ItemId, target); + break; + case BY_TURN: + if (!spoil) { + looter = getCheckedNextLooter(ItemId, target); + } + break; + case BY_TURN_INCLUDING_SPOIL: + looter = getCheckedNextLooter(ItemId, target); + break; + } + + return looter != null ? looter : player; + } + + /** + * Broadcasts UI update and User Info for new party leader. + */ + public void broadcastToPartyMembersNewLeader() { + for (L2PcInstance member : getMembers()) { + if (member != null) { + member.sendPacket(PartySmallWindowDeleteAll.STATIC_PACKET); + member.sendPacket(new PartySmallWindowAll(member, this)); + member.broadcastUserInfo(); + } + } + } + + /** + * Send a Server->Client packet to all other L2PcInstance of the Party.
    + */ + public void broadcastToPartyMembers(L2PcInstance player, L2GameServerPacket msg) { + for (L2PcInstance member : getMembers()) { + if ((member != null) && (member.getObjectId() != player.getObjectId())) { + member.sendPacket(msg); + } + } + } + + /** + * adds new member to party + */ + public void addPartyMember(L2PcInstance player) { + if (getMembers().contains(player)) { + return; + } + + if (_changeRequestDistributionType != null) { + finishLootRequest(false); // cancel on invite + } + // sends new member party window for all members + // we do all actions before adding member to a list, this speeds things up a little + player.sendPacket(new PartySmallWindowAll(player, this)); + + // sends pets/summons of party members + for (L2PcInstance pMember : getMembers()) { + if ((pMember != null) && pMember.hasSummon()) { + player.sendPacket(new ExPartyPetWindowAdd(pMember.getSummon())); + } + } + + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.YOU_JOINED_S1_PARTY); + msg.addString(getLeader().getName()); + player.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_JOINED_PARTY); + msg.addString(player.getName()); + broadcastPacket(msg); + broadcastPacket(new PartySmallWindowAdd(player, this)); + // send the position of all party members to the new party member + // player.sendPacket(new PartyMemberPosition(this)); + // send the position of the new party member to all party members (except the new one - he knows his own position) + // broadcastToPartyMembers(player, new PartyMemberPosition(this)); + + // if member has pet/summon add it to other as well + if (player.hasSummon()) { + broadcastPacket(new ExPartyPetWindowAdd(player.getSummon())); + } + + // add player to party, adjust party level + getMembers().add(player); + if (player.getLevel() > _partyLvl) { + _partyLvl = player.getLevel(); + } + + // update partySpelled + L2Summon summon; + for (L2PcInstance member : getMembers()) { + if (member != null) { + member.updateEffectIcons(true); // update party icons only + summon = member.getSummon(); + member.broadcastUserInfo(); + if (summon != null) { + summon.updateEffectIcons(); + } + } + } + + if (isInDimensionalRift()) { + _dr.partyMemberInvited(); + } + + // open the CCInformationwindow + if (isInCommandChannel()) { + player.sendPacket(ExOpenMPCC.STATIC_PACKET); + } + + if (_positionBroadcastTask == null) { + _positionBroadcastTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(() -> { + if (_positionPacket == null) { + _positionPacket = new PartyMemberPosition(this); + } else { + _positionPacket.reuse(this); + } + broadcastPacket(_positionPacket); + }, PARTY_POSITION_BROADCAST_INTERVAL.toMillis() / 2, PARTY_POSITION_BROADCAST_INTERVAL.toMillis()); + } + } + + /** + * Removes a party member using its name. + * @param name player the player to be removed from the party. + * @param type the message type {@link messageType}. + */ + public void removePartyMember(String name, messageType type) { + removePartyMember(getPlayerByName(name), type); + } + + /** + * Removes a party member instance. + * @param player the player to be removed from the party. + * @param type the message type {@link messageType}. + */ + public void removePartyMember(L2PcInstance player, messageType type) { + if (getMembers().contains(player)) { + final boolean isLeader = isLeader(player); + if (!_disbanding) { + if ((getMembers().size() == 2) || (isLeader && !character().leavePartyLeader() && (type != messageType.Disconnected))) { + disbandParty(); + return; + } + } + + getMembers().remove(player); + recalculatePartyLevel(); + + if (player.isFestivalParticipant()) { + SevenSignsFestival.getInstance().updateParticipants(player, this); + } + + try { + // Channeling a player! + if (player.isChanneling() && (player.getSkillChannelizer().hasChannelized())) { + player.abortCast(); + } else if (player.isChannelized()) { + player.getSkillChannelized().abortChannelization(); + } + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + + SystemMessage msg; + if (type == messageType.Expelled) { + player.sendPacket(SystemMessageId.HAVE_BEEN_EXPELLED_FROM_PARTY); + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_WAS_EXPELLED_FROM_PARTY); + msg.addString(player.getName()); + broadcastPacket(msg); + } else if ((type == messageType.Left) || (type == messageType.Disconnected)) { + player.sendPacket(SystemMessageId.YOU_LEFT_PARTY); + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_LEFT_PARTY); + msg.addString(player.getName()); + broadcastPacket(msg); + } + + // UI update. + player.sendPacket(PartySmallWindowDeleteAll.STATIC_PACKET); + player.setParty(null); + broadcastPacket(new PartySmallWindowDelete(player)); + if (player.hasSummon()) { + broadcastPacket(new ExPartyPetWindowDelete(player.getSummon())); + } + + if (isInDimensionalRift()) { + _dr.partyMemberExited(player); + } + + // Close the CCInfoWindow + if (isInCommandChannel()) { + player.sendPacket(new ExCloseMPCC()); + } + if (isLeader && (getMembers().size() > 1) && (character().leavePartyLeader() || (type == messageType.Disconnected))) { + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_HAS_BECOME_A_PARTY_LEADER); + msg.addString(getLeader().getName()); + broadcastPacket(msg); + broadcastToPartyMembersNewLeader(); + } else if (getMembers().size() == 1) { + if (isInCommandChannel()) { + // delete the whole command channel when the party who opened the channel is disbanded + if (getCommandChannel().getLeader().getObjectId() == getLeader().getObjectId()) { + getCommandChannel().disbandChannel(); + } else { + getCommandChannel().removeParty(this); + } + } + + if (getLeader() != null) { + getLeader().setParty(null); + } + + if (_changeDistributionTypeRequestTask != null) { + _changeDistributionTypeRequestTask.cancel(true); + _changeDistributionTypeRequestTask = null; + } + if (_positionBroadcastTask != null) { + _positionBroadcastTask.cancel(false); + _positionBroadcastTask = null; + } + _members.clear(); + } + } + } + + /** + * Disperse a party and send a message to all its members. + */ + public void disbandParty() { + _disbanding = true; + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.PARTY_DISPERSED)); + for (L2PcInstance member : _members) { + if (member != null) { + removePartyMember(member, messageType.None); + } + } + } + + /** + * Change party leader (used for string arguments) + * @param name the name of the player to set as the new party leader + */ + public void changePartyLeader(String name) { + setLeader(getPlayerByName(name)); + } + + @Override + public void setLeader(L2PcInstance player) { + if ((player != null) && !player.isInDuel()) { + if (getMembers().contains(player)) { + if (isLeader(player)) { + player.sendPacket(SystemMessageId.YOU_CANNOT_TRANSFER_RIGHTS_TO_YOURSELF); + } else { + // Swap party members + L2PcInstance temp = getLeader(); + int p1 = getMembers().indexOf(player); + getMembers().set(0, player); + getMembers().set(p1, temp); + + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_HAS_BECOME_A_PARTY_LEADER); + msg.addString(getLeader().getName()); + broadcastPacket(msg); + broadcastToPartyMembersNewLeader(); + if (isInCommandChannel() && _commandChannel.isLeader(temp)) { + _commandChannel.setLeader(getLeader()); + msg = SystemMessage.getSystemMessage(SystemMessageId.COMMAND_CHANNEL_LEADER_NOW_C1); + msg.addString(_commandChannel.getLeader().getName()); + _commandChannel.broadcastPacket(msg); + } + if (player.isInPartyMatchRoom()) { + PartyMatchRoom room = PartyMatchRoomList.getInstance().getPlayerRoom(player); + room.changeLeader(player); + } + } + } else { + player.sendPacket(SystemMessageId.YOU_CAN_TRANSFER_RIGHTS_ONLY_TO_ANOTHER_PARTY_MEMBER); + } + } + } + + /** + * finds a player in the party by name + */ + private L2PcInstance getPlayerByName(String name) { + for (L2PcInstance member : getMembers()) { + if (member.getName().equalsIgnoreCase(name)) { + return member; + } + } + return null; + } + + private boolean isItemToEvenlyDistribute(int itemId) { + L2Item item = ItemTable.getInstance().getTemplate(itemId); + if (item != null) { + if ((character().getPartyEvenlyDistributeAllStackableItems() && item.isStackable()) || (character().getPartyEvenlyDistributeAllOtherItems() && !item.isStackable())) { + return true; + } + } + + return character().getPartyEvenlyDistributeItems().contains(itemId); + } + + /** + * distribute item(s) to party members + */ + public void distributeItem(L2PcInstance player, L2ItemInstance item) { + if (isItemToEvenlyDistribute(item.getId()) && item.getCount() > 1) { + distributeItemEvenly(player, item.getId(), item.getCount(), player); + ItemTable.getInstance().destroyItem("Party", item, player, null); + return; + } + + L2PcInstance target = getActualLooter(player, item.getId(), false, player); + target.addItem("Party", item, player, true); + + // Send messages to other party members about reward + if (item.getCount() > 0) { + if (item.getCount() > 1) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_OBTAINED_S3_S2); + msg.addString(target.getName()); + msg.addItemName(item); + msg.addLong(item.getCount()); + broadcastToPartyMembers(target, msg); + } else { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_OBTAINED_S2); + msg.addString(target.getName()); + msg.addItemName(item); + broadcastToPartyMembers(target, msg); + } + } + } + + /** + * Distributes item loot between party members. + * @param player the reference player + * @param itemId the item ID + * @param itemCount the item count + * @param spoil {@code true} if it's spoil loot + * @param target the NPC target + */ + public void distributeItem(L2PcInstance player, int itemId, long itemCount, boolean spoil, L2Attackable target) { + if (isItemToEvenlyDistribute(itemId) && itemCount > 1) { + distributeItemEvenly(player, itemId, itemCount, target); + return; + } + + L2PcInstance looter = getActualLooter(player, itemId, spoil, target); + + looter.addItem(spoil ? "Sweeper Party" : "Party", itemId, itemCount, target, true); + + // Send messages to other party members about reward + if (itemCount > 0) { + if (itemCount > 1) { + SystemMessage msg = spoil ? SystemMessage.getSystemMessage(SystemMessageId.C1_SWEEPED_UP_S3_S2) : SystemMessage.getSystemMessage(SystemMessageId.C1_OBTAINED_S3_S2); + msg.addString(looter.getName()); + msg.addItemName(itemId); + msg.addLong(itemCount); + broadcastToPartyMembers(looter, msg); + } else { + SystemMessage msg = spoil ? SystemMessage.getSystemMessage(SystemMessageId.C1_SWEEPED_UP_S2) : SystemMessage.getSystemMessage(SystemMessageId.C1_OBTAINED_S2); + msg.addString(looter.getName()); + msg.addItemName(itemId); + broadcastToPartyMembers(looter, msg); + } + } + } + + /** + * Method overload for {@link L2Party#distributeItem(L2PcInstance, int, long, boolean, L2Attackable)} + * @param player the reference player + * @param item the item holder + * @param spoil {@code true} if it's spoil loot + * @param target the NPC target + */ + public void distributeItem(L2PcInstance player, ItemHolder item, boolean spoil, L2Attackable target) { + distributeItem(player, item.getId(), item.getCount(), spoil, target); + } + + /** + * Distribute an item evenly to party members in range.
    + * Check the number of party members that must be rewarded
    + * @param player owner (picker) + * @param itemId the id of the item to split + * @param itemAmount the amount of the item to split + * @param target the target who drop / pick the adena + */ + public void distributeItemEvenly(L2PcInstance player, int itemId, long itemAmount, L2Character target) { + final var toReward = new HashMap(9); + + for (var member : getMembers()) { + if (Util.checkIfInRange(character().getPartyRange2(), target, member, true)) { + toReward.put(member, 0L); + } + } + + if (!toReward.isEmpty()) { + long leftOver = itemAmount % toReward.size(); + final long count = itemAmount / toReward.size(); + + if (count > 0) { + for (var member : toReward.keySet()) { + toReward.put(member, count); + } + } + + if (leftOver > 0) { + final var keys = new ArrayList<>(toReward.keySet()); + while (leftOver-- > 0) { + final var rndMember = keys.get(Rnd.get(keys.size())); + toReward.compute(rndMember, (_, v) -> v + 1); + } + } + + for (var member : toReward.entrySet()) { + if (member.getValue() > 0) { + if (itemId == Inventory.ADENA_ID) { + member.getKey().addAdena("Party", member.getValue(), player, true); + } else { + member.getKey().addItem("Party", itemId, member.getValue(), target, true); + } + } + } + } + } + + /** + * Distribute Experience and SP rewards to L2PcInstance Party members in the known area of the last attacker.
    + *
    + * Actions : + *
  • Get the L2PcInstance owner of the L2ServitorInstance (if necessary)
  • + *
  • Calculate the Experience and SP reward distribution rate
  • + *
  • Add Experience and SP to the L2PcInstance

  • + * @param xpReward The Experience reward to distribute + * @param spReward The SP reward to distribute + * @param rewardedMembers The list of L2PcInstance to reward + * @param topLvl + * @param partyDmg + * @param target + */ + public void distributeXpAndSp(long xpReward, int spReward, List rewardedMembers, int topLvl, int partyDmg, L2Attackable target) { + final List validMembers = getValidMembers(rewardedMembers, topLvl); + + xpReward *= getExpBonus(validMembers.size()); + spReward *= getSpBonus(validMembers.size()); + + int sqLevelSum = 0; + for (L2PcInstance member : validMembers) { + sqLevelSum += (member.getLevel() * member.getLevel()); + } + + final float vitalityPoints = (target.getVitalityPoints(partyDmg) * rates().getRatePartyXp()) / validMembers.size(); + final boolean useVitalityRate = target.useVitalityRate(); + + for (L2PcInstance member : rewardedMembers) { + if (member.isDead()) { + continue; + } + + // Calculate and add the EXP and SP reward to the member + if (validMembers.contains(member)) { + // The servitor penalty + final float penalty = member.hasServitor() ? ((L2ServitorInstance) member.getSummon()).getExpMultiplier() : 1; + + final double sqLevel = member.getLevel() * member.getLevel(); + final double preCalculation = (sqLevel / sqLevelSum) * penalty; + + // Add the XP/SP points to the requested party member + long addexp = Math.round(xpReward * preCalculation); + int addsp = (int) (spReward * preCalculation); + + addexp = calculateExpSpPartyCutoff(member.getActingPlayer(), topLvl, addexp, addsp, useVitalityRate); + if (addexp > 0) { + member.updateVitalityPoints(vitalityPoints, true, false); + if (!member.isInsideZone(ZoneId.PEACE) && ((member.getLevel() - target.getLevel()) <= MAXIMUM_LEVEL_DIFFERENCE)) { + if (hunting().getNevitEnable()) { + member.getHuntingSystem().startHuntingSystemTask(); + if ((member.getHuntingSystem().getHuntingBonusTime() < hunting().getHuntingBonusMaxTime()) || !hunting().getHuntingBonusLimit()) { + member.getHuntingSystem().addPoints(hunting().getNevitNormalPoints()); + } + } + + member.getRecSystem().startBonusTask(true); + } + } + } else { + member.addExpAndSp(0, 0); + } + } + } + + private long calculateExpSpPartyCutoff(L2PcInstance player, int topLvl, long addExp, int addSp, boolean vit) { + long xp = addExp; + int sp; + if (character().getPartyXpCutoffMethod().equalsIgnoreCase("highfive")) { + int i = 0; + final int lvlDiff = topLvl - player.getLevel(); + for (Entry gap : character().getPartyXpCutoffGaps().entrySet()) { + if ((lvlDiff >= gap.getKey()) && (lvlDiff <= gap.getValue())) { + xp = (addExp * character().getPartyXpCutoffGapPercent().get(i)) / 100; + sp = (addSp * character().getPartyXpCutoffGapPercent().get(i)) / 100; + player.addExpAndSp(xp, sp, vit); + break; + } + i++; + } + } else { + player.addExpAndSp(addExp, addSp, vit); + } + return xp; + } + + /** + * refresh party level + */ + public void recalculatePartyLevel() { + int newLevel = 0; + for (L2PcInstance member : getMembers()) { + if (member == null) { + getMembers().remove(member); + continue; + } + + if (member.getLevel() > newLevel) { + newLevel = member.getLevel(); + } + } + _partyLvl = newLevel; + } + + private List getValidMembers(List members, int topLvl) { + final List validMembers = new ArrayList<>(); + + // Fixed LevelDiff cutoff point + if (character().getPartyXpCutoffMethod().equalsIgnoreCase("level")) { + for (L2PcInstance member : members) { + if ((topLvl - member.getLevel()) <= character().getPartyXpCutoffLevel()) { + validMembers.add(member); + } + } + } + // Fixed MinPercentage cutoff point + else if (character().getPartyXpCutoffMethod().equalsIgnoreCase("percentage")) { + int sqLevelSum = 0; + for (L2PcInstance member : members) { + sqLevelSum += (member.getLevel() * member.getLevel()); + } + + for (L2PcInstance member : members) { + int sqLevel = member.getLevel() * member.getLevel(); + if ((sqLevel * 100) >= (sqLevelSum * character().getPartyXpCutoffPercent())) { + validMembers.add(member); + } + } + } + // Automatic cutoff method + else if (character().getPartyXpCutoffMethod().equalsIgnoreCase("auto")) { + int sqLevelSum = 0; + for (L2PcInstance member : members) { + sqLevelSum += (member.getLevel() * member.getLevel()); + } + + int i = members.size() - 1; + if (i < 1) { + return members; + } + if (i >= BONUS_EXP_SP.length) { + i = BONUS_EXP_SP.length - 1; + } + + for (L2PcInstance member : members) { + int sqLevel = member.getLevel() * member.getLevel(); + if (sqLevel >= (sqLevelSum / (members.size() * members.size()))) { + validMembers.add(member); + } + } + } + // High Five cutoff method + else if (character().getPartyXpCutoffMethod().equalsIgnoreCase("highfive")) { + validMembers.addAll(members); + } else if (character().getPartyXpCutoffMethod().equalsIgnoreCase("none")) { + validMembers.addAll(members); + } + return validMembers; + } + + private double getBaseExpSpBonus(int membersCount) { + int i = membersCount - 1; + if (i < 1) { + return 1; + } + if (i >= BONUS_EXP_SP.length) { + i = BONUS_EXP_SP.length - 1; + } + + return BONUS_EXP_SP[i]; + } + + private double getExpBonus(int membersCount) { + return (membersCount < 2) ? (getBaseExpSpBonus(membersCount)) : (getBaseExpSpBonus(membersCount) * rates().getRatePartyXp()); + } + + private double getSpBonus(int membersCount) { + return (membersCount < 2) ? (getBaseExpSpBonus(membersCount)) : (getBaseExpSpBonus(membersCount) * rates().getRatePartySp()); + } + + @Override + public int getLevel() { + return _partyLvl; + } + + public PartyDistributionType getDistributionType() { + return _distributionType; + } + + public boolean isInCommandChannel() { + return _commandChannel != null; + } + + public L2CommandChannel getCommandChannel() { + return _commandChannel; + } + + public void setCommandChannel(L2CommandChannel channel) { + _commandChannel = channel; + } + + public boolean isInDimensionalRift() { + return _dr != null; + } + + public void setDimensionalRift(DimensionalRift dr) { + _dr = dr; + } + + public DimensionalRift getDimensionalRift() { + return _dr; + } + + /** + * @return the leader of this party + */ + @Override + public L2PcInstance getLeader() { + return _members.getFirst(); + } + + public synchronized void requestLootChange(PartyDistributionType partyDistributionType) { + if (_changeRequestDistributionType != null) { + return; + } + _changeRequestDistributionType = partyDistributionType; + _changeDistributionTypeAnswers = new HashSet<>(); + _changeDistributionTypeRequestTask = ThreadPoolManager.getInstance().scheduleGeneral(() -> finishLootRequest(false), PARTY_DISTRIBUTION_TYPE_REQUEST_TIMEOUT.toMillis()); + + broadcastToPartyMembers(getLeader(), new ExAskModifyPartyLooting(getLeader().getName(), partyDistributionType)); + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.REQUESTING_APPROVAL_CHANGE_PARTY_LOOT_S1); + sm.addSystemString(partyDistributionType.getSysStringId()); + getLeader().sendPacket(sm); + } + + public synchronized void answerLootChangeRequest(L2PcInstance member, boolean answer) { + if (_changeRequestDistributionType == null) { + return; + } + + if (_changeDistributionTypeAnswers.contains(member.getObjectId())) { + return; + } + + if (!answer) { + finishLootRequest(false); + return; + } + + _changeDistributionTypeAnswers.add(member.getObjectId()); + if (_changeDistributionTypeAnswers.size() >= (getMemberCount() - 1)) { + finishLootRequest(true); + } + } + + private synchronized void finishLootRequest(boolean success) { + if (_changeRequestDistributionType == null) { + return; + } + if (_changeDistributionTypeRequestTask != null) { + _changeDistributionTypeRequestTask.cancel(false); + _changeDistributionTypeRequestTask = null; + } + if (success) { + broadcastPacket(new ExSetPartyLooting(1, _changeRequestDistributionType)); + _distributionType = _changeRequestDistributionType; + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PARTY_LOOT_CHANGED_S1); + sm.addSystemString(_changeRequestDistributionType.getSysStringId()); + broadcastPacket(sm); + } else { + broadcastPacket(new ExSetPartyLooting(0, _distributionType)); + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.PARTY_LOOT_CHANGE_CANCELLED)); + } + _changeRequestDistributionType = null; + _changeDistributionTypeAnswers = null; + } + + /** + * @return a list of all members of this party + */ + @Override + public List getMembers() { + return _members; + } + + /** + * Check whether the leader of this party is the same as the leader of the specified party (which essentially means they're the same group). + * @param party the other party to check against + * @return {@code true} if this party equals the specified party, {@code false} otherwise + */ + public boolean equals(L2Party party) { + return (party != null) && (getLeaderObjectId() == party.getLeaderObjectId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2PetData.java b/src/main/java/com/l2jserver/gameserver/model/L2PetData.java new file mode 100644 index 00000000..e3ef351f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2PetData.java @@ -0,0 +1,188 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.IntStream; + +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.holders.SkillHolder; + +/** + * Class hold information about basic pet stats which are same on each level. + * @author JIV + */ +public class L2PetData { + private final Map _levelStats = new HashMap<>(); + private final List _skills = new ArrayList<>(); + + private final int _npcId; + private final int _itemId; + private int _hungryLimit = 1; + private int _minLvl = Byte.MAX_VALUE; + private boolean _syncLevel = false; + private final List _food = new ArrayList<>(); + + public L2PetData(int npcId, int itemId) { + _npcId = npcId; + _itemId = itemId; + } + + /** + * @return the npc id representing this pet. + */ + public int getNpcId() { + return _npcId; + } + + /** + * @return the item id that could summon this pet. + */ + public int getItemId() { + return _itemId; + } + + /** + * @param level the pet's level. + * @param data the pet's data. + */ + public void addNewStat(int level, L2PetLevelData data) { + if (_minLvl > level) { + _minLvl = level; + } + _levelStats.put(level, data); + } + + /** + * @param petLevel the pet's level. + * @return the pet data associated to that pet level. + */ + public L2PetLevelData getPetLevelData(int petLevel) { + return _levelStats.get(petLevel); + } + + /** + * @return the pet's hunger limit. + */ + public int getHungryLimit() { + return _hungryLimit; + } + + /** + * @return {@code true} if pet synchronizes it's level with his master's + */ + public boolean isSyncLevel() { + return _syncLevel; + } + + /** + * @return the pet's minimum level. + */ + public int getMinLevel() { + return _minLvl; + } + + /** + * @return the pet's food list. + */ + public List getFood() { + return _food; + } + + /** + * @param foodId the pet's food Id to add. + */ + public void addFood(Integer foodId) { + _food.add(foodId); + } + + /** + * @param limit the hunger limit to set. + */ + public void setHungryLimit(int limit) { + _hungryLimit = limit; + } + + /** + * @param val synchronizes level with master or not. + */ + public void setSyncLevel(boolean val) { + _syncLevel = val; + } + + /** + * @param skillId the skill Id to add. + * @param skillLvl the skill level. + * @param petLvl the pet's level when this skill is available. + */ + public void addNewSkill(int skillId, int skillLvl, int petLvl) { + _skills.add(new L2PetSkillLearn(skillId, skillLvl, petLvl)); + } + + public int getAvailableLevel(final L2Summon pet, int skillId) { + final StatsSet parameters = pet.getTemplate().getParameters(); + final int currentStep = (int) Math.floor((pet.getLevel() / 5.) - 11); + + return IntStream.rangeClosed(0, currentStep) + .map(i -> currentStep - i) + .mapToObj(step -> IntStream.iterate(1, i -> i + 1) + .mapToObj(skillNum -> parameters.getObject("step" + step + "_skill0" + skillNum, SkillHolder.class)) + .takeWhile(Objects::nonNull) + .filter(skill -> skill.getSkillId() == skillId) + .findFirst()) + .filter(Optional::isPresent) + .map(Optional::get) + .mapToInt(SkillHolder::getSkillLvl) + .findFirst() + .orElse(0); + } + + /** + * @return the list with the pet's skill data. + */ + public List getAvailableSkills() { + return _skills; + } + + public static final class L2PetSkillLearn extends SkillHolder { + private final int _minLevel; + + /** + * @param id the skill Id. + * @param lvl the skill level. + * @param minLvl the minimum level when this skill is available. + */ + public L2PetSkillLearn(int id, int lvl, int minLvl) { + super(id, lvl); + _minLevel = minLvl; + } + + /** + * @return the minimum level for the pet to get the skill. + */ + public int getMinLevel() { + return _minLevel; + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2PetLevelData.java b/src/main/java/com/l2jserver/gameserver/model/L2PetLevelData.java new file mode 100644 index 00000000..ab8e2fe0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2PetLevelData.java @@ -0,0 +1,194 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.stats.MoveType; + +/** + * Stats definition for each pet level. + * @author JIV + * @author Zoey76 + */ +public class L2PetLevelData { + private final int _ownerExpTaken; + private final int _petFeedBattle; + private final int _petFeedNormal; + private final float _petMAtk; + private final long _petMaxExp; + private final int _petMaxFeed; + private final float _petMaxHP; + private final float _petMaxMP; + private final float _petMDef; + private final float _petPAtk; + private final float _petPDef; + private final float _petRegenHP; + private final float _petRegenMP; + private final short _petSoulShot; + private final short _petSpiritShot; + private final double _walkSpeedOnRide; + private final double _runSpeedOnRide; + private final double _slowSwimSpeedOnRide; + private final double _fastSwimSpeedOnRide; + private final double _slowFlySpeedOnRide; + private final double _fastFlySpeedOnRide; + + public L2PetLevelData(StatsSet set) { + _ownerExpTaken = set.getInt("get_exp_type"); + _petMaxExp = (long) set.getDouble("exp"); + _petMaxHP = set.getFloat("org_hp"); + _petMaxMP = set.getFloat("org_mp"); + _petPAtk = set.getFloat("org_pattack"); + _petPDef = set.getFloat("org_pdefend"); + _petMAtk = set.getFloat("org_mattack"); + _petMDef = set.getFloat("org_mdefend"); + _petMaxFeed = set.getInt("max_meal"); + _petFeedBattle = set.getInt("consume_meal_in_battle"); + _petFeedNormal = set.getInt("consume_meal_in_normal"); + _petRegenHP = set.getFloat("org_hp_regen"); + _petRegenMP = set.getFloat("org_mp_regen"); + _petSoulShot = set.getShort("soulshot_count"); + _petSpiritShot = set.getShort("spiritshot_count"); + _walkSpeedOnRide = set.getDouble("walkSpeedOnRide", 0); + _runSpeedOnRide = set.getDouble("runSpeedOnRide", 0); + _slowSwimSpeedOnRide = set.getDouble("slowSwimSpeedOnRide", 0); + _fastSwimSpeedOnRide = set.getDouble("fastSwimSpeedOnRide", 0); + _slowFlySpeedOnRide = set.getDouble("slowFlySpeedOnRide", 0); + _fastFlySpeedOnRide = set.getDouble("fastFlySpeedOnRide", 0); + } + + /** + * @return the owner's experience points consumed by the pet. + */ + public int getOwnerExpTaken() { + return _ownerExpTaken; + } + + /** + * @return the pet's food consume rate at battle state. + */ + public int getPetFeedBattle() { + return _petFeedBattle; + } + + /** + * @return the pet's food consume rate at normal state. + */ + public int getPetFeedNormal() { + return _petFeedNormal; + } + + /** + * @return the pet's Magical Attack. + */ + public float getPetMAtk() { + return _petMAtk; + } + + /** + * @return the pet's maximum experience points. + */ + public long getPetMaxExp() { + return _petMaxExp; + } + + /** + * @return the pet's maximum feed points. + */ + public int getPetMaxFeed() { + return _petMaxFeed; + } + + /** + * @return the pet's maximum HP. + */ + public float getPetMaxHP() { + return _petMaxHP; + } + + /** + * @return the pet's maximum MP. + */ + public float getPetMaxMP() { + return _petMaxMP; + } + + /** + * @return the pet's Magical Defense. + */ + public float getPetMDef() { + return _petMDef; + } + + /** + * @return the pet's Physical Attack. + */ + public float getPetPAtk() { + return _petPAtk; + } + + /** + * @return the pet's Physical Defense. + */ + public float getPetPDef() { + return _petPDef; + } + + /** + * @return the pet's HP regeneration rate. + */ + public float getPetRegenHP() { + return _petRegenHP; + } + + /** + * @return the pet's MP regeneration rate. + */ + public float getPetRegenMP() { + return _petRegenMP; + } + + /** + * @return the pet's soulshot use count. + */ + public short getPetSoulShot() { + return _petSoulShot; + } + + /** + * @return the pet's spiritshot use count. + */ + public short getPetSpiritShot() { + return _petSpiritShot; + } + + /** + * @param mt movement type + * @return the base riding speed of given movement type. + */ + public double getSpeedOnRide(MoveType mt) { + return switch (mt) { + case WALK -> _walkSpeedOnRide; + case RUN -> _runSpeedOnRide; + case SLOW_SWIM -> _slowSwimSpeedOnRide; + case FAST_SWIM -> _fastSwimSpeedOnRide; + case SLOW_FLY -> _slowFlySpeedOnRide; + case FAST_FLY -> _fastFlySpeedOnRide; + }; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2PremiumItem.java b/src/main/java/com/l2jserver/gameserver/model/L2PremiumItem.java new file mode 100644 index 00000000..c457afe6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2PremiumItem.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + ** @author Gnacik + */ +public class L2PremiumItem { + private final int _itemId; + private long _count; + private final String _sender; + + public L2PremiumItem(int itemId, long count, String sender) { + _itemId = itemId; + _count = count; + _sender = sender; + } + + public void updateCount(long newCount) { + _count = newCount; + } + + public int getItemId() { + return _itemId; + } + + public long getCount() { + return _count; + } + + public String getSender() { + return _sender; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Radar.java b/src/main/java/com/l2jserver/gameserver/model/L2Radar.java new file mode 100644 index 00000000..259ac7e9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Radar.java @@ -0,0 +1,110 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.network.serverpackets.RadarControl.RadarAction.ADD; +import static com.l2jserver.gameserver.network.serverpackets.RadarControl.RadarAction.DELETE; +import static com.l2jserver.gameserver.network.serverpackets.RadarControl.RadarAction.DELETE_ALL; +import static com.l2jserver.gameserver.network.serverpackets.RadarControl.RadarType.FLAG_1; +import static com.l2jserver.gameserver.network.serverpackets.RadarControl.RadarType.FLAG_2; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.RadarControl; + +/** + * Radar. + * @author dalrond + * @author Zoey76 + */ +public final class L2Radar { + private final L2PcInstance _player; + private final Set _markers = ConcurrentHashMap.newKeySet(); + + public L2Radar(L2PcInstance player) { + _player = player; + } + + public void showRadar(int x, int y, int z, int type) { + _markers.add(new RadarMarker(x, y, z)); + _player.sendPacket(new RadarControl(DELETE_ALL, FLAG_2, x, y, z)); + _player.sendPacket(new RadarControl(ADD, FLAG_1, x, y, z)); + } + + public void deleteRadar(int x, int y, int z, int type) { + _markers.remove(new RadarMarker(x, y, z)); + _player.sendPacket(new RadarControl(DELETE, FLAG_1, x, y, z)); + } + + public void deleteAllRadar(int type) { + for (var r : _markers) { + _player.sendPacket(new RadarControl(DELETE_ALL, FLAG_2, r._x, r._y, r._z)); + } + _markers.clear(); + } + + public void loadMarkers() { + _player.sendPacket(new RadarControl(DELETE_ALL, FLAG_2, _player.getX(), _player.getY(), _player.getZ())); + for (var r : _markers) { + _player.sendPacket(new RadarControl(ADD, FLAG_1, r._x, r._y, r._z)); + } + } + + static class RadarMarker { + int _type, _x, _y, _z; + + RadarMarker(int type, int x, int y, int z) { + _type = type; + _x = x; + _y = y; + _z = z; + } + + RadarMarker(int x, int y, int z) { + _type = 1; + _x = x; + _y = y; + _z = z; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (prime * result) + _type; + result = (prime * result) + _x; + result = (prime * result) + _y; + result = (prime * result) + _z; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RadarMarker other)) { + return false; + } + return (_type == other._type) && (_x == other._x) && (_y == other._y) && (_z == other._z); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2RecipeInstance.java b/src/main/java/com/l2jserver/gameserver/model/L2RecipeInstance.java new file mode 100644 index 00000000..45312c47 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2RecipeInstance.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * This class describes a RecipeList component (1 line of the recipe : Item-Quantity needed). + */ +public class L2RecipeInstance { + /** The Identifier of the item needed in the L2RecipeInstance */ + private final int _itemId; + + /** The item quantity needed in the L2RecipeInstance */ + private final int _quantity; + + /** + * Constructor of L2RecipeInstance (create a new line in a RecipeList). + * @param itemId + * @param quantity + */ + public L2RecipeInstance(int itemId, int quantity) { + _itemId = itemId; + _quantity = quantity; + } + + /** + * @return the Identifier of the L2RecipeInstance Item needed. + */ + public int getItemId() { + return _itemId; + } + + /** + * @return the Item quantity needed of the L2RecipeInstance. + */ + public int getQuantity() { + return _quantity; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2RecipeList.java b/src/main/java/com/l2jserver/gameserver/model/L2RecipeList.java new file mode 100644 index 00000000..e8040f30 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2RecipeList.java @@ -0,0 +1,224 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * This class describes a Recipe used by Dwarf to craft Item. All L2RecipeList are made of L2RecipeInstance (1 line of the recipe : Item-Quantity needed). + */ +public class L2RecipeList { + /** The table containing all L2RecipeInstance (1 line of the recipe : Item-Quantity needed) of the L2RecipeList */ + private L2RecipeInstance[] _recipes; + + /** The table containing all L2RecipeStatInstance for the statUse parameter of the L2RecipeList */ + private L2RecipeStatInstance[] _statUse; + + /** The table containing all L2RecipeStatInstance for the altStatChange parameter of the L2RecipeList */ + private L2RecipeStatInstance[] _altStatChange; + + /** The Identifier of the Instance */ + private final int _id; + + /** The crafting level needed to use this L2RecipeList */ + private final int _level; + + /** The Identifier of the L2RecipeList */ + private final int _recipeId; + + /** The name of the L2RecipeList */ + private final String _recipeName; + + /** The crafting success rate when using the L2RecipeList */ + private final int _successRate; + + /** The Identifier of the Item crafted with this L2RecipeList */ + private final int _itemId; + + /** The quantity of Item crafted when using this L2RecipeList */ + private final int _count; + + /** The Identifier of the Rare Item crafted with this L2RecipeList */ + private int _rareItemId; + + /** The quantity of Rare Item crafted when using this L2RecipeList */ + private int _rareCount; + + /** The chance of Rare Item crafted when using this L2RecipeList */ + private int _rarity; + + /** If this a common or a dwarven recipe */ + private final boolean _isDwarvenRecipe; + + /** + * Constructor of L2RecipeList (create a new Recipe). + * @param set + * @param haveRare + */ + public L2RecipeList(StatsSet set, boolean haveRare) { + _recipes = new L2RecipeInstance[0]; + _statUse = new L2RecipeStatInstance[0]; + _altStatChange = new L2RecipeStatInstance[0]; + _id = set.getInt("id"); + _level = set.getInt("craftLevel"); + _recipeId = set.getInt("recipeId"); + _recipeName = set.getString("recipeName"); + _successRate = set.getInt("successRate"); + _itemId = set.getInt("itemId"); + _count = set.getInt("count"); + if (haveRare) { + _rareItemId = set.getInt("rareItemId"); + _rareCount = set.getInt("rareCount"); + _rarity = set.getInt("rarity"); + } + _isDwarvenRecipe = set.getBoolean("isDwarvenRecipe"); + } + + /** + * Add a L2RecipeInstance to the L2RecipeList (add a line Item-Quantity needed to the Recipe). + * @param recipe + */ + public void addRecipe(L2RecipeInstance recipe) { + int len = _recipes.length; + L2RecipeInstance[] tmp = new L2RecipeInstance[len + 1]; + System.arraycopy(_recipes, 0, tmp, 0, len); + tmp[len] = recipe; + _recipes = tmp; + } + + /** + * Add a L2RecipeStatInstance of the statUse parameter to the L2RecipeList. + * @param statUse + */ + public void addStatUse(L2RecipeStatInstance statUse) { + int len = _statUse.length; + L2RecipeStatInstance[] tmp = new L2RecipeStatInstance[len + 1]; + System.arraycopy(_statUse, 0, tmp, 0, len); + tmp[len] = statUse; + _statUse = tmp; + } + + /** + * Add a L2RecipeStatInstance of the altStatChange parameter to the L2RecipeList. + * @param statChange + */ + public void addAltStatChange(L2RecipeStatInstance statChange) { + int len = _altStatChange.length; + L2RecipeStatInstance[] tmp = new L2RecipeStatInstance[len + 1]; + System.arraycopy(_altStatChange, 0, tmp, 0, len); + tmp[len] = statChange; + _altStatChange = tmp; + } + + /** + * @return the Identifier of the Instance. + */ + public int getId() { + return _id; + } + + /** + * @return the crafting level needed to use this L2RecipeList. + */ + public int getLevel() { + return _level; + } + + /** + * @return the Identifier of the L2RecipeList. + */ + public int getRecipeId() { + return _recipeId; + } + + /** + * @return the name of the L2RecipeList. + */ + public String getRecipeName() { + return _recipeName; + } + + /** + * @return the crafting success rate when using the L2RecipeList. + */ + public int getSuccessRate() { + return _successRate; + } + + /** + * @return the Identifier of the Item crafted with this L2RecipeList. + */ + public int getItemId() { + return _itemId; + } + + /** + * @return the quantity of Item crafted when using this L2RecipeList. + */ + public int getCount() { + return _count; + } + + /** + * @return the Identifier of the Rare Item crafted with this L2RecipeList. + */ + public int getRareItemId() { + return _rareItemId; + } + + /** + * @return the quantity of Rare Item crafted when using this L2RecipeList. + */ + public int getRareCount() { + return _rareCount; + } + + /** + * @return the chance of Rare Item crafted when using this L2RecipeList. + */ + public int getRarity() { + return _rarity; + } + + /** + * @return {@code true} if this a Dwarven recipe or {@code false} if its a Common recipe + */ + public boolean isDwarvenRecipe() { + return _isDwarvenRecipe; + } + + /** + * @return the table containing all L2RecipeInstance (1 line of the recipe : Item-Quantity needed) of the L2RecipeList. + */ + public L2RecipeInstance[] getRecipes() { + return _recipes; + } + + /** + * @return the table containing all L2RecipeStatInstance of the statUse parameter of the L2RecipeList. + */ + public L2RecipeStatInstance[] getStatUse() { + return _statUse; + } + + /** + * @return the table containing all L2RecipeStatInstance of the AltStatChange parameter of the L2RecipeList. + */ + public L2RecipeStatInstance[] getAltStatChange() { + return _altStatChange; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2RecipeStatInstance.java b/src/main/java/com/l2jserver/gameserver/model/L2RecipeStatInstance.java new file mode 100644 index 00000000..eb4495e4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2RecipeStatInstance.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.enums.StatType; + +/** + * This class describes a RecipeList statUse and altStatChange component. + */ +public class L2RecipeStatInstance { + /** The Identifier of the statType */ + private final StatType _type; + + /** The value of the statType */ + private final int _value; + + public L2RecipeStatInstance(String type, int value) { + _type = Enum.valueOf(StatType.class, type); + _value = value; + } + + public StatType getType() { + return _type; + } + + public int getValue() { + return _value; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Request.java b/src/main/java/com/l2jserver/gameserver/model/L2Request.java new file mode 100644 index 00000000..1ef848c1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Request.java @@ -0,0 +1,135 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.L2GameClientPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * This class manages requests (transactions) between two L2PcInstance. + * @author kriau + */ +public class L2Request { + private static final int REQUEST_TIMEOUT = 15; // in secs + + protected L2PcInstance _player; + protected L2PcInstance _partner; + protected boolean _isRequestor; + protected boolean _isAnswerer; + protected L2GameClientPacket _requestPacket; + + public L2Request(L2PcInstance player) { + _player = player; + } + + protected void clear() { + _partner = null; + _requestPacket = null; + _isRequestor = false; + _isAnswerer = false; + } + + /** + * Set the L2PcInstance member of a transaction (ex : FriendInvite, JoinAlly, JoinParty...). + * @param partner + */ + private synchronized void setPartner(L2PcInstance partner) { + _partner = partner; + } + + /** + * @return the L2PcInstance member of a transaction (ex : FriendInvite, JoinAlly, JoinParty...). + */ + public L2PcInstance getPartner() { + return _partner; + } + + /** + * Set the packet incomed from requester. + * @param packet + */ + private synchronized void setRequestPacket(L2GameClientPacket packet) { + _requestPacket = packet; + } + + /** + * Return the packet originally incomed from requester. + * @return + */ + public L2GameClientPacket getRequestPacket() { + return _requestPacket; + } + + /** + * Checks if request can be made and in success case puts both PC on request state. + * @param partner + * @param packet + * @return + */ + public synchronized boolean setRequest(L2PcInstance partner, L2GameClientPacket packet) { + if (partner == null) { + _player.sendPacket(SystemMessageId.YOU_HAVE_INVITED_THE_WRONG_TARGET); + return false; + } + if (partner.getRequest().isProcessingRequest()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_IS_BUSY_TRY_LATER); + sm.addString(partner.getName()); + _player.sendPacket(sm); + return false; + } + if (isProcessingRequest()) { + _player.sendPacket(SystemMessageId.WAITING_FOR_ANOTHER_REPLY); + return false; + } + + _partner = partner; + _requestPacket = packet; + setOnRequestTimer(true); + _partner.getRequest().setPartner(_player); + _partner.getRequest().setRequestPacket(packet); + _partner.getRequest().setOnRequestTimer(false); + return true; + } + + private void setOnRequestTimer(boolean isRequestor) { + _isRequestor = isRequestor; + _isAnswerer = !isRequestor; + ThreadPoolManager.getInstance().scheduleGeneral(this::clear, REQUEST_TIMEOUT * 1000); + } + + /** + * Clears PC request state. Should be called after answer packet receive. + */ + public void onRequestResponse() { + if (_partner != null) { + _partner.getRequest().clear(); + } + clear(); + } + + /** + * @return {@code true} if a transaction is in progress. + */ + public boolean isProcessingRequest() { + return _partner != null; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Seed.java b/src/main/java/com/l2jserver/gameserver/model/L2Seed.java new file mode 100644 index 00000000..c43797d3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Seed.java @@ -0,0 +1,123 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.rates; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.items.L2Item; + +public final class L2Seed { + private final int _seedId; + private final int _cropId; // crop type + private final int _level; // seed level + private final int _matureId; // mature crop type + private final int _reward1; + private final int _reward2; + private final int _castleId; // id of manor (castle id) where seed can be farmed + private final boolean _isAlternative; + private final int _limitSeeds; + private final int _limitCrops; + private final int _seedReferencePrice; + private final int _cropReferencePrice; + + public L2Seed(StatsSet set) { + _cropId = set.getInt("id"); + _seedId = set.getInt("seedId"); + _level = set.getInt("level"); + _matureId = set.getInt("mature_Id"); + _reward1 = set.getInt("reward1"); + _reward2 = set.getInt("reward2"); + _castleId = set.getInt("castleId"); + _isAlternative = set.getBoolean("alternative"); + _limitCrops = set.getInt("limit_crops"); + _limitSeeds = set.getInt("limit_seed"); + // Set prices + L2Item item = ItemTable.getInstance().getTemplate(_cropId); + _cropReferencePrice = (item != null) ? item.getReferencePrice() : 1; + item = ItemTable.getInstance().getTemplate(_seedId); + _seedReferencePrice = (item != null) ? item.getReferencePrice() : 1; + } + + public int getCastleId() { + return _castleId; + } + + public int getSeedId() { + return _seedId; + } + + public int getCropId() { + return _cropId; + } + + public int getMatureId() { + return _matureId; + } + + public int getReward(int type) { + return (type == 1) ? _reward1 : _reward2; + } + + public int getLevel() { + return _level; + } + + public boolean isAlternative() { + return _isAlternative; + } + + public int getSeedLimit() { + return _limitSeeds * rates().getRateDropManor(); + } + + public int getCropLimit() { + return _limitCrops * rates().getRateDropManor(); + } + + public int getSeedReferencePrice() { + return _seedReferencePrice; + } + + public int getSeedMaxPrice() { + return _seedReferencePrice * 10; + } + + public int getSeedMinPrice() { + return (int) (_seedReferencePrice * 0.6); + } + + public int getCropReferencePrice() { + return _cropReferencePrice; + } + + public int getCropMaxPrice() { + return _cropReferencePrice * 10; + } + + public int getCropMinPrice() { + return (int) (_cropReferencePrice * 0.6); + } + + @Override + public String toString() { + return "SeedData [_id=" + _seedId + ", _level=" + _level + ", _crop=" + _cropId + ", _mature=" + _matureId + ", _type1=" + _reward1 + ", _type2=" + _reward2 + // + ", _manorId=" + _castleId + ", _isAlternative=" + _isAlternative + ", _limitSeeds=" + _limitSeeds + ", _limitCrops=" + _limitCrops + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/L2SiegeClan.java b/src/main/java/com/l2jserver/gameserver/model/L2SiegeClan.java new file mode 100644 index 00000000..54564003 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2SiegeClan.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.l2jserver.gameserver.model.actor.L2Npc; + +public class L2SiegeClan { + private final int _clanId; + private final List _flag = new CopyOnWriteArrayList<>(); + private SiegeClanType _type; + + public enum SiegeClanType { + OWNER, + DEFENDER, + ATTACKER, + DEFENDER_PENDING + } + + public L2SiegeClan(int clanId, SiegeClanType type) { + _clanId = clanId; + _type = type; + } + + public int getNumFlags() { + return _flag.size(); + } + + public void addFlag(L2Npc flag) { + _flag.add(flag); + } + + public boolean removeFlag(L2Npc flag) { + boolean ret = _flag.remove(flag); + flag.deleteMe(); + return ret; + } + + public void removeFlags() { + _flag.forEach(L2Object::decayMe); + _flag.clear(); + } + + public final int getClanId() { + return _clanId; + } + + public final List getFlag() { + return _flag; + } + + public SiegeClanType getType() { + return _type; + } + + public void setType(SiegeClanType setType) { + _type = setType; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2SkillLearn.java b/src/main/java/com/l2jserver/gameserver/model/L2SkillLearn.java new file mode 100644 index 00000000..3678f9a2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2SkillLearn.java @@ -0,0 +1,273 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.LinkedList; +import java.util.List; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.base.SocialClass; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; + +/** + * Skill Learn. + * @author Zoey76 + */ +public final class L2SkillLearn { + private final String _skillName; + private final int _skillId; + private final int _skillLvl; + private final int _getLevel; + private final boolean _autoGet; + private final int _levelUpSp; + private final List _requiredItems = new LinkedList<>(); + private final List _races = new LinkedList<>(); + private final List _preReqSkills = new LinkedList<>(); + private SocialClass _socialClass; + private final boolean _residenceSkill; + private final List _residenceIds = new LinkedList<>(); + private final List _subClassLvlNumber = new LinkedList<>(); + private final boolean _learnedByNpc; + private final boolean _learnedByFS; + + public static class SubClassData { + private final int slot; + private final int lvl; + + public SubClassData(int pSlot, int pLvl) { + slot = pSlot; + lvl = pLvl; + } + + public int getSlot() { + return slot; + } + + public int getLvl() { + return lvl; + } + } + + /** + * Constructor for L2SkillLearn. + * @param set the set with the L2SkillLearn data. + */ + public L2SkillLearn(StatsSet set) { + _skillName = set.getString("skillName"); + _skillId = set.getInt("skillId"); + _skillLvl = set.getInt("skillLvl"); + _getLevel = set.getInt("getLevel"); + _autoGet = set.getBoolean("autoGet", false); + _levelUpSp = set.getInt("levelUpSp", 0); + _residenceSkill = set.getBoolean("residenceSkill", false); + _learnedByNpc = set.getBoolean("learnedByNpc", false); + _learnedByFS = set.getBoolean("learnedByFS", false); + } + + /** + * @return the name of this skill. + */ + public String getName() { + return _skillName; + } + + /** + * @return the ID of this skill. + */ + public int getSkillId() { + return _skillId; + } + + /** + * @return the level of this skill. + */ + public int getSkillLevel() { + return _skillLvl; + } + + /** + * @return the minimum level required to acquire this skill. + */ + public int getGetLevel() { + return _getLevel; + } + + /** + * @return the amount of SP/Clan Reputation to acquire this skill. + */ + public int getLevelUpSp() { + return _levelUpSp; + } + + /** + * @return {@code true} if the skill is auto-get, this skill is automatically delivered. + */ + public boolean isAutoGet() { + return _autoGet; + } + + /** + * @return the list with the item holders required to acquire this skill. + */ + public List getRequiredItems() { + return _requiredItems; + } + + /** + * Adds a required item holder to learn this skill. + * @param item the required item holder. + */ + public void addRequiredItem(ItemHolder item) { + _requiredItems.add(item); + } + + /** + * @return a list with the races that can acquire this skill. + */ + public List getRaces() { + return _races; + } + + /** + * Adds a required race to learn this skill. + * @param race the required race. + */ + public void addRace(Race race) { + _races.add(race); + } + + /** + * @return the list of skill holders required to acquire this skill. + */ + public List getPreReqSkills() { + return _preReqSkills; + } + + /** + * Adds a required skill holder to learn this skill. + * @param skill the required skill holder. + */ + public void addPreReqSkill(SkillHolder skill) { + _preReqSkills.add(skill); + } + + /** + * @return the social class required to get this skill. + */ + public SocialClass getSocialClass() { + return _socialClass; + } + + /** + * Sets the social class if hasn't been set before. + * @param socialClass the social class to set. + */ + public void setSocialClass(SocialClass socialClass) { + if (_socialClass == null) { + _socialClass = socialClass; + } + } + + /** + * @return {@code true} if this skill is a Residence skill. + */ + public boolean isResidentialSkill() { + return _residenceSkill; + } + + /** + * @return a list with the Ids where this skill is available. + */ + public List getResidenceIds() { + return _residenceIds; + } + + /** + * Adds a required residence Id. + * @param id the residence Id to add. + */ + public void addResidenceId(Integer id) { + _residenceIds.add(id); + } + + /** + * @return a list with Sub-Class conditions, amount of subclasses and level. + */ + public List getSubClassConditions() { + return _subClassLvlNumber; + } + + /** + * Adds a required residence Id. + * @param slot the sub-class slot. + * @param lvl the required sub-class level. + */ + public void addSubclassConditions(int slot, int lvl) { + _subClassLvlNumber.add(new SubClassData(slot, lvl)); + } + + /** + * @return {@code true} if this skill is learned from Npc. + */ + public boolean isLearnedByNpc() { + return _learnedByNpc; + } + + /** + * @return {@code true} if this skill is learned by Forgotten Scroll. + */ + public boolean isLearnedByFS() { + return _learnedByFS; + } + + /** + * Used for AltGameSkillLearn mod.
    + * If the alternative skill learn system is enabled and the player is learning a skill from a different class apply a fee.
    + * If the player is learning a skill from other class type (mage learning warrior skills or vice versa) the fee is higher. + * @param playerClass the player class Id. + * @param learningClass the skill learning player class Id. + * @return the amount of SP required to acquire this skill, by calculating the cost for the alternative skill learn system. + */ + public int getCalculatedLevelUpSp(ClassId playerClass, ClassId learningClass) { + if ((playerClass == null) || (learningClass == null)) { + return _levelUpSp; + } + + int levelUpSp = _levelUpSp; + // If the alternative skill learn system is enabled and the player is learning a skill from a different class apply a fee. + if (character().skillLearn() && (playerClass != learningClass)) { + // If the player is learning a skill from other class type (mage learning warrior skills or vice versa) the fee is higher. + if (playerClass.isMage() != learningClass.isMage()) { + levelUpSp *= 3; + } else { + levelUpSp *= 2; + } + } + return levelUpSp; + } + + @Override + public String toString() { + return _skillName + " (" + _skillId + ", " + _skillLvl + ")"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Spawn.java b/src/main/java/com/l2jserver/gameserver/model/L2Spawn.java new file mode 100644 index 00000000..9cf17251 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Spawn.java @@ -0,0 +1,746 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.lang.reflect.Constructor; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.TerritoryTable; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.datatables.NpcPersonalAIData; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.interfaces.INamable; +import com.l2jserver.gameserver.model.interfaces.IPositionable; +import com.l2jserver.gameserver.model.zone.type.NpcSpawnTerritory; + +/** + * This class manages the spawn and respawn of a group of L2NpcInstance that are in the same are and have the same type.
    + * Concept:
    + * L2NpcInstance can be spawned either in a random position into a location area (if Lox=0 and Locy=0), either at an exact position.
    + * The heading of the L2NpcInstance can be a random heading if not defined (value= -1) or an exact heading (ex : merchant...). + * @author Nightmare + */ +public class L2Spawn implements IPositionable, IIdentifiable, INamable { + private static final Logger LOG = LoggerFactory.getLogger(L2Spawn.class); + + /** String identifier of this spawn */ + private String _name; + /** The link on the L2NpcTemplate object containing generic and static properties of this spawn (ex : RewardExp, RewardSP, AggroRange...) */ + private final L2NpcTemplate _template; + /** The maximum number of L2NpcInstance that can manage this L2Spawn */ + private int _maximumCount; + /** The current number of L2NpcInstance managed by this L2Spawn */ + private int _currentCount; + /** The current number of SpawnTask in progress or stand by of this L2Spawn */ + private int _scheduledCount; + /** The identifier of the location area where L2NpcInstance can be spwaned */ + private int _locationId; + /** The Location of this NPC spawn. */ + private Location _location = new Location(0, 0, 0, 0, 0); + /** Link to NPC spawn territory */ + private NpcSpawnTerritory _spawnTerritory = null; + /** Minimum respawn delay */ + private int _respawnMinDelay; + /** Maximum respawn delay */ + private int _respawnMaxDelay; + /** The generic constructor of L2NpcInstance managed by this L2Spawn */ + private Constructor _constructor; + /** If True a L2NpcInstance is respawned each time that another is killed */ + private boolean _doRespawn; + /** If true then spawn is custom */ + private boolean _customSpawn; + private static final List _spawnListeners = new CopyOnWriteArrayList<>(); + private final Deque _spawnedNpcs = new ConcurrentLinkedDeque<>(); + private Map _lastSpawnPoints; + private boolean _isNoRndWalk = false; // Is no random walk + private String _areaName; + private int _globalMapId; + + /** The task launching the function doSpawn() */ + class SpawnTask implements Runnable { + private final L2Npc _oldNpc; + + public SpawnTask(L2Npc pOldNpc) { + _oldNpc = pOldNpc; + } + + @Override + public void run() { + try { + // doSpawn(); + respawnNpc(_oldNpc); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + + _scheduledCount--; + } + } + + /** + * Constructor of L2Spawn.
    + * Concept:
    + * Each L2Spawn owns generic and static properties (ex : RewardExp, RewardSP, AggroRange...).
    + * All of those properties are stored in a different L2NpcTemplate for each type of L2Spawn. Each template is loaded once in the server cache memory (reduce memory use).
    + * When a new instance of L2Spawn is created, server just create a link between the instance and the template.
    + * This link is stored in _template Each L2NpcInstance is linked to a L2Spawn that manages its spawn and respawn (delay, location...).
    + * This link is stored in _spawn of the L2NpcInstance.
    + * Actions:
    + *
      + *
    • Set the _template of the L2Spawn
    • + *
    • Calculate the implementationName used to generate the generic constructor of L2NpcInstance managed by this L2Spawn
    • + *
    • Create the generic constructor of L2NpcInstance managed by this L2Spawn
    • + *
    + * @param template The L2NpcTemplate to link to this L2Spawn + * @throws SecurityException + * @throws ClassNotFoundException + * @throws NoSuchMethodException + * @throws ClassCastException when template type is not subclass of L2Npc + */ + public L2Spawn(L2NpcTemplate template) throws SecurityException, ClassNotFoundException, NoSuchMethodException, ClassCastException { + // Set the _template of the L2Spawn + _template = template; + + if (_template == null) { + return; + } + + String className = "com.l2jserver.gameserver.model.actor.instance." + _template.getType() + "Instance"; + + // Create the generic constructor of L2Npc managed by this L2Spawn + _constructor = Class.forName(className).asSubclass(L2Npc.class).getConstructor(int.class, L2NpcTemplate.class); + } + + /** + * Creates a spawn. + * @param npcId the NPC ID + * @throws ClassCastException + * @throws NoSuchMethodException + * @throws ClassNotFoundException + * @throws SecurityException + */ + public L2Spawn(int npcId) throws SecurityException, ClassNotFoundException, NoSuchMethodException, ClassCastException { + this(NpcData.getInstance().getTemplate(npcId)); + } + + /** + * @return the maximum number of L2NpcInstance that this L2Spawn can manage. + */ + public int getAmount() { + return _maximumCount; + } + + /** + * @return the String Identifier of this spawn. + */ + @Override + public String getName() { + return _name; + } + + /** + * Set the String Identifier of this spawn. + * @param name + */ + public void setName(String name) { + _name = name; + } + + /** + * @return the Identifier of the location area where L2NpcInstance can be spwaned. + */ + public int getLocationId() { + return _locationId; + } + + @Override + public Location getLocation() { + return _location; + } + + public Location getLocation(L2Object obj) { + return ((_lastSpawnPoints == null) || (obj == null) || !_lastSpawnPoints.containsKey(obj.getObjectId())) ? _location : _lastSpawnPoints.get(obj.getObjectId()); + } + + @Override + public int getX() { + return _location.getX(); + } + + /** + * @param obj object to check + * @return the X position of the last spawn point of given NPC. + */ + public int getX(L2Object obj) { + return getLocation(obj).getX(); + } + + /** + * Set the X position of the spawn point. + * @param x the x coordinate + */ + @Override + public void setX(int x) { + _location.setX(x); + } + + @Override + public int getY() { + return _location.getY(); + } + + /** + * @param obj object to check + * @return the Y position of the last spawn point of given NPC. + */ + public int getY(L2Object obj) { + return getLocation(obj).getY(); + } + + /** + * Set the Y position of the spawn point. + * @param y the y coordinate + */ + @Override + public void setY(int y) { + _location.setY(y); + } + + @Override + public int getZ() { + return _location.getZ(); + } + + /** + * @param obj object to check + * @return the Z position of the last spawn point of given NPC. + */ + public int getZ(L2Object obj) { + return getLocation(obj).getZ(); + } + + /** + * Set the Z position of the spawn point. + * @param z the z coordinate + */ + @Override + public void setZ(int z) { + _location.setZ(z); + } + + /** + * Set the x, y, z position of the spawn point. + * @param x The x coordinate. + * @param y The y coordinate. + * @param z The z coordinate. + */ + @Override + public void setXYZ(int x, int y, int z) { + setX(x); + setY(y); + setZ(z); + } + + /** + * Set the x, y, z position of the spawn point. + * @param loc The location. + */ + @Override + public void setXYZ(ILocational loc) { + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + } + + /** + * @return the heading of L2NpcInstance when they are spawned. + */ + @Override + public int getHeading() { + return _location.getHeading(); + } + + /** + * Set the heading of L2NpcInstance when they are spawned. + * @param heading + */ + @Override + public void setHeading(int heading) { + _location.setHeading(heading); + } + + /** + * Set the XYZ position of the spawn point. + * @param loc + */ + @Override + public void setLocation(Location loc) { + _location = loc; + } + + /** + * Gets the NPC ID. + * @return the NPC ID + */ + @Override + public int getId() { + return _template.getId(); + } + + /** + * @return min respawn delay. + */ + public int getRespawnMinDelay() { + return _respawnMinDelay; + } + + /** + * @return max respawn delay. + */ + public int getRespawnMaxDelay() { + return _respawnMaxDelay; + } + + /** + * Set the maximum number of L2NpcInstance that this L2Spawn can manage. + * @param amount + */ + public void setAmount(int amount) { + _maximumCount = amount; + } + + /** + * Set the Identifier of the location area where L2NpcInstance can be spawned. + * @param id + */ + public void setLocationId(int id) { + _locationId = id; + } + + /** + * Set Minimum Respawn Delay. + * @param date + */ + public void setRespawnMinDelay(int date) { + _respawnMinDelay = date; + } + + /** + * Set Maximum Respawn Delay. + * @param date + */ + public void setRespawnMaxDelay(int date) { + _respawnMaxDelay = date; + } + + /** + * Set the spawn as custom.
    + * @param custom + */ + public void setCustom(boolean custom) { + _customSpawn = custom; + } + + /** + * @return type of spawn. + */ + public boolean isCustom() { + return _customSpawn; + } + + /** + * Decrease the current number of NPC instances of this spawn and if necessary create a spawn task to launch after the respawn delay.
    + * Actions: + *
  • Decrease the current number of L2NpcInstance of this L2Spawn
  • + *
  • Check if respawn is possible to prevent multiple respawning caused by lag
  • + *
  • Update the current number of SpawnTask in progress or stand by of this L2Spawn
  • + *
  • Create a new SpawnTask to launch after the respawn Delay
  • + * + * Caution: A respawn is possible ONLY if _doRespawn=True and _scheduledCount + _currentCount < _maximumCount + * @param oldNpc + */ + public void decreaseCount(L2Npc oldNpc) { + // sanity check + if (_currentCount <= 0) { + return; + } + + // Decrease the current number of L2NpcInstance of this L2Spawn + _currentCount--; + + // Remove this NPC from list of spawned + _spawnedNpcs.remove(oldNpc); + + // Remove spawn point for old NPC + if (_lastSpawnPoints != null) { + _lastSpawnPoints.remove(oldNpc.getObjectId()); + } + + // Check if respawn is possible to prevent multiple respawning caused by lag + if (_doRespawn && ((_scheduledCount + _currentCount) < _maximumCount)) { + // Update the current number of SpawnTask in progress or stand by of this L2Spawn + _scheduledCount++; + + // Create a new SpawnTask to launch after the respawn Delay + // ClientScheduler.getInstance().scheduleLow(new SpawnTask(npcId), _respawnDelay); + ThreadPoolManager.getInstance().scheduleGeneral(new SpawnTask(oldNpc), hasRespawnRandom() ? Rnd.get(_respawnMinDelay, _respawnMaxDelay) : _respawnMinDelay); + } + } + + /** + * Create the initial spawning and set _doRespawn to False, if respawn time set to 0, or set it to True otherwise. + * @return The number of L2NpcInstance that were spawned + */ + public int init() { + while (_currentCount < _maximumCount) { + doSpawn(); + } + _doRespawn = _respawnMinDelay != 0; + + return _currentCount; + } + + /** + * Create a L2NpcInstance in this L2Spawn. + * @param val + * @return + */ + public L2Npc spawnOne(boolean val) { + return doSpawn(val); + } + + /** + * @return true if respawn enabled + */ + public boolean isRespawnEnabled() { + return _doRespawn; + } + + /** + * Set _doRespawn to False to stop respawn in this L2Spawn. + */ + public void stopRespawn() { + _doRespawn = false; + } + + /** + * Set _doRespawn to True to start or restart respawn in this L2Spawn. + */ + public void startRespawn() { + _doRespawn = true; + } + + public L2Npc doSpawn() { + return doSpawn(false); + } + + /** + * Create the L2NpcInstance, add it to the world and launch its OnSpawn action.
    + * Concept:
    + * L2NpcInstance can be spawned either in a random position into a location area (if Lox=0 and Locy=0), either at an exact position.
    + * The heading of the L2NpcInstance can be a random heading if not defined (value= -1) or an exact heading (ex : merchant...).
    + * Actions for an random spawn into location area: (if Locx=0 and Locy=0) + *
      + *
    • Get L2NpcInstance Init parameters and its generate an Identifier
    • + *
    • Call the constructor of the L2NpcInstance
    • + *
    • Calculate the random position in the location area (if Locx=0 and Locy=0) or get its exact position from the L2Spawn
    • + *
    • Set the position of the L2NpcInstance
    • + *
    • Set the HP and MP of the L2NpcInstance to the max
    • + *
    • Set the heading of the L2NpcInstance (random heading if not defined : value=-1)
    • + *
    • Link the L2NpcInstance to this L2Spawn
    • + *
    • Init other values of the L2NpcInstance (ex : from its L2CharTemplate for INT, STR, DEX...) and add it in the world
    • + *
    • Launch the action OnSpawn fo the L2NpcInstance
    • + *
    • Increase the current number of L2NpcInstance managed by this L2Spawn
    • + *
    + * @param isSummonSpawn + * @return + */ + public L2Npc doSpawn(boolean isSummonSpawn) { + try { + // Check if the L2Spawn is not a L2Pet or L2Minion or L2Decoy spawn + if (_template.isType("L2Pet") || _template.isType("L2Decoy") || _template.isType("L2Trap")) { + _currentCount++; + + return null; + } + + // Call the constructor of the L2Npc + final var objectId = IdFactory.getInstance().getNextId(); + final var npc = _constructor.newInstance(objectId, _template); + npc.setInstanceId(getInstanceId()); // Must be done before object is spawned into visible world + if (isSummonSpawn) { + npc.setShowSummonAnimation(isSummonSpawn); + } + + // Check for certain AI data, overridden in spawnlist + if (_name != null) { + NpcPersonalAIData.getInstance().initializeNpcParameters(npc, this, _name); + } + + return initializeNpcInstance(npc); + } catch (Exception e) { + LOG.warn("NPC {} class not found", _template.getId(), e); + } + return null; + } + + private L2Npc initializeNpcInstance(L2Npc mob) { + int newlocx = 0; + int newlocy = 0; + int newlocz = 0; + + // If Locx and Locy are not defined, the L2NpcInstance must be spawned in an area defined by location or spawn territory + // New method + if (isTerritoryBased()) { + int[] p = _spawnTerritory.getRandomPoint(); + newlocx = p[0]; + newlocy = p[1]; + newlocz = p[2]; + } + // Old method (for backward compatibility) + else if ((getX() == 0) && (getY() == 0)) { + if (getLocationId() == 0) { + return mob; + } + + // Calculate the random position in the location area + final Location location = TerritoryTable.getInstance().getRandomPoint(getLocationId()); + + // Set the calculated position of the L2NpcInstance + if (location != null) { + newlocx = location.getX(); + newlocy = location.getY(); + newlocz = location.getZ(); + } + } else { + // The L2NpcInstance is spawned at the exact position (Lox, Locy, Locz) + newlocx = getX(); + newlocy = getY(); + newlocz = getZ(); + } + + // don't correct z of flying npc's + if (!mob.isFlying()) { + newlocz = GeoData.getInstance().getSpawnHeight(newlocx, newlocy, newlocz); + } + + mob.stopAllEffects(); + + mob.setIsDead(false); + // Reset decay info + mob.setDecayed(false); + // Clear script variables + if (mob.hasVariables()) { + mob.getVariables().getSet().clear(); + } + // Set is not random walk default value + mob.setIsNoRndWalk(isNoRndWalk()); + + // Set the heading of the L2NpcInstance (random heading if not defined) + if (getHeading() == -1) { + mob.setHeading(Rnd.nextInt(61794)); + } else { + mob.setHeading(getHeading()); + } + + if (mob instanceof L2Attackable) { + ((L2Attackable) mob).setChampion(false); + } + + if (customs().championEnable()) { + // Set champion on next spawn + if (mob.isMonster() && !getTemplate().isUndying() && !mob.isRaid() && !mob.isRaidMinion() && // + (customs().getChampionFrequency() > 0) && // + (mob.getLevel() >= customs().getChampionMinLevel()) && // + (mob.getLevel() <= customs().getChampionMaxLevel()) && // + (customs().championEnableInInstances() || (getInstanceId() == 0))) { + if (Rnd.get(100) < customs().getChampionFrequency()) { + ((L2Attackable) mob).setChampion(true); + } + } + } + + // Set the HP and MP of the L2NpcInstance to the max + mob.setCurrentHpMp(mob.getMaxHp(), mob.getMaxMp()); + // Reset summoner + mob.setSummoner(null); + // Reset summoned list + mob.resetSummonedNpcs(); + // Link the L2NpcInstance to this L2Spawn + mob.setSpawn(this); + + // Spawn NPC + mob.spawnMe(newlocx, newlocy, newlocz); + + notifyNpcSpawned(mob); + + _spawnedNpcs.add(mob); + if (_lastSpawnPoints != null) { + _lastSpawnPoints.put(mob.getObjectId(), new Location(newlocx, newlocy, newlocz)); + } + + if (general().debug()) { + LOG.trace("Spawned Mob Id: {} , at: X: {} Y: {} Z: {}", _template.getId(), mob.getX(), mob.getY(), mob.getZ()); + } + // Increase the current number of L2NpcInstance managed by this L2Spawn + _currentCount++; + return mob; + } + + public static void addSpawnListener(SpawnListener listener) { + _spawnListeners.add(listener); + } + + public static void removeSpawnListener(SpawnListener listener) { + _spawnListeners.remove(listener); + } + + public static void notifyNpcSpawned(L2Npc npc) { + for (SpawnListener listener : _spawnListeners) { + listener.npcSpawned(npc); + } + } + + /** + * Set bounds for random calculation and delay for respawn + * @param delay delay in seconds + * @param randomInterval random interval in seconds + */ + public void setRespawnDelay(int delay, int randomInterval) { + if (delay != 0) { + if (delay < 0) { + LOG.warn("Respawn delay is negative for spawn: {}", this); + } + + int minDelay = delay - randomInterval; + int maxDelay = delay + randomInterval; + + _respawnMinDelay = Math.max(10, minDelay) * 1000; + _respawnMaxDelay = Math.max(10, maxDelay) * 1000; + } + + else { + _respawnMinDelay = 0; + _respawnMaxDelay = 0; + } + } + + public void setRespawnDelay(int delay) { + setRespawnDelay(delay, 0); + } + + public int getRespawnDelay() { + return (_respawnMinDelay + _respawnMaxDelay) / 2; + } + + public boolean hasRespawnRandom() { + return _respawnMinDelay != _respawnMaxDelay; + } + + public void setSpawnTerritory(NpcSpawnTerritory territory) { + _spawnTerritory = territory; + _lastSpawnPoints = new ConcurrentHashMap<>(); + } + + public NpcSpawnTerritory getSpawnTerritory() { + return _spawnTerritory; + } + + public boolean isTerritoryBased() { + return (_spawnTerritory != null) && (_location.getX() == 0) && (_location.getY() == 0); + } + + public L2Npc getLastSpawn() { + return _spawnedNpcs.peekLast(); + } + + public final Deque getSpawnedNpcs() { + return _spawnedNpcs; + } + + /** + * @param oldNpc + */ + public void respawnNpc(L2Npc oldNpc) { + if (_doRespawn) { + oldNpc.refreshID(); + initializeNpcInstance(oldNpc); + } + } + + public L2NpcTemplate getTemplate() { + return _template; + } + + @Override + public int getInstanceId() { + return _location.getInstanceId(); + } + + @Override + public void setInstanceId(int instanceId) { + _location.setInstanceId(instanceId); + } + + public final boolean isNoRndWalk() { + return _isNoRndWalk; + } + + public final void setIsNoRndWalk(boolean value) { + _isNoRndWalk = value; + } + + public String getAreaName() { + return _areaName; + } + + public void setAreaName(String areaName) { + _areaName = areaName; + } + + public int getGlobalMapId() { + return _globalMapId; + } + + public void setGlobalMapId(int globalMapId) { + _globalMapId = globalMapId; + } + + @Override + public String toString() { + return "L2Spawn ID: " + getId() + " " + getLocation(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2TeleportLocation.java b/src/main/java/com/l2jserver/gameserver/model/L2TeleportLocation.java new file mode 100644 index 00000000..4c86f975 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2TeleportLocation.java @@ -0,0 +1,89 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * Teleport location. + * @since 2005/03/27 15:29:32 + */ +public class L2TeleportLocation { + private int _teleId; + private int _locX; + private int _locY; + private int _locZ; + private int _price; + private boolean _forNoble; + private int _itemId; + + public void setTeleId(int id) { + _teleId = id; + } + + public void setLocX(int locX) { + _locX = locX; + } + + public void setLocY(int locY) { + _locY = locY; + } + + public void setLocZ(int locZ) { + _locZ = locZ; + } + + public void setPrice(int price) { + _price = price; + } + + public void setIsForNoble(boolean val) { + _forNoble = val; + } + + public void setItemId(int val) { + _itemId = val; + } + + public int getTeleId() { + return _teleId; + } + + public int getLocX() { + return _locX; + } + + public int getLocY() { + return _locY; + } + + public int getLocZ() { + return _locZ; + } + + public int getPrice() { + return _price; + } + + public boolean getIsForNoble() { + return _forNoble; + } + + public int getItemId() { + return _itemId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2Territory.java b/src/main/java/com/l2jserver/gameserver/model/L2Territory.java new file mode 100644 index 00000000..9b38f056 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2Territory.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; + +/** + * @version 0.1, 2005-03-12 + * @author Balancer + */ +public class L2Territory { + private static final Logger LOG = LoggerFactory.getLogger(L2Territory.class); + + protected static class Point { + protected int _x, _y, _zmin, _zmax, _proc; + + Point(int x, int y, int zmin, int zmax, int proc) { + _x = x; + _y = y; + _zmin = zmin; + _zmax = zmax; + _proc = proc; + } + } + + private final List _points = new CopyOnWriteArrayList<>(); + private final int _terr; + private int _xMin; + private int _xMax; + private int _yMin; + private int _yMax; + private int _zMin; + private int _zMax; + private int _procMax; + + public L2Territory(int terr) { + _terr = terr; + _xMin = 999999; + _xMax = -999999; + _yMin = 999999; + _yMax = -999999; + _zMin = 999999; + _zMax = -999999; + _procMax = 0; + } + + public void add(int x, int y, int zmin, int zmax, int proc) { + _points.add(new Point(x, y, zmin, zmax, proc)); + if (x < _xMin) { + _xMin = x; + } + if (y < _yMin) { + _yMin = y; + } + if (x > _xMax) { + _xMax = x; + } + if (y > _yMax) { + _yMax = y; + } + if (zmin < _zMin) { + _zMin = zmin; + } + if (zmax > _zMax) { + _zMax = zmax; + } + _procMax += proc; + } + + public boolean isIntersect(int x, int y, Point p1, Point p2) { + double dy1 = p1._y - y; + double dy2 = p2._y - y; + + if (Math.abs(Math.signum(dy1) - Math.signum(dy2)) <= 1e-6) { + return false; + } + + double dx1 = p1._x - x; + double dx2 = p2._x - x; + + if ((dx1 >= 0) && (dx2 >= 0)) { + return true; + } + + if ((dx1 < 0) && (dx2 < 0)) { + return false; + } + + double dx0 = (dy1 * (p1._x - p2._x)) / (p1._y - p2._y); + + return dx0 <= dx1; + } + + public boolean isInside(int x, int y) { + int intersect_count = 0; + for (int i = 0; i < _points.size(); i++) { + Point p1 = _points.get(i > 0 ? i - 1 : _points.size() - 1); + Point p2 = _points.get(i); + + if (isIntersect(x, y, p1, p2)) { + intersect_count++; + } + } + + return (intersect_count % 2) == 1; + } + + public Location getRandomPoint() { + if (_procMax > 0) { + int pos = 0; + int rnd = Rnd.nextInt(_procMax); + for (Point p1 : _points) { + pos += p1._proc; + if (rnd <= pos) { + return new Location(p1._x, p1._y, Rnd.get(p1._zmin, p1._zmax)); + } + } + + } + for (int i = 0; i < 100; i++) { + int x = Rnd.get(_xMin, _xMax); + int y = Rnd.get(_yMin, _yMax); + if (isInside(x, y)) { + double curdistance = 0; + int zmin = _zMin; + for (Point p1 : _points) { + double distance = Math.hypot(p1._x - x, p1._y - y); + if ((curdistance == 0) || (distance < curdistance)) { + curdistance = distance; + zmin = p1._zmin; + } + } + return new Location(x, y, Rnd.get(zmin, _zMax)); + } + } + LOG.warn("Can't make point for territory {}", _terr); + return null; + } + + public int getProcMax() { + return _procMax; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2WalkRoute.java b/src/main/java/com/l2jserver/gameserver/model/L2WalkRoute.java new file mode 100644 index 00000000..1ca913f7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2WalkRoute.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; + +/** + * @author GKR + */ +public class L2WalkRoute { + private final String _name; + private final List _nodeList; // List of nodes + private final boolean _repeatWalk; // Does repeat walk, after arriving into last point in list, or not + private boolean _stopAfterCycle; // Make only one cycle or endlessly + private final byte _repeatType; // Repeat style: 0 - go back, 1 - go to first point (circle style), 2 - teleport to first point (conveyor style), 3 - random walking between points + + public L2WalkRoute(String name, List route, boolean repeat, boolean once, byte repeatType) { + _name = name; + _nodeList = route; + _repeatType = repeatType; + _repeatWalk = (_repeatType >= 0) && (_repeatType <= 2) && repeat; + } + + public String getName() { + return _name; + } + + public List getNodeList() { + return _nodeList; + } + + public L2NpcWalkerNode getLastNode() { + return _nodeList.get(_nodeList.size() - 1); + } + + public boolean repeatWalk() { + return _repeatWalk; + } + + public boolean doOnce() { + return _stopAfterCycle; + } + + public byte getRepeatType() { + return _repeatType; + } + + public int getNodesCount() { + return _nodeList.size(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2World.java b/src/main/java/com/l2jserver/gameserver/model/L2World.java new file mode 100644 index 00000000..96433c3c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2World.java @@ -0,0 +1,537 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; + +public final class L2World { + + private static final Logger LOG = LoggerFactory.getLogger(L2World.class); + + /** Gracia border Flying objects not allowed to the east of it. */ + public static final int GRACIA_MAX_X = -166168; + public static final int GRACIA_MAX_Z = 6105; + public static final int GRACIA_MIN_Z = -895; + + /** Biteshift, defines number of regions note, shifting by 15 will result in regions corresponding to map tiles shifting by 12 divides one tile to 8x8 regions. */ + public static final int SHIFT_BY = 12; + + private static final int TILE_SIZE = 32768; + + /** Map dimensions */ + public static final int TILE_X_MIN = 11; + public static final int TILE_Y_MIN = 10; + public static final int TILE_X_MAX = 26; + public static final int TILE_Y_MAX = 26; + public static final int TILE_ZERO_COORD_X = 20; + public static final int TILE_ZERO_COORD_Y = 18; + public static final int MAP_MIN_X = (TILE_X_MIN - TILE_ZERO_COORD_X) * TILE_SIZE; + public static final int MAP_MIN_Y = (TILE_Y_MIN - TILE_ZERO_COORD_Y) * TILE_SIZE; + + public static final int MAP_MAX_X = ((TILE_X_MAX - TILE_ZERO_COORD_X) + 1) * TILE_SIZE; + public static final int MAP_MAX_Y = ((TILE_Y_MAX - TILE_ZERO_COORD_Y) + 1) * TILE_SIZE; + + /** calculated offset used so top left region is 0,0 */ + public static final int OFFSET_X = Math.abs(MAP_MIN_X >> SHIFT_BY); + public static final int OFFSET_Y = Math.abs(MAP_MIN_Y >> SHIFT_BY); + + /** number of regions */ + private static final int REGIONS_X = (MAP_MAX_X >> SHIFT_BY) + OFFSET_X; + private static final int REGIONS_Y = (MAP_MAX_Y >> SHIFT_BY) + OFFSET_Y; + + /** Map containing all the players in game. */ + private final Map _allPlayers = new ConcurrentHashMap<>(); + /** Map containing all visible objects. */ + private final Map _allObjects = new ConcurrentHashMap<>(); + /** Map with the pets instances and their owner ID. */ + private final Map _petsInstance = new ConcurrentHashMap<>(); + + private L2WorldRegion[][] _worldRegions; + + /** Constructor of L2World. */ + protected L2World() { + initRegions(); + } + + /** + * Adds an object to the world.
    + * Example of use: + *
      + *
    • Withdraw an item from the warehouse, create an item
    • + *
    • Spawn a L2Character (PC, NPC, Pet)
    • + *
    + * @param object + */ + public void storeObject(L2Object object) { + if (_allObjects.containsKey(object.getObjectId())) { + LOG.warn("Current object: {} already exist in OID map!", object); + return; + } + _allObjects.put(object.getObjectId(), object); + } + + /** + * Removes an object from the world.
    + * Example of use: + *
      + *
    • Delete item from inventory, transfer Item from inventory to warehouse
    • + *
    • Crystallize item
    • + *
    • Remove NPC/PC/Pet from the world
    • + *
    + * @param object the object to remove + */ + public void removeObject(L2Object object) { + _allObjects.remove(object.getObjectId()); + } + + /** + * Example of use: + *
      + *
    • Client packets : Action, AttackRequest, RequestJoinParty, RequestJoinPledge...
    • + *
    + * @param objectId Identifier of the L2Object + * @return the L2Object object that belongs to an ID or null if no object found. + */ + public L2Object findObject(int objectId) { + return _allObjects.get(objectId); + } + + public Collection getVisibleObjects() { + return _allObjects.values(); + } + + /** + * Get the count of all visible objects in world. + * @return count off all L2World objects + */ + public int getVisibleObjectsCount() { + return _allObjects.size(); + } + + public List getAllGMs() { + return AdminData.getInstance().getAllGms(true); + } + + public Collection getPlayers() { + return _allPlayers.values(); + } + + /** + * Gets all players sorted by the given comparator. + * @param comparator the comparator + * @return the players sorted by the comparator + */ + public L2PcInstance[] getPlayersSortedBy(Comparator comparator) { + final L2PcInstance[] players = _allPlayers.values().toArray(new L2PcInstance[_allPlayers.size()]); + Arrays.sort(players, comparator); + return players; + } + + /** + * Return how many players are online. + * @return number of online players. + */ + public int getAllPlayersCount() { + return _allPlayers.size(); + } + + /** + * If you have access to player objectId use {@link #getPlayer(int playerObjId)} + * @param name Name of the player to get Instance + * @return the player instance corresponding to the given name. + */ + public L2PcInstance getPlayer(String name) { + return getPlayer(CharNameTable.getInstance().getIdByName(name)); + } + + /** + * @param objectId of the player to get Instance + * @return the player instance corresponding to the given object ID. + */ + public L2PcInstance getPlayer(int objectId) { + return _allPlayers.get(objectId); + } + + /** + * @param ownerId ID of the owner + * @return the pet instance from the given ownerId. + */ + public L2PetInstance getPet(int ownerId) { + return _petsInstance.get(ownerId); + } + + /** + * Add the given pet instance from the given ownerId. + * @param ownerId ID of the owner + * @param pet L2PetInstance of the pet + * @return + */ + public L2PetInstance addPet(int ownerId, L2PetInstance pet) { + return _petsInstance.put(ownerId, pet); + } + + /** + * Remove the given pet instance. + * @param ownerId ID of the owner + */ + public void removePet(int ownerId) { + _petsInstance.remove(ownerId); + } + + /** + * Remove the given pet instance. + * @param pet the pet to remove + */ + public void removePet(L2PetInstance pet) { + _petsInstance.remove(pet.getOwner().getObjectId()); + } + + /** + * Add a L2Object in the world. Concept : L2Object (including L2PcInstance) are identified in _visibleObjects of his current L2WorldRegion and in _knownObjects of other surrounding L2Characters
    + * L2PcInstance are identified in _allPlayers of L2World, in _allPlayers of his current L2WorldRegion and in _knownPlayer of other surrounding L2Characters Actions : + *
  • Add the L2Object object in _allPlayers* of L2World
  • + *
  • Add the L2Object object in _gmList** of GmListTable
  • + *
  • Add object in _knownObjects and _knownPlayer* of all surrounding L2WorldRegion L2Characters

  • + *
  • If object is a L2Character, add all surrounding L2Object in its _knownObjects and all surrounding L2PcInstance in its _knownPlayer

  • + * * only if object is a L2PcInstance
    + * ** only if object is a GM L2PcInstance Caution : This method DOESN'T ADD the object in _visibleObjects and _allPlayers* of L2WorldRegion (need synchronisation)
    + * Caution : This method DOESN'T ADD the object to _allObjects and _allPlayers* of L2World (need synchronisation) Example of use : + *
  • Drop an Item
  • + *
  • Spawn a L2Character
  • + *
  • Apply Death Penalty of a L2PcInstance
  • + * @param object L2object to add in the world + * @param newRegion L2WorldRegion in wich the object will be add (not used) + */ + public void addVisibleObject(L2Object object, L2WorldRegion newRegion) { + if (!newRegion.isActive()) { + return; + } + // Get all visible objects contained in the _visibleObjects of L2WorldRegions + // in a circular area of 2000 units + List visibles = getVisibleObjects(object, 2000); + if (general().debug()) { + LOG.debug("Objects in range {}.", visibles.size()); + } + + // tell the player about the surroundings + // Go through the visible objects contained in the circular area + for (L2Object visible : visibles) { + if (visible == null) { + continue; + } + + // Add the object in L2ObjectHashSet(L2Object) _knownObjects of the visible L2Character according to conditions : + // - L2Character is visible + // - object is not already known + // - object is in the watch distance + // If L2Object is a L2PcInstance, add L2Object in L2ObjectHashSet(L2PcInstance) _knownPlayer of the visible L2Character + visible.getKnownList().addKnownObject(object); + + // Add the visible L2Object in L2ObjectHashSet(L2Object) _knownObjects of the object according to conditions + // If visible L2Object is a L2PcInstance, add visible L2Object in L2ObjectHashSet(L2PcInstance) _knownPlayer of the object + object.getKnownList().addKnownObject(visible); + } + } + + /** + * Adds the player to the world. + * @param player the player to add + */ + public void addPlayerToWorld(L2PcInstance player) { + _allPlayers.put(player.getObjectId(), player); + } + + /** + * Remove the player from the world. + * @param player the player to remove + */ + public void removeFromAllPlayers(L2PcInstance player) { + _allPlayers.remove(player.getObjectId()); + } + + /** + * Remove an object from the world.
    + * Concept : L2Object (including L2PcInstance) are identified in _visibleObjects of his current L2WorldRegion and in _knownObjects of other surrounding L2Characters
    + * L2PcInstance are identified in _allPlayers of L2World, in _allPlayers of his current L2WorldRegion and in _knownPlayer of other surrounding L2Characters Actions : + *
  • Remove the L2Object object from _allPlayers* of L2World
  • + *
  • Remove the L2Object object from _visibleObjects and _allPlayers* of L2WorldRegion
  • + *
  • Remove the L2Object object from _gmList** of GmListTable
  • + *
  • Remove object from _knownObjects and _knownPlayer* of all surrounding L2WorldRegion L2Characters

  • + *
  • If object is a L2Character, remove all L2Object from its _knownObjects and all L2PcInstance from its _knownPlayer
  • Caution : This method DOESN'T REMOVE the object from _allObjects of L2World * only if object is a L2PcInstance
    + * ** only if object is a GM L2PcInstance Example of use : + *
  • Pickup an Item
  • + *
  • Decay a L2Character
  • + * @param object L2object to remove from the world + * @param oldWorldRegion L2WorldRegion in which the object was before removing + */ + public void removeVisibleObject(L2Object object, L2WorldRegion oldWorldRegion) { + if (object == null) { + return; + } + + if (oldWorldRegion == null) { + return; + } + + // Removes the object from the visible objects of world region. + // If object is a player, removes it from the players map of this world region. + oldWorldRegion.removeVisibleObject(object); + + // Goes through all surrounding world region's creatures. + // And removes the object from their known lists. + for (L2WorldRegion worldRegion : oldWorldRegion.getSurroundingRegions()) { + for (L2Object obj : worldRegion.getVisibleObjects().values()) { + if (obj != null) { + obj.getKnownList().removeKnownObject(object); + } + } + } + + // Removes all objects from the object's known list. + object.getKnownList().removeAllKnownObjects(); + } + + /** + * Return all visible objects of the L2WorldRegion object's and of its surrounding L2WorldRegion. Concept : All visible object are identified in _visibleObjects of their current L2WorldRegion
    + * All surrounding L2WorldRegion are identified in _surroundingRegions of the selected L2WorldRegion in order to scan a large area around a L2Object Example of use : + *
  • Find Close Objects for L2Character

  • + * @param object L2object that determine the current L2WorldRegion + * @return + */ + public List getVisibleObjects(L2Object object) { + final List result = new LinkedList<>(); + for (L2WorldRegion regi : object.getWorldRegion().getSurroundingRegions()) { + for (L2Object visibleObject : regi.getVisibleObjects().values()) { + if ((visibleObject == null) || visibleObject.equals(object) || !visibleObject.isVisible()) { + continue; // skip our own character + } + result.add(visibleObject); + } + } + + return result; + } + + /** + * Gets the visible objects to the given object inside the given radius. + * @param object the origin + * @param radius the radius to check + * @return the visible objects in the radius + */ + public List getVisibleObjects(L2Object object, int radius) { + return getVisibleObjects(object, radius, true); + } + + /** + * Gets the visible objects around a given object. + * @param object the origin + * @param radius the radius to check + * @param excludeTarget if the target should be excluded + * @return the visible objects in the radius + */ + public List getVisibleObjects(L2Object object, int radius, boolean excludeTarget) { + return getVisibleObjectsStream(object, radius, excludeTarget).toList(); + } + + public Stream getVisibleObjectsStream(L2Object object, int radius, boolean excludeTarget) { + if ((object == null) || !object.isVisible()) { + return Stream.of(); + } + final int sqRadius = radius * radius; + return object.getWorldRegion() + .getSurroundingRegions() + .stream() + .flatMap(r -> r.getVisibleObjects().values().stream()) + .filter(Objects::nonNull) + .filter(o -> !excludeTarget || !o.equals(object)) + .filter(o -> sqRadius > object.calculateDistance(o, false, true)); + } + + /** + * Return all visible objects of the L2WorldRegions in the spheric area (radius) centered on the object. Concept : All visible object are identified in _visibleObjects of their current L2WorldRegion
    + * All surrounding L2WorldRegion are identified in _surroundingRegions of the selected L2WorldRegion in order to scan a large area around a L2Object Example of use : + *
  • Define the target list of a skill
  • + *
  • Define the target list of a polearme attack
  • + * @param object L2object that determine the center of the circular area + * @param radius Radius of the spherical area + * @return + */ + public List getVisibleObjects3D(L2Object object, int radius) { + if ((object == null) || !object.isVisible()) { + return new ArrayList<>(); + } + + final int sqRadius = radius * radius; + + // Create a list in order to contain all visible objects. + final List result = new LinkedList<>(); + for (L2WorldRegion regi : object.getWorldRegion().getSurroundingRegions()) { + for (L2Object _object : regi.getVisibleObjects().values()) { + if ((_object == null) || _object.equals(object)) { + continue; // skip our own character + } + + if (sqRadius > object.calculateDistance(_object, true, true)) { + result.add(_object); + } + } + } + + return result; + } + + /** + * Concept : All visible object are identified in _visibleObjects of their current L2WorldRegion
    + * All surrounding L2WorldRegion are identified in _surroundingRegions of the selected L2WorldRegion in order to scan a large area around a L2Object Example of use : + *
  • Find Close Objects for L2Character

  • + * @param object L2object that determine the current L2WorldRegion + * @return all visible players of the L2WorldRegion object's and of its surrounding L2WorldRegion. + */ + public List getVisiblePlayable(L2Object object) { + L2WorldRegion reg = object.getWorldRegion(); + if (reg == null) { + return null; + } + + // Create a list in order to contain all visible objects. + final List result = new LinkedList<>(); + for (L2WorldRegion regi : reg.getSurroundingRegions()) { + // Create an Iterator to go through the visible L2Object of the L2WorldRegion + Map _allpls = regi.getVisiblePlayable(); + Collection _playables = _allpls.values(); + // Go through visible object of the selected region + for (L2Playable _object : _playables) { + if ((_object == null) || _object.equals(object)) { + continue; // skip our own character + } + + if (!_object.isVisible()) { + continue; // skip dying objects + } + + result.add(_object); + } + } + + return result; + } + + /** + * Calculate the current L2WorldRegions of the object according to its position (x,y). Example of use : + *
  • Set position of a new L2Object (drop, spawn...)
  • + *
  • Update position of a L2Object after a movement

  • + * @param point position of the object + * @return + */ + public L2WorldRegion getRegion(Location point) { + return _worldRegions[(point.getX() >> SHIFT_BY) + OFFSET_X][(point.getY() >> SHIFT_BY) + OFFSET_Y]; + } + + public L2WorldRegion getRegion(int x, int y) { + return _worldRegions[(x >> SHIFT_BY) + OFFSET_X][(y >> SHIFT_BY) + OFFSET_Y]; + } + + /** + * Returns the whole 2d array containing the world regions used by ZoneData.java to setup zones inside the world regions + * @return + */ + public L2WorldRegion[][] getWorldRegions() { + return _worldRegions; + } + + /** + * Check if the current L2WorldRegions of the object is valid according to its position (x,y). Example of use : + *
  • Init L2WorldRegions

  • + * @param x X position of the object + * @param y Y position of the object + * @return True if the L2WorldRegion is valid + */ + private boolean validRegion(int x, int y) { + return ((x >= 0) && (x <= REGIONS_X) && (y >= 0) && (y <= REGIONS_Y)); + } + + /** + * Initialize the world regions. + */ + private void initRegions() { + _worldRegions = new L2WorldRegion[REGIONS_X + 1][REGIONS_Y + 1]; + + for (int i = 0; i <= REGIONS_X; i++) { + for (int j = 0; j <= REGIONS_Y; j++) { + _worldRegions[i][j] = new L2WorldRegion(i, j); + } + } + + for (int x = 0; x <= REGIONS_X; x++) { + for (int y = 0; y <= REGIONS_Y; y++) { + for (int a = -1; a <= 1; a++) { + for (int b = -1; b <= 1; b++) { + if (validRegion(x + a, y + b)) { + _worldRegions[x + a][y + b].addSurroundingRegion(_worldRegions[x][y]); + } + } + } + } + } + + LOG.info("{} by {} world region grid set up.", REGIONS_X, REGIONS_Y); + + } + + /** + * Deleted all spawns in the world. + */ + public void deleteVisibleNpcSpawns() { + LOG.info("Deleting all visible NPCs."); + for (int i = 0; i <= REGIONS_X; i++) { + for (int j = 0; j <= REGIONS_Y; j++) { + _worldRegions[i][j].deleteVisibleNpcSpawns(); + } + } + LOG.info("All visible NPCs deleted."); + } + + public static L2World getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final L2World INSTANCE = new L2World(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/L2WorldRegion.java b/src/main/java/com/l2jserver/gameserver/model/L2WorldRegion.java new file mode 100644 index 00000000..8780460a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/L2WorldRegion.java @@ -0,0 +1,415 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Vehicle; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.L2ZoneType; +import com.l2jserver.gameserver.model.zone.type.L2PeaceZone; + +public final class L2WorldRegion { + private static final Logger LOG = LoggerFactory.getLogger(L2WorldRegion.class); + + /** Map containing all playable characters in game in this world region. */ + private final Map _allPlayable = new ConcurrentHashMap<>(); + + /** Map containing visible objects in this world region. */ + private final Map _visibleObjects = new ConcurrentHashMap<>(); + + private final Queue _surroundingRegions = new ConcurrentLinkedQueue<>(); + private final int _tileX, _tileY; + private boolean _active; + private ScheduledFuture _neighborsTask = null; + private final List _zones = new CopyOnWriteArrayList<>(); + + public L2WorldRegion(int pTileX, int pTileY) { + _tileX = pTileX; + _tileY = pTileY; + + // default a newly initialized region to inactive, unless always on is specified + _active = general().gridsAlwaysOn(); + } + + public int getTileX() { + return _tileX; + } + + public int getTileY() { + return _tileY; + } + + public List getZones() { + return _zones; + } + + public void addZone(L2ZoneType zone) { + _zones.add(zone); + } + + public void removeZone(L2ZoneType zone) { + _zones.remove(zone); + } + + public void revalidateZones(L2Character character) { + // do NOT update the world region while the character is still in the process of teleporting + // Once the teleport is COMPLETED, revalidation occurs safely, at that time. + + if (character.isTeleporting()) { + return; + } + + for (L2ZoneType z : getZones()) { + if (z != null) { + z.revalidateInZone(character); + } + } + } + + public void removeFromZones(L2Character character) { + for (L2ZoneType z : getZones()) { + if (z != null) { + z.removeCharacter(character); + } + } + } + + public boolean containsZone(int zoneId) { + for (L2ZoneType z : getZones()) { + if (z.getId() == zoneId) { + return true; + } + } + return false; + } + + public boolean checkEffectRangeInsidePeaceZone(Skill skill, final int x, final int y, final int z) { + final int range = skill.getEffectRange(); + final int up = y + range; + final int down = y - range; + final int left = x + range; + final int right = x - range; + + for (L2ZoneType e : getZones()) { + if (e instanceof L2PeaceZone) { + if (e.isInsideZone(x, up, z)) { + return false; + } + + if (e.isInsideZone(x, down, z)) { + return false; + } + + if (e.isInsideZone(left, y, z)) { + return false; + } + + if (e.isInsideZone(right, y, z)) { + return false; + } + + if (e.isInsideZone(x, y, z)) { + return false; + } + } + } + return true; + } + + public void onDeath(L2Character character) { + for (L2ZoneType z : getZones()) { + if (z != null) { + z.onDieInside(character); + } + } + } + + public void onRevive(L2Character character) { + for (L2ZoneType z : getZones()) { + if (z != null) { + z.onReviveInside(character); + } + } + } + + /** Task of AI notification */ + public class NeighborsTask implements Runnable { + private final boolean _isActivating; + + public NeighborsTask(boolean isActivating) { + _isActivating = isActivating; + } + + @Override + public void run() { + if (_isActivating) { + // for each neighbor, if it's not active, activate. + for (L2WorldRegion neighbor : getSurroundingRegions()) { + neighbor.setActive(true); + } + } else { + if (areNeighborsEmpty()) { + setActive(false); + } + + // check and deactivate + for (L2WorldRegion neighbor : getSurroundingRegions()) { + if (neighbor.areNeighborsEmpty()) { + neighbor.setActive(false); + } + } + } + } + } + + private void switchAI(boolean isOn) { + int c = 0; + if (!isOn) { + for (L2Object o : _visibleObjects.values()) { + if (o instanceof L2Attackable mob) { + c++; + + // Set target to null and cancel Attack or Cast + mob.setTarget(null); + + // Stop movement + mob.stopMove(null); + + // Stop all active skills effects in progress on the L2Character + mob.stopAllEffects(); + + mob.clearAggroList(); + mob.getAttackByList().clear(); + mob.getKnownList().removeAllKnownObjects(); + + // stop the ai tasks + if (mob.hasAI()) { + mob.getAI().setIntention(com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE); + mob.getAI().stopAITask(); + } + } else if (o instanceof L2Vehicle vehicle) { + c++; + vehicle.getKnownList().removeAllKnownObjects(); + } + } + LOG.debug("{} mobs were turned off", c); + } else { + for (L2Object o : _visibleObjects.values()) { + if (o instanceof L2Attackable attackable) { + c++; + // Start HP/MP/CP Regeneration task + attackable.getStatus().startHpMpRegeneration(); + } else if (o instanceof L2Npc npc) { + npc.startRandomAnimationTimer(); + } + } + LOG.debug("{} mobs were turned on", c); + } + } + + public boolean isActive() { + return _active; + } + + // check if all 9 neighbors (including self) are inactive or active but with no players. + // returns true if the above condition is met. + public boolean areNeighborsEmpty() { + // if this region is occupied, return false. + if (isActive() && !_allPlayable.isEmpty()) { + return false; + } + + // if any one of the neighbors is occupied, return false + for (L2WorldRegion neighbor : _surroundingRegions) { + if (neighbor.isActive() && !neighbor._allPlayable.isEmpty()) { + return false; + } + } + + // in all other cases, return true. + return true; + } + + /** + * this function turns this region's AI and geodata on or off + * @param value + */ + public void setActive(boolean value) { + if (_active == value) { + return; + } + + _active = value; + + // turn the AI on or off to match the region's activation. + switchAI(value); + + // TODO + // turn the geodata on or off to match the region's activation. + if (value) { + LOG.debug("Starting Grid {}, {}", _tileX, _tileY); + } else { + LOG.debug("Stopping Grid {}, {}", _tileX, _tileY); + } + } + + /** + * Immediately sets self as active and starts a timer to set neighbors as active this timer is to avoid turning on neighbors in the case when a person just teleported into a region and then teleported out immediately...there is no reason to activate all the neighbors in that case. + */ + private void startActivation() { + // first set self to active and do self-tasks... + setActive(true); + + // if the timer to deactivate neighbors is running, cancel it. + synchronized (this) { + if (_neighborsTask != null) { + _neighborsTask.cancel(true); + _neighborsTask = null; + } + + // then, set a timer to activate the neighbors + _neighborsTask = ThreadPoolManager.getInstance().scheduleGeneral(new NeighborsTask(true), 1000L * general().getGridNeighborTurnOnTime()); + } + } + + /** + * starts a timer to set neighbors (including self) as inactive this timer is to avoid turning off neighbors in the case when a person just moved out of a region that he may very soon return to. There is no reason to turn self & neighbors off in that case. + */ + private void startDeactivation() { + // if the timer to activate neighbors is running, cancel it. + synchronized (this) { + if (_neighborsTask != null) { + _neighborsTask.cancel(true); + _neighborsTask = null; + } + + // start a timer to "suggest" a deactivate to self and neighbors. + // suggest means: first check if a neighbor has L2PcInstances in it. If not, deactivate. + _neighborsTask = ThreadPoolManager.getInstance().scheduleGeneral(new NeighborsTask(false), 1000L * general().getGridNeighborTurnOffTime()); + } + } + + /** + * Add the L2Object in the L2ObjectHashSet(L2Object) _visibleObjects containing L2Object visible in this L2WorldRegion
    + * If L2Object is a L2PcInstance, Add the L2PcInstance in the L2ObjectHashSet(L2PcInstance) _allPlayable containing L2PcInstance of all player in game in this L2WorldRegion
    + * Assert : object.getCurrentWorldRegion() == this + * @param object + */ + public void addVisibleObject(L2Object object) { + if (object == null) { + return; + } + + assert object.getWorldRegion() == this; + + _visibleObjects.put(object.getObjectId(), object); + + if (object instanceof L2Playable playable) { + _allPlayable.put(object.getObjectId(), playable); + + // if this is the first player to enter the region, activate self & neighbors + if ((_allPlayable.size() == 1) && !general().gridsAlwaysOn()) { + startActivation(); + } + } + } + + /** + * Remove the L2Object from the L2ObjectHashSet(L2Object) _visibleObjects in this L2WorldRegion. If L2Object is a L2PcInstance, remove it from the L2ObjectHashSet(L2PcInstance) _allPlayable of this L2WorldRegion
    + * Assert : object.getCurrentWorldRegion() == this || object.getCurrentWorldRegion() == null + * @param object + */ + public void removeVisibleObject(L2Object object) { + if (object == null) { + return; + } + + assert (object.getWorldRegion() == this) || (object.getWorldRegion() == null); + + _visibleObjects.remove(object.getObjectId()); + + if (object instanceof L2Playable) { + _allPlayable.remove(object.getObjectId()); + + if (_allPlayable.isEmpty() && !general().gridsAlwaysOn()) { + startDeactivation(); + } + } + } + + public void addSurroundingRegion(L2WorldRegion region) { + _surroundingRegions.add(region); + } + + /** + * @return the list containing all L2WorldRegion around the current world region + */ + public Queue getSurroundingRegions() { + return _surroundingRegions; + } + + public Map getVisiblePlayable() { + return _allPlayable; + } + + public Map getVisibleObjects() { + return _visibleObjects; + } + + public String getName() { + return "(" + _tileX + ", " + _tileY + ")"; + } + + /** + * Deleted all spawns in the world. + */ + public void deleteVisibleNpcSpawns() { + LOG.debug("Deleting all visible NPCs in Region: {}", getName()); + for (L2Object obj : _visibleObjects.values()) { + if (obj instanceof L2Npc target) { + target.deleteMe(); + L2Spawn spawn = target.getSpawn(); + if (spawn != null) { + spawn.stopRespawn(); + SpawnTable.getInstance().deleteSpawn(spawn, false); + } + LOG.trace("Removed NPC {}", target.getObjectId()); + } + } + LOG.info("All visible NPCs deleted in Region: {}", getName()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Location.java b/src/main/java/com/l2jserver/gameserver/model/Location.java new file mode 100644 index 00000000..0dbe99a3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Location.java @@ -0,0 +1,194 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.interfaces.IPositionable; + +/** + * Location data transfer object.
    + * Contains coordinates data, heading and instance Id. + * @author Zoey76 + */ +public class Location implements IPositionable { + private int _x; + private int _y; + private int _z; + private int _heading; + private int _instanceId; + + public Location(int x, int y, int z) { + this(x, y, z, 0, -1); + } + + public Location(int x, int y, int z, int heading) { + this(x, y, z, heading, -1); + } + + public Location(L2Object obj) { + this(obj.getX(), obj.getY(), obj.getZ(), obj.getHeading(), obj.getInstanceId()); + } + + public Location(int x, int y, int z, int heading, int instanceId) { + _x = x; + _y = y; + _z = z; + _heading = heading; + _instanceId = instanceId; + } + + /** + * Get the x coordinate. + * @return the x coordinate + */ + @Override + public int getX() { + return _x; + } + + /** + * Set the x coordinate. + * @param x the x coordinate + */ + @Override + public void setX(int x) { + _x = x; + } + + /** + * Get the y coordinate. + * @return the y coordinate + */ + @Override + public int getY() { + return _y; + } + + /** + * Set the y coordinate. + * @param y the x coordinate + */ + @Override + public void setY(int y) { + _y = y; + } + + /** + * Get the z coordinate. + * @return the z coordinate + */ + @Override + public int getZ() { + return _z; + } + + /** + * Set the z coordinate. + * @param z the z coordinate + */ + @Override + public void setZ(int z) { + _z = z; + } + + /** + * Set the x, y, z coordinates. + * @param x the x coordinate + * @param y the y coordinate + * @param z the z coordinate + */ + @Override + public void setXYZ(int x, int y, int z) { + setX(x); + setY(y); + setZ(z); + } + + /** + * Set the x, y, z coordinates. + * @param loc The location. + */ + @Override + public void setXYZ(ILocational loc) { + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + } + + /** + * Get the heading. + * @return the heading + */ + @Override + public int getHeading() { + return _heading; + } + + /** + * Set the heading. + * @param heading the heading + */ + @Override + public void setHeading(int heading) { + _heading = heading; + } + + /** + * Get the instance Id. + * @return the instance Id + */ + @Override + public int getInstanceId() { + return _instanceId; + } + + /** + * Set the instance Id. + * @param instanceId the instance Id to set + */ + @Override + public void setInstanceId(int instanceId) { + _instanceId = instanceId; + } + + @Override + public IPositionable getLocation() { + return this; + } + + @Override + public void setLocation(Location loc) { + _x = loc.getX(); + _y = loc.getY(); + _z = loc.getZ(); + _heading = loc.getHeading(); + _instanceId = loc.getInstanceId(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Location loc) { + return (getX() == loc.getX()) && (getY() == loc.getY()) && (getZ() == loc.getZ()) && (getHeading() == loc.getHeading()) && (getInstanceId() == loc.getInstanceId()); + } + return false; + } + + @Override + public String toString() { + return "[Location] X: " + getX() + " Y: " + getY() + " Z: " + getZ() + " Heading: " + _heading + " Instance Id: " + _instanceId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Macro.java b/src/main/java/com/l2jserver/gameserver/model/Macro.java new file mode 100644 index 00000000..2495251a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Macro.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; + +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.interfaces.INamable; + +public class Macro implements IIdentifiable, INamable { + private int _id; + private final int _icon; + private final String _name; + private final String _descr; + private final String _acronym; + private final List _commands; + + /** + * Constructor for macros. + * @param id the macro ID + * @param icon the icon ID + * @param name the macro name + * @param descr the macro description + * @param acronym the macro acronym + * @param list the macro command list + */ + public Macro(int id, int icon, String name, String descr, String acronym, List list) { + _id = id; + _icon = icon; + _name = name; + _descr = descr; + _acronym = acronym; + _commands = list; + } + + /** + * Gets the marco ID. + * @returns the marco ID + */ + @Override + public int getId() { + return _id; + } + + /** + * Sets the marco ID. + * @param id the marco ID + */ + public void setId(int id) { + _id = id; + } + + /** + * Gets the macro icon ID. + * @return the icon + */ + public int getIcon() { + return _icon; + } + + /** + * Gets the macro name. + * @return the name + */ + @Override + public String getName() { + return _name; + } + + /** + * Gets the macro description. + * @return the description + */ + public String getDescr() { + return _descr; + } + + /** + * Gets the macro acronym. + * @return the acronym + */ + public String getAcronym() { + return _acronym; + } + + /** + * Gets the macro command list. + * @return the macro command list + */ + public List getCommands() { + return _commands; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/MacroCmd.java b/src/main/java/com/l2jserver/gameserver/model/MacroCmd.java new file mode 100644 index 00000000..1c0d0ac2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/MacroCmd.java @@ -0,0 +1,81 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.enums.MacroType; + +/** + * Macro Cmd DTO. + * @author Zoey76 + */ +public class MacroCmd { + private final int _entry; + private final MacroType _type; + private final int _d1; // skill_id or page for shortcuts + private final int _d2; // shortcut + private final String _cmd; + + public MacroCmd(int entry, MacroType type, int d1, int d2, String cmd) { + _entry = entry; + _type = type; + _d1 = d1; + _d2 = d2; + _cmd = cmd; + } + + /** + * Gets the entry index. + * @return the entry index + */ + public int getEntry() { + return _entry; + } + + /** + * Gets the macro type. + * @return the macro type + */ + public MacroType getType() { + return _type; + } + + /** + * Gets the skill ID, item ID, page ID, depending on the marco use. + * @return the first value + */ + public int getD1() { + return _d1; + } + + /** + * Gets the skill level, shortcut ID, depending on the marco use. + * @return the second value + */ + public int getD2() { + return _d2; + } + + /** + * Gets the command. + * @return the command + */ + public String getCmd() { + return _cmd; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/MacroList.java b/src/main/java/com/l2jserver/gameserver/model/MacroList.java new file mode 100644 index 00000000..d5fa9577 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/MacroList.java @@ -0,0 +1,191 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.enums.MacroType; +import com.l2jserver.gameserver.enums.ShortcutType; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.IRestorable; +import com.l2jserver.gameserver.network.serverpackets.SendMacroList; +import com.l2jserver.gameserver.util.StringUtil; + +public class MacroList implements IRestorable { + private static final Logger LOG = LoggerFactory.getLogger(MacroList.class); + + private final L2PcInstance _owner; + + private int _revision; + + private int _macroId; + + private final Map _macros = Collections.synchronizedMap(new LinkedHashMap<>()); + + public MacroList(L2PcInstance owner) { + _owner = owner; + _revision = 1; + _macroId = 1000; + } + + public int getRevision() { + return _revision; + } + + public Map getAllMacros() { + return _macros; + } + + public void registerMacro(Macro macro) { + if (macro.getId() == 0) { + do { + macro.setId(_macroId++); + } + while (_macros.containsKey(macro.getId())); + _macros.put(macro.getId(), macro); + registerMacroInDb(macro); + } else { + final Macro old = _macros.put(macro.getId(), macro); + if (old != null) { + deleteMacroFromDb(old); + } + registerMacroInDb(macro); + } + sendUpdate(); + } + + public void deleteMacro(int id) { + final Macro removed = _macros.remove(id); + if (removed != null) { + deleteMacroFromDb(removed); + } + + final Shortcut[] allShortCuts = _owner.getAllShortCuts(); + for (Shortcut sc : allShortCuts) { + if ((sc.getId() == id) && (sc.getType() == ShortcutType.MACRO)) { + _owner.deleteShortCut(sc.getSlot(), sc.getPage()); + } + } + + sendUpdate(); + } + + public void sendUpdate() { + _revision++; + final Collection allMacros = _macros.values(); + synchronized (_macros) { + if (allMacros.isEmpty()) { + _owner.sendPacket(new SendMacroList(_revision, 0, null)); + } else { + for (Macro m : allMacros) { + _owner.sendPacket(new SendMacroList(_revision, allMacros.size(), m)); + } + } + } + } + + private void registerMacroInDb(Macro macro) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO character_macroses (charId,id,icon,name,descr,acronym,commands) values(?,?,?,?,?,?,?)")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, macro.getId()); + ps.setInt(3, macro.getIcon()); + ps.setString(4, macro.getName()); + ps.setString(5, macro.getDescr()); + ps.setString(6, macro.getAcronym()); + final StringBuilder sb = new StringBuilder(300); + for (MacroCmd cmd : macro.getCommands()) { + StringUtil.append(sb, String.valueOf(cmd.getType().ordinal()), ",", String.valueOf(cmd.getD1()), ",", String.valueOf(cmd.getD2())); + if ((cmd.getCmd() != null) && (!cmd.getCmd().isEmpty())) { + StringUtil.append(sb, ",", cmd.getCmd()); + } + sb.append(';'); + } + + if (sb.length() > 255) { + sb.setLength(255); + } + + ps.setString(7, sb.toString()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not store macro:", e); + } + } + + private void deleteMacroFromDb(Macro macro) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM character_macroses WHERE charId=? AND id=?")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, macro.getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not delete macro:", e); + } + } + + @Override + public boolean restoreMe() { + _macros.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT charId, id, icon, name, descr, acronym, commands FROM character_macroses WHERE charId=?")) { + ps.setInt(1, _owner.getObjectId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int id = rs.getInt("id"); + int icon = rs.getInt("icon"); + String name = rs.getString("name"); + String descr = rs.getString("descr"); + String acronym = rs.getString("acronym"); + List commands = new ArrayList<>(); + StringTokenizer st1 = new StringTokenizer(rs.getString("commands"), ";"); + while (st1.hasMoreTokens()) { + StringTokenizer st = new StringTokenizer(st1.nextToken(), ","); + if (st.countTokens() < 3) { + continue; + } + MacroType type = MacroType.values()[Integer.parseInt(st.nextToken())]; + int d1 = Integer.parseInt(st.nextToken()); + int d2 = Integer.parseInt(st.nextToken()); + String cmd = ""; + if (st.hasMoreTokens()) { + cmd = st.nextToken(); + } + commands.add(new MacroCmd(commands.size(), type, d1, d2, cmd)); + } + _macros.put(id, new Macro(id, icon, name, descr, acronym, commands)); + } + } + } catch (Exception e) { + LOG.warn("Could not store shortcuts:", e); + return false; + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/MobGroup.java b/src/main/java/com/l2jserver/gameserver/model/MobGroup.java new file mode 100644 index 00000000..2b245309 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/MobGroup.java @@ -0,0 +1,335 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2ControllableMobAI; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2ControllableMobInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @author littlecrow + */ +public final class MobGroup { + private final L2NpcTemplate _npcTemplate; + private final int _groupId; + private final int _maxMobCount; + + private List _mobs; + + public MobGroup(int groupId, L2NpcTemplate npcTemplate, int maxMobCount) { + _groupId = groupId; + _npcTemplate = npcTemplate; + _maxMobCount = maxMobCount; + } + + public int getActiveMobCount() { + return getMobs().size(); + } + + public int getGroupId() { + return _groupId; + } + + public int getMaxMobCount() { + return _maxMobCount; + } + + public List getMobs() { + if (_mobs == null) { + _mobs = new CopyOnWriteArrayList<>(); + } + + return _mobs; + } + + public String getStatus() { + try { + L2ControllableMobAI mobGroupAI = (L2ControllableMobAI) getMobs().get(0).getAI(); + return switch (mobGroupAI.getAlternateAI()) { + case L2ControllableMobAI.AI_FORCE_ATTACK -> "Force Attacking"; + case L2ControllableMobAI.AI_FOLLOW -> "Following"; + case L2ControllableMobAI.AI_CAST -> "Casting"; + case L2ControllableMobAI.AI_ATTACK_GROUP -> "Attacking Group"; + default -> "Idle"; + }; + } catch (Exception e) { + return "Unspawned"; + } + } + + public L2NpcTemplate getTemplate() { + return _npcTemplate; + } + + public boolean isGroupMember(L2ControllableMobInstance mobInst) { + for (L2ControllableMobInstance groupMember : getMobs()) { + if (groupMember == null) { + continue; + } + + if (groupMember.getObjectId() == mobInst.getObjectId()) { + return true; + } + } + + return false; + } + + public void spawnGroup(int x, int y, int z) { + if (getActiveMobCount() > 0) { + return; + } + + try { + for (int i = 0; i < getMaxMobCount(); i++) { + L2GroupSpawn spawn = new L2GroupSpawn(getTemplate()); + + int signX = (Rnd.nextInt(2) == 0) ? -1 : 1; + int signY = (Rnd.nextInt(2) == 0) ? -1 : 1; + int randX = Rnd.nextInt(MobGroupTable.RANDOM_RANGE); + int randY = Rnd.nextInt(MobGroupTable.RANDOM_RANGE); + + spawn.setX(x + (signX * randX)); + spawn.setY(y + (signY * randY)); + spawn.setZ(z); + spawn.stopRespawn(); + + SpawnTable.getInstance().addNewSpawn(spawn, false); + getMobs().add((L2ControllableMobInstance) spawn.doGroupSpawn()); + } + } catch (Exception ex) { + + } + } + + public void spawnGroup(L2PcInstance activeChar) { + spawnGroup(activeChar.getX(), activeChar.getY(), activeChar.getZ()); + } + + public void teleportGroup(L2PcInstance player) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + if (!mobInst.isDead()) { + int x = player.getX() + Rnd.nextInt(50); + int y = player.getY() + Rnd.nextInt(50); + + mobInst.teleToLocation(new Location(x, y, player.getZ()), true); + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.follow(player); + } + } + } + + public L2ControllableMobInstance getRandomMob() { + removeDead(); + + if (getActiveMobCount() == 0) { + return null; + } + + int choice = Rnd.nextInt(getActiveMobCount()); + return getMobs().get(choice); + } + + public void unspawnGroup() { + removeDead(); + + if (getActiveMobCount() == 0) { + return; + } + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + if (!mobInst.isDead()) { + mobInst.deleteMe(); + } + + SpawnTable.getInstance().deleteSpawn(mobInst.getSpawn(), false); + } + + getMobs().clear(); + } + + public void killGroup(L2PcInstance activeChar) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + if (!mobInst.isDead()) { + mobInst.reduceCurrentHp(mobInst.getMaxHp() + 1, activeChar, null); + } + + SpawnTable.getInstance().deleteSpawn(mobInst.getSpawn(), false); + } + + getMobs().clear(); + } + + public void setAttackRandom() { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.setAlternateAI(L2ControllableMobAI.AI_NORMAL); + ai.setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + } + + public void setAttackTarget(L2Character target) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.forceAttack(target); + } + } + + public void setIdleMode() { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.stop(); + } + } + + public void returnGroup(L2Character activeChar) { + setIdleMode(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + int signX = (Rnd.nextInt(2) == 0) ? -1 : 1; + int signY = (Rnd.nextInt(2) == 0) ? -1 : 1; + int randX = Rnd.nextInt(MobGroupTable.RANDOM_RANGE); + int randY = Rnd.nextInt(MobGroupTable.RANDOM_RANGE); + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.move(activeChar.getX() + (signX * randX), activeChar.getY() + (signY * randY), activeChar.getZ()); + } + } + + public void setFollowMode(L2Character character) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.follow(character); + } + } + + public void setCastMode() { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.setAlternateAI(L2ControllableMobAI.AI_CAST); + } + } + + public void setNoMoveMode(boolean enabled) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.setNotMoving(enabled); + } + } + + protected void removeDead() { + List deadMobs = new LinkedList<>(); + for (L2ControllableMobInstance mobInst : getMobs()) { + if ((mobInst != null) && mobInst.isDead()) { + deadMobs.add(mobInst); + } + } + + getMobs().removeAll(deadMobs); + } + + public void setInvul(boolean invulState) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst != null) { + mobInst.setInvul(invulState); + } + } + } + + public void setAttackGroup(MobGroup otherGrp) { + removeDead(); + + for (L2ControllableMobInstance mobInst : getMobs()) { + if (mobInst == null) { + continue; + } + + L2ControllableMobAI ai = (L2ControllableMobAI) mobInst.getAI(); + ai.forceAttackGroup(otherGrp); + ai.setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/MobGroupTable.java b/src/main/java/com/l2jserver/gameserver/model/MobGroupTable.java new file mode 100644 index 00000000..dd1d4ae1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/MobGroupTable.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.actor.instance.L2ControllableMobInstance; + +/** + * @author littlecrow + */ +public class MobGroupTable { + private final Map _groupMap; + + public static final int FOLLOW_RANGE = 300; + public static final int RANDOM_RANGE = 300; + + protected MobGroupTable() { + _groupMap = new ConcurrentHashMap<>(); + } + + public static MobGroupTable getInstance() { + return SingletonHolder._instance; + } + + public void addGroup(int groupKey, MobGroup group) { + _groupMap.put(groupKey, group); + } + + public MobGroup getGroup(int groupKey) { + return _groupMap.get(groupKey); + } + + public int getGroupCount() { + return _groupMap.size(); + } + + public MobGroup getGroupForMob(L2ControllableMobInstance mobInst) { + for (MobGroup mobGroup : _groupMap.values()) { + if (mobGroup.isGroupMember(mobInst)) { + return mobGroup; + } + } + + return null; + } + + public MobGroup[] getGroups() { + return _groupMap.values().toArray(new MobGroup[getGroupCount()]); + } + + public boolean removeGroup(int groupKey) { + return (_groupMap.remove(groupKey) != null); + } + + private static class SingletonHolder { + protected static final MobGroupTable _instance = new MobGroupTable(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/PageResult.java b/src/main/java/com/l2jserver/gameserver/model/PageResult.java new file mode 100644 index 00000000..cc12d9a4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/PageResult.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author UnAfraid + */ +public class PageResult { + private final int _pages; + private final StringBuilder _pagerTemplate; + private final StringBuilder _bodyTemplate; + + public PageResult(int pages, StringBuilder pagerTemplate, StringBuilder bodyTemplate) { + _pages = pages; + _pagerTemplate = pagerTemplate; + _bodyTemplate = bodyTemplate; + } + + public int getPages() { + return _pages; + } + + public StringBuilder getPagerTemplate() { + return _pagerTemplate; + } + + public StringBuilder getBodyTemplate() { + return _bodyTemplate; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoom.java b/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoom.java new file mode 100644 index 00000000..0eef7ab6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoom.java @@ -0,0 +1,177 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExManagePartyRoomMember; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * @author Gnacik + */ +public class PartyMatchRoom implements IIdentifiable { + private final int _id; + private String _title; + private int _loot; + private int _minlvl; + private int _maxlvl; + private int _maxmem; + private final List _members = new ArrayList<>(); + + public PartyMatchRoom(int id, String title, int loot, int minlvl, int maxlvl, int maxmem, L2PcInstance owner) { + _id = id; + _title = title; + _loot = loot; + _minlvl = minlvl; + _maxlvl = maxlvl; + _maxmem = maxmem; + _members.add(owner); + } + + public List getPartyMembers() { + return _members; + } + + public void addMember(L2PcInstance player) { + _members.add(player); + } + + public void deleteMember(L2PcInstance player) { + if (player != getOwner()) { + _members.remove(player); + notifyMembersAboutExit(player); + } else if (_members.size() == 1) { + PartyMatchRoomList.getInstance().deleteRoom(_id); + } else { + changeLeader(_members.get(1)); + deleteMember(player); + } + } + + public void notifyMembersAboutExit(L2PcInstance player) { + for (L2PcInstance _member : _members) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_LEFT_PARTY_ROOM); + sm.addCharName(player); + _member.sendPacket(sm); + _member.sendPacket(new ExManagePartyRoomMember(player, this, 2)); + } + } + + public void changeLeader(L2PcInstance newLeader) { + // Get current leader + L2PcInstance oldLeader = _members.get(0); + // Remove new leader + _members.remove(newLeader); + // Move him to first position + _members.set(0, newLeader); + // Add old leader as normal member + _members.add(oldLeader); + // Broadcast change + for (L2PcInstance member : _members) { + member.sendPacket(new ExManagePartyRoomMember(newLeader, this, 1)); + member.sendPacket(new ExManagePartyRoomMember(oldLeader, this, 1)); + member.sendPacket(SystemMessageId.PARTY_ROOM_LEADER_CHANGED); + } + } + + @Override + public int getId() { + return _id; + } + + public int getLootType() { + return _loot; + } + + public int getMinLvl() { + return _minlvl; + } + + public int getMaxLvl() { + return _maxlvl; + } + + /** + *
      + *
    • 1 : Talking Island
    • + *
    • 2 : Gludio
    • + *
    • 3 : Dark Elven Ter.
    • + *
    • 4 : Elven Territory
    • + *
    • 5 : Dion
    • + *
    • 6 : Giran
    • + *
    • 7 : Neutral Zone
    • + *
    • 8 : Lyonn
    • + *
    • 9 : Schuttgart
    • + *
    • 10 : Oren
    • + *
    • 11 : Hunters Village
    • + *
    • 12 : Innadril
    • + *
    • 13 : Aden
    • + *
    • 14 : Rune
    • + *
    • 15 : Goddard
    • + *
    + * @return the id + */ + public int getLocation() { + return MapRegionManager.getInstance().getMapRegion(_members.get(0)).getBbs(); + } + + public int getMembers() { + return _members.size(); + } + + public int getMaxMembers() { + return _maxmem; + } + + public String getTitle() { + return _title; + } + + public L2PcInstance getOwner() { + return _members.get(0); + } + + /* SET */ + + public void setMinLvl(int minlvl) { + _minlvl = minlvl; + } + + public void setMaxLvl(int maxlvl) { + _maxlvl = maxlvl; + } + + public void setLootType(int loot) { + _loot = loot; + } + + public void setMaxMembers(int maxmem) { + _maxmem = maxmem; + } + + public void setTitle(String title) { + _title = title; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoomList.java b/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoomList.java new file mode 100644 index 00000000..8bc83b72 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/PartyMatchRoomList.java @@ -0,0 +1,105 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExClosePartyRoom; + +/** + * @author Gnacik + */ +public class PartyMatchRoomList { + private int _maxid = 1; + private final Map _rooms; + + protected PartyMatchRoomList() { + _rooms = new ConcurrentHashMap<>(); + } + + public synchronized void addPartyMatchRoom(int id, PartyMatchRoom room) { + _rooms.put(id, room); + _maxid++; + } + + public void deleteRoom(int id) { + for (L2PcInstance _member : getRoom(id).getPartyMembers()) { + if (_member == null) { + continue; + } + + _member.sendPacket(new ExClosePartyRoom()); + _member.sendPacket(SystemMessageId.PARTY_ROOM_DISBANDED); + + _member.setPartyRoom(0); + // _member.setPartyMatching(0); + _member.broadcastUserInfo(); + } + _rooms.remove(id); + } + + public PartyMatchRoom getRoom(int id) { + return _rooms.get(id); + } + + public PartyMatchRoom[] getRooms() { + return _rooms.values().toArray(new PartyMatchRoom[_rooms.size()]); + } + + public int getPartyMatchRoomCount() { + return _rooms.size(); + } + + public int getMaxId() { + return _maxid; + } + + public PartyMatchRoom getPlayerRoom(L2PcInstance player) { + for (PartyMatchRoom _room : _rooms.values()) { + for (L2PcInstance member : _room.getPartyMembers()) { + if (member.equals(player)) { + return _room; + } + } + } + return null; + } + + public int getPlayerRoomId(L2PcInstance player) { + for (PartyMatchRoom _room : _rooms.values()) { + for (L2PcInstance member : _room.getPartyMembers()) { + if (member.equals(player)) { + return _room.getId(); + } + } + } + return -1; + } + + public static PartyMatchRoomList getInstance() { + return SingletonHolder._instance; + } + + private static class SingletonHolder { + protected static final PartyMatchRoomList _instance = new PartyMatchRoomList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/PartyMatchWaitingList.java b/src/main/java/com/l2jserver/gameserver/model/PartyMatchWaitingList.java new file mode 100644 index 00000000..e9f4154d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/PartyMatchWaitingList.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.util.Strings; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Party Match Waiting List. + * @author Zoey76 + */ +public class PartyMatchWaitingList { + + private final Set _members = ConcurrentHashMap.newKeySet(1); + + protected PartyMatchWaitingList() { + // Do nothing. + } + + public void addPlayer(L2PcInstance player) { + _members.add(player); + } + + public void removePlayer(L2PcInstance player) { + _members.remove(player); + } + + public Set getPlayers() { + return _members; + } + + public List findPlayers(int minLevel, int maxLevel, Set classes, String filter) { + return _members.stream() // + .filter(p -> p.getLevel() >= minLevel) // + .filter(p -> p.getLevel() <= maxLevel) // + .filter(p -> classes.isEmpty() || classes.contains(p.getClassId().getId())) // + .filter(p -> Strings.isBlank(filter) || p.getName().toLowerCase().contains(filter.toLowerCase())) // + .filter(L2PcInstance::isPartyWaiting) // + .collect(Collectors.toList()); + } + + public static PartyMatchWaitingList getInstance() { + return SingletonHolder.INSTANCE; + } + + private static class SingletonHolder { + protected static final PartyMatchWaitingList INSTANCE = new PartyMatchWaitingList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/PcCondOverride.java b/src/main/java/com/l2jserver/gameserver/model/PcCondOverride.java new file mode 100644 index 00000000..478e3903 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/PcCondOverride.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author UnAfraid + */ +public enum PcCondOverride { + MAX_STATS_VALUE(0, "Overrides maximum states conditions"), + ITEM_CONDITIONS(1, "Overrides item usage conditions"), + SKILL_CONDITIONS(2, "Overrides skill usage conditions"), + ZONE_CONDITIONS(3, "Overrides zone conditions"), + CASTLE_CONDITIONS(4, "Overrides castle conditions"), + FORTRESS_CONDITIONS(5, "Overrides fortress conditions"), + CLANHALL_CONDITIONS(6, "Overrides clan hall conditions"), + FLOOD_CONDITIONS(7, "Overrides floods conditions"), + CHAT_CONDITIONS(8, "Overrides chat conditions"), + INSTANCE_CONDITIONS(9, "Overrides instance conditions"), + QUEST_CONDITIONS(10, "Overrides quest conditions"), + DEATH_PENALTY(11, "Overrides death penalty conditions"), + DESTROY_ALL_ITEMS(12, "Overrides item destroy conditions"), + SEE_ALL_PLAYERS(13, "Overrides the conditions to see hidden players"), + TARGET_ALL(14, "Overrides target conditions"), + DROP_ALL_ITEMS(15, "Overrides item drop conditions"); + + private final int _mask; + private final String _descr; + + PcCondOverride(int id, String descr) { + _mask = 1 << id; + _descr = descr; + } + + public int getMask() { + return _mask; + } + + public String getDescription() { + return _descr; + } + + public static PcCondOverride getCondOverride(int ordinal) { + try { + return values()[ordinal]; + } catch (Exception e) { + return null; + } + } + + public static long getAllExceptionsMask() { + long result = 0L; + for (PcCondOverride ex : values()) { + result |= ex.getMask(); + } + return result; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Petition.java b/src/main/java/com/l2jserver/gameserver/model/Petition.java new file mode 100644 index 00000000..b76688c8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Petition.java @@ -0,0 +1,155 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import com.l2jserver.gameserver.enums.PetitionState; +import com.l2jserver.gameserver.enums.PetitionType; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.PetitionManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.PetitionVotePacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Petition + * @author xban1x + */ +public final class Petition { + private final long _submitTime = System.currentTimeMillis(); + private final int _id; + private final PetitionType _type; + private PetitionState _state = PetitionState.PENDING; + private final String _content; + private final List _messageLog = new CopyOnWriteArrayList<>(); + private final L2PcInstance _petitioner; + private L2PcInstance _responder; + + public Petition(L2PcInstance petitioner, String petitionText, int petitionType) { + _id = IdFactory.getInstance().getNextId(); + _type = PetitionType.values()[--petitionType]; + _content = petitionText; + _petitioner = petitioner; + } + + public boolean addLogMessage(CreatureSay cs) { + return _messageLog.add(cs); + } + + public List getLogMessages() { + return _messageLog; + } + + public boolean endPetitionConsultation(PetitionState endState) { + setState(endState); + + if ((getResponder() != null) && getResponder().isOnline()) { + if (endState == PetitionState.RESPONDER_REJECT) { + getPetitioner().sendMessage("Your petition was rejected. Please try again later."); + } else { + // Ending petition consultation with . + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PETITION_ENDED_WITH_C1); + sm.addString(getPetitioner().getName()); + getResponder().sendPacket(sm); + + if (endState == PetitionState.PETITIONER_CANCEL) { + // Receipt No. petition cancelled. + sm = SystemMessage.getSystemMessage(SystemMessageId.RECENT_NO_S1_CANCELED); + sm.addInt(getId()); + getResponder().sendPacket(sm); + } + } + } + + // End petition consultation and inform them, if they are still online. And if petitioner is online, enable Evaluation button + if ((getPetitioner() != null) && getPetitioner().isOnline()) { + getPetitioner().sendPacket(SystemMessageId.THIS_END_THE_PETITION_PLEASE_PROVIDE_FEEDBACK); + getPetitioner().sendPacket(PetitionVotePacket.STATIC_PACKET); + } + + PetitionManager.getInstance().getCompletedPetitions().put(getId(), this); + return (PetitionManager.getInstance().getPendingPetitions().remove(getId()) != null); + } + + public String getContent() { + return _content; + } + + public int getId() { + return _id; + } + + public L2PcInstance getPetitioner() { + return _petitioner; + } + + public L2PcInstance getResponder() { + return _responder; + } + + public long getSubmitTime() { + return _submitTime; + } + + public PetitionState getState() { + return _state; + } + + public String getTypeAsString() { + return _type.toString().replace("_", " "); + } + + public void sendPetitionerPacket(L2GameServerPacket responsePacket) { + if ((getPetitioner() == null) || !getPetitioner().isOnline()) { + // Allows petitioners to see the results of their petition when + // they log back into the game. + + // endPetitionConsultation(PetitionState.Petitioner_Missing); + return; + } + + getPetitioner().sendPacket(responsePacket); + } + + public void sendResponderPacket(L2GameServerPacket responsePacket) { + if ((getResponder() == null) || !getResponder().isOnline()) { + endPetitionConsultation(PetitionState.RESPONDER_MISSING); + return; + } + + getResponder().sendPacket(responsePacket); + } + + public void setState(PetitionState state) { + _state = state; + } + + public void setResponder(L2PcInstance respondingAdmin) { + if (getResponder() != null) { + return; + } + + _responder = respondingAdmin; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/SeedProduction.java b/src/main/java/com/l2jserver/gameserver/model/SeedProduction.java new file mode 100644 index 00000000..b095294c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/SeedProduction.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author xban1x + */ +public class SeedProduction { + private final int _seedId; + private final long _price; + private final long _startAmount; + private final AtomicLong _amount; + + public SeedProduction(int id, long amount, long price, long startAmount) { + _seedId = id; + _amount = new AtomicLong(amount); + _price = price; + _startAmount = startAmount; + } + + public final int getId() { + return _seedId; + } + + public final long getAmount() { + return _amount.get(); + } + + public final long getPrice() { + return _price; + } + + public final long getStartAmount() { + return _startAmount; + } + + public final void setAmount(long amount) { + _amount.set(amount); + } + + public final boolean decreaseAmount(long val) { + long current, next; + do { + current = _amount.get(); + next = current - val; + if (next < 0) { + return false; + } + } + while (!_amount.compareAndSet(current, next)); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/ShortCuts.java b/src/main/java/com/l2jserver/gameserver/model/ShortCuts.java new file mode 100644 index 00000000..6f162bf1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/ShortCuts.java @@ -0,0 +1,196 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Map; +import java.util.TreeMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.enums.ShortcutType; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.interfaces.IRestorable; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.EtcItemType; +import com.l2jserver.gameserver.network.serverpackets.ExAutoSoulShot; +import com.l2jserver.gameserver.network.serverpackets.ShortCutInit; +import com.l2jserver.gameserver.network.serverpackets.ShortCutRegister; + +public class ShortCuts implements IRestorable { + private static final Logger LOG = LoggerFactory.getLogger(ShortCuts.class); + + private static final int MAX_SHORTCUTS_PER_BAR = 12; + private final L2PcInstance _owner; + private final Map _shortCuts = new TreeMap<>(); + + public ShortCuts(L2PcInstance owner) { + _owner = owner; + } + + public Shortcut[] getAllShortCuts() { + return _shortCuts.values().toArray(new Shortcut[0]); + } + + public Shortcut getShortCut(int slot, int page) { + Shortcut sc = _shortCuts.get(slot + (page * MAX_SHORTCUTS_PER_BAR)); + // Verify shortcut + if ((sc != null) && (sc.getType() == ShortcutType.ITEM)) { + if (_owner.getInventory().getItemByObjectId(sc.getId()) == null) { + deleteShortCut(sc.getSlot(), sc.getPage()); + sc = null; + } + } + return sc; + } + + public synchronized void registerShortCut(Shortcut shortcut) { + // Verify shortcut + if (shortcut.getType() == ShortcutType.ITEM) { + final L2ItemInstance item = _owner.getInventory().getItemByObjectId(shortcut.getId()); + if (item == null) { + return; + } + shortcut.setSharedReuseGroup(item.getSharedReuseGroup()); + } + final Shortcut oldShortCut = _shortCuts.put(shortcut.getSlot() + (shortcut.getPage() * MAX_SHORTCUTS_PER_BAR), shortcut); + registerShortCutInDb(shortcut, oldShortCut); + } + + private void registerShortCutInDb(Shortcut shortcut, Shortcut oldShortCut) { + if (oldShortCut != null) { + deleteShortCutFromDb(oldShortCut); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO character_shortcuts (charId,slot,page,type,shortcut_id,level,class_index) values(?,?,?,?,?,?,?)")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, shortcut.getSlot()); + ps.setInt(3, shortcut.getPage()); + ps.setInt(4, shortcut.getType().ordinal()); + ps.setInt(5, shortcut.getId()); + ps.setInt(6, shortcut.getLevel()); + ps.setInt(7, _owner.getClassIndex()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not store character shortcut: {}", e.getMessage(), e); + } + } + + public synchronized void deleteShortCut(int slot, int page) { + final Shortcut old = _shortCuts.remove(slot + (page * MAX_SHORTCUTS_PER_BAR)); + if ((old == null) || (_owner == null)) { + return; + } + deleteShortCutFromDb(old); + if (old.getType() == ShortcutType.ITEM) { + L2ItemInstance item = _owner.getInventory().getItemByObjectId(old.getId()); + + if ((item != null) && (item.getItemType() == EtcItemType.SHOT)) { + if (_owner.removeAutoSoulShot(item.getId())) { + _owner.sendPacket(new ExAutoSoulShot(item.getId(), 0)); + } + } + } + + _owner.sendPacket(new ShortCutInit(_owner)); + + for (int shotId : _owner.getAutoSoulShot()) { + _owner.sendPacket(new ExAutoSoulShot(shotId, 1)); + } + } + + public synchronized void deleteShortCutByObjectId(int objectId) { + for (Shortcut shortcut : _shortCuts.values()) { + if ((shortcut.getType() == ShortcutType.ITEM) && (shortcut.getId() == objectId)) { + deleteShortCut(shortcut.getSlot(), shortcut.getPage()); + break; + } + } + } + + private void deleteShortCutFromDb(Shortcut shortcut) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM character_shortcuts WHERE charId=? AND slot=? AND page=? AND class_index=?")) { + ps.setInt(1, _owner.getObjectId()); + ps.setInt(2, shortcut.getSlot()); + ps.setInt(3, shortcut.getPage()); + ps.setInt(4, _owner.getClassIndex()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Could not delete character shortcut: {}", e.getMessage(), e); + } + } + + @Override + public boolean restoreMe() { + _shortCuts.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement("SELECT charId, slot, page, type, shortcut_id, level FROM character_shortcuts WHERE charId=? AND class_index=?")) { + statement.setInt(1, _owner.getObjectId()); + statement.setInt(2, _owner.getClassIndex()); + + try (var rs = statement.executeQuery()) { + while (rs.next()) { + int slot = rs.getInt("slot"); + int page = rs.getInt("page"); + int type = rs.getInt("type"); + int id = rs.getInt("shortcut_id"); + int level = rs.getInt("level"); + + _shortCuts.put(slot + (page * MAX_SHORTCUTS_PER_BAR), new Shortcut(slot, page, ShortcutType.values()[type], id, level, 1)); + } + } + } catch (Exception e) { + LOG.warn("Could not restore character shortcuts: {}", e.getMessage(), e); + return false; + } + + // Verify shortcuts + for (Shortcut sc : getAllShortCuts()) { + if (sc.getType() == ShortcutType.ITEM) { + L2ItemInstance item = _owner.getInventory().getItemByObjectId(sc.getId()); + if (item == null) { + deleteShortCut(sc.getSlot(), sc.getPage()); + } else if (item.isEtcItem()) { + sc.setSharedReuseGroup(item.getEtcItem().getSharedReuseGroup()); + } + } + } + + return true; + } + + /** + * Updates the shortcut bars with the new skill. + * @param skillId the skill Id to search and update. + * @param skillLevel the skill level to update. + */ + public synchronized void updateShortCuts(int skillId, int skillLevel) { + // Update all the shortcuts for this skill + for (Shortcut sc : _shortCuts.values()) { + if ((sc.getId() == skillId) && (sc.getType() == ShortcutType.SKILL)) { + final var shortcut = new Shortcut(sc.getSlot(), sc.getPage(), sc.getType(), sc.getId(), skillLevel, 1); + _owner.sendPacket(new ShortCutRegister(shortcut)); + _owner.registerShortCut(shortcut); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/Shortcut.java b/src/main/java/com/l2jserver/gameserver/model/Shortcut.java new file mode 100644 index 00000000..bf4a7eec --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/Shortcut.java @@ -0,0 +1,115 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.enums.ShortcutType; + +/** + * Shortcut DTO. + * @author Zoey76 + */ +public class Shortcut { + /** Slot from 0 to 11. */ + private final int _slot; + /** Page from 0 to 9. */ + private final int _page; + /** Type: item, skill, action, macro, recipe, bookmark. */ + private final ShortcutType _type; + /** Shortcut ID. */ + private final int _id; + /** Shortcut level (skills). */ + private final int _level; + /** Character type: 1 player, 2 summon. */ + private final int _characterType; + /** Shared reuse group. */ + private int _sharedReuseGroup = -1; + + public Shortcut(int slot, int page, ShortcutType type, int id, int level, int characterType) { + _slot = slot; + _page = page; + _type = type; + _id = id; + _level = level; + _characterType = characterType; + } + + /** + * Gets the shortcut ID. + * @return the ID + */ + public int getId() { + return _id; + } + + /** + * Gets the shortcut level. + * @return the level + */ + public int getLevel() { + return _level; + } + + /** + * Gets the shortcut page. + * @return the page + */ + public int getPage() { + return _page; + } + + /** + * Gets the shortcut slot. + * @return the slot + */ + public int getSlot() { + return _slot; + } + + /** + * Gets the shortcut type. + * @return the type + */ + public ShortcutType getType() { + return _type; + } + + /** + * Gets the shortcut character type. + * @return the character type + */ + public int getCharacterType() { + return _characterType; + } + + /** + * Gets the shared reuse group. + * @return the shared reuse group + */ + public int getSharedReuseGroup() { + return _sharedReuseGroup; + } + + /** + * Sets the shared reuse group. + * @param sharedReuseGroup the shared reuse group to set + */ + public void setSharedReuseGroup(int sharedReuseGroup) { + _sharedReuseGroup = sharedReuseGroup; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/SiegeScheduleDate.java b/src/main/java/com/l2jserver/gameserver/model/SiegeScheduleDate.java new file mode 100644 index 00000000..67d7c918 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/SiegeScheduleDate.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.Calendar; + +/** + * @author UnAfraid + */ +public class SiegeScheduleDate { + private final int _day; + private final int _hour; + private final int _maxConcurrent; + + public SiegeScheduleDate(StatsSet set) { + _day = set.getInt("day", Calendar.SUNDAY); + _hour = set.getInt("hour", 16); + _maxConcurrent = set.getInt("maxConcurrent", 5); + } + + public int getDay() { + return _day; + } + + public int getHour() { + return _hour; + } + + public int getMaxConcurrent() { + return _maxConcurrent; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/SpawnListener.java b/src/main/java/com/l2jserver/gameserver/model/SpawnListener.java new file mode 100644 index 00000000..274b349d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/SpawnListener.java @@ -0,0 +1,28 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.actor.L2Npc; + +/** + * @since 2004/06/27 08:12:59 + */ +public interface SpawnListener { + void npcSpawned(L2Npc npc); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/StatsSet.java b/src/main/java/com/l2jserver/gameserver/model/StatsSet.java new file mode 100644 index 00000000..b851b7e4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/StatsSet.java @@ -0,0 +1,514 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.holders.MinionHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.interfaces.IParserAdvUtils; + +/** + * This class is meant to hold a set of (key,value) pairs.
    + * They are stored as object but can be retrieved in any type wanted. As long as cast is available.
    + * @author mkizub + */ +public class StatsSet implements IParserAdvUtils { + private static final Logger LOG = LoggerFactory.getLogger(StatsSet.class); + + /** Static empty immutable map, used to avoid multiple null checks over the source. */ + public static final StatsSet EMPTY_STATSET = new StatsSet(Collections.emptyMap()); + + private final Map _set; + + public StatsSet() { + this(new LinkedHashMap<>()); + } + + public StatsSet(Map map) { + _set = map; + } + + /** + * Returns the set of values + * @return HashMap + */ + public final Map getSet() { + return _set; + } + + /** + * Add a set of couple values in the current set + * @param newSet : StatsSet pointing out the list of couples to add in the current set + */ + public void add(StatsSet newSet) { + _set.putAll(newSet.getSet()); + } + + /** + * Verifies if the stat set is empty. + * @return {@code true} if the stat set is empty, {@code false} otherwise + */ + public boolean isEmpty() { + return _set.isEmpty(); + } + + public boolean containsKey(String key) { + return _set.containsKey(key); + } + + /** + * Return the boolean value associated with key. + * @param key : String designating the key in the set + * @return boolean : value associated to the key + * @throws IllegalArgumentException : If value is not set or value is not boolean + */ + @Override + public boolean getBoolean(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Boolean value required, but not specified"); + } + if (val instanceof Boolean) { + return (Boolean) val; + } + try { + return Boolean.parseBoolean((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Boolean value required, but found: " + val); + } + } + + /** + * Return the boolean value associated with key.
    + * If no value is associated with key, or type of value is wrong, returns defaultValue. + * @param key : String designating the key in the entry set + * @return boolean : value associated to the key + */ + @Override + public boolean getBoolean(String key, boolean defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Boolean) { + return (Boolean) val; + } + try { + return Boolean.parseBoolean((String) val); + } catch (Exception e) { + return defaultValue; + } + } + + @Override + public byte getByte(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Byte value required, but not specified"); + } + if (val instanceof Number) { + return ((Number) val).byteValue(); + } + try { + return Byte.parseByte((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Byte value required, but found: " + val); + } + } + + @Override + public byte getByte(String key, byte defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).byteValue(); + } + try { + return Byte.parseByte((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Byte value required, but found: " + val); + } + } + + public byte[] getByteArray(String key, String splitOn) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Byte value required, but not specified"); + } + if (val instanceof Number) { + return new byte[] { + ((Number) val).byteValue() + }; + } + int c = 0; + String[] values = ((String) val).split(splitOn); + byte[] result = new byte[values.length]; + for (String v : values) { + try { + result[c++] = Byte.parseByte(v); + } catch (Exception e) { + throw new IllegalArgumentException("Byte value required, but found: " + val); + } + } + return result; + } + + public List getByteList(String key, String splitOn) { + List result = new ArrayList<>(); + for (Byte i : getByteArray(key, splitOn)) { + result.add(i); + } + return result; + } + + @Override + public short getShort(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Short value required, but not specified"); + } + if (val instanceof Number) { + return ((Number) val).shortValue(); + } + try { + return Short.parseShort((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Short value required, but found: " + val); + } + } + + @Override + public short getShort(String key, short defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).shortValue(); + } + try { + return Short.parseShort((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Short value required, but found: " + val); + } + } + + @Override + public int getInt(String key) { + final Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Integer value required, but not specified: " + key + "!"); + } + + if (val instanceof Number) { + return ((Number) val).intValue(); + } + + try { + return Integer.parseInt((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Integer value required, but found: " + val + "!"); + } + } + + @Override + public int getInt(String key, int defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).intValue(); + } + try { + return Integer.parseInt((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Integer value required, but found: " + val); + } + } + + public int[] getIntArray(String key, String splitOn) { + return getIntArray(key, null, splitOn); + } + + public int[] getIntArray(String key, String defaultValue, String splitOn) { + Object val = _set.getOrDefault(key, defaultValue); + if (val == null) { + throw new IllegalArgumentException("Integer value required, but not specified"); + } + if (val instanceof Number) { + return new int[] { + ((Number) val).intValue() + }; + } + int c = 0; + String[] values = ((String) val).split(splitOn); + int[] result = new int[values.length]; + for (String v : values) { + try { + result[c++] = Integer.parseInt(v); + } catch (Exception e) { + throw new IllegalArgumentException("Integer value required, but found: " + val); + } + } + return result; + } + + public List getIntegerList(String key, String splitOn) { + List result = new ArrayList<>(); + for (int i : getIntArray(key, splitOn)) { + result.add(i); + } + return result; + } + + @Override + public long getLong(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Long value required, but not specified"); + } + if (val instanceof Number) { + return ((Number) val).longValue(); + } + try { + return Long.parseLong((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Long value required, but found: " + val); + } + } + + @Override + public long getLong(String key, long defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).longValue(); + } + try { + return Long.parseLong((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Long value required, but found: " + val); + } + } + + @Override + public float getFloat(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Float value required, but not specified"); + } + if (val instanceof Number) { + return ((Number) val).floatValue(); + } + try { + return Float.parseFloat((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Float value required, but found: " + val); + } + } + + @Override + public float getFloat(String key, float defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).floatValue(); + } + try { + return Float.parseFloat((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Float value required, but found: " + val); + } + } + + @Override + public double getDouble(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Double value required, but not specified"); + } + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + try { + return Double.parseDouble((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Double value required, but found: " + val); + } + } + + @Override + public double getDouble(String key, double defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (val instanceof Number) { + return ((Number) val).doubleValue(); + } + try { + return Double.parseDouble((String) val); + } catch (Exception e) { + throw new IllegalArgumentException("Double value required, but found: " + val); + } + } + + @Override + public String getString(String key) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("String value required, but not specified"); + } + return String.valueOf(val); + } + + @Override + public String getString(String key, String defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + return String.valueOf(val); + } + + @Override + @SuppressWarnings("unchecked") + public > T getEnum(String key, Class enumClass) { + Object val = _set.get(key); + if (val == null) { + throw new IllegalArgumentException("Enum value of type " + enumClass.getName() + " required, but not specified"); + } + if (enumClass.isInstance(val)) { + return (T) val; + } + try { + return Enum.valueOf(enumClass, String.valueOf(val)); + } catch (Exception e) { + throw new IllegalArgumentException("Enum value of type " + enumClass.getName() + " required, but found: " + val); + } + } + + @Override + @SuppressWarnings("unchecked") + public > T getEnum(String key, Class enumClass, T defaultValue) { + Object val = _set.get(key); + if (val == null) { + return defaultValue; + } + if (enumClass.isInstance(val)) { + return (T) val; + } + try { + return Enum.valueOf(enumClass, String.valueOf(val)); + } catch (Exception e) { + throw new IllegalArgumentException("Enum value of type " + enumClass.getName() + " required, but found: " + val); + } + } + + @SuppressWarnings("unchecked") + public final A getObject(String name, Class type) { + Object obj = _set.get(name); + if ((obj == null) || !type.isAssignableFrom(obj.getClass())) { + return null; + } + + return (A) obj; + } + + public SkillHolder getSkillHolder(String key) { + Object obj = _set.get(key); + if (!(obj instanceof SkillHolder)) { + return null; + } + return (SkillHolder) obj; + } + + @SuppressWarnings("unchecked") + public List getMinionList(String key) { + Object obj = _set.get(key); + if (!(obj instanceof List)) { + return Collections.emptyList(); + } + return (List) obj; + } + + public void set(String name, Object value) { + _set.put(name, value); + } + + public void set(String key, boolean value) { + _set.put(key, value); + } + + public void set(String key, byte value) { + _set.put(key, value); + } + + public void set(String key, short value) { + _set.put(key, value); + } + + public void set(String key, int value) { + _set.put(key, value); + } + + public void set(String key, long value) { + _set.put(key, value); + } + + public void set(String key, float value) { + _set.put(key, value); + } + + public void set(String key, double value) { + _set.put(key, value); + } + + public void set(String key, String value) { + _set.put(key, value); + } + + public void set(String key, Enum value) { + _set.put(key, value); + } + + public void safeSet(String key, int value, int min, int max, String reference) { + assert !(((min <= max) && ((value < min) || (value >= max)))); + if ((min <= max) && ((value < min) || (value >= max))) { + LOG.error("Incorrect value: {} for: {} Ref: {}", value, key, reference); + } + + set(key, value); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TeleportBookmark.java b/src/main/java/com/l2jserver/gameserver/model/TeleportBookmark.java new file mode 100644 index 00000000..fbcf4bd8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TeleportBookmark.java @@ -0,0 +1,64 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * @author UnAfraid + */ +public class TeleportBookmark extends Location { + private final int _id; + private int _icon; + private String _name, _tag; + + public TeleportBookmark(int id, int x, int y, int z, int icon, String tag, String name) { + super(x, y, z); + _id = id; + _icon = icon; + _name = name; + _tag = tag; + } + + public String getName() { + return _name; + } + + public void setName(String name) { + _name = name; + } + + public int getId() { + return _id; + } + + public int getIcon() { + return _icon; + } + + public void setIcon(int icon) { + _icon = icon; + } + + public String getTag() { + return _tag; + } + + public void setTag(String tag) { + _tag = tag; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TeleportWhereType.java b/src/main/java/com/l2jserver/gameserver/model/TeleportWhereType.java new file mode 100644 index 00000000..30acfa51 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TeleportWhereType.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +/** + * All teleport types. + * @author xban1x + */ +public enum TeleportWhereType { + CASTLE, + CLANHALL, + SIEGEFLAG, + TOWN, + FORTRESS +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TempItem.java b/src/main/java/com/l2jserver/gameserver/model/TempItem.java new file mode 100644 index 00000000..80405e48 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TempItem.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +/** + * Class explanation:
    + * For item counting or checking purposes. When you don't want to modify inventory
    + * class contains itemId, quantity, ownerId, referencePrice, but not objectId
    + * is stored, this will be only "list" of items with it's owner + */ +public final class TempItem { + private final int _itemId; + private int _quantity; + private final int _referencePrice; + private final String _itemName; + + public TempItem(L2ItemInstance item, int quantity) { + _itemId = item.getId(); + _quantity = quantity; + _itemName = item.getItem().getName(); + _referencePrice = item.getReferencePrice(); + } + + public int getQuantity() { + return _quantity; + } + + public void setQuantity(int quantity) { + _quantity = quantity; + } + + public int getReferencePrice() { + return _referencePrice; + } + + public int getItemId() { + return _itemId; + } + + public String getItemName() { + return _itemName; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TerritoryWard.java b/src/main/java/com/l2jserver/gameserver/model/TerritoryWard.java new file mode 100644 index 00000000..7eb61186 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TerritoryWard.java @@ -0,0 +1,166 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class TerritoryWard { + + protected L2PcInstance _player = null; + public int playerId = 0; + private L2ItemInstance _item = null; + private L2Npc _npc; + + private Location _location; + private Location _oldLocation; + + private final int _itemId; + private int _ownerCastleId; + + private final int _territoryId; + + public TerritoryWard(int territory_id, int x, int y, int z, int heading, int item_id, int castleId, L2Npc npc) { + _territoryId = territory_id; + _location = new Location(x, y, z, heading); + _itemId = item_id; + _ownerCastleId = castleId; + _npc = npc; + } + + public int getTerritoryId() { + return _territoryId; + } + + public int getOwnerCastleId() { + return _ownerCastleId; + } + + public void setOwnerCastleId(int newOwner) { + _ownerCastleId = newOwner; + } + + public L2Npc getNpc() { + return _npc; + } + + public void setNpc(L2Npc npc) { + _npc = npc; + } + + public L2PcInstance getPlayer() { + return _player; + } + + public synchronized void spawnBack() { + if (_player != null) { + dropIt(); + } + + // Init the dropped L2WardInstance and add it in the world as a visible object at the position where last Pc got it + _npc = TerritoryWarManager.getInstance().spawnNPC(36491 + _territoryId, _oldLocation); + } + + public synchronized void spawnMe() { + if (_player != null) { + dropIt(); + } + + // Init the dropped L2WardInstance and add it in the world as a visible object at the position where Pc was last + _npc = TerritoryWarManager.getInstance().spawnNPC(36491 + _territoryId, _location); + } + + public synchronized void unSpawnMe() { + if (_player != null) { + dropIt(); + } + if ((_npc != null) && !_npc.isDecayed()) { + _npc.deleteMe(); + } + } + + public boolean activate(L2PcInstance player, L2ItemInstance item) { + if (player.isMounted()) { + player.sendPacket(SystemMessageId.CANNOT_EQUIP_ITEM_DUE_TO_BAD_CONDITION); + player.destroyItem("CombatFlag", item, null, true); + spawnMe(); + return false; + } else if (TerritoryWarManager.getInstance().getRegisteredTerritoryId(player) == 0) { + player.sendMessage("Non participants can't pickup Territory Wards!"); + player.destroyItem("CombatFlag", item, null, true); + spawnMe(); + return false; + } + + // Player holding it data + _player = player; + playerId = _player.getObjectId(); + _oldLocation = new Location(_npc.getX(), _npc.getY(), _npc.getZ(), _npc.getHeading()); + _npc = null; + + // Equip with the weapon + if (item == null) { + _item = ItemTable.getInstance().createItem("Combat", _itemId, 1, null, null); + } else { + _item = item; + } + _player.getInventory().equipItem(_item); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_EQUIPPED); + sm.addItemName(_item); + _player.sendPacket(sm); + + // Refresh inventory + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(_item); + _player.sendPacket(iu); + } else { + _player.sendPacket(new ItemList(_player, false)); + } + + // Refresh player stats + _player.broadcastUserInfo(); + _player.setCombatFlagEquipped(true); + _player.sendPacket(SystemMessageId.YOU_VE_ACQUIRED_THE_WARD); + TerritoryWarManager.getInstance().giveTWPoint(player, _territoryId, 5); + return true; + } + + public void dropIt() { + // Reset player stats + _player.setCombatFlagEquipped(false); + int slot = _player.getInventory().getSlotFromItem(_item); + _player.getInventory().unEquipItemInBodySlot(slot); + _player.destroyItem("CombatFlag", _item, null, true); + _item = null; + _player.broadcastUserInfo(); + _location = new Location(_player.getX(), _player.getY(), _player.getZ(), _player.getHeading()); + _player = null; + playerId = 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TimeStamp.java b/src/main/java/com/l2jserver/gameserver/model/TimeStamp.java new file mode 100644 index 00000000..64aaa02e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TimeStamp.java @@ -0,0 +1,143 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Simple class containing all necessary information to maintain
    + * valid time stamps and reuse for skills and items reuse upon re-login.
    + * Filter this carefully as it becomes redundant to store reuse for small delays. + * @author Yesod + * @author Zoey76 + */ +public class TimeStamp { + /** Item or skill ID. */ + private final int _id1; + /** Item object ID or skill level. */ + private final int _id2; + /** Item or skill reuse time. */ + private final long _reuse; + /** Time stamp. */ + private final long _stamp; + /** Shared reuse group. */ + private final int _group; + + /** + * Skill time stamp constructor. + * @param skill the skill upon the stamp will be created. + * @param reuse the reuse time for this skill. + * @param systime overrides the system time with a customized one. + */ + public TimeStamp(Skill skill, long reuse, long systime) { + _id1 = skill.getId(); + _id2 = skill.getLevel(); + _reuse = reuse; + _stamp = systime > 0 ? systime : System.currentTimeMillis() + reuse; + _group = -1; + } + + /** + * Item time stamp constructor. + * @param item the item upon the stamp will be created. + * @param reuse the reuse time for this item. + * @param systime overrides the system time with a customized one. + */ + public TimeStamp(L2ItemInstance item, long reuse, long systime) { + _id1 = item.getId(); + _id2 = item.getObjectId(); + _reuse = reuse; + _stamp = systime > 0 ? systime : System.currentTimeMillis() + reuse; + _group = item.getSharedReuseGroup(); + } + + /** + * Gets the time stamp. + * @return the time stamp, either the system time where this time stamp was created or the custom time assigned + */ + public long getStamp() { + return _stamp; + } + + /** + * Gets the item ID. + * @return the item ID + */ + public int getItemId() { + return _id1; + } + + /** + * Gets the item object ID. + * @return the item object ID + */ + public int getItemObjectId() { + return _id2; + } + + /** + * Gets the skill ID. + * @return the skill ID + */ + public int getSkillId() { + return _id1; + } + + /** + * Gets the skill level. + * @return the skill level + */ + public int getSkillLvl() { + return _id2; + } + + /** + * Gets the reuse. + * @return the reuse + */ + public long getReuse() { + return _reuse; + } + + /** + * Get the shared reuse group.
    + * Only used on items. + * @return the shared reuse group + */ + public int getSharedReuseGroup() { + return _group; + } + + /** + * Gets the remaining time. + * @return the remaining time for this time stamp to expire + */ + public long getRemaining() { + return Math.max(_stamp - System.currentTimeMillis(), 0); + } + + /** + * Verifies if the reuse delay has passed. + * @return {@code true} if this time stamp has expired, {@code false} otherwise + */ + public boolean hasNotPassed() { + return System.currentTimeMillis() < _stamp; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TowerSpawn.java b/src/main/java/com/l2jserver/gameserver/model/TowerSpawn.java new file mode 100644 index 00000000..a6bf9b0d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TowerSpawn.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.List; + +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; + +/** + * @author malyelfik + */ +public class TowerSpawn implements IIdentifiable { + private final int _npcId; + private final Location _location; + private List _zoneList = null; + private int _upgradeLevel = 0; + + public TowerSpawn(int npcId, Location location) { + _location = location; + _npcId = npcId; + } + + public TowerSpawn(int npcId, Location location, List zoneList) { + _location = location; + _npcId = npcId; + _zoneList = zoneList; + } + + @Override + public int getId() { + return _npcId; + } + + public Location getLocation() { + return _location; + } + + public List getZoneList() { + return _zoneList; + } + + public void setUpgradeLevel(int level) { + _upgradeLevel = level; + } + + public int getUpgradeLevel() { + return _upgradeLevel; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/TradeItem.java b/src/main/java/com/l2jserver/gameserver/model/TradeItem.java new file mode 100644 index 00000000..9d846b7b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TradeItem.java @@ -0,0 +1,163 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; + +public class TradeItem { + private int _objectId; + private final L2Item _item; + private final int _location; + private int _enchant; + private final int _type1; + private final int _type2; + private long _count; + private long _storeCount; + private long _price; + private final byte _elemAtkType; + private final int _elemAtkPower; + private final int[] _elemDefAttr = { + 0, + 0, + 0, + 0, + 0, + 0 + }; + private final int[] _enchantOptions; + + public TradeItem(L2ItemInstance item, long count, long price) { + _objectId = item.getObjectId(); + _item = item.getItem(); + _location = item.getLocationSlot(); + _enchant = item.getEnchantLevel(); + _type1 = item.getCustomType1(); + _type2 = item.getCustomType2(); + _count = count; + _price = price; + _elemAtkType = item.getAttackElementType(); + _elemAtkPower = item.getAttackElementPower(); + for (byte i = 0; i < 6; i++) { + _elemDefAttr[i] = item.getElementDefAttr(i); + } + _enchantOptions = item.getEnchantOptions(); + } + + public TradeItem(L2Item item, long count, long price) { + _objectId = 0; + _item = item; + _location = 0; + _enchant = 0; + _type1 = 0; + _type2 = 0; + _count = count; + _storeCount = count; + _price = price; + _elemAtkType = Elementals.NONE; + _elemAtkPower = 0; + _enchantOptions = L2ItemInstance.DEFAULT_ENCHANT_OPTIONS; + } + + public TradeItem(TradeItem item, long count, long price) { + _objectId = item.getObjectId(); + _item = item.getItem(); + _location = item.getLocationSlot(); + _enchant = item.getEnchant(); + _type1 = item.getCustomType1(); + _type2 = item.getCustomType2(); + _count = count; + _storeCount = count; + _price = price; + _elemAtkType = item.getAttackElementType(); + _elemAtkPower = item.getAttackElementPower(); + for (byte i = 0; i < 6; i++) { + _elemDefAttr[i] = item.getElementDefAttr(i); + } + _enchantOptions = item.getEnchantOptions(); + } + + public void setObjectId(int objectId) { + _objectId = objectId; + } + + public int getObjectId() { + return _objectId; + } + + public L2Item getItem() { + return _item; + } + + public int getLocationSlot() { + return _location; + } + + public void setEnchant(int enchant) { + _enchant = enchant; + } + + public int getEnchant() { + return _enchant; + } + + public int getCustomType1() { + return _type1; + } + + public int getCustomType2() { + return _type2; + } + + public void setCount(long count) { + _count = count; + } + + public long getCount() { + return _count; + } + + public long getStoreCount() { + return _storeCount; + } + + public void setPrice(long price) { + _price = price; + } + + public long getPrice() { + return _price; + } + + public byte getAttackElementType() { + return _elemAtkType; + } + + public int getAttackElementPower() { + return _elemAtkPower; + } + + public int getElementDefAttr(byte i) { + return _elemDefAttr[i]; + } + + public int[] getEnchantOptions() { + return _enchantOptions; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/TradeList.java b/src/main/java/com/l2jserver/gameserver/model/TradeList.java new file mode 100644 index 00000000..15dc950d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/TradeList.java @@ -0,0 +1,887 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.PcInventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * @author Advi + */ +public class TradeList { + private static final Logger LOG = LoggerFactory.getLogger(TradeList.class); + + private final L2PcInstance _owner; + private L2PcInstance _partner; + private final List _items = new CopyOnWriteArrayList<>(); + private String _title; + private boolean _packaged; + + private boolean _confirmed = false; + private boolean _locked = false; + + public TradeList(L2PcInstance owner) { + _owner = owner; + } + + public L2PcInstance getOwner() { + return _owner; + } + + public void setPartner(L2PcInstance partner) { + _partner = partner; + } + + public L2PcInstance getPartner() { + return _partner; + } + + public void setTitle(String title) { + _title = title; + } + + public String getTitle() { + return _title; + } + + public boolean isLocked() { + return _locked; + } + + public boolean isConfirmed() { + return _confirmed; + } + + public boolean isPackaged() { + return _packaged; + } + + public void setPackaged(boolean value) { + _packaged = value; + } + + /** + * @return all items from TradeList + */ + public TradeItem[] getItems() { + return _items.toArray(new TradeItem[0]); + } + + /** + * Returns the list of items in inventory available for transaction + * @return L2ItemInstance : items in inventory + */ + public List getAvailableItems(PcInventory inventory) { + final List list = new LinkedList<>(); + for (TradeItem item : _items) { + item = new TradeItem(item, item.getCount(), item.getPrice()); + inventory.adjustAvailableItem(item); + list.add(item); + } + return list; + } + + /** + * @return Item List size + */ + public int getItemCount() { + return _items.size(); + } + + /** + * Adjust available item from Inventory by the one in this list + * @param item : L2ItemInstance to be adjusted + * @return TradeItem representing adjusted item + */ + public TradeItem adjustAvailableItem(L2ItemInstance item) { + if (item.isStackable()) { + for (TradeItem exclItem : _items) { + if (exclItem.getItem().getId() == item.getId()) { + if (item.getCount() <= exclItem.getCount()) { + return null; + } + return new TradeItem(item, item.getCount() - exclItem.getCount(), item.getReferencePrice()); + } + } + } + return new TradeItem(item, item.getCount(), item.getReferencePrice()); + } + + /** + * Adjust ItemRequest by corresponding item in this list using its ObjectId + * @param item : ItemRequest to be adjusted + */ + public void adjustItemRequest(ItemRequest item) { + for (TradeItem filtItem : _items) { + if (filtItem.getObjectId() == item.getObjectId()) { + if (filtItem.getCount() < item.getCount()) { + item.setCount(filtItem.getCount()); + } + return; + } + } + item.setCount(0); + } + + /** + * Add simplified item to TradeList + * @param objectId : int + * @param count : int + * @return + */ + public TradeItem addItem(int objectId, long count) { + return addItem(objectId, count, 0); + } + + /** + * Add item to TradeList + * @param objectId : int + * @param count : long + * @param price : long + * @return + */ + public synchronized TradeItem addItem(int objectId, long count, long price) { + if (isLocked()) { + LOG.warn("{}: Attempt to modify locked TradeList!", _owner.getName()); + return null; + } + + L2Object o = L2World.getInstance().findObject(objectId); + if (!(o instanceof L2ItemInstance item)) { + LOG.warn("{}: Trying to add something other than an item!", _owner.getName()); + return null; + } + + if (!(item.isTradeable() || (getOwner().isGM() && general().gmTradeRestrictedItems())) || item.isQuestItem()) { + LOG.warn("{}: Attempt to add a restricted item!", _owner.getName()); + return null; + } + + if (!getOwner().getInventory().canManipulateWithItemId(item.getId())) { + LOG.warn("{}: Attempt to add an item that can't manipulate!", _owner.getName()); + return null; + } + + if ((count <= 0) || (count > item.getCount())) { + LOG.warn("{}: Attempt to add an item with invalid item count!", _owner.getName()); + return null; + } + + if (!item.isStackable() && (count > 1)) { + LOG.warn("{}: Attempt to add non-stackable item to TradeList with count > 1!", _owner.getName()); + return null; + } + + if ((character().getMaxAdena() / count) < price) { + LOG.warn("{}: Attempt to overflow adena !", _owner.getName()); + return null; + } + + for (TradeItem checkitem : _items) { + if (checkitem.getObjectId() == objectId) { + LOG.warn("{}: Attempt to add an item that is already present!", _owner.getName()); + return null; + } + } + + TradeItem titem = new TradeItem(item, count, price); + _items.add(titem); + + // If Player has already confirmed this trade, invalidate the confirmation + invalidateConfirmation(); + return titem; + } + + /** + * Add item to TradeList + */ + public synchronized TradeItem addItemByItemId(int itemId, long count, long price) { + if (isLocked()) { + LOG.warn("{}: Attempt to modify locked TradeList!", _owner.getName()); + return null; + } + + L2Item item = ItemTable.getInstance().getTemplate(itemId); + if (item == null) { + LOG.warn("{}: Attempt to add invalid item to TradeList!", _owner.getName()); + return null; + } + + if (!item.isTradeable() || item.isQuestItem()) { + return null; + } + + if (!item.isStackable() && (count > 1)) { + LOG.warn("{}: Attempt to add non-stackable item to TradeList with count > 1!", _owner.getName()); + return null; + } + + if ((character().getMaxAdena() / count) < price) { + LOG.warn("{}: Attempt to overflow adena !", _owner.getName()); + return null; + } + + TradeItem titem = new TradeItem(item, count, price); + _items.add(titem); + + // If Player has already confirmed this trade, invalidate the confirmation + invalidateConfirmation(); + return titem; + } + + /** + * Remove item from TradeList + * @param objectId : int + * @param itemId + * @param count : int + * @return + */ + public synchronized TradeItem removeItem(int objectId, int itemId, long count) { + if (isLocked()) { + LOG.warn("{}: Attempt to modify locked TradeList!", _owner.getName()); + return null; + } + + for (TradeItem titem : _items) { + if ((titem.getObjectId() == objectId) || (titem.getItem().getId() == itemId)) { + // If Partner has already confirmed this trade, invalidate the confirmation + if (_partner != null) { + TradeList partnerList = _partner.getActiveTradeList(); + if (partnerList == null) { + LOG.warn("{}: Trading partner ({}) is invalid in this trade!", _partner.getName(), _partner.getName()); + return null; + } + partnerList.invalidateConfirmation(); + } + + // Reduce item count or complete item + if ((count != -1) && (titem.getCount() > count)) { + titem.setCount(titem.getCount() - count); + } else { + _items.remove(titem); + } + + return titem; + } + } + return null; + } + + /** + * Update items in TradeList according their quantity in owner inventory + */ + public synchronized void updateItems() { + for (TradeItem titem : _items) { + L2ItemInstance item = _owner.getInventory().getItemByObjectId(titem.getObjectId()); + if ((item == null) || (titem.getCount() < 1)) { + removeItem(titem.getObjectId(), -1, -1); + } else if (item.getCount() < titem.getCount()) { + titem.setCount(item.getCount()); + } + } + } + + /** + * Lockes TradeList, no further changes are allowed + */ + public void lock() { + _locked = true; + } + + /** + * Clears item list + */ + public synchronized void clear() { + _items.clear(); + _locked = false; + } + + /** + * Confirms TradeList + * @return : boolean + */ + public boolean confirm() { + if (_confirmed) { + return true; // Already confirmed + } + + // If Partner has already confirmed this trade, proceed exchange + if (_partner != null) { + TradeList partnerList = _partner.getActiveTradeList(); + if (partnerList == null) { + LOG.warn("{}: Trading partner ({}) is invalid in this trade!", _partner.getName(), _partner.getName()); + return false; + } + + // Synchronization order to avoid deadlock + TradeList sync1, sync2; + if (getOwner().getObjectId() > partnerList.getOwner().getObjectId()) { + sync1 = partnerList; + sync2 = this; + } else { + sync1 = this; + sync2 = partnerList; + } + + synchronized (sync1) { + synchronized (sync2) { + _confirmed = true; + if (partnerList.isConfirmed()) { + partnerList.lock(); + lock(); + if (!partnerList.validate()) { + return false; + } + if (!validate()) { + return false; + } + + doExchange(partnerList); + } else { + _partner.onTradeConfirm(_owner); + } + } + } + } else { + _confirmed = true; + } + + return _confirmed; + } + + /** + * Cancels TradeList confirmation + */ + public void invalidateConfirmation() { + _confirmed = false; + } + + /** + * Validates TradeList with owner inventory + */ + private boolean validate() { + // Check for Owner validity + if ((_owner == null) || (L2World.getInstance().getPlayer(_owner.getObjectId()) == null)) { + LOG.warn("Invalid owner of TradeList"); + return false; + } + + // Check for Item validity + for (TradeItem titem : _items) { + L2ItemInstance item = _owner.checkItemManipulation(titem.getObjectId(), titem.getCount(), "transfer"); + if ((item == null) || (item.getCount() < 1)) { + LOG.warn("{}: Invalid Item in TradeList", _owner.getName()); + return false; + } + } + + return true; + } + + /** + * Transfers all TradeItems from inventory to partner + */ + private boolean TransferItems(L2PcInstance partner, InventoryUpdate ownerIU, InventoryUpdate partnerIU) { + for (TradeItem titem : _items) { + L2ItemInstance oldItem = _owner.getInventory().getItemByObjectId(titem.getObjectId()); + if (oldItem == null) { + return false; + } + L2ItemInstance newItem = _owner.getInventory().transferItem("Trade", titem.getObjectId(), titem.getCount(), partner.getInventory(), _owner, _partner); + if (newItem == null) { + return false; + } + + // Add changes to inventory update packets + if (ownerIU != null) { + if ((oldItem.getCount() > 0) && (oldItem != newItem)) { + ownerIU.addModifiedItem(oldItem); + } else { + ownerIU.addRemovedItem(oldItem); + } + } + + if (partnerIU != null) { + if (newItem.getCount() > titem.getCount()) { + partnerIU.addModifiedItem(newItem); + } else { + partnerIU.addNewItem(newItem); + } + } + } + return true; + } + + /** + * @param partner + * @return items slots count + */ + public int countItemsSlots(L2PcInstance partner) { + int slots = 0; + + for (TradeItem item : _items) { + if (item == null) { + continue; + } + L2Item template = ItemTable.getInstance().getTemplate(item.getItem().getId()); + if (template == null) { + continue; + } + if (!template.isStackable()) { + slots += item.getCount(); + } else if (partner.getInventory().getItemByItemId(item.getItem().getId()) == null) { + slots++; + } + } + + return slots; + } + + /** + * @return the weight of items in tradeList + */ + public int calcItemsWeight() { + long weight = 0; + + for (TradeItem item : _items) { + if (item == null) { + continue; + } + L2Item template = ItemTable.getInstance().getTemplate(item.getItem().getId()); + if (template == null) { + continue; + } + weight += item.getCount() * template.getWeight(); + } + + return (int) Math.min(weight, Integer.MAX_VALUE); + } + + /** + * Proceeds with trade + * @param partnerList + */ + private void doExchange(TradeList partnerList) { + boolean success = false; + + // check weight and slots + if ((!getOwner().getInventory().validateWeight(partnerList.calcItemsWeight())) || !(partnerList.getOwner().getInventory().validateWeight(calcItemsWeight()))) { + partnerList.getOwner().sendPacket(SystemMessageId.WEIGHT_LIMIT_EXCEEDED); + getOwner().sendPacket(SystemMessageId.WEIGHT_LIMIT_EXCEEDED); + } else if ((!getOwner().getInventory().validateCapacity(partnerList.countItemsSlots(getOwner()))) || (!partnerList.getOwner().getInventory().validateCapacity(countItemsSlots(partnerList.getOwner())))) { + partnerList.getOwner().sendPacket(SystemMessageId.SLOTS_FULL); + getOwner().sendPacket(SystemMessageId.SLOTS_FULL); + } else { + // Prepare inventory update packet + InventoryUpdate ownerIU = general().forceInventoryUpdate() ? null : new InventoryUpdate(); + InventoryUpdate partnerIU = general().forceInventoryUpdate() ? null : new InventoryUpdate(); + + // Transfer items + partnerList.TransferItems(getOwner(), partnerIU, ownerIU); + TransferItems(partnerList.getOwner(), ownerIU, partnerIU); + + // Send inventory update packet + _owner.sendPacket(Objects.requireNonNullElseGet(ownerIU, () -> new ItemList(_owner, false))); + _partner.sendPacket(Objects.requireNonNullElseGet(partnerIU, () -> new ItemList(_partner, false))); + + // Update current load as well + StatusUpdate playerSU = new StatusUpdate(_owner); + playerSU.addAttribute(StatusUpdate.CUR_LOAD, _owner.getCurrentLoad()); + _owner.sendPacket(playerSU); + playerSU = new StatusUpdate(_partner); + playerSU.addAttribute(StatusUpdate.CUR_LOAD, _partner.getCurrentLoad()); + _partner.sendPacket(playerSU); + + success = true; + } + // Finish the trade + partnerList.getOwner().onTradeFinish(success); + getOwner().onTradeFinish(success); + } + + /** + * Buy items from this PrivateStore list + * @param player + * @param items + * @return int: result of trading. 0 - ok, 1 - canceled (no adena), 2 - failed (item error) + */ + public synchronized int privateStoreBuy(L2PcInstance player, Set items) { + if (_locked) { + return 1; + } + + if (!validate()) { + lock(); + return 1; + } + + if (!_owner.isOnline() || !player.isOnline()) { + return 1; + } + + int slots = 0; + int weight = 0; + long totalPrice = 0; + + final PcInventory ownerInventory = _owner.getInventory(); + final PcInventory playerInventory = player.getInventory(); + + for (ItemRequest item : items) { + boolean found = false; + + for (TradeItem ti : _items) { + if (ti.getObjectId() == item.getObjectId()) { + if (ti.getPrice() == item.getPrice()) { + if (ti.getCount() < item.getCount()) { + item.setCount(ti.getCount()); + } + found = true; + } + break; + } + } + // item with this objectId and price not found in tradelist + if (!found) { + if (isPackaged()) { + Util.handleIllegalPlayerAction(player, "[TradeList.privateStoreBuy()] Player " + player.getName() + " tried to cheat the package sell and buy only a part of the package! Ban this player for bot usage!"); + return 2; + } + + item.setCount(0); + continue; + } + + // check for overflow in the single item + if ((character().getMaxAdena() / item.getCount()) < item.getPrice()) { + // private store attempting to overflow - disable it + lock(); + return 1; + } + + totalPrice += item.getCount() * item.getPrice(); + // check for overflow of the total price + if ((character().getMaxAdena() < totalPrice) || (totalPrice < 0)) { + // private store attempting to overflow - disable it + lock(); + return 1; + } + + // Check if requested item is available for manipulation + L2ItemInstance oldItem = _owner.checkItemManipulation(item.getObjectId(), item.getCount(), "sell"); + if ((oldItem == null) || !oldItem.isTradeable()) { + // private store sell invalid item - disable it + lock(); + return 2; + } + + L2Item template = ItemTable.getInstance().getTemplate(item.getItemId()); + if (template == null) { + continue; + } + weight += item.getCount() * template.getWeight(); + if (!template.isStackable()) { + slots += item.getCount(); + } else if (playerInventory.getItemByItemId(item.getItemId()) == null) { + slots++; + } + } + + if (totalPrice > playerInventory.getAdena()) { + player.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + return 1; + } + + if (!playerInventory.validateWeight(weight)) { + player.sendPacket(SystemMessageId.WEIGHT_LIMIT_EXCEEDED); + return 1; + } + + if (!playerInventory.validateCapacity(slots)) { + player.sendPacket(SystemMessageId.SLOTS_FULL); + return 1; + } + + // Prepare inventory update packets + final InventoryUpdate ownerIU = new InventoryUpdate(); + final InventoryUpdate playerIU = new InventoryUpdate(); + + final L2ItemInstance adenaItem = playerInventory.getAdenaInstance(); + if (!playerInventory.reduceAdena("PrivateStore", totalPrice, player, _owner)) { + player.sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + return 1; + } + playerIU.addItem(adenaItem); + ownerInventory.addAdena("PrivateStore", totalPrice, _owner, player); + // ownerIU.addItem(ownerInventory.getAdenaInstance()); + + boolean ok = true; + + // Transfer items + for (ItemRequest item : items) { + if (item.getCount() == 0) { + continue; + } + + // Check if requested item is available for manipulation + L2ItemInstance oldItem = _owner.checkItemManipulation(item.getObjectId(), item.getCount(), "sell"); + if (oldItem == null) { + // should not happens - validation already done + lock(); + ok = false; + break; + } + + // Proceed with item transfer + L2ItemInstance newItem = ownerInventory.transferItem("PrivateStore", item.getObjectId(), item.getCount(), playerInventory, _owner, player); + if (newItem == null) { + ok = false; + break; + } + removeItem(item.getObjectId(), -1, item.getCount()); + + // Add changes to inventory update packets + if ((oldItem.getCount() > 0) && (oldItem != newItem)) { + ownerIU.addModifiedItem(oldItem); + } else { + ownerIU.addRemovedItem(oldItem); + } + if (newItem.getCount() > item.getCount()) { + playerIU.addModifiedItem(newItem); + } else { + playerIU.addNewItem(newItem); + } + + // Send messages about the transaction to both players + if (newItem.isStackable()) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_PURCHASED_S3_S2_S); + msg.addString(player.getName()); + msg.addItemName(newItem); + msg.addLong(item.getCount()); + _owner.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.PURCHASED_S3_S2_S_FROM_C1); + msg.addString(_owner.getName()); + msg.addItemName(newItem); + msg.addLong(item.getCount()); + player.sendPacket(msg); + } else { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_PURCHASED_S2); + msg.addString(player.getName()); + msg.addItemName(newItem); + _owner.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.PURCHASED_S2_FROM_C1); + msg.addString(_owner.getName()); + msg.addItemName(newItem); + player.sendPacket(msg); + } + } + + // Send inventory update packet + _owner.sendPacket(ownerIU); + player.sendPacket(playerIU); + if (ok) { + return 0; + } + return 2; + } + + /** + * Sell items to this PrivateStore list + * @param player + * @param items + * @return : boolean true if success + */ + public synchronized boolean privateStoreSell(L2PcInstance player, ItemRequest[] items) { + if (_locked) { + return false; + } + + if (!_owner.isOnline() || !player.isOnline()) { + return false; + } + + boolean ok = false; + + final PcInventory ownerInventory = _owner.getInventory(); + final PcInventory playerInventory = player.getInventory(); + + // Prepare inventory update packet + final InventoryUpdate ownerIU = new InventoryUpdate(); + final InventoryUpdate playerIU = new InventoryUpdate(); + + long totalPrice = 0; + + for (ItemRequest item : items) { + // searching item in tradelist using itemId + boolean found = false; + + for (TradeItem ti : _items) { + if (ti.getItem().getId() == item.getItemId()) { + // price should be the same + if (ti.getPrice() == item.getPrice()) { + // if requesting more than available - decrease count + if (ti.getCount() < item.getCount()) { + item.setCount(ti.getCount()); + } + found = item.getCount() > 0; + } + break; + } + } + // not found any item in the tradelist with same itemId and price + // maybe another player already sold this item ? + if (!found) { + continue; + } + + // check for overflow in the single item + if ((character().getMaxAdena() / item.getCount()) < item.getPrice()) { + lock(); + break; + } + + long _totalPrice = totalPrice + (item.getCount() * item.getPrice()); + // check for overflow of the total price + if ((character().getMaxAdena() < _totalPrice) || (_totalPrice < 0)) { + lock(); + break; + } + + if (ownerInventory.getAdena() < _totalPrice) { + continue; + } + + // Check if requested item is available for manipulation + int objectId = item.getObjectId(); + L2ItemInstance oldItem = player.checkItemManipulation(objectId, item.getCount(), "sell"); + // private store - buy use same objectId for buying several non-stackable items + if (oldItem == null) { + // searching other items using same itemId + oldItem = playerInventory.getItemByItemId(item.getItemId()); + if (oldItem == null) { + continue; + } + objectId = oldItem.getObjectId(); + oldItem = player.checkItemManipulation(objectId, item.getCount(), "sell"); + if (oldItem == null) { + continue; + } + } + if (oldItem.getId() != item.getItemId()) { + Util.handleIllegalPlayerAction(player, player + " is cheating with sell items"); + return false; + } + + if (!oldItem.isTradeable()) { + continue; + } + + // Proceed with item transfer + L2ItemInstance newItem = playerInventory.transferItem("PrivateStore", objectId, item.getCount(), ownerInventory, player, _owner); + if (newItem == null) { + continue; + } + + removeItem(-1, item.getItemId(), item.getCount()); + ok = true; + + // increase total price only after successful transaction + totalPrice = _totalPrice; + + // Add changes to inventory update packets + if ((oldItem.getCount() > 0) && (oldItem != newItem)) { + playerIU.addModifiedItem(oldItem); + } else { + playerIU.addRemovedItem(oldItem); + } + if (newItem.getCount() > item.getCount()) { + ownerIU.addModifiedItem(newItem); + } else { + ownerIU.addNewItem(newItem); + } + + // Send messages about the transaction to both players + if (newItem.isStackable()) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.PURCHASED_S3_S2_S_FROM_C1); + msg.addString(player.getName()); + msg.addItemName(newItem); + msg.addLong(item.getCount()); + _owner.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_PURCHASED_S3_S2_S); + msg.addString(_owner.getName()); + msg.addItemName(newItem); + msg.addLong(item.getCount()); + player.sendPacket(msg); + } else { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.PURCHASED_S2_FROM_C1); + msg.addString(player.getName()); + msg.addItemName(newItem); + _owner.sendPacket(msg); + + msg = SystemMessage.getSystemMessage(SystemMessageId.C1_PURCHASED_S2); + msg.addString(_owner.getName()); + msg.addItemName(newItem); + player.sendPacket(msg); + } + } + + if (totalPrice > 0) { + // Transfer adena + if (totalPrice > ownerInventory.getAdena()) { + // should not happens, just a precaution + return false; + } + final L2ItemInstance adenaItem = ownerInventory.getAdenaInstance(); + ownerInventory.reduceAdena("PrivateStore", totalPrice, _owner, player); + ownerIU.addItem(adenaItem); + playerInventory.addAdena("PrivateStore", totalPrice, player, _owner); + playerIU.addItem(playerInventory.getAdenaInstance()); + } + + if (ok) { + // Send inventory update packet + _owner.sendPacket(ownerIU); + player.sendPacket(playerIU); + } + return ok; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/UIKeysSettings.java b/src/main/java/com/l2jserver/gameserver/model/UIKeysSettings.java new file mode 100644 index 00000000..294977f5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/UIKeysSettings.java @@ -0,0 +1,178 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.data.xml.impl.UIData; + +/** + * UI Keys Settings class. + * @author mrTJO + * @author Zoey76 + */ +public class UIKeysSettings { + private static final Logger LOG = LoggerFactory.getLogger(UIKeysSettings.class); + + private final int _playerObjId; + private Map> _storedKeys; + private Map> _storedCategories; + private boolean _saved = true; + + public UIKeysSettings(int playerObjId) { + _playerObjId = playerObjId; + loadFromDB(); + } + + public void storeAll(Map> catMap, Map> keyMap) { + _saved = false; + _storedCategories = catMap; + _storedKeys = keyMap; + } + + public void storeCategories(Map> catMap) { + _saved = false; + _storedCategories = catMap; + } + + public Map> getCategories() { + return _storedCategories; + } + + public void storeKeys(Map> keyMap) { + _saved = false; + _storedKeys = keyMap; + } + + public Map> getKeys() { + return _storedKeys; + } + + private void loadFromDB() { + getCatsFromDB(); + getKeysFromDB(); + } + + /** + * Save Categories and Mapped Keys into GameServer DataBase + */ + public void saveInDB() { + StringBuilder query; + if (_saved) { + return; + } + + // TODO(Zoey76): Refactor this to use batch. + query = new StringBuilder("REPLACE INTO character_ui_categories (`charId`, `catId`, `order`, `cmdId`) VALUES "); + for (int category : _storedCategories.keySet()) { + int order = 0; + for (int key : _storedCategories.get(category)) { + query.append("(").append(_playerObjId).append(", ").append(category).append(", ").append(order++).append(", ").append(key).append("),"); + } + } + query = new StringBuilder(query.substring(0, query.length() - 1) + "; "); + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement(query.toString())) { + statement.execute(); + } catch (Exception e) { + LOG.warn("Exception: saveInDB(): {}", e.getMessage(), e); + } + + query = new StringBuilder("REPLACE INTO character_ui_actions (`charId`, `cat`, `order`, `cmd`, `key`, `tgKey1`, `tgKey2`, `show`) VALUES"); + for (List keyLst : _storedKeys.values()) { + int order = 0; + for (ActionKey key : keyLst) { + query.append(key.getSqlSaveString(_playerObjId, order++)).append(","); + } + } + query = new StringBuilder(query.substring(0, query.length() - 1) + ";"); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var statement = con.prepareStatement(query.toString())) { + statement.execute(); + } catch (Exception e) { + LOG.warn("Exception: saveInDB(): {}", e.getMessage(), e); + } + _saved = true; + } + + private void getCatsFromDB() { + if (_storedCategories != null) { + return; + } + + _storedCategories = new HashMap<>(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM character_ui_categories WHERE `charId` = ? ORDER BY `catId`, `order`")) { + ps.setInt(1, _playerObjId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + UIData.addCategory(_storedCategories, rs.getInt("catId"), rs.getInt("cmdId")); + } + } + } catch (Exception e) { + LOG.warn("Exception: getCatsFromDB(): {}", e.getMessage(), e); + } + + if (_storedCategories.isEmpty()) { + _storedCategories = UIData.getInstance().getCategories(); + } + } + + private void getKeysFromDB() { + if (_storedKeys != null) { + return; + } + + _storedKeys = new HashMap<>(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM character_ui_actions WHERE `charId` = ? ORDER BY `cat`, `order`")) { + ps.setInt(1, _playerObjId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + int cat = rs.getInt("cat"); + int cmd = rs.getInt("cmd"); + int key = rs.getInt("key"); + int tgKey1 = rs.getInt("tgKey1"); + int tgKey2 = rs.getInt("tgKey2"); + int show = rs.getInt("show"); + UIData.addKey(_storedKeys, cat, new ActionKey(cat, cmd, key, tgKey1, tgKey2, show)); + } + } + } catch (Exception e) { + LOG.warn("Exception: getKeysFromDB(): {}", e.getMessage(), e); + } + + if (_storedKeys.isEmpty()) { + _storedKeys = UIData.getInstance().getKeys(); + } + } + + public boolean isSaved() { + return _saved; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/VehiclePathPoint.java b/src/main/java/com/l2jserver/gameserver/model/VehiclePathPoint.java new file mode 100644 index 00000000..91622fdb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/VehiclePathPoint.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +public final class VehiclePathPoint extends Location { + private final int _moveSpeed; + private final int _rotationSpeed; + + public VehiclePathPoint(int x, int y, int z) { + super(x, y, z); + _moveSpeed = 350; + _rotationSpeed = 4000; + } + + public VehiclePathPoint(int x, int y, int z, int moveSpeed, int rotationSpeed) { + super(x, y, z); + _moveSpeed = moveSpeed; + _rotationSpeed = rotationSpeed; + } + + public int getMoveSpeed() { + return _moveSpeed; + } + + public int getRotationSpeed() { + return _rotationSpeed; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/WalkInfo.java b/src/main/java/com/l2jserver/gameserver/model/WalkInfo.java new file mode 100644 index 00000000..2a128d87 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/WalkInfo.java @@ -0,0 +1,184 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model; + +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcMoveRouteFinished; + +/** + * Holds info about current walk progress. + * @author GKR + * @author UnAfraid + */ +public class WalkInfo { + private final String _routeName; + private ScheduledFuture _walkCheckTask; + private boolean _blocked = false; + private boolean _suspended = false; + private boolean _stoppedByAttack = false; + private int _currentNode = 0; + private boolean _forward = true; // Determines first --> last or first <-- last direction + private long _lastActionTime; // Debug field + + public WalkInfo(String routeName) { + _routeName = routeName; + } + + /** + * @return name of route of this WalkInfo. + */ + public L2WalkRoute getRoute() { + return WalkingManager.getInstance().getRoute(_routeName); + } + + /** + * @return current node of this WalkInfo. + */ + public L2NpcWalkerNode getCurrentNode() { + return getRoute().getNodeList().get(_currentNode); + } + + /** + * Calculate next node for this WalkInfo and send debug message from given npc + * @param npc NPC to debug message to be sent from + */ + public synchronized void calculateNextNode(L2Npc npc) { + // Check this first, within the bounds of random moving, we have no conception of "first" or "last" node + if (getRoute().getRepeatType() == WalkingManager.REPEAT_RANDOM) { + int newNode = _currentNode; + + while (newNode == _currentNode) { + newNode = Rnd.get(getRoute().getNodesCount()); + } + + _currentNode = newNode; + npc.sendDebugMessage("Route: " + getRoute().getName() + ", next random node is " + _currentNode); + } else { + if (_forward) { + _currentNode++; + } else { + _currentNode--; + } + + if (_currentNode == getRoute().getNodesCount()) // Last node arrived + { + // Notify quest + EventDispatcher.getInstance().notifyEventAsync(new NpcMoveRouteFinished(npc), npc); + npc.sendDebugMessage("Route: " + getRoute().getName() + ", last node arrived"); + + if (!getRoute().repeatWalk()) { + WalkingManager.getInstance().cancelMoving(npc); + return; + } + + switch (getRoute().getRepeatType()) { + case WalkingManager.REPEAT_GO_BACK -> { + _forward = false; + _currentNode -= 2; + } + case WalkingManager.REPEAT_GO_FIRST -> _currentNode = 0; + case WalkingManager.REPEAT_TELE_FIRST -> { + npc.teleToLocation(npc.getSpawn().getLocation()); + _currentNode = 0; + } + } + } else if (_currentNode == WalkingManager.NO_REPEAT) { + // First node arrived, when direction is first <-- last + _currentNode = 1; + _forward = true; + } + } + } + + /** + * @return {@code true} if walking task is blocked, {@code false} otherwise, + */ + public boolean isBlocked() { + return _blocked; + } + + public void setBlocked(boolean val) { + _blocked = val; + } + + /** + * @return {@code true} if walking task is suspended, {@code false} otherwise, + */ + public boolean isSuspended() { + return _suspended; + } + + public void setSuspended(boolean val) { + _suspended = val; + } + + /** + * @return {@code true} if walking task shall be stopped by attack, {@code false} otherwise, + */ + public boolean isStoppedByAttack() { + return _stoppedByAttack; + } + + public void setStoppedByAttack(boolean val) { + _stoppedByAttack = val; + } + + /** + * @return the id of the current node in this walking task. + */ + public int getCurrentNodeId() { + return _currentNode; + } + + /** + * @return {@code long} last action time used only for debugging. + */ + public long getLastAction() { + return _lastActionTime; + } + + public void setLastAction(long val) { + _lastActionTime = val; + } + + /** + * @return walking check task. + */ + public ScheduledFuture getWalkCheckTask() { + return _walkCheckTask; + } + + /** + * @param val walking check task. + */ + public void setWalkCheckTask(ScheduledFuture val) { + _walkCheckTask = val; + } + + @Override + public String toString() { + return "WalkInfo [_routeName=" + _routeName + ", _walkCheckTask=" + _walkCheckTask + ", _blocked=" + _blocked + ", _suspended=" + _suspended + ", _stoppedByAttack=" + _stoppedByAttack + ", _currentNode=" + _currentNode + ", _forward=" + _forward + ", _lastActionTime=" + _lastActionTime + + "]"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Attackable.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Attackable.java new file mode 100644 index 00000000..fca039c8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Attackable.java @@ -0,0 +1,1452 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.hunting; +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.config.Configuration.rates; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2AttackableAI; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2FortSiegeGuardAI; +import com.l2jserver.gameserver.ai.L2SiegeGuardAI; +import com.l2jserver.gameserver.datatables.EventDroplist; +import com.l2jserver.gameserver.datatables.EventDroplist.DateDrop; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.AbsorberInfo; +import com.l2jserver.gameserver.model.AggroInfo; +import com.l2jserver.gameserver.model.DamageDoneInfo; +import com.l2jserver.gameserver.model.L2CommandChannel; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.L2Seed; +import com.l2jserver.gameserver.model.actor.instance.L2GrandBossInstance; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2ServitorInstance; +import com.l2jserver.gameserver.model.actor.knownlist.AttackableKnownList; +import com.l2jserver.gameserver.model.actor.status.AttackableStatus; +import com.l2jserver.gameserver.model.actor.tasks.attackable.CommandChannelTimer; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.drops.DropListScope; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableAggroRangeEnter; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableAttack; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableKill; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; +import com.l2jserver.gameserver.util.Util; + +public class L2Attackable extends L2Npc { + private static final Logger LOG = LoggerFactory.getLogger(L2Attackable.class); + // Raid + private boolean _isRaid = false; + private boolean _isRaidMinion = false; + // + private boolean _champion = false; + private final Map _aggroList = new ConcurrentHashMap<>(); + private boolean _isReturningToSpawnPoint = false; + private boolean _canReturnToSpawnPoint = true; + private boolean _seeThroughSilentMove = false; + // Manor + private boolean _seeded = false; + private L2Seed _seed = null; + private int _seederObjId = 0; + private final AtomicReference _harvestItem = new AtomicReference<>(); + // Spoil + private int _spoilerObjectId; + private final AtomicReference> _sweepItems = new AtomicReference<>(); + // Over-hit + private boolean _overhit; + private double _overhitDamage; + private L2Character _overhitAttacker; + // Command channel + private volatile L2CommandChannel _firstCommandChannelAttacked = null; + private CommandChannelTimer _commandChannelTimer = null; + private long _commandChannelLastAttack = 0; + // Soul crystal + private boolean _absorbed; + private final Map _absorbersList = new ConcurrentHashMap<>(); + // Misc + private boolean _mustGiveExpSp; + protected int _onKillDelay = 5000; + + /** + * Creates an attackable NPC. + * @param template the attackable NPC template + */ + public L2Attackable(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2Attackable); + setIsInvul(false); + _mustGiveExpSp = true; + } + + @Override + public AttackableKnownList getKnownList() { + return (AttackableKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new AttackableKnownList(this)); + } + + @Override + public AttackableStatus getStatus() { + return (AttackableStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new AttackableStatus(this)); + } + + @Override + protected L2CharacterAI initAI() { + return new L2AttackableAI(this); + } + + public final Map getAggroList() { + return _aggroList; + } + + public final boolean isReturningToSpawnPoint() { + return _isReturningToSpawnPoint; + } + + public final void setisReturningToSpawnPoint(boolean value) { + _isReturningToSpawnPoint = value; + } + + public final boolean canReturnToSpawnPoint() { + return _canReturnToSpawnPoint; + } + + public final void setCanReturnToSpawnPoint(boolean value) { + _canReturnToSpawnPoint = value; + } + + public boolean canSeeThroughSilentMove() { + return _seeThroughSilentMove; + } + + public void setSeeThroughSilentMove(boolean val) { + _seeThroughSilentMove = val; + } + + /** + * Use the skill if minimum checks are pass. + * @param skill the skill + */ + public void useMagic(Skill skill) { + if ((skill == null) || isAlikeDead() || skill.isPassive() || isCastingNow() || isSkillDisabled(skill)) { + return; + } + + if ((getCurrentMp() < (getStat().getMpConsume1(skill) + getStat().getMpConsume2(skill))) || (getCurrentHp() <= skill.getHpConsume())) { + return; + } + + if (!skill.isStatic()) { + if (skill.isMagic()) { + if (isMuted()) { + return; + } + } else { + if (isPhysicalMuted()) { + return; + } + } + } + + final var targets = skill.getTargets(this); + if (!targets.isEmpty()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_CAST, skill, targets.get(0)); + } + } + + /** + * Reduce the current HP of the L2Attackable. + * @param damage The HP decrease value + * @param attacker The L2Character who attacks + */ + @Override + public void reduceCurrentHp(double damage, L2Character attacker, Skill skill) { + reduceCurrentHp(damage, attacker, true, false, skill); + } + + /** + * Reduce the current HP of the L2Attackable, update its _aggroList and launch the doDie Task if necessary. + * @param damage The HP decrease value + * @param attacker The L2Character who attacks + * @param awake The awake state (If True : stop sleeping) + * @param isDOT + * @param skill + */ + @Override + public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + if (isRaid() && !isMinion() && (attacker != null) && (attacker.getParty() != null) && attacker.getParty().isInCommandChannel() && attacker.getParty().getCommandChannel().meetRaidWarCondition(this)) { + if (_firstCommandChannelAttacked == null) // looting right isn't set + { + synchronized (this) { + if (_firstCommandChannelAttacked == null) { + _firstCommandChannelAttacked = attacker.getParty().getCommandChannel(); + if (_firstCommandChannelAttacked != null) { + _commandChannelTimer = new CommandChannelTimer(this); + _commandChannelLastAttack = System.currentTimeMillis(); + ThreadPoolManager.getInstance().scheduleGeneral(_commandChannelTimer, 10000); // check for last attack + _firstCommandChannelAttacked.broadcastPacket(new CreatureSay(0, Say2.PARTYROOM_ALL, "", "You have looting rights!")); // TODO: retail msg + } + } + } + } else if (attacker.getParty().getCommandChannel().equals(_firstCommandChannelAttacked)) // is in same channel + { + _commandChannelLastAttack = System.currentTimeMillis(); // update last attack time + } + } + + if (isEventMob()) { + return; + } + + // Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList + if (attacker != null) { + addDamage(attacker, (int) damage, skill); + } + + // If this L2Attackable is a L2MonsterInstance and it has spawned minions, call its minions to battle + if (this instanceof L2MonsterInstance master) { + if (master.hasMinions()) { + master.getMinionList().onAssist(this, attacker); + } + + master = master.getLeader(); + if ((master != null) && master.hasMinions()) { + master.getMinionList().onAssist(this, attacker); + } + } + // Reduce the current HP of the L2Attackable and launch the doDie Task if necessary + super.reduceCurrentHp(damage, attacker, awake, isDOT, skill); + } + + public synchronized void setMustRewardExpSp(boolean value) { + _mustGiveExpSp = value; + } + + public synchronized boolean getMustRewardExpSP() { + return _mustGiveExpSp; + } + + /** + * Kill the L2Attackable (the corpse disappeared after 7 seconds), distribute rewards (EXP, SP, Drops...) and notify Quest Engine.
    + * Actions:
    + * Distribute Exp and SP rewards to L2PcInstance (including Summon owner) that hit the L2Attackable and to their Party members
    + * Notify the Quest Engine of the L2Attackable death if necessary.
    + * Kill the L2NpcInstance (the corpse disappeared after 7 seconds)
    + * Caution: This method DOESN'T GIVE rewards to L2PetInstance. + * @param killer The L2Character that has killed the L2Attackable + */ + @Override + public boolean doDie(L2Character killer) { + // Kill the L2NpcInstance (the corpse disappeared after 7 seconds) + if (!super.doDie(killer)) { + return false; + } + + if ((killer != null) && killer.isPlayable()) { + // Delayed notification + EventDispatcher.getInstance().notifyEventAsyncDelayed(new AttackableKill(killer.getActingPlayer(), this, killer.isSummon()), this, _onKillDelay); + } + + // Notify to minions if there are. + if (isMonster()) { + final L2MonsterInstance mob = (L2MonsterInstance) this; + if ((mob.getLeader() != null) && mob.getLeader().hasMinions()) { + var respawnTime = npc().getCustomMinionsRespawnTime().get(getId()); + respawnTime = respawnTime != null ? respawnTime * 1000 : -1; + mob.getLeader().getMinionList().onMinionDie(mob, respawnTime); + } + + if (mob.hasMinions()) { + mob.getMinionList().onMasterDie(false); + } + } + return true; + } + + /** + * Distribute Exp and SP rewards to L2PcInstance (including Summon owner) that hit the L2Attackable and to their Party members.
    + * Actions:
    + * Get the L2PcInstance owner of the L2ServitorInstance (if necessary) and L2Party in progress.
    + * Calculate the Experience and SP rewards in function of the level difference.
    + * Add Exp and SP rewards to L2PcInstance (including Summon penalty) and to Party members in the known area of the last attacker.
    + * Caution : This method DOESN'T GIVE rewards to L2PetInstance. + * @param lastAttacker The L2Character that has killed the L2Attackable + */ + @Override + protected void calculateRewards(L2Character lastAttacker) { + try { + if (_aggroList.isEmpty()) { + return; + } + + // NOTE: Concurrent-safe map is used because while iterating to verify all conditions sometimes an entry must be removed. + final Map rewards = new ConcurrentHashMap<>(); + + L2PcInstance maxDealer = null; + int maxDamage = 0; + long totalDamage = 0; + // While Iterating over This Map Removing Object is Not Allowed + // Go through the _aggroList of the L2Attackable + for (AggroInfo info : _aggroList.values()) { + if (info == null) { + continue; + } + + // Get the L2Character corresponding to this attacker + final L2PcInstance attacker = info.getAttacker().getActingPlayer(); + if (attacker != null) { + // Get damages done by this attacker + final int damage = info.getDamage(); + + // Prevent unwanted behavior + if (damage > 1) { + // Check if damage dealer isn't too far from this (killed monster) + if (!Util.checkIfInRange(character().getPartyRange(), this, attacker, true)) { + continue; + } + + totalDamage += damage; + + // Calculate real damages (Summoners should get own damage plus summon's damage) + final DamageDoneInfo reward = rewards.computeIfAbsent(attacker, DamageDoneInfo::new); + reward.addDamage(damage); + + if (reward.getDamage() > maxDamage) { + maxDealer = attacker; + maxDamage = reward.getDamage(); + } + } + } + } + + // Manage Base, Quests and Sweep drops of the L2Attackable + doItemDrop((maxDealer != null) && maxDealer.isOnline() ? maxDealer : lastAttacker); + + // Manage drop of Special Events created by GM for a defined period + doEventDrop(lastAttacker); + + if (!getMustRewardExpSP()) { + return; + } + + if (!rewards.isEmpty()) { + for (DamageDoneInfo reward : rewards.values()) { + if (reward == null) { + continue; + } + + // Attacker to be rewarded + final L2PcInstance attacker = reward.getAttacker(); + + // Total amount of damage done + final int damage = reward.getDamage(); + + // Get party + final L2Party attackerParty = attacker.getParty(); + + // Penalty applied to the attacker's XP + // If this attacker have servitor, get Exp Penalty applied for the servitor. + final float penalty = attacker.hasServitor() ? ((L2ServitorInstance) attacker.getSummon()).getExpMultiplier() : 1; + + // If there's NO party in progress + if (attackerParty == null) { + // Calculate Exp and SP rewards + if (attacker.getKnownList().knowsObject(this)) { + // Calculate the difference of level between this attacker (player or servitor owner) and the L2Attackable + // mob = 24, atk = 10, diff = -14 (full xp) + // mob = 24, atk = 28, diff = 4 (some xp) + // mob = 24, atk = 50, diff = 26 (no xp) + final int levelDiff = attacker.getLevel() - getLevel(); + + final int[] expSp = calculateExpAndSp(levelDiff, damage, totalDamage); + long exp = expSp[0]; + int sp = expSp[1]; + + if (customs().championEnable() && isChampion()) { + exp *= customs().getChampionRewardsExpSp(); + sp *= customs().getChampionRewardsExpSp(); + } + + exp *= penalty; + + // Check for an over-hit enabled strike + L2Character overhitAttacker = getOverhitAttacker(); + if (isOverhit() && (overhitAttacker != null) && (overhitAttacker.getActingPlayer() != null) && (attacker == overhitAttacker.getActingPlayer())) { + attacker.sendPacket(SystemMessageId.OVER_HIT); + exp += calculateOverhitExp(exp); + } + + // Distribute the Exp and SP between the L2PcInstance and its L2Summon + if (!attacker.isDead()) { + attacker.addExpAndSp(exp, sp, useVitalityRate()); + if (exp > 0) { + attacker.updateVitalityPoints(getVitalityPoints(damage), true, false); + if (!attacker.isInsideZone(ZoneId.PEACE) && ((attacker.getLevel() - getLevel()) <= 9)) { + if (hunting().getNevitEnable()) { + attacker.getHuntingSystem().startHuntingSystemTask(); + if ((attacker.getHuntingSystem().getHuntingBonusTime() < hunting().getHuntingBonusMaxTime()) || !hunting().getHuntingBonusLimit()) { + attacker.getHuntingSystem().addPoints(hunting().getNevitNormalPoints()); + } + } + + attacker.getRecSystem().startBonusTask(true); + } + } + } + } + } else { + // share with party members + int partyDmg = 0; + float partyMul = 1; + int partyLvl = 0; + + // Get all L2Character that can be rewarded in the party + final List rewardedMembers = new ArrayList<>(); + // Go through all L2PcInstance in the party + final List groupMembers = attackerParty.isInCommandChannel() ? attackerParty.getCommandChannel().getMembers() : attackerParty.getMembers(); + for (L2PcInstance partyPlayer : groupMembers) { + if ((partyPlayer == null) || partyPlayer.isDead()) { + continue; + } + + // Get the RewardInfo of this L2PcInstance from L2Attackable rewards + final DamageDoneInfo reward2 = rewards.get(partyPlayer); + + // If the L2PcInstance is in the L2Attackable rewards add its damages to party damages + if (reward2 != null) { + if (Util.checkIfInRange(character().getPartyRange(), this, partyPlayer, true)) { + partyDmg += reward2.getDamage(); // Add L2PcInstance damages to party damages + rewardedMembers.add(partyPlayer); + + if (partyPlayer.getLevel() > partyLvl) { + if (attackerParty.isInCommandChannel()) { + partyLvl = attackerParty.getCommandChannel().getLevel(); + } else { + partyLvl = partyPlayer.getLevel(); + } + } + } + rewards.remove(partyPlayer); // Remove the L2PcInstance from the L2Attackable rewards + } else { + // Add L2PcInstance of the party (that have attacked or not) to members that can be rewarded + // and in range of the monster. + if (Util.checkIfInRange(character().getPartyRange(), this, partyPlayer, true)) { + rewardedMembers.add(partyPlayer); + if (partyPlayer.getLevel() > partyLvl) { + if (attackerParty.isInCommandChannel()) { + partyLvl = attackerParty.getCommandChannel().getLevel(); + } else { + partyLvl = partyPlayer.getLevel(); + } + } + } + } + } + + // If the party didn't killed this L2Attackable alone + if (partyDmg < totalDamage) { + partyMul = ((float) partyDmg / totalDamage); + } + + // Calculate the level difference between Party and L2Attackable + final int levelDiff = partyLvl - getLevel(); + + // Calculate Exp and SP rewards + final int[] expSp = calculateExpAndSp(levelDiff, partyDmg, totalDamage); + long exp = expSp[0]; + int sp = expSp[1]; + + if (customs().championEnable() && isChampion()) { + exp *= customs().getChampionRewardsExpSp(); + sp *= customs().getChampionRewardsExpSp(); + } + + exp *= partyMul; + sp *= partyMul; + + // Check for an over-hit enabled strike + // (When in party, the over-hit exp bonus is given to the whole party and split proportionally through the party members) + L2Character overhitAttacker = getOverhitAttacker(); + if (isOverhit() && (overhitAttacker != null) && (overhitAttacker.getActingPlayer() != null) && (attacker == overhitAttacker.getActingPlayer())) { + attacker.sendPacket(SystemMessageId.OVER_HIT); + exp += calculateOverhitExp(exp); + } + + // Distribute Experience and SP rewards to L2PcInstance Party members in the known area of the last attacker + if (partyDmg > 0) { + attackerParty.distributeXpAndSp(exp, sp, rewardedMembers, partyLvl, partyDmg, this); + } + } + } + } + } catch (Exception ex) { + LOG.error("Error calculating rewards!", ex); + } + } + + @Override + public void addAttackerToAttackByList(L2Character player) { + if ((player == null) || (player == this) || getAttackByList().contains(player)) { + return; + } + getAttackByList().add(player); + } + + /** + * Add damage and hate to the attacker AggroInfo of the L2Attackable _aggroList. + * @param attacker The L2Character that gave damages to this L2Attackable + * @param damage The number of damages given by the attacker L2Character + * @param skill + */ + public void addDamage(L2Character attacker, int damage, Skill skill) { + if (attacker == null) { + return; + } + + // Notify the L2Attackable AI with EVT_ATTACKED + if (!isDead()) { + try { + // If monster is on walk - stop it + if (isWalker() && !isCoreAIDisabled() && WalkingManager.getInstance().isOnWalk(this)) { + WalkingManager.getInstance().stopMoving(this, false, true); + } + + getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, attacker); + addDamageHate(attacker, damage, (damage * 100) / (getLevel() + 7)); + + final L2PcInstance player = attacker.getActingPlayer(); + if (player != null) { + EventDispatcher.getInstance().notifyEventAsync(new AttackableAttack(player, this, damage, skill, attacker.isSummon()), this); + } + } catch (Exception ex) { + LOG.error("Error adding damage!", ex); + } + } + } + + /** + * Adds damage and hate to the attacker aggression list for this character. + * @param attacker The L2Character that gave damages to this L2Attackable + * @param damage The number of damages given by the attacker L2Character + * @param aggro The hate (=damage) given by the attacker L2Character + */ + public void addDamageHate(L2Character attacker, int damage, long aggro) { + if ((attacker == null) || (attacker == this)) { + return; + } + + // Get the AggroInfo of the attacker L2Character from the _aggroList of the L2Attackable + final AggroInfo ai = _aggroList.computeIfAbsent(attacker, AggroInfo::new); + ai.addDamage(damage); + + // Traps does not cause aggro + // making this hack because not possible to determine if damage made by trap + // so just check for triggered trap here + final L2PcInstance targetPlayer = attacker.getActingPlayer(); + if ((targetPlayer == null) || (targetPlayer.getTrap() == null) || !targetPlayer.getTrap().isTriggered()) { + ai.addHate(aggro); + } + + if ((targetPlayer != null) && (aggro == 0)) { + addDamageHate(attacker, 0, 1); + + // Set the intention to the L2Attackable to AI_INTENTION_ACTIVE + if (getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new AttackableAggroRangeEnter(this, targetPlayer, attacker.isSummon()), this); + } else if ((targetPlayer == null) && (aggro == 0)) { + aggro = 1; + ai.addHate(1); + } + + // Set the intention to the L2Attackable to AI_INTENTION_ACTIVE + if ((aggro != 0) && (getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE)) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + } + + public void reduceHate(L2Character target, long amount) { + if ((getAI() instanceof L2SiegeGuardAI) || (getAI() instanceof L2FortSiegeGuardAI)) { + // TODO: this just prevents error until siege guards are handled properly + stopHating(target); + setTarget(null); + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + return; + } + + if (target == null) // whole aggrolist + { + L2Character mostHated = getMostHated(); + if (mostHated == null) // makes target passive for a moment more + { + ((L2AttackableAI) getAI()).setGlobalAggro(-25); + return; + } + + for (AggroInfo ai : _aggroList.values()) { + ai.addHate(amount); + } + + amount = getHating(mostHated); + if (amount >= 0) { + ((L2AttackableAI) getAI()).setGlobalAggro(-25); + clearAggroList(); + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + setWalking(); + } + return; + } + + AggroInfo ai = _aggroList.get(target); + if (ai == null) { + LOG.info("Target {} not present in aggro list of {}.", target, this); + return; + } + + ai.addHate(amount); + if ((ai.getHate() >= 0) && (getMostHated() == null)) { + ((L2AttackableAI) getAI()).setGlobalAggro(-25); + clearAggroList(); + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + setWalking(); + } + } + + /** + * Clears _aggroList hate of the L2Character without removing from the list. + * @param target + */ + public void stopHating(L2Character target) { + if (target == null) { + return; + } + AggroInfo ai = _aggroList.get(target); + if (ai != null) { + ai.stopHate(); + } + } + + /** + * @return the most hated L2Character of the L2Attackable _aggroList. + */ + public L2Character getMostHated() { + if (_aggroList.isEmpty() || isAlikeDead()) { + return null; + } + + L2Character mostHated = null; + long maxHate = 0; + + // While Interacting over This Map Removing Object is Not Allowed + // Go through the aggroList of the L2Attackable + for (AggroInfo ai : _aggroList.values()) { + if (ai == null) { + continue; + } + + if (ai.checkHate(this) > maxHate) { + mostHated = ai.getAttacker(); + maxHate = ai.getHate(); + } + } + + return mostHated; + } + + /** + * @return the 2 most hated L2Character of the L2Attackable _aggroList. + */ + public List get2MostHated() { + if (_aggroList.isEmpty() || isAlikeDead()) { + return null; + } + + L2Character mostHated = null; + L2Character secondMostHated = null; + long maxHate = 0; + List result = new ArrayList<>(); + + // While iterating over this map removing objects is not allowed + // Go through the aggroList of the L2Attackable + for (AggroInfo ai : _aggroList.values()) { + if (ai == null) { + continue; + } + + if (ai.checkHate(this) > maxHate) { + secondMostHated = mostHated; + mostHated = ai.getAttacker(); + maxHate = ai.getHate(); + } + } + + result.add(mostHated); + + if (getAttackByList().contains(secondMostHated)) { + result.add(secondMostHated); + } else { + result.add(null); + } + return result; + } + + public List getHateList() { + if (_aggroList.isEmpty() || isAlikeDead()) { + return null; + } + + List result = new ArrayList<>(); + for (AggroInfo ai : _aggroList.values()) { + if (ai == null) { + continue; + } + ai.checkHate(this); + + result.add(ai.getAttacker()); + } + return result; + } + + /** + * @param target The L2Character whose hate level must be returned + * @return the hate level of the L2Attackable against this L2Character contained in _aggroList. + */ + public long getHating(final L2Character target) { + if (_aggroList.isEmpty() || (target == null)) { + return 0; + } + + final AggroInfo ai = _aggroList.get(target); + if (ai == null) { + return 0; + } + + if (ai.getAttacker() instanceof L2PcInstance act) { + if (act.isInvisible() || ai.getAttacker().isInvul() || act.isSpawnProtected()) { + // Remove Object Should Use This Method and Can be Blocked While Interacting + _aggroList.remove(target); + return 0; + } + } + + if (!ai.getAttacker().isVisible() || ai.getAttacker().isInvisible()) { + _aggroList.remove(target); + return 0; + } + + if (ai.getAttacker().isAlikeDead()) { + ai.stopHate(); + return 0; + } + return ai.getHate(); + } + + public void doItemDrop(L2Character mainDamageDealer) { + doItemDrop(getTemplate(), mainDamageDealer); + } + + /** + * Manage Base, Quests and Special Events drops of L2Attackable (called by calculateRewards).
    + * Concept:
    + * During a Special Event all L2Attackable can drop extra Items.
    + * Those extra Items are defined in the table allNpcDateDrops of the EventDroplist.
    + * Each Special Event has a start and end date to stop to drop extra Items automatically.
    + * Actions:
    + * Manage drop of Special Events created by GM for a defined period.
    + * Get all possible drops of this L2Attackable from L2NpcTemplate and add it Quest drops.
    + * For each possible drops (base + quests), calculate which one must be dropped (random).
    + * Get each Item quantity dropped (random).
    + * Create this or these L2ItemInstance corresponding to each Item Identifier dropped.
    + * If the autoLoot mode is actif and if the L2Character that has killed the L2Attackable is a L2PcInstance, Give the item(s) to the L2PcInstance that has killed the L2Attackable.
    + * If the autoLoot mode isn't actif or if the L2Character that has killed the L2Attackable is not a L2PcInstance, add this or these item(s) in the world as a visible object at the position where mob was last. + * @param npcTemplate + * @param mainDamageDealer + */ + public void doItemDrop(L2NpcTemplate npcTemplate, L2Character mainDamageDealer) { + if (mainDamageDealer == null) { + return; + } + + L2PcInstance player = mainDamageDealer.getActingPlayer(); + + // Don't drop anything if the last attacker or owner isn't L2PcInstance + if (player == null) { + return; + } + + CursedWeaponsManager.getInstance().checkDrop(this, player); + + if (isSpoiled()) { + _sweepItems.set(npcTemplate.calculateDrops(DropListScope.CORPSE, this, player)); + } + + Collection deathItems = npcTemplate.calculateDrops(DropListScope.DEATH, this, player); + if (deathItems != null) { + for (ItemHolder drop : deathItems) { + L2Item item = ItemTable.getInstance().getTemplate(drop.getId()); + // Check if the autoLoot mode is active + // Give the item(s) to the L2PcInstance inventory has killed the L2Attackable + if ((!item.hasExImmediateEffect() && ((!isRaid() && character().autoLoot()) || (isRaid() && character().autoLootRaids()))) || (item.hasExImmediateEffect() && character().autoLootHerbs())) { + player.doAutoLoot(this, drop); + } else if ((!item.hasExImmediateEffect() && ((!isRaid() && ((player.isAutoLoot() && !player.isAutoLootItem()) || (player.isAutoLootItem() && customs().getAutoLootItemsList().contains(item.getId())))))) || // + (item.hasExImmediateEffect() && ((player.isAutoLootHerb() && customs().getAutoLootHerbsList().contains(item.getId()))))) { + player.doAutoLoot(this, drop); + } else { + dropItem(player, drop); // Drop the rest or all item(s) on the ground + } + + // Broadcast message if RaidBoss was defeated + if (isRaid() && !isRaidMinion() && (drop.getCount() > 0)) { + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DIED_DROPPED_S3_S2); + sm.addCharName(this); + sm.addItemName(item); + sm.addLong(drop.getCount()); + broadcastPacket(sm); + } + } + } + + // Apply Special Item drop with random(rnd) quantity(qty) for champions. + if (customs().championEnable() && isChampion() && ((customs().getChampionRewardLowerLvlItemChance() > 0) || (customs().getChampionRewardHigherLvlItemChance() > 0))) { + int champqty = Rnd.get(customs().getChampionRewardItemQty()); + ItemHolder item = new ItemHolder(customs().getChampionRewardItemID(), ++champqty); + + // Give the item(s) to the L2PcInstance inventory has killed the L2Attackable + if ((player.getLevel() <= getLevel()) && (Rnd.get(100) < customs().getChampionRewardLowerLvlItemChance())) { + if (character().autoLoot()) { + player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); + } else if ((player.isAutoLoot() && !player.isAutoLootItem()) || (player.isAutoLootItem() && customs().getAutoLootItemsList().contains(item.getId()))) { + player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); + } else { + dropItem(player, item); + } + } else if ((player.getLevel() > getLevel()) && (Rnd.get(100) < customs().getChampionRewardHigherLvlItemChance())) { + if (character().autoLoot()) { + player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); + } else if ((player.isAutoLoot() && !player.isAutoLootItem()) || (player.isAutoLootItem() && customs().getAutoLootItemsList().contains(item.getId()))) { + player.addItem("ChampionLoot", item.getId(), item.getCount(), this, true); + } else { + dropItem(player, item); + } + } + } + } + + /** + * Manage Special Events drops created by GM for a defined period.
    + * Concept:
    + * During a Special Event all L2Attackable can drop extra Items.
    + * Those extra Items are defined in the table allNpcDateDrops of the EventDroplist.
    + * Each Special Event has a start and end date to stop to drop extra Items automatically.
    + * Actions: If an extra drop must be generated
    + * Get an Item Identifier (random) from the DateDrop Item table of this Event.
    + * Get the Item quantity dropped (random).
    + * Create this or these L2ItemInstance corresponding to this Item Identifier.
    + * If the autoLoot mode is actif and if the L2Character that has killed the L2Attackable is a L2PcInstance, Give the item(s) to the L2PcInstance that has killed the L2Attackable
    + * If the autoLoot mode isn't actif or if the L2Character that has killed the L2Attackable is not a L2PcInstance, add this or these item(s) in the world as a visible object at the position where mob was last + * @param lastAttacker The L2Character that has killed the L2Attackable + */ + public void doEventDrop(L2Character lastAttacker) { + if (lastAttacker == null) { + return; + } + + L2PcInstance player = lastAttacker.getActingPlayer(); + + // Don't drop anything if the last attacker or owner isn't L2PcInstance + if (player == null) { + return; + } + + if ((player.getLevel() - getLevel()) > 9) { + return; + } + + // Go through DateDrop of EventDroplist allNpcDateDrops within the date range + for (DateDrop drop : EventDroplist.getInstance().getAllDrops()) { + if (Rnd.get(1000000) < drop.getEventDrop().getDropChance()) { + final int itemId = drop.getEventDrop().getItemIdList()[Rnd.get(drop.getEventDrop().getItemIdList().length)]; + final long itemCount = Rnd.get(drop.getEventDrop().getMinCount(), drop.getEventDrop().getMaxCount()); + // Give the item(s) to the L2PcInstance inventory has killed the L2Attackable + if (character().autoLoot()) { + player.doAutoLoot(this, itemId, itemCount); + } else if ((player.isAutoLoot() && !player.isAutoLootItem()) || (player.isAutoLootItem() && customs().getAutoLootItemsList().contains(itemId))) { + player.doAutoLoot(this, itemId, itemCount); + } else { + dropItem(player, itemId, itemCount); // Drop the rest or all item(s) on the ground + } + } + } + } + + /** + * @return the active weapon of this L2Attackable (= null). + */ + public L2ItemInstance getActiveWeapon() { + return null; + } + + /** + * Verifies if the creature is in the aggro list. + * @param creature the creature + * @return {@code true} if the creature is in the aggro list, {@code false} otherwise + */ + public boolean isInAggroList(L2Character creature) { + return _aggroList.containsKey(creature); + } + + /** + * Clear the _aggroList of the L2Attackable. + */ + public void clearAggroList() { + _aggroList.clear(); + + // clear overhit values + _overhit = false; + _overhitDamage = 0; + _overhitAttacker = null; + } + + /** + * @return {@code true} if there is a loot to sweep, {@code false} otherwise. + */ + @Override + public boolean isSweepActive() { + return _sweepItems.get() != null; + } + + /** + * @return a copy of dummy items for the spoil loot. + */ + public List getSpoilLootItems() { + final Collection sweepItems = _sweepItems.get(); + final List lootItems = new LinkedList<>(); + if (sweepItems != null) { + for (ItemHolder item : sweepItems) { + lootItems.add(ItemTable.getInstance().getTemplate(item.getId())); + } + } + return lootItems; + } + + /** + * @return table containing all L2ItemInstance that can be spoiled. + */ + public Collection takeSweep() { + return _sweepItems.getAndSet(null); + } + + /** + * @return table containing all L2ItemInstance that can be harvested. + */ + public ItemHolder takeHarvest() { + return _harvestItem.getAndSet(null); + } + + /** + * Checks if the corpse is too old. + * @param attacker the player to validate + * @param remainingTime the time to check + * @param sendMessage if {@code true} will send a message of corpse too old + * @return {@code true} if the corpse is too old + */ + public boolean isOldCorpse(L2PcInstance attacker, int remainingTime, boolean sendMessage) { + if (isDead() && (DecayTaskManager.getInstance().getRemainingTime(this) < remainingTime)) { + if (sendMessage && (attacker != null)) { + attacker.sendPacket(SystemMessageId.CORPSE_TOO_OLD_SKILL_NOT_USED); + } + return true; + } + return false; + } + + /** + * @param sweeper the player to validate. + * @param sendMessage sendMessage if {@code true} will send a message of sweep not allowed. + * @return {@code true} if is the spoiler or is in the spoiler party. + */ + public boolean checkSpoilOwner(L2PcInstance sweeper, boolean sendMessage) { + if ((sweeper.getObjectId() != getSpoilerObjectId()) && !sweeper.isInLooterParty(getSpoilerObjectId())) { + if (sendMessage) { + sweeper.sendPacket(SystemMessageId.SWEEP_NOT_ALLOWED); + } + return false; + } + return true; + } + + /** + * Set the over-hit flag on the L2Attackable. + * @param status The status of the over-hit flag + */ + public void overhitEnabled(boolean status) { + _overhit = status; + } + + /** + * Set the over-hit values like the attacker who did the strike and the amount of damage done by the skill. + * @param attacker The L2Character who hit on the L2Attackable using the over-hit enabled skill + * @param damage The amount of damage done by the over-hit enabled skill on the L2Attackable + */ + public void setOverhitValues(L2Character attacker, double damage) { + // Calculate the over-hit damage + // Ex: mob had 10 HP left, over-hit skill did 50 damage total, over-hit damage is 40 + double overhitDmg = -(getCurrentHp() - damage); + if (overhitDmg < 0) { + // we didn't killed the mob with the over-hit strike. (it wasn't really an over-hit strike) + // let's just clear all the over-hit related values + overhitEnabled(false); + _overhitDamage = 0; + _overhitAttacker = null; + return; + } + overhitEnabled(true); + _overhitDamage = overhitDmg; + _overhitAttacker = attacker; + } + + /** + * Return the L2Character who hit on the L2Attackable using an over-hit enabled skill. + * @return L2Character attacker + */ + public L2Character getOverhitAttacker() { + return _overhitAttacker; + } + + /** + * Return the amount of damage done on the L2Attackable using an over-hit enabled skill. + * @return double damage + */ + public double getOverhitDamage() { + return _overhitDamage; + } + + /** + * @return True if the L2Attackable was hit by an over-hit enabled skill. + */ + public boolean isOverhit() { + return _overhit; + } + + /** + * Activate the absorbed soul condition on the L2Attackable. + */ + public void absorbSoul() { + _absorbed = true; + } + + /** + * @return True if the L2Attackable had his soul absorbed. + */ + public boolean isAbsorbed() { + return _absorbed; + } + + /** + * Adds an attacker that successfully absorbed the soul of this L2Attackable into the _absorbersList. + * @param attacker + */ + public void addAbsorber(L2PcInstance attacker) { + // If we have no _absorbersList initiated, do it + final AbsorberInfo ai = _absorbersList.get(attacker.getObjectId()); + + // If the L2Character attacker isn't already in the _absorbersList of this L2Attackable, add it + if (ai == null) { + _absorbersList.put(attacker.getObjectId(), new AbsorberInfo(attacker.getObjectId(), getCurrentHp())); + } else { + ai.setAbsorbedHp(getCurrentHp()); + } + + // Set this L2Attackable as absorbed + absorbSoul(); + } + + public void resetAbsorbList() { + _absorbed = false; + _absorbersList.clear(); + } + + public Map getAbsorbersList() { + return _absorbersList; + } + + /** + * Calculate the Experience and SP to distribute to attacker (L2PcInstance, L2ServitorInstance or L2Party) of the L2Attackable. + * @param diff The difference of level between attacker (L2PcInstance, L2ServitorInstance or L2Party) and the L2Attackable + * @param damage The damages given by the attacker (L2PcInstance, L2ServitorInstance or L2Party) + * @param totalDamage The total damage done + * @return + */ + private int[] calculateExpAndSp(int diff, int damage, long totalDamage) { + double xp; + double sp; + + if (diff < -5) { + diff = -5; // makes possible to use ALT_GAME_EXPONENT configuration + } + + xp = ((double) getExpReward() * damage) / totalDamage; + if (character().getExponentXp() != 0) { + xp *= Math.pow(2., -diff / character().getExponentXp()); + } + + sp = ((double) getSpReward() * damage) / totalDamage; + if (character().getExponentSp() != 0) { + sp *= Math.pow(2., -diff / character().getExponentSp()); + } + + if ((character().getExponentXp() == 0) && (character().getExponentSp() == 0)) { + // formula revised May 07 + if (diff > 5) { + double pow = Math.pow((double) 5 / 6, diff - 5); + xp = xp * pow; + sp = sp * pow; + } + + if (xp <= 0) { + xp = 0; + sp = 0; + } else if (sp <= 0) { + sp = 0; + } + } + return new int[] { + (int) xp, + (int) sp + }; + } + + public long calculateOverhitExp(long normalExp) { + // Get the percentage based on the total of extra (over-hit) damage done relative to the total (maximum) amount of HP on the L2Attackable + double overhitPercentage = ((getOverhitDamage() * 100) / getMaxHp()); + + // Over-hit damage percentages are limited to 25% max + if (overhitPercentage > 25) { + overhitPercentage = 25; + } + + // Get the overhit exp bonus according to the above over-hit damage percentage + // (1/1 basis - 13% of over-hit damage, 13% of extra exp is given, and so on...) + double overhitExp = ((overhitPercentage / 100) * normalExp); + + // Return the rounded amount of exp points to be added to the player's normal exp reward + return Math.round(overhitExp); + } + + @Override + public boolean canBeAttacked() { + return true; + } + + @Override + public void onSpawn() { + super.onSpawn(); + // Clear mob spoil, seed + setSpoilerObjectId(0); + // Clear all aggro char from list + clearAggroList(); + // Clear Harvester reward + _harvestItem.set(null); + // Clear mod Seeded stat + _seeded = false; + _seed = null; + _seederObjId = 0; + // Clear overhit value + overhitEnabled(false); + + _sweepItems.set(null); + resetAbsorbList(); + + setWalking(); + + // check the region where this mob is, do not activate the AI if region is inactive. + if (!isInActiveRegion()) { + if (hasAI()) { + getAI().stopAITask(); + } + } + } + + /** + * Checks if its spoiled. + * @return {@code true} if its spoiled, {@code false} otherwise + */ + public final boolean isSpoiled() { + return _spoilerObjectId != 0; + } + + /** + * Gets the spoiler object id. + * @return the spoiler object id if its spoiled, 0 otherwise + */ + public final int getSpoilerObjectId() { + return _spoilerObjectId; + } + + /** + * Sets the spoiler object id. + * @param spoilerObjectId the spoiler object id + */ + public final void setSpoilerObjectId(int spoilerObjectId) { + _spoilerObjectId = spoilerObjectId; + } + + /** + * Sets state of the mob to seeded. Parameters needed to be set before. + * @param seeder + */ + public final void setSeeded(L2PcInstance seeder) { + if ((_seed != null) && (_seederObjId == seeder.getObjectId())) { + _seeded = true; + + int count = 1; + for (int skillId : getTemplate().getSkills().keySet()) { + switch (skillId) { + case 4303 -> count *= 2; // Strong type x2 + case 4304 -> count *= 3; // Strong type x3 + case 4305 -> count *= 4; // Strong type x4 + case 4306 -> count *= 5; // Strong type x5 + case 4307 -> count *= 6; // Strong type x6 + case 4308 -> count *= 7; // Strong type x7 + case 4309 -> count *= 8; // Strong type x8 + case 4310 -> count *= 9; // Strong type x9 + } + } + + // hi-lvl mobs bonus + final int diff = getLevel() - _seed.getLevel() - 5; + if (diff > 0) { + count += diff; + } + _harvestItem.set(new ItemHolder(_seed.getCropId(), count * rates().getRateDropManor())); + } + } + + /** + * Sets the seed parameters, but not the seed state + * @param seed - instance {@link L2Seed} of used seed + * @param seeder - player who sows the seed + */ + public final void setSeeded(L2Seed seed, L2PcInstance seeder) { + if (!_seeded) { + _seed = seed; + _seederObjId = seeder.getObjectId(); + } + } + + public final int getSeederId() { + return _seederObjId; + } + + public final L2Seed getSeed() { + return _seed; + } + + public final boolean isSeeded() { + return _seeded; + } + + /** + * Set delay for onKill() call, in ms Default: 5000 ms + * @param delay + */ + public final void setOnKillDelay(int delay) { + _onKillDelay = delay; + } + + public final int getOnKillDelay() { + return _onKillDelay; + } + + /** + * Check if the server allows Random Animation. + */ + // This is located here because L2Monster and L2FriendlyMob both extend this class. The other non-pc instances extend either L2NpcInstance or L2MonsterInstance. + @Override + public boolean hasRandomAnimation() { + return ((general().getMaxMonsterAnimation() > 0) && isRandomAnimationEnabled() && !(this instanceof L2GrandBossInstance)); + } + + @Override + public boolean isMob() { + return true; // This means we use MAX_MONSTER_ANIMATION instead of MAX_NPC_ANIMATION + } + + public void setCommandChannelTimer(CommandChannelTimer commandChannelTimer) { + _commandChannelTimer = commandChannelTimer; + } + + public CommandChannelTimer getCommandChannelTimer() { + return _commandChannelTimer; + } + + public L2CommandChannel getFirstCommandChannelAttacked() { + return _firstCommandChannelAttacked; + } + + public void setFirstCommandChannelAttacked(L2CommandChannel firstCommandChannelAttacked) { + _firstCommandChannelAttacked = firstCommandChannelAttacked; + } + + /** + * @return the _commandChannelLastAttack + */ + public long getCommandChannelLastAttack() { + return _commandChannelLastAttack; + } + + /** + * @param channelLastAttack the _commandChannelLastAttack to set + */ + public void setCommandChannelLastAttack(long channelLastAttack) { + _commandChannelLastAttack = channelLastAttack; + } + + public void returnHome() { + clearAggroList(); + + if (hasAI() && (getSpawn() != null)) { + getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, getSpawn().getLocation(this)); + } + } + + /* + * Return vitality points decrease (if positive) or increase (if negative) based on damage. Maximum for damage = maxHp. + */ + public float getVitalityPoints(int damage) { + // sanity check + if (damage <= 0) { + return 0; + } + + final float divider = (getLevel() > 0) && (getExpReward() > 0) ? (getTemplate().getBaseHpMax() * 9 * getLevel() * getLevel()) / (100 * getExpReward()) : 0; + if (divider == 0) { + return 0; + } + + // negative value - vitality will be consumed + return -Math.min(damage, getMaxHp()) / divider; + } + + /** + * Determines whether the vitality rate for EXP and SP should be applied. + * + * @return {@code true} if the vitality rate should be applied, {@code false} otherwise. + */ + public boolean useVitalityRate() { + return !isChampion() || customs().championEnableVitality(); + } + + /** Return True if the L2Character is RaidBoss or his minion. */ + @Override + public boolean isRaid() { + return _isRaid; + } + + /** + * Set this Npc as a Raid instance. + * @param isRaid + */ + public void setIsRaid(boolean isRaid) { + _isRaid = isRaid; + } + + /** + * Set this Npc as a Minion instance. + * @param val + */ + public void setIsRaidMinion(boolean val) { + _isRaid = val; + _isRaidMinion = val; + } + + @Override + public boolean isRaidMinion() { + return _isRaidMinion; + } + + @Override + public boolean isMinion() { + return getLeader() != null; + } + + /** + * @return leader of this minion or null. + */ + public L2Attackable getLeader() { + return null; + } + + public void setChampion(boolean champ) { + _champion = champ; + } + + @Override + public boolean isChampion() { + return _champion; + } + + @Override + public boolean isAttackable() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Character.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Character.java new file mode 100644 index 00000000..f4d736e4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Character.java @@ -0,0 +1,5735 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ATTACK; +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.geodata; +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.config.Configuration.territoryWar; +import static com.l2jserver.gameserver.model.skills.targets.AffectScope.PARTY; +import static com.l2jserver.gameserver.model.stats.Stats.NUM_STATS; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.StampedLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2AttackableAI; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.data.xml.impl.CategoryData; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.CategoryType; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.enums.ShotType; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.CharEffectList; +import com.l2jserver.gameserver.model.L2AccessLevel; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.TimeStamp; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.actor.instance.L2RiftInvaderInstance; +import com.l2jserver.gameserver.model.actor.knownlist.CharKnownList; +import com.l2jserver.gameserver.model.actor.stat.CharStat; +import com.l2jserver.gameserver.model.actor.status.CharStatus; +import com.l2jserver.gameserver.model.actor.tasks.character.FlyToLocationTask; +import com.l2jserver.gameserver.model.actor.tasks.character.HitTask; +import com.l2jserver.gameserver.model.actor.tasks.character.MagicUseTask; +import com.l2jserver.gameserver.model.actor.tasks.character.NotifyAITask; +import com.l2jserver.gameserver.model.actor.tasks.character.QueuedMagicUseTask; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.model.actor.transform.Transform; +import com.l2jserver.gameserver.model.actor.transform.TransformTemplate; +import com.l2jserver.gameserver.model.effects.EffectFlag; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.events.Containers; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.EventType; +import com.l2jserver.gameserver.model.events.impl.character.CreatureAttack; +import com.l2jserver.gameserver.model.events.impl.character.CreatureAttackAvoid; +import com.l2jserver.gameserver.model.events.impl.character.CreatureAttacked; +import com.l2jserver.gameserver.model.events.impl.character.CreatureDamageDealt; +import com.l2jserver.gameserver.model.events.impl.character.CreatureDamageReceived; +import com.l2jserver.gameserver.model.events.impl.character.CreatureKill; +import com.l2jserver.gameserver.model.events.impl.character.CreatureSkillUse; +import com.l2jserver.gameserver.model.events.impl.character.CreatureTeleported; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcSkillSee; +import com.l2jserver.gameserver.model.events.listeners.AbstractEventListener; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.holders.InvulSkillHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.holders.SkillUseHolder; +import com.l2jserver.gameserver.model.interfaces.IDeletable; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.interfaces.ISkillsHolder; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.options.OptionsSkillHolder; +import com.l2jserver.gameserver.model.options.OptionsSkillType; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.AbnormalVisualEffect; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.model.skills.EffectScope; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.skills.SkillChannelized; +import com.l2jserver.gameserver.model.skills.SkillChannelizer; +import com.l2jserver.gameserver.model.skills.targets.TargetType; +import com.l2jserver.gameserver.model.stats.Calculator; +import com.l2jserver.gameserver.model.stats.Formulas; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.functions.AbstractFunction; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.Attack; +import com.l2jserver.gameserver.network.serverpackets.ChangeMoveType; +import com.l2jserver.gameserver.network.serverpackets.ChangeWaitType; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.ExRotation; +import com.l2jserver.gameserver.network.serverpackets.FlyToLocation.FlyType; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillCanceled; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillLaunched; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.MoveToLocation; +import com.l2jserver.gameserver.network.serverpackets.Revive; +import com.l2jserver.gameserver.network.serverpackets.ServerObjectInfo; +import com.l2jserver.gameserver.network.serverpackets.SetupGauge; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.StopMove; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.TeleportToLocation; +import com.l2jserver.gameserver.pathfinding.AbstractNodeLoc; +import com.l2jserver.gameserver.pathfinding.PathFinding; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; +import com.l2jserver.gameserver.util.EmptyQueue; +import com.l2jserver.gameserver.util.Util; + +/** + * Mother class of all character objects of the world (PC, NPC...)
    + * L2Character:
    + *
      + *
    • L2DoorInstance
    • + *
    • L2Playable
    • + *
    • L2Npc
    • + *
    • L2StaticObjectInstance
    • + *
    • L2Trap
    • + *
    • L2Vehicle
    • + *
    + *
    + * Concept of L2CharTemplate:
    + * Each L2Character owns generic and static properties (ex : all Keltir have the same number of HP...).
    + * All of those properties are stored in a different template for each type of L2Character.
    + * Each template is loaded once in the server cache memory (reduce memory use).
    + * When a new instance of L2Character is spawned, server just create a link between the instance and the template.
    + * This link is stored in {@link #_template} + * @since 2005/04/11 10:06:08 + */ +public abstract class L2Character extends L2Object implements ISkillsHolder, IDeletable { + private static final Logger LOG = LoggerFactory.getLogger(L2Character.class); + + private volatile Set _attackByList; + private volatile boolean _isCastingNow = false; + private volatile boolean _isCastingSimultaneouslyNow = false; + private Skill _lastSkillCast; + private Skill _lastSimultaneousSkillCast; + + private boolean _isDead = false; + private boolean _isImmobilized = false; + private boolean _isOverloaded = false; // the char is carrying too much + private boolean _isPendingRevive = false; + private boolean _isRunning = false; + private boolean _isNoRndWalk = false; // Is no random walk + protected boolean _showSummonAnimation = false; + protected boolean _isTeleporting = false; + private boolean _isInvul = false; + private boolean _isMortal = true; // Char will die when HP decreased to 0 + private boolean _isFlying = false; + + private CharStat _stat; + private CharStatus _status; + private L2CharTemplate _template; // The link on the L2CharTemplate object containing generic and static properties of this L2Character type (ex : Max HP, Speed...) + private String _title; + + public static final double MAX_HP_BAR_PX = 352.0; + + private double _hpUpdateIncCheck = .0; + private double _hpUpdateDecCheck = .0; + private double _hpUpdateInterval = .0; + + /** Table of Calculators containing all used calculator */ + private Calculator[] _calculators; + /** Map containing all skills of this character. */ + private final Map _skills = new ConcurrentHashMap<>(); + /** Map containing the skill reuse time stamps. */ + private volatile Map _reuseTimeStampsSkills = null; + /** Map containing the item reuse time stamps. */ + private volatile Map _reuseTimeStampsItems = null; + /** Map containing all the disabled skills. */ + private volatile Map _disabledSkills = null; + private boolean _allSkillsDisabled; + + private final byte[] _zones = new byte[ZoneId.getZoneCount()]; + protected byte _zoneValidateCounter = 4; + + private L2Character _debugger = null; + + private final ReentrantLock _teleportLock = new ReentrantLock(); + private final StampedLock _attackLock = new StampedLock(); + + private Team _team = Team.NONE; + + protected long _exceptions = 0L; + + private boolean _lethalable = true; + + private volatile Map _triggerSkills; + + private volatile Map _invulAgainst; + /** Creatures effect list. */ + private final CharEffectList _effectList = new CharEffectList(this); + /** The character that summons this character. */ + private L2Character _summoner = null; + + private SkillChannelizer _channelizer = null; + + private SkillChannelized _channelized = null; + + /** Map 32 bits, containing all abnormal visual effects in progress. */ + private int _abnormalVisualEffects; + /** Map 32 bits, containing all special abnormal visual effects in progress. */ + private int _abnormalVisualEffectsSpecial; + /** Map 32 bits, containing all event abnormal visual effects in progress. */ + private int _abnormalVisualEffectsEvent; + + /** Movement data of this L2Character */ + protected MoveData _move; + + /** This creature's target. */ + private L2Object _target; + /** Represents the time where the attack should end, in nanoseconds. */ + private volatile long _attackEndTime; + private long _disableBowAttackEndTime; + + private int _castInterruptTime; + + /** Table of calculators containing all standard NPC calculator (ex : ACCURACY_COMBAT, EVASION_RATE) */ + private static final Calculator[] NPC_STD_CALCULATOR = Formulas.getStdNPCCalculators(); + + private volatile L2CharacterAI _ai = null; + + /** Future Skill Cast */ + protected Future _skillCast; + protected Future _skillCast2; + + /** + * Constructor of L2Character.
    + * Concept:
    + * Each L2Character owns generic and static properties (ex : all Keltir have the same number of HP...).
    + * All of those properties are stored in a different template for each type of L2Character. Each template is loaded once in the server cache memory (reduce memory use).
    + * When a new instance of L2Character is spawned, server just create a link between the instance and the template This link is stored in _template
    + * Actions: + *
      + *
    • Set the _template of the L2Character
    • + *
    • Set _overloaded to false (the character can take more items)
    • + *
    • If L2Character is a L2NPCInstance, copy skills from template to object
    • + *
    • If L2Character is a L2NPCInstance, link _calculators to NPC_STD_CALCULATOR
    • + *
    • If L2Character is NOT a L2NPCInstance, create an empty _skills slot
    • + *
    • If L2Character is a L2PcInstance or L2Summon, copy basic Calculator set to object
    • + *
    + * @param objectId Identifier of the object to initialized + * @param template The L2CharTemplate to apply to the object + */ + public L2Character(int objectId, L2CharTemplate template) { + super(objectId); + if (template == null) { + throw new NullPointerException("Template is null!"); + } + + setInstanceType(InstanceType.L2Character); + initCharStat(); + initCharStatus(); + + // Set its template to the new L2Character + _template = template; + + if (isDoor()) { + _calculators = Formulas.getStdDoorCalculators(); + } else if (isNpc()) { + _calculators = NPC_STD_CALCULATOR; + } else { + _calculators = new Calculator[NUM_STATS]; + } + + setIsInvul(true); + } + + public final CharEffectList getEffectList() { + return _effectList; + } + + /** + * Verify if this character is under debug. + * @return {@code true} if this character is under debug, {@code false} otherwise + */ + public boolean isDebug() { + return _debugger != null; + } + + /** + * Sets character instance, to which debug packets will be send. + * @param debugger the character debugging this character + */ + public void setDebug(L2Character debugger) { + _debugger = debugger; + } + + /** + * Send debug packet. + * @param pkt + */ + public void sendDebugPacket(L2GameServerPacket pkt) { + if (_debugger != null) { + _debugger.sendPacket(pkt); + } + } + + /** + * Send debug text string + * @param msg + */ + public void sendDebugMessage(String msg) { + if (_debugger != null) { + _debugger.sendMessage(msg); + } + } + + /** + * @return character inventory, default null, overridden in L2Playable types and in L2NPcInstance + */ + public Inventory getInventory() { + return null; + } + + public boolean destroyItemByItemId(String process, int itemId, long count, L2Object reference, boolean sendMessage) { + // Default: NPCs consume virtual items for their skills + // TODO: should be logged if even happens.. should be false + return true; + } + + public boolean destroyItem(String process, int objectId, long count, L2Object reference, boolean sendMessage) { + // Default: NPCs consume virtual items for their skills + // TODO: should be logged if even happens.. should be false + return true; + } + + /** + * Check if the character is in the given zone Id. + * @param zone the zone Id to check + * @return {code true} if the character is in that zone + */ + @Override + public final boolean isInsideZone(ZoneId zone) { + Instance instance = InstanceManager.getInstance().getInstance(getInstanceId()); + switch (zone) { + case PVP: + if ((instance != null) && instance.isPvPInstance()) { + return true; + } + return (_zones[ZoneId.PVP.ordinal()] > 0) && (_zones[ZoneId.PEACE.ordinal()] == 0); + case PEACE: + if ((instance != null) && instance.isPvPInstance()) { + return false; + } + } + return _zones[zone.ordinal()] > 0; + } + + /** + * @param zone + * @param state + */ + public final void setInsideZone(ZoneId zone, final boolean state) { + synchronized (_zones) { + if (state) { + _zones[zone.ordinal()]++; + } else if (_zones[zone.ordinal()] > 0) { + _zones[zone.ordinal()]--; + } + } + } + + /** + * This will return true if the player is transformed,
    + * but if the player is not transformed it will return false. + * @return transformation status + */ + public boolean isTransformed() { + return false; + } + + public Transform getTransformation() { + return null; + } + + /** + * This will untransform a player if they are an instance of L2Pcinstance and if they are transformed. + */ + public void untransform() { + // Just a place holder + } + + /** + * This will return true if the player is GM,
    + * but if the player is not GM it will return false. + * @return GM status + */ + public boolean isGM() { + return false; + } + + /** + * Overridden in L2PcInstance. + * @return the access level. + */ + public L2AccessLevel getAccessLevel() { + return null; + } + + protected void initCharStatusUpdateValues() { + _hpUpdateIncCheck = getMaxHp(); + _hpUpdateInterval = _hpUpdateIncCheck / MAX_HP_BAR_PX; + _hpUpdateDecCheck = _hpUpdateIncCheck - _hpUpdateInterval; + } + + /** + * Remove the L2Character from the world when the decay task is launched.
    + * Caution : This method DOESN'T REMOVE the object from _allObjects of L2World
    + * Caution : This method DOESN'T SEND Server->Client packets to players + */ + public void onDecay() { + decayMe(); + + final L2WorldRegion reg = getWorldRegion(); + if (reg != null) { + reg.removeFromZones(this); + } + } + + @Override + public void onSpawn() { + super.onSpawn(); + revalidateZone(true); + } + + public void onTeleported() { + if (!_teleportLock.tryLock()) { + return; + } + try { + if (!isTeleporting()) { + return; + } + spawnMe(getX(), getY(), getZ()); + setIsTeleporting(false); + EventDispatcher.getInstance().notifyEventAsync(new CreatureTeleported(this), this); + } finally { + _teleportLock.unlock(); + } + } + + /** + * Add L2Character instance that is attacking to the attacker list. + * @param player The L2Character that attacks this one + */ + public void addAttackerToAttackByList(L2Character player) { + // DS: moved to L2Attackable + } + + /** + * Send a packet to the L2Character AND to all L2PcInstance in the _KnownPlayers of the L2Character.
    + * Concept:
    + * L2PcInstance in the detection area of the L2Character are identified in _knownPlayers.
    + * In order to inform other players of state modification on the L2Character, server just need to go through _knownPlayers to send Server->Client Packet + * @param mov + */ + public void broadcastPacket(L2GameServerPacket mov) { + mov.setInvisible(isInvisible()); + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if (player != null) { + player.sendPacket(mov); + } + } + } + + /** + * Send a packet to the L2Character AND to all L2PcInstance in the radius (max knownlist radius) from the L2Character.
    + * Concept:
    + * L2PcInstance in the detection area of the L2Character are identified in _knownPlayers.
    + * In order to inform other players of state modification on the L2Character, server just need to go through _knownPlayers to send Server->Client Packet + * @param mov + * @param radiusInKnownlist + */ + public void broadcastPacket(L2GameServerPacket mov, int radiusInKnownlist) { + mov.setInvisible(isInvisible()); + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if ((player != null) && isInsideRadius(player, radiusInKnownlist, false, false)) { + player.sendPacket(mov); + } + } + } + + /** + * @return true if hp update should be done, false if not + */ + protected boolean needHpUpdate() { + double currentHp = getCurrentHp(); + double maxHp = getMaxHp(); + + if ((currentHp <= 1.0) || (maxHp < MAX_HP_BAR_PX)) { + return true; + } + + if ((currentHp < _hpUpdateDecCheck) || (Math.abs(currentHp - _hpUpdateDecCheck) <= 1e-6) || (currentHp > _hpUpdateIncCheck) || (Math.abs(currentHp - _hpUpdateIncCheck) <= 1e-6)) { + if (Math.abs(currentHp - maxHp) <= 1e-6) { + _hpUpdateIncCheck = currentHp + 1; + _hpUpdateDecCheck = currentHp - _hpUpdateInterval; + } else { + double doubleMulti = currentHp / _hpUpdateInterval; + int intMulti = (int) doubleMulti; + + _hpUpdateDecCheck = _hpUpdateInterval * (doubleMulti < intMulti ? intMulti-- : intMulti); + _hpUpdateIncCheck = _hpUpdateDecCheck + _hpUpdateInterval; + } + + return true; + } + + return false; + } + + /** + * Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform.
    + * Actions: + *
      + *
    • Create the Server->Client packet StatusUpdate with current HP and MP
    • + *
    • Send the Server->Client packet StatusUpdate with current HP and MP to all L2Character called _statusListener that must be informed of HP/MP updates of this L2Character
    • + *
    + * Caution: This method DOESN'T SEND CP information + */ + public void broadcastStatusUpdate() { + if (getStatus().getStatusListener().isEmpty() || !needHpUpdate()) { + return; + } + + // Create the Server->Client packet StatusUpdate with current HP + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.MAX_HP, getMaxHp()); + su.addAttribute(StatusUpdate.CUR_HP, (int) getCurrentHp()); + + // Go through the StatusListener + // Send the Server->Client packet StatusUpdate with current HP and MP + for (L2Character temp : getStatus().getStatusListener()) { + if (temp != null) { + temp.sendPacket(su); + } + } + } + + /** + * @param text + */ + public void sendMessage(String text) { + // default implementation + } + + /** + * Teleport a L2Character and its pet if necessary.
    + * Actions: + *
      + *
    • Stop the movement of the L2Character
    • + *
    • Set the x,y,z position of the L2Object and if necessary modify its _worldRegion
    • + *
    • Send a Server->Client packet TeleportToLocationt to the L2Character AND to all L2PcInstance in its _KnownPlayers
    • + *
    • Modify the position of the pet if necessary
    • + *
    + * @param x + * @param y + * @param z + * @param heading + * @param instanceId + * @param randomOffset + */ + public void teleToLocation(int x, int y, int z, int heading, int instanceId, int randomOffset) { + setInstanceId(instanceId); + + if (_isPendingRevive) { + doRevive(); + } + + stopMove(null, false); + abortAttack(); + abortCast(); + + setIsTeleporting(true); + setTarget(null); + + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + + if (character().offsetOnTeleport() && (randomOffset > 0)) { + x += Rnd.get(-randomOffset, randomOffset); + y += Rnd.get(-randomOffset, randomOffset); + } + + z += 5; + + // Send a Server->Client packet TeleportToLocationt to the L2Character AND to all L2PcInstance in the _KnownPlayers of the L2Character + broadcastPacket(new TeleportToLocation(this, x, y, z, heading)); + + // remove the object from its old location + decayMe(); + + // Set the x,y,z position of the L2Object and if necessary modify its _worldRegion + setXYZ(x, y, z); + + // temporary fix for heading on teleports + if (heading != 0) { + setHeading(heading); + } + + // allow recall of the detached characters + if (!isPlayer() || ((getActingPlayer().getClient() != null) && getActingPlayer().getClient().isDetached())) { + onTeleported(); + } + + revalidateZone(true); + } + + public void teleToLocation(int x, int y, int z, int heading, int instanceId, boolean randomOffset) { + teleToLocation(x, y, z, heading, instanceId, (randomOffset) ? character().getMaxOffsetOnTeleport() : 0); + } + + public void teleToLocation(int x, int y, int z, int heading, int instanceId) { + teleToLocation(x, y, z, heading, instanceId, 0); + } + + public void teleToLocation(int x, int y, int z, int heading, boolean randomOffset) { + teleToLocation(x, y, z, heading, -1, (randomOffset) ? character().getMaxOffsetOnTeleport() : 0); + } + + public void teleToLocation(int x, int y, int z, int heading) { + teleToLocation(x, y, z, heading, -1, 0); + } + + public void teleToLocation(int x, int y, int z, boolean randomOffset) { + teleToLocation(x, y, z, 0, -1, (randomOffset) ? character().getMaxOffsetOnTeleport() : 0); + } + + public void teleToLocation(int x, int y, int z) { + teleToLocation(x, y, z, 0, -1, 0); + } + + public void teleToLocation(ILocational loc, int randomOffset) { + teleToLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getHeading(), loc.getInstanceId(), randomOffset); + } + + public void teleToLocation(ILocational loc, int instanceId, int randomOffset) { + teleToLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getHeading(), instanceId, randomOffset); + } + + public void teleToLocation(ILocational loc, boolean randomOffset) { + teleToLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getHeading(), loc.getInstanceId(), (randomOffset) ? character().getMaxOffsetOnTeleport() : 0); + } + + public void teleToLocation(ILocational loc) { + teleToLocation(loc.getX(), loc.getY(), loc.getZ(), loc.getHeading(), loc.getInstanceId(), 0); + } + + public void teleToLocation(TeleportWhereType teleportWhere) { + teleToLocation(MapRegionManager.getInstance().getTeleToLocation(this, teleportWhere), true); + } + + private boolean canUseRangeWeapon() { + if (isTransformed()) { + return true; + } + L2Weapon weaponItem = getActiveWeaponItem(); + + final long betweenRangedAttack = _disableBowAttackEndTime - System.currentTimeMillis(); + // Check for arrows and MP + if (isPlayer()) { + if ((weaponItem == null) || !weaponItem.isRange()) { + return false; + } + + // Equip arrows needed in left hand and send a Server->Client packet ItemList to the L2PcInstance then return True + if (!checkAndEquipArrows()) { + // Cancel the action because the L2PcInstance have no arrow + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(weaponItem.isBow() ? SystemMessageId.NOT_ENOUGH_ARROWS : SystemMessageId.NOT_ENOUGH_BOLTS); + return false; + } + + // Verify if the bow can be use + if (betweenRangedAttack > 0) { + // Cancel the action because the bow can't be re-use at this moment + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_READY_TO_ACT), betweenRangedAttack); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + // Verify if L2PcInstance owns enough MP + int mpConsume = weaponItem.getMpConsume(); + if ((weaponItem.getReducedMpConsume() > 0) && (Rnd.get(100) < weaponItem.getReducedMpConsumeChance())) { + mpConsume = weaponItem.getReducedMpConsume(); + } + mpConsume = (int) calcStat(Stats.BOW_MP_CONSUME_RATE, mpConsume, null, null); + + if (getCurrentMp() < mpConsume) { + // If L2PcInstance doesn't have enough MP, stop the attack + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_READY_TO_ACT), 100); + sendPacket(SystemMessageId.NOT_ENOUGH_MP); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // If L2PcInstance have enough MP, the bow consumes it + if (mpConsume > 0) { + getStatus().reduceMp(mpConsume); + } + } else if (isNpc()) { + // Verify reuse attack + if (betweenRangedAttack > 0) { + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_READY_TO_ACT), betweenRangedAttack); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + return true; + } + + /** + * Launch a physical attack against a target (Simple, Bow, Pole or Dual).
    + * Actions: + *
      + *
    • Get the active weapon (always equipped in the right hand)
    • + *
    • If weapon is a bow, check for arrows, MP and bow re-use delay (if necessary, equip the L2PcInstance with arrows in left hand)
    • + *
    • If weapon is a bow, consume MP and set the new period of bow non re-use
    • + *
    • Get the Attack Speed of the L2Character (delay (in milliseconds) before next attack)
    • + *
    • Select the type of attack to start (Simple, Bow, Pole or Dual) and verify if SoulShot are charged then start calculation
    • + *
    • If the Server->Client packet Attack contains at least 1 hit, send the Server->Client packet Attack to the L2Character AND to all L2PcInstance in the _KnownPlayers of the L2Character
    • + *
    • Notify AI with EVT_READY_TO_ACT
    • + *
    + * @param target The L2Character targeted + */ + public void doAttack(L2Character target) { + final long stamp = _attackLock.tryWriteLock(); + if (stamp == 0) { + return; + } + try { + if ((target == null) || isAttackingDisabled()) { + return; + } + + // Notify to scripts + final TerminateReturn attackReturn = EventDispatcher.getInstance().notifyEvent(new CreatureAttack(this, target), this, TerminateReturn.class); + if ((attackReturn != null) && attackReturn.terminate()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + // Notify to scripts + final TerminateReturn attackedReturn = EventDispatcher.getInstance().notifyEvent(new CreatureAttacked(this, target), target, TerminateReturn.class); + if ((attackedReturn != null) && attackedReturn.terminate()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + if (!isAlikeDead()) { + if ((isNpc() && target.isAlikeDead()) || !getKnownList().knowsObject(target)) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } else if (isPlayer()) { + if (target.isDead()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + final L2PcInstance actor = getActingPlayer(); + if (actor.isTransformed() && !actor.getTransformation().canAttack()) { + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + } + } + + // Check if attacker's weapon can attack + if (getActiveWeaponItem() != null) { + L2Weapon wpn = getActiveWeaponItem(); + if (!wpn.isAttackWeapon() && !isGM()) { + if (wpn.getItemType() == WeaponType.FISHINGROD) { + sendPacket(SystemMessageId.CANNOT_ATTACK_WITH_FISHING_POLE); + } else { + sendPacket(SystemMessageId.THAT_WEAPON_CANT_ATTACK); + } + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + } + + if (getActingPlayer() != null) { + if (getActingPlayer().inObserverMode()) { + sendPacket(SystemMessageId.OBSERVERS_CANNOT_PARTICIPATE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + else if ((target.getActingPlayer() != null) && (getActingPlayer().getSiegeState() > 0) && isInsideZone(ZoneId.SIEGE) && (target.getActingPlayer().getSiegeState() == getActingPlayer().getSiegeState()) && (target.getActingPlayer() != this) + && (target.getActingPlayer().getSiegeSide() == getActingPlayer().getSiegeSide())) { + if (TerritoryWarManager.getInstance().isTWInProgress()) { + sendPacket(SystemMessageId.YOU_CANNOT_ATTACK_A_MEMBER_OF_THE_SAME_TERRITORY); + } else { + sendPacket(SystemMessageId.FORCED_ATTACK_IS_IMPOSSIBLE_AGAINST_SIEGE_SIDE_TEMPORARY_ALLIED_MEMBERS); + } + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + // Checking if target has moved to peace zone + else if (target.isInsidePeaceZone(getActingPlayer())) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + } else if (isInsidePeaceZone(this, target)) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + stopEffectsOnAction(); + + if (!GeoData.getInstance().canSeeTarget(this, target)) { + sendPacket(SystemMessageId.CANT_SEE_TARGET); + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + target.getKnownList().addKnownObject(this); + + L2Weapon weaponItem = getActiveWeaponItem(); + final int timeAtk = calculateTimeBetweenAttacks(); + final int timeToHit = timeAtk / 2; + + Attack attack = new Attack(this, target, isChargedShot(ShotType.SOULSHOTS), (weaponItem != null) ? weaponItem.getItemGradeSPlus().getId() : 0); + setHeading(Util.calculateHeadingFrom(this, target)); + int reuse = calculateReuseTime(weaponItem); + + if (isNpc()) { + if (weaponItem == null) { + reuse = timeAtk; + } + reuse = reuse + getTemplate().getBaseReuseDelay(); + } + + boolean hitted; + switch (getAttackType()) { + case BOW: { + if (!canUseRangeWeapon()) { + return; + } + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitByBow(attack, target, timeAtk, reuse); + break; + } + case CROSSBOW: { + if (!canUseRangeWeapon()) { + return; + } + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitByCrossBow(attack, target, timeAtk, reuse); + break; + } + case POLE: { + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitByPole(attack, target, timeToHit); + break; + } + case FIST: { + if (!isPlayer()) { + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitSimple(attack, target, timeToHit); + break; + } + } + case DUAL: + case DUALFIST: + case DUALDAGGER: { + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitByDual(attack, target, timeToHit); + break; + } + default: { + _attackEndTime = System.nanoTime() + TimeUnit.NANOSECONDS.convert(timeAtk, TimeUnit.MILLISECONDS); + hitted = doAttackHitSimple(attack, target, timeToHit); + break; + } + } + + // Flag the attacker if it's a L2PcInstance outside a PvP area + final L2PcInstance player = getActingPlayer(); + if (player != null) { + AttackStanceTaskManager.getInstance().addAttackStanceTask(player); + if (player.getSummon() != target) { + player.updatePvPStatus(target); + } + } + + // Check if hit isn't missed + if (!hitted) { + abortAttack(); // Abort the attack of the L2Character and send Server->Client ActionFailed packet + } else { + // If we didn't miss the hit, discharge the shoulshots, if any + setChargedShot(ShotType.SOULSHOTS, false); + + if (player != null) { + if (player.isCursedWeaponEquipped()) { + // If hit by a cursed weapon, CP is reduced to 0 + if (!target.isInvul()) { + target.setCurrentCp(0); + } + } else if (player.isHero()) { + // If a cursed weapon is hit by a Hero, CP is reduced to 0 + if (target.isPlayer() && target.getActingPlayer().isCursedWeaponEquipped()) { + target.setCurrentCp(0); + } + } + } + } + + // If the Server->Client packet Attack contains at least 1 hit, send the Server->Client packet Attack + // to the L2Character AND to all L2PcInstance in the _KnownPlayers of the L2Character + if (attack.hasHits()) { + broadcastPacket(attack); + } + + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_READY_TO_ACT), timeAtk); + } finally { + _attackLock.unlockWrite(stamp); + } + } + + /** + * Launch a Bow attack.
    + * Actions: + *
      + *
    • Calculate if hit is missed or not
    • + *
    • Consume arrows
    • + *
    • If hit isn't missed, calculate if shield defense is efficient
    • + *
    • If hit isn't missed, calculate if hit is critical
    • + *
    • If hit isn't missed, calculate physical damages
    • + *
    • If the L2Character is a L2PcInstance, Send a Server->Client packet SetupGauge
    • + *
    • Create a new hit task with Medium priority
    • + *
    • Calculate and set the disable delay of the bow in function of the Attack Speed
    • + *
    • Add this hit to the Server-Client packet Attack
    • + *
    + * @param attack Server->Client packet Attack in which the hit will be added + * @param target The L2Character targeted + * @param sAtk The Attack Speed of the attacker + * @param reuse + * @return True if the hit isn't missed + */ + private boolean doAttackHitByBow(Attack attack, L2Character target, int sAtk, int reuse) { + int damage1 = 0; + byte shld1 = 0; + boolean crit1 = false; + + // Calculate if hit is missed or not + boolean miss1 = Formulas.calcHitMiss(this, target); + + // Consume arrows + reduceArrowCount(false); + + _move = null; + + // Check if hit isn't missed + if (!miss1) { + // Calculate if shield defense is efficient + shld1 = Formulas.calcShldUse(this, target); + + // Calculate if hit is critical + crit1 = Formulas.calcCrit(this, target); + + // Calculate physical damages + damage1 = (int) Formulas.calcPhysDam(this, target, shld1, crit1, attack.hasSoulshot()); + + // Bows Ranged Damage Formula (Damage gradually decreases when 60% or lower than full hit range, and increases when 60% or higher). + // full hit range is 500 which is the base bow range, and the 60% of this is 800. + damage1 *= (calculateDistance(target, true, false) / 4000) + 0.8; + } + + // Check if the L2Character is a L2PcInstance + if (isPlayer()) { + sendPacket(new SetupGauge(SetupGauge.RED, sAtk + reuse)); + } + + // Create a new hit task with Medium priority + ThreadPoolManager.getInstance().scheduleAi(new HitTask(this, target, damage1, crit1, miss1, attack.hasSoulshot(), shld1), sAtk); + + // Calculate and set the disable delay of the bow in function of the Attack Speed + _disableBowAttackEndTime = (sAtk + reuse) + System.currentTimeMillis(); + + // Add this hit to the Server-Client packet Attack + attack.addHit(target, damage1, miss1, crit1, shld1); + + // Return true if hit isn't missed + return !miss1; + } + + /** + * Launch a CrossBow attack.
    + * Actions: + *
      + *
    • Calculate if hit is missed or not
    • + *
    • Consume bolts
    • + *
    • If hit isn't missed, calculate if shield defense is efficient
    • + *
    • If hit isn't missed, calculate if hit is critical
    • + *
    • If hit isn't missed, calculate physical damages
    • + *
    • If the L2Character is a L2PcInstance, Send a Server->Client packet SetupGauge
    • + *
    • Create a new hit task with Medium priority
    • + *
    • Calculate and set the disable delay of the crossbow in function of the Attack Speed
    • + *
    • Add this hit to the Server-Client packet Attack
    • + * @param attack Server->Client packet Attack in which the hit will be added + * @param target The L2Character targeted + * @param sAtk The Attack Speed of the attacker + * @param reuse + * @return True if the hit isn't missed + */ + private boolean doAttackHitByCrossBow(Attack attack, L2Character target, int sAtk, int reuse) { + int damage1 = 0; + byte shld1 = 0; + boolean crit1 = false; + + // Calculate if hit is missed or not + boolean miss1 = Formulas.calcHitMiss(this, target); + + // Consume bolts + reduceArrowCount(true); + + _move = null; + + // Check if hit isn't missed + if (!miss1) { + // Calculate if shield defense is efficient + shld1 = Formulas.calcShldUse(this, target); + + // Calculate if hit is critical + crit1 = Formulas.calcCrit(this, target); + + // Calculate physical damages + damage1 = (int) Formulas.calcPhysDam(this, target, shld1, crit1, attack.hasSoulshot()); + } + + // Check if the L2Character is a L2PcInstance + if (isPlayer()) { + // Send a system message + sendPacket(SystemMessageId.CROSSBOW_PREPARING_TO_FIRE); + + // Send a Server->Client packet SetupGauge + SetupGauge sg = new SetupGauge(SetupGauge.RED, sAtk + reuse); + sendPacket(sg); + } + + // Create a new hit task with Medium priority + ThreadPoolManager.getInstance().scheduleAi(new HitTask(this, target, damage1, crit1, miss1, attack.hasSoulshot(), shld1), sAtk); + + // Calculate and set the disable delay of the bow in function of the Attack Speed + _disableBowAttackEndTime = (sAtk + reuse) + System.currentTimeMillis(); + + // Add this hit to the Server-Client packet Attack + attack.addHit(target, damage1, miss1, crit1, shld1); + + // Return true if hit isn't missed + return !miss1; + } + + /** + * Launch a Dual attack.
      + * Actions: + *
        + *
      • Calculate if hits are missed or not
      • + *
      • If hits aren't missed, calculate if shield defense is efficient
      • + *
      • If hits aren't missed, calculate if hit is critical
      • + *
      • If hits aren't missed, calculate physical damages
      • + *
      • Create 2 new hit tasks with Medium priority
      • + *
      • Add those hits to the Server-Client packet Attack
      • + *
      + * @param attack Server->Client packet Attack in which the hit will be added + * @param target The L2Character targeted + * @param sAtk + * @return True if hit 1 or hit 2 isn't missed + */ + private boolean doAttackHitByDual(Attack attack, L2Character target, int sAtk) { + int damage1 = 0; + int damage2 = 0; + byte shld1 = 0; + byte shld2 = 0; + boolean crit1 = false; + boolean crit2 = false; + + // Calculate if hits are missed or not + boolean miss1 = Formulas.calcHitMiss(this, target); + boolean miss2 = Formulas.calcHitMiss(this, target); + + // Check if hit 1 isn't missed + if (!miss1) { + // Calculate if shield defense is efficient against hit 1 + shld1 = Formulas.calcShldUse(this, target); + + // Calculate if hit 1 is critical + crit1 = Formulas.calcCrit(this, target); + + // Calculate physical damages of hit 1 + damage1 = (int) Formulas.calcPhysDam(this, target, shld1, crit1, attack.hasSoulshot()); + damage1 /= 2; + } + + // Check if hit 2 isn't missed + if (!miss2) { + // Calculate if shield defense is efficient against hit 2 + shld2 = Formulas.calcShldUse(this, target); + + // Calculate if hit 2 is critical + crit2 = Formulas.calcCrit(this, target); + + // Calculate physical damages of hit 2 + damage2 = (int) Formulas.calcPhysDam(this, target, shld2, crit2, attack.hasSoulshot()); + damage2 /= 2; + } + + // Create a new hit task with Medium priority for hit 1 + ThreadPoolManager.getInstance().scheduleAi(new HitTask(this, target, damage1, crit1, miss1, attack.hasSoulshot(), shld1), sAtk / 2); + + // Create a new hit task with Medium priority for hit 2 with a higher delay + ThreadPoolManager.getInstance().scheduleAi(new HitTask(this, target, damage2, crit2, miss2, attack.hasSoulshot(), shld2), sAtk); + + // Add those hits to the Server-Client packet Attack + attack.addHit(target, damage1, miss1, crit1, shld1); + attack.addHit(target, damage2, miss2, crit2, shld2); + + // Return true if hit 1 or hit 2 isn't missed + return (!miss1 || !miss2); + } + + /** + * Launch a Pole attack.
      + * Actions: + *
        + *
      • Get all visible objects in a spherical area near the L2Character to obtain possible targets
      • + *
      • If possible target is the L2Character targeted, launch a simple attack against it
      • + *
      • If possible target isn't the L2Character targeted but is attackable, launch a simple attack against it
      • + *
      + * @param attack Server->Client packet Attack in which the hit will be added + * @param target + * @param sAtk + * @return True if one hit isn't missed + */ + private boolean doAttackHitByPole(Attack attack, L2Character target, int sAtk) { + boolean hitted = doAttackHitSimple(attack, target, 100, sAtk); + + if (isAffected(EffectFlag.SINGLE_TARGET)) { + return hitted; + } + + // double angleChar; + int maxRadius = getStat().getPhysicalAttackRange(); + int maxAngleDiff = getStat().getPhysicalAttackAngle(); + + // o1 x: 83420 y: 148158 (Giran) + // o2 x: 83379 y: 148081 (Giran) + // dx = -41 + // dy = -77 + // distance between o1 and o2 = 87.24 + // arctan2 = -120 (240) degree (excel arctan2(dx, dy); java arctan2(dy, dx)) + // + // o2 + // + // o1 ----- (heading) + // In the diagram above: + // o1 has a heading of 0/360 degree from horizontal (facing East) + // Degree of o2 in respect to o1 = -120 (240) degree + // + // o2 / (heading) + // / + // o1 + // In the diagram above + // o1 has a heading of -80 (280) degree from horizontal (facing north east) + // Degree of o2 in respect to 01 = -40 (320) degree + + // Get char's heading degree + // angleChar = Util.convertHeadingToDegree(getHeading()); + + // H5 Changes: without Polearm Mastery (skill 216) max simultaneous attacks is 3 (1 by default + 2 in skill 3599). + int attackRandomCountMax = (int) getStat().calcStat(Stats.ATTACK_COUNT_MAX, 1, null, null); + int attackcount = 0; + + // if (angleChar <= 0) angleChar += 360; + double attackpercent = 85; + L2Character temp; + Collection objs = getKnownList().getKnownObjects().values(); + + for (L2Object obj : objs) { + if ((obj == target) || (obj == null)) { + continue; // do not hit twice + } + // Check if the L2Object is a L2Character + if (obj.isCharacter()) { + if (obj.isPet() && isPlayer() && (((L2PetInstance) obj).getOwner() == getActingPlayer())) { + continue; + } + + if (!Util.checkIfInRange(maxRadius, this, obj, false)) { + continue; + } + + // otherwise hit too high/low. 650 because mob z coord + // sometimes wrong on hills + if (Math.abs(obj.getZ() - getZ()) > 650) { + continue; + } + if (!isFacing(obj, maxAngleDiff)) { + continue; + } + + if (isAttackable() && obj.isPlayer() && obj.isAttackable()) { + continue; + } + + if (isAttackable() && obj.isAttackable() && !((L2Attackable) this).isChaos()) { + continue; + } + + temp = (L2Character) obj; + + // Launch a simple attack against the L2Character targeted + if (!temp.isAlikeDead()) { + if ((temp == getAI().getAttackTarget()) || temp.isAutoAttackable(this)) { + hitted |= doAttackHitSimple(attack, temp, attackpercent, sAtk); + attackpercent /= 1.15; + + attackcount++; + if (attackcount > attackRandomCountMax) { + break; + } + } + } + } + } + + // Return true if one hit isn't missed + return hitted; + } + + /** + * Launch a simple attack.
      + * Actions: + *
        + *
      • Calculate if hit is missed or not
      • + *
      • If hit isn't missed, calculate if shield defense is efficient
      • + *
      • If hit isn't missed, calculate if hit is critical
      • + *
      • If hit isn't missed, calculate physical damages
      • + *
      • Create a new hit task with Medium priority
      • + *
      • Add this hit to the Server-Client packet Attack
      • + *
      + * @param attack Server->Client packet Attack in which the hit will be added + * @param target The L2Character targeted + * @param sAtk + * @return True if the hit isn't missed + */ + private boolean doAttackHitSimple(Attack attack, L2Character target, int sAtk) { + return doAttackHitSimple(attack, target, 100, sAtk); + } + + private boolean doAttackHitSimple(Attack attack, L2Character target, double attackpercent, int sAtk) { + int damage1 = 0; + byte shld1 = 0; + boolean crit1 = false; + + // Calculate if hit is missed or not + boolean miss1 = Formulas.calcHitMiss(this, target); + + // Check if hit isn't missed + if (!miss1) { + // Calculate if shield defense is efficient + shld1 = Formulas.calcShldUse(this, target); + + // Calculate if hit is critical + crit1 = Formulas.calcCrit(this, target); + + // Calculate physical damages + damage1 = (int) Formulas.calcPhysDam(this, target, shld1, crit1, attack.hasSoulshot()); + + if (attackpercent != 100) { + damage1 = (int) ((damage1 * attackpercent) / 100); + } + } + + // Create a new hit task with Medium priority + ThreadPoolManager.getInstance().scheduleAi(new HitTask(this, target, damage1, crit1, miss1, attack.hasSoulshot(), shld1), sAtk); + + // Add this hit to the Server-Client packet Attack + attack.addHit(target, damage1, miss1, crit1, shld1); + + // Return true if hit isn't missed + return !miss1; + } + + /** + * Manage the casting task (casting and interrupt time, re-use delay...) and display the casting bar and animation on client.
      + * Actions: + *
        + *
      • Verify the possibility of the the cast : skill is a spell, caster isn't muted...
      • + *
      • Get the list of all targets (ex : area effects) and define the L2Charcater targeted (its stats will be used in calculation)
      • + *
      • Calculate the casting time (base + modifier of MAtkSpd), interrupt time and re-use delay
      • + *
      • Send a Server->Client packet MagicSkillUser (to display casting animation), a packet SetupGauge (to display casting bar) and a system message
      • + *
      • Disable all skills during the casting time (create a task EnableAllSkills)
      • + *
      • Disable the skill during the re-use delay (create a task EnableSkill)
      • + *
      • Create a task MagicUseTask (that will call method onMagicUseTimer) to launch the Magic Skill at the end of the casting time
      • + *
      + * @param skill The L2Skill to use + */ + public void doCast(Skill skill) { + beginCast(skill, false); + } + + /** + * SkillHolder version of {@link #doCast(Skill)} method. + */ + public void doCast(SkillHolder skill) { + beginCast(skill.getSkill(), false); + } + + public void doSimultaneousCast(Skill skill) { + beginCast(skill, true); + } + + /** + * SkillHolder version of {@link #doSimultaneousCast(Skill)} method. + */ + public void doSimultaneousCast(SkillHolder skill) { + beginCast(skill.getSkill(), true); + } + + public void doCast(Skill skill, L2Character target, List targets) { + if (!checkDoCastConditions(skill)) { + setIsCastingNow(false); + return; + } + + // Override casting type + if (skill.isSimultaneousCast()) { + doSimultaneousCast(skill, target, targets); + return; + } + + stopEffectsOnAction(); + + // Recharge AutoSoulShot + // this method should not used with L2Playable + + beginCast(skill, false, target, targets); + } + + public void doSimultaneousCast(Skill skill, L2Character target, List targets) { + if (!checkDoCastConditions(skill)) { + setIsCastingSimultaneouslyNow(false); + return; + } + stopEffectsOnAction(); + + beginCast(skill, true, target, targets); + } + + private void beginCast(Skill skill, boolean simultaneously) { + if (!checkDoCastConditions(skill)) { + if (simultaneously) { + setIsCastingSimultaneouslyNow(false); + } else { + setIsCastingNow(false); + } + if (isPlayer()) { + getAI().setIntention(AI_INTENTION_ACTIVE); + } + return; + } + + // Override casting type + if (skill.isSimultaneousCast() && !simultaneously) { + simultaneously = true; + } + + stopEffectsOnAction(); + + final var target = skill.getTargetType().getTarget(skill, this, _target); + final var targets = skill.getTargetType().getTargets(skill, this, _target); + beginCast(skill, simultaneously, (L2Character) target, targets); + } + + private void beginCast(Skill skill, boolean simultaneously, L2Character target, List targets) { + if (target == null) { + if (simultaneously) { + setIsCastingSimultaneouslyNow(false); + } else { + setIsCastingNow(false); + } + if (isPlayer()) { + sendPacket(ActionFailed.STATIC_PACKET); + getAI().setIntention(AI_INTENTION_ACTIVE); + } + return; + } + + final var term = EventDispatcher.getInstance().notifyEvent(new CreatureSkillUse(this, skill, simultaneously, target, targets), this, TerminateReturn.class); + if ((term != null) && term.terminate()) { + if (simultaneously) { + setIsCastingSimultaneouslyNow(false); + } else { + setIsCastingNow(false); + } + if (isPlayer()) { + sendPacket(ActionFailed.STATIC_PACKET); + getAI().setIntention(AI_INTENTION_ACTIVE); + } + return; + } + + // TODO: Unhardcode using event listeners! + if (skill.hasEffectType(L2EffectType.RESURRECTION)) { + if (isResurrectionBlocked() || target.isResurrectionBlocked()) { + sendPacket(SystemMessageId.REJECT_RESURRECTION); // Reject resurrection + target.sendPacket(SystemMessageId.REJECT_RESURRECTION); // Reject resurrection + + if (simultaneously) { + setIsCastingSimultaneouslyNow(false); + } else { + setIsCastingNow(false); + } + + if (isPlayer()) { + getAI().setIntention(AI_INTENTION_ACTIVE); + sendPacket(ActionFailed.STATIC_PACKET); + } + return; + } + } + + // Get the Identifier of the skill + int magicId = skill.getId(); + + // Get the Base Casting Time of the Skills. + double skillAnimTime = Formulas.calcCastTime(this, skill); + + // queue herbs and potions + if (isCastingSimultaneouslyNow() && simultaneously) { + ThreadPoolManager.getInstance().scheduleAi(() -> beginCast(skill, simultaneously, target, targets), 100); + return; + } + + if (simultaneously) { + setIsCastingSimultaneouslyNow(true); + setLastSimultaneousSkillCast(skill); + } else { + setIsCastingNow(true); + _castInterruptTime = (-200 / GameTimeController.MILLIS_IN_TICK) // 200ms converted to ticks + + GameTimeController.getInstance().getGameTicks() + ((int) skillAnimTime / GameTimeController.MILLIS_IN_TICK); + setLastSkillCast(skill); + } + + // Calculate the Reuse Time of the Skill + int reuseDelay; + if (skill.isReuseDelayLocked() || skill.isStatic()) { + reuseDelay = (skill.getReuseDelay()); + } else if (skill.isMagic()) { + reuseDelay = (int) (skill.getReuseDelay() * calcStat(Stats.MAGIC_REUSE_RATE, 1, null, null)); + } else if (skill.isPhysical()) { + reuseDelay = (int) (skill.getReuseDelay() * calcStat(Stats.P_REUSE, 1, null, null)); + } else { + reuseDelay = (int) (skill.getReuseDelay() * calcStat(Stats.DANCE_REUSE, 1, null, null)); + } + + boolean skillMastery = Formulas.calcSkillMastery(this, skill); + + // Skill reuse check + if ((reuseDelay > 30000) && !skillMastery) { + addTimeStamp(skill, reuseDelay); + } + + // Check if this skill consume mp on start casting + int initmpcons = getStat().getMpConsume1(skill); + if (initmpcons > 0) { + getStatus().reduceMp(initmpcons); + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_MP, (int) getCurrentMp()); + sendPacket(su); + } + + // Disable the skill during the re-use delay and create a task EnableSkill with Medium priority to enable it at the end of the re-use delay + if (reuseDelay > 10) { + if (skillMastery) { + reuseDelay = 100; + + if (getActingPlayer() != null) { + getActingPlayer().sendPacket(SystemMessageId.SKILL_READY_TO_USE_AGAIN); + } + } + + disableSkill(skill, reuseDelay); + } + + // Make sure that char is facing selected target + if (target != this) { + setHeading(Util.calculateHeadingFrom(this, target)); + broadcastPacket(new ExRotation(getObjectId(), getHeading())); + } + + if (isPlayable()) { + if (skill.getItemConsumeId() > 0) { + if (!destroyItemByItemId("Consume", skill.getItemConsumeId(), skill.getItemConsumeCount(), null, true)) { + getActingPlayer().sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + abortCast(); + return; + } + } + + // reduce talisman mana on skill use + if ((skill.getReferenceItemId() > 0) && (ItemTable.getInstance().getTemplate(skill.getReferenceItemId()).getBodyPart() == L2Item.SLOT_DECO)) { + for (L2ItemInstance item : getInventory().getItemsByItemId(skill.getReferenceItemId())) { + if (item.isEquipped()) { + if (item.getMana() < item.useSkillDisTime()) { + abortCast(); + return; + } + item.decreaseMana(false, item.useSkillDisTime()); + break; + } + } + } + } + + // Send a Server->Client packet MagicSkillUser with target, displayId, level, skillTime, reuseDelay + // to the L2Character AND to all L2PcInstance in the _KnownPlayers of the L2Character + broadcastPacket(new MagicSkillUse(this, target, skill.getDisplayId(), skill.getDisplayLevel(), (int) skillAnimTime, reuseDelay)); + + // Send a system message to the player. + if (isPlayer() && !skill.isAbnormalInstant()) { + SystemMessage sm = null; + switch (magicId) { + case 1312 -> { + // Fishing + // Done in L2PcInstance.startFishing() + } + case 2046 -> sm = SystemMessage.getSystemMessage(SystemMessageId.SUMMON_A_PET); // Wolf Collar + default -> { + sm = SystemMessage.getSystemMessage(SystemMessageId.USE_S1); + sm.addSkillName(skill); + } + } + sendPacket(sm); + } + + if (skill.hasEffects(EffectScope.START)) { + skill.applyEffectScope(EffectScope.START, new BuffInfo(this, target, skill), true, false); + } + + // Before start AI Cast Broadcast Fly Effect is Need + if (skill.isFlyType()) { + ThreadPoolManager.getInstance().scheduleEffect(new FlyToLocationTask(this, target, FlyType.CHARGE), 50); + } + + MagicUseTask mut = new MagicUseTask(this, targets, skill, (int) skillAnimTime, simultaneously); + + // launch the magic in skillTime milliseconds + if (skillAnimTime > 0) { + // Send a Server->Client packet SetupGauge with the color of the gauge and the casting time + if (isPlayer() && !simultaneously) { + sendPacket(new SetupGauge(SetupGauge.BLUE, (int) skillAnimTime)); + } + + if (skill.isChanneling() && (skill.getChannelingSkillId() > 0)) { + getSkillChannelizer().startChanneling(skill); + } else if (skill.isContinuous()) { + // For client animation reasons (party buffs especially) 300 ms before! + skillAnimTime -= 300; + } + + if (simultaneously) { + Future future = _skillCast2; + if (future != null) { + future.cancel(true); + _skillCast2 = null; + } + _skillCast2 = ThreadPoolManager.getInstance().scheduleEffect(mut, (int) skillAnimTime - 400); + } else { + Future future = _skillCast; + if (future != null) { + future.cancel(true); + _skillCast = null; + } + _skillCast = ThreadPoolManager.getInstance().scheduleEffect(mut, (int) skillAnimTime - 400); + } + } else { + mut.setSkillTime(0); + onMagicLaunchedTimer(mut); + } + } + + /** + * Check if casting of skill is possible + * @param skill + * @return True if casting is possible + */ + public boolean checkDoCastConditions(Skill skill) { + if ((skill == null) || isSkillDisabled(skill) || (skill.isFlyType() && isMovementDisabled())) { + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the caster has enough MP + if (getCurrentMp() < (getStat().getMpConsume1(skill) + getStat().getMpConsume2(skill))) { + // Send a System Message to the caster + sendPacket(SystemMessageId.NOT_ENOUGH_MP); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the caster has enough HP + if (getCurrentHp() <= skill.getHpConsume()) { + // Send a System Message to the caster + sendPacket(SystemMessageId.NOT_ENOUGH_HP); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Skill mute checks. + if (!skill.isStatic()) { + // Check if the skill is a magic spell and if the L2Character is not muted + if (skill.isMagic()) { + if (isMuted()) { + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } else { + // Check if the skill is physical and if the L2Character is not physical_muted + if (isPhysicalMuted()) { + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + } + + // prevent casting signets to peace zone + if (skill.isChanneling() && (skill.getChannelingSkillId() > 0)) { + L2WorldRegion region = getWorldRegion(); + if (region == null) { + return false; + } + boolean canCast = true; + if ((skill.getTargetType() == TargetType.GROUND) && isPlayer()) { + Location wp = getActingPlayer().getCurrentSkillWorldPosition(); + if (!region.checkEffectRangeInsidePeaceZone(skill, wp.getX(), wp.getY(), wp.getZ())) { + canCast = false; + } + } else if (!region.checkEffectRangeInsidePeaceZone(skill, getX(), getY(), getZ())) { + canCast = false; + } + if (!canCast) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + sendPacket(sm); + return false; + } + } + + // Check if the caster's weapon is limited to use only its own skills + if (getActiveWeaponItem() != null) { + L2Weapon wep = getActiveWeaponItem(); + if (wep.useWeaponSkillsOnly() && !isGM() && wep.hasSkills()) { + boolean found = false; + for (SkillHolder sh : wep.getSkills()) { + if (sh.getSkillId() == skill.getId()) { + found = true; + break; + } + } + + if (!found) { + if (getActingPlayer() != null) { + sendPacket(SystemMessageId.WEAPON_CAN_USE_ONLY_WEAPON_SKILL); + } + return false; + } + } + } + + // Check if the spell consumes an Item + // TODO: combine check and consume + if ((skill.getItemConsumeId() > 0) && (getInventory() != null)) { + // Get the L2ItemInstance consumed by the spell + L2ItemInstance requiredItems = getInventory().getItemByItemId(skill.getItemConsumeId()); + + // Check if the caster owns enough consumed Item to cast + if ((requiredItems == null) || (requiredItems.getCount() < skill.getItemConsumeCount())) { + // Checked: when a summon skill failed, server show required consume item count + if (skill.hasEffectType(L2EffectType.SUMMON)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.SUMMONING_SERVITOR_COSTS_S2_S1); + sm.addItemName(skill.getItemConsumeId()); + sm.addInt(skill.getItemConsumeCount()); + sendPacket(sm); + } else { + // Send a System Message to the caster + sendPacket(SystemMessageId.THERE_ARE_NOT_ENOUGH_NECESSARY_ITEMS_TO_USE_THE_SKILL); + } + return false; + } + } + return true; + } + + /** + * Gets the item reuse time stamps map. + * @return the item reuse time stamps map + */ + public final Map getItemReuseTimeStamps() { + return _reuseTimeStampsItems; + } + + /** + * Adds a item reuse time stamp. + * @param item the item + * @param reuse the reuse + */ + public final void addTimeStampItem(L2ItemInstance item, long reuse) { + addTimeStampItem(item, reuse, -1); + } + + /** + * Adds a item reuse time stamp.
      + * Used for restoring purposes. + * @param item the item + * @param reuse the reuse + * @param systime the system time + */ + public final void addTimeStampItem(L2ItemInstance item, long reuse, long systime) { + if (_reuseTimeStampsItems == null) { + synchronized (this) { + if (_reuseTimeStampsItems == null) { + _reuseTimeStampsItems = new ConcurrentHashMap<>(); + } + } + } + _reuseTimeStampsItems.put(item.getObjectId(), new TimeStamp(item, reuse, systime)); + } + + /** + * Gets the item remaining reuse time for a given item object ID. + * @param itemObjId the item object ID + * @return if the item has a reuse time stamp, the remaining time, otherwise -1 + */ + public synchronized final long getItemRemainingReuseTime(int itemObjId) { + final TimeStamp reuseStamp = (_reuseTimeStampsItems != null) ? _reuseTimeStampsItems.get(itemObjId) : null; + return reuseStamp != null ? reuseStamp.getRemaining() : -1; + } + + /** + * Gets the item remaining reuse time for a given shared reuse item group. + * @param group the shared reuse item group + * @return if the shared reuse item group has a reuse time stamp, the remaining time, otherwise -1 + */ + public final long getReuseDelayOnGroup(int group) { + if ((group > 0) && (_reuseTimeStampsItems != null)) { + for (TimeStamp ts : _reuseTimeStampsItems.values()) { + if ((ts.getSharedReuseGroup() == group) && ts.hasNotPassed()) { + return ts.getRemaining(); + } + } + } + return -1; + } + + /** + * Gets the skill reuse time stamps map. + * @return the skill reuse time stamps map + */ + public final Map getSkillReuseTimeStamps() { + return _reuseTimeStampsSkills; + } + + /** + * Adds the skill reuse time stamp. + * @param skill the skill + * @param reuse the delay + */ + public final void addTimeStamp(Skill skill, long reuse) { + addTimeStamp(skill, reuse, -1); + } + + /** + * Adds the skill reuse time stamp.
      + * Used for restoring purposes. + * @param skill the skill + * @param reuse the reuse + * @param systime the system time + */ + public final void addTimeStamp(Skill skill, long reuse, long systime) { + if (_reuseTimeStampsSkills == null) { + synchronized (this) { + if (_reuseTimeStampsSkills == null) { + _reuseTimeStampsSkills = new ConcurrentHashMap<>(); + } + } + } + _reuseTimeStampsSkills.put(skill.getReuseHashCode(), new TimeStamp(skill, reuse, systime)); + } + + /** + * Removes a skill reuse time stamp. + * @param skill the skill to remove + */ + public synchronized final void removeTimeStamp(Skill skill) { + if (_reuseTimeStampsSkills != null) { + _reuseTimeStampsSkills.remove(skill.getReuseHashCode()); + } + } + + /** + * Removes all skill reuse time stamps. + */ + public synchronized final void resetTimeStamps() { + if (_reuseTimeStampsSkills != null) { + _reuseTimeStampsSkills.clear(); + } + } + + /** + * Gets the skill remaining reuse time for a given skill hash code. + * @param hashCode the skill hash code + * @return if the skill has a reuse time stamp, the remaining time, otherwise -1 + */ + public synchronized final long getSkillRemainingReuseTime(int hashCode) { + final TimeStamp reuseStamp = (_reuseTimeStampsSkills != null) ? _reuseTimeStampsSkills.get(hashCode) : null; + return reuseStamp != null ? reuseStamp.getRemaining() : -1; + } + + /** + * Verifies if the skill is under reuse time. + * @param hashCode the skill hash code + * @return {@code true} if the skill is under reuse time, {@code false} otherwise + */ + public synchronized final boolean hasSkillReuse(int hashCode) { + final TimeStamp reuseStamp = (_reuseTimeStampsSkills != null) ? _reuseTimeStampsSkills.get(hashCode) : null; + return (reuseStamp != null) && reuseStamp.hasNotPassed(); + } + + /** + * Gets the skill reuse time stamp. + * @param hashCode the skill hash code + * @return if the skill has a reuse time stamp, the skill reuse time stamp, otherwise {@code null} + */ + public synchronized final TimeStamp getSkillReuseTimeStamp(int hashCode) { + return _reuseTimeStampsSkills != null ? _reuseTimeStampsSkills.get(hashCode) : null; + } + + /** + * Gets the disabled skills map. + * @return the disabled skills map + */ + public Map getDisabledSkills() { + return _disabledSkills; + } + + /** + * Enables a skill. + * @param skill the skill to enable + */ + public void enableSkill(Skill skill) { + if ((skill == null) || (_disabledSkills == null)) { + return; + } + _disabledSkills.remove(skill.getReuseHashCode()); + } + + /** + * Disables a skill for a given time.
      + * If delay is lesser or equal than zero, skill will be disabled "forever". + * @param skill the skill to disable + * @param delay delay in milliseconds + */ + public void disableSkill(Skill skill, long delay) { + if (skill == null) { + return; + } + + if (_disabledSkills == null) { + synchronized (this) { + if (_disabledSkills == null) { + _disabledSkills = new ConcurrentHashMap<>(); + } + } + } + + _disabledSkills.put(skill.getReuseHashCode(), delay > 0 ? System.currentTimeMillis() + delay : Long.MAX_VALUE); + } + + /** + * Removes all the disabled skills. + */ + public synchronized final void resetDisabledSkills() { + if (_disabledSkills != null) { + _disabledSkills.clear(); + } + } + + /** + * Verifies if the skill is disabled. + * @param skill the skill + * @return {@code true} if the skill is disabled, {@code false} otherwise + */ + public boolean isSkillDisabled(Skill skill) { + return (skill != null) && isSkillDisabled(skill.getReuseHashCode()); + } + + /** + * Verifies if the skill is disabled. + * @param hashCode the skill hash code + * @return {@code true} if the skill is disabled, {@code false} otherwise + */ + public boolean isSkillDisabled(int hashCode) { + if (isAllSkillsDisabled()) { + return true; + } + + if (_disabledSkills == null) { + return false; + } + + final Long stamp = _disabledSkills.get(hashCode); + if (stamp == null) { + return false; + } + + if (stamp < System.currentTimeMillis()) { + _disabledSkills.remove(hashCode); + return false; + } + return true; + } + + /** + * Disables all skills. + */ + public void disableAllSkills() { + _allSkillsDisabled = true; + } + + /** + * Enables all skills, except those under reuse time or previously disabled. + */ + public void enableAllSkills() { + _allSkillsDisabled = false; + } + + /** + * Kill the L2Character.
      + * Actions: + *
        + *
      • Set target to null and cancel Attack or Cast
      • + *
      • Stop movement
      • + *
      • Stop HP/MP/CP Regeneration task
      • + *
      • Stop all active skills effects in progress on the L2Character
      • + *
      • Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform
      • + *
      • Notify L2Character AI
      • + *
      + * @param killer The L2Character who killed it + * @return false if the player is already dead. + */ + public boolean doDie(L2Character killer) { + final TerminateReturn returnBack = EventDispatcher.getInstance().notifyEvent(new CreatureKill(killer, this), this, TerminateReturn.class); + if ((returnBack != null) && returnBack.terminate()) { + return false; + } + + // killing is only possible one time + synchronized (this) { + if (isDead()) { + return false; + } + + // now reset currentHp to zero + setCurrentHp(0); + setIsDead(true); + } + + // Set target to null and cancel Attack or Cast + setTarget(null); + + // Stop movement + stopMove(null); + + // Stop HP/MP/CP Regeneration task + getStatus().stopHpMpRegeneration(); + + stopAllEffectsExceptThoseThatLastThroughDeath(); + + calculateRewards(killer); + + // Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + broadcastStatusUpdate(); + + // Notify L2Character AI + if (hasAI()) { + getAI().notifyEvent(CtrlEvent.EVT_DEAD); + } + + if (getWorldRegion() != null) { + getWorldRegion().onDeath(this); + } + + getAttackByList().clear(); + + if (isChannelized()) { + getSkillChannelized().abortChannelization(); + } + return true; + } + + @Override + public boolean deleteMe() { + setDebug(null); + + if (hasAI()) { + getAI().stopAITask(); + } + return true; + } + + public void detachAI() { + if (isWalker()) { + return; + } + setAI(null); + } + + protected void calculateRewards(L2Character killer) { + } + + /** Sets HP, MP and CP and revives the L2Character. */ + public void doRevive() { + if (!isDead()) { + return; + } + if (!isTeleporting()) { + setIsPendingRevive(false); + setIsDead(false); + + if ((character().getRespawnRestoreCP() > 0) && (getCurrentCp() < (getMaxCp() * character().getRespawnRestoreCP()))) { + _status.setCurrentCp(getMaxCp() * character().getRespawnRestoreCP()); + } + if ((character().getRespawnRestoreHP() > 0) && (getCurrentHp() < (getMaxHp() * character().getRespawnRestoreHP()))) { + _status.setCurrentHp(getMaxHp() * character().getRespawnRestoreHP()); + } + if ((character().getRespawnRestoreMP() > 0) && (getCurrentMp() < (getMaxMp() * character().getRespawnRestoreMP()))) { + _status.setCurrentMp(getMaxMp() * character().getRespawnRestoreMP()); + } + + // Start broadcast status + broadcastPacket(new Revive(this)); + if (getWorldRegion() != null) { + getWorldRegion().onRevive(this); + } + } else { + setIsPendingRevive(true); + } + } + + /** + * Revives the L2Character using skill. + * @param revivePower + */ + public void doRevive(double revivePower) { + doRevive(); + } + + /** + * Gets this creature's AI. + * @return the AI + */ + public final L2CharacterAI getAI() { + if (_ai == null) { + synchronized (this) { + if (_ai == null) { + // Return the new AI within the synchronized block + // to avoid being nulled by other threads + return _ai = initAI(); + } + } + } + return _ai; + } + + /** + * Initialize this creature's AI.
      + * OOP approach to be overridden in child classes. + * @return the new AI + */ + protected L2CharacterAI initAI() { + return new L2CharacterAI(this); + } + + public void setAI(L2CharacterAI newAI) { + final L2CharacterAI oldAI = _ai; + if ((oldAI != newAI) && (oldAI instanceof L2AttackableAI)) { + oldAI.stopAITask(); + } + _ai = newAI; + } + + /** + * Verifies if this creature has an AI, + * @return {@code true} if this creature has an AI, {@code false} otherwise + */ + public boolean hasAI() { + return _ai != null; + } + + /** + * @return True if the L2Character is RaidBoss or his minion. + */ + public boolean isRaid() { + return false; + } + + /** + * @return True if the L2Character is minion. + */ + public boolean isMinion() { + return false; + } + + /** + * @return True if the L2Character is minion of RaidBoss. + */ + public boolean isRaidMinion() { + return false; + } + + /** + * @return a list of L2Character that attacked. + */ + public final Set getAttackByList() { + if (_attackByList == null) { + synchronized (this) { + if (_attackByList == null) { + _attackByList = ConcurrentHashMap.newKeySet(); + } + } + } + return _attackByList; + } + + public final Skill getLastSimultaneousSkillCast() { + return _lastSimultaneousSkillCast; + } + + public void setLastSimultaneousSkillCast(Skill skill) { + _lastSimultaneousSkillCast = skill; + } + + public final Skill getLastSkillCast() { + return _lastSkillCast; + } + + public void setLastSkillCast(Skill skill) { + _lastSkillCast = skill; + } + + public final boolean isNoRndWalk() { + return _isNoRndWalk; + } + + public final void setIsNoRndWalk(boolean value) { + _isNoRndWalk = value; + } + + public final boolean isAfraid() { + return isAffected(EffectFlag.FEAR); + } + + /** + * @return True if the L2Character can't use its skills (ex : stun, sleep...). + */ + public final boolean isAllSkillsDisabled() { + return _allSkillsDisabled || isStunned() || isSleeping(); + } + + /** + * @return True if the L2Character can't attack (stun, sleep, attackEndTime, fakeDeath, attackMute). + */ + public boolean isAttackingDisabled() { + return isFlying() || isStunned() || isSleeping() || isAttackingNow() || isAlikeDead() || isPhysicalAttackMuted() || isCoreAIDisabled(); + } + + public final Calculator[] getCalculators() { + return _calculators; + } + + public final boolean isConfused() { + return isAffected(EffectFlag.CONFUSED); + } + + /** + * @return True if the L2Character is dead or use fake death. + */ + public boolean isAlikeDead() { + return _isDead; + } + + /** + * @return True if the L2Character is dead. + */ + public final boolean isDead() { + return _isDead; + } + + public final void setIsDead(boolean value) { + _isDead = value; + } + + public boolean isImmobilized() { + return _isImmobilized; + } + + public void setIsImmobilized(boolean value) { + _isImmobilized = value; + } + + public final boolean isMuted() { + return isAffected(EffectFlag.MUTED); + } + + public final boolean isPhysicalMuted() { + return isAffected(EffectFlag.PSYCHICAL_MUTED); + } + + public final boolean isPhysicalAttackMuted() { + return isAffected(EffectFlag.PSYCHICAL_ATTACK_MUTED); + } + + /** + * Verifies if the creature cannot move. + * @return {@code true} if the creature cannot move, {@code false} otherwise + */ + public boolean isMovementDisabled() { + return isStunned() || isRooted() || isSleeping() || isOverloaded() || isImmobilized() || isAlikeDead() || isTeleporting(); + } + + /** + * @return True if the L2Character can not be controlled by the player (confused, afraid). + */ + public final boolean isOutOfControl() { + return isConfused() || isAfraid(); + } + + public final boolean isOverloaded() { + return _isOverloaded; + } + + /** + * Set the overloaded status of the L2Character is overloaded (if True, the L2PcInstance can't take more item). + * @param value + */ + public final void setIsOverloaded(boolean value) { + _isOverloaded = value; + } + + public final boolean isPendingRevive() { + return isDead() && _isPendingRevive; + } + + public final void setIsPendingRevive(boolean value) { + _isPendingRevive = value; + } + + public final boolean isDisarmed() { + return isAffected(EffectFlag.DISARMED); + } + + /** + * @return the summon + */ + public L2Summon getSummon() { + return null; + } + + /** + * @return {@code true} if the character has a summon, {@code false} otherwise + */ + public final boolean hasSummon() { + return getSummon() != null; + } + + /** + * @return {@code true} if the character has a pet, {@code false} otherwise + */ + public final boolean hasPet() { + return hasSummon() && getSummon().isPet(); + } + + /** + * @return {@code true} if the character has a servitor, {@code false} otherwise + */ + public final boolean hasServitor() { + return hasSummon() && getSummon().isServitor(); + } + + public final boolean isRooted() { + return isAffected(EffectFlag.ROOTED); + } + + /** + * @return True if the L2Character is running. + */ + public boolean isRunning() { + return _isRunning; + } + + public final void setIsRunning(boolean value) { + if (_isRunning == value) { + return; + } + + _isRunning = value; + if (getRunSpeed() != 0) { + broadcastPacket(new ChangeMoveType(this)); + } + if (isPlayer()) { + getActingPlayer().broadcastUserInfo(); + } else if (isSummon()) { + broadcastStatusUpdate(); + } else if (isNpc()) { + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if ((player == null) || !isVisibleFor(player)) { + continue; + } else if (getRunSpeed() == 0) { + player.sendPacket(new ServerObjectInfo((L2Npc) this, player)); + } else { + player.sendPacket(new AbstractNpcInfo.NpcInfo((L2Npc) this, player)); + } + } + } + } + + /** Set the L2Character movement type to run and send Server->Client packet ChangeMoveType to all others L2PcInstance. */ + public final void setRunning() { + if (!isRunning()) { + setIsRunning(true); + } + } + + public final boolean isSleeping() { + return isAffected(EffectFlag.SLEEP); + } + + public final boolean isStunned() { + return isAffected(EffectFlag.STUNNED); + } + + public final boolean isBetrayed() { + return isAffected(EffectFlag.BETRAYED); + } + + public final boolean isTeleporting() { + return _isTeleporting; + } + + public void setIsTeleporting(boolean value) { + _isTeleporting = value; + } + + public void setIsInvul(boolean b) { + _isInvul = b; + } + + public boolean isInvul() { + return _isInvul || _isTeleporting; + } + + public boolean isHpBlocked() { + return isAffected(EffectFlag.BLOCK_HP); + } + + public boolean isMpBlocked() { + return isAffected(EffectFlag.BLOCK_MP); + } + + public boolean isBuffBlocked() { + return isAffected(EffectFlag.BLOCK_BUFF); + } + + public boolean isDebuffBlocked() { + return isAffected(EffectFlag.BLOCK_DEBUFF); + } + + public void setIsMortal(boolean b) { + _isMortal = b; + } + + public boolean isMortal() { + return _isMortal; + } + + public boolean isUndead() { + return false; + } + + public boolean isResurrectionBlocked() { + return isAffected(EffectFlag.BLOCK_RESURRECTION); + } + + public final boolean isFlying() { + return _isFlying; + } + + public final void setIsFlying(boolean mode) { + _isFlying = mode; + } + + @Override + public CharKnownList getKnownList() { + return ((CharKnownList) super.getKnownList()); + } + + @Override + public void initKnownList() { + setKnownList(new CharKnownList(this)); + } + + public CharStat getStat() { + return _stat; + } + + /** + * Initializes the CharStat class of the L2Object, is overwritten in classes that require a different CharStat Type.
      + * Removes the need for instanceof checks. + */ + public void initCharStat() { + _stat = new CharStat(this); + } + + public final void setStat(CharStat value) { + _stat = value; + } + + public CharStatus getStatus() { + return _status; + } + + /** + * Initializes the CharStatus class of the L2Object, is overwritten in classes that require a different CharStatus Type.
      + * Removes the need for instanceof checks. + */ + public void initCharStatus() { + _status = new CharStatus(this); + } + + public final void setStatus(CharStatus value) { + _status = value; + } + + public L2CharTemplate getTemplate() { + return _template; + } + + /** + * Set the template of the L2Character.
      + * Concept:
      + * Each L2Character owns generic and static properties (ex : all Keltir have the same number of HP...).
      + * All of those properties are stored in a different template for each type of L2Character.
      + * Each template is loaded once in the server cache memory (reduce memory use).
      + * When a new instance of L2Character is spawned, server just create a link between the instance and the template This link is stored in _template. + * @param template + */ + protected final void setTemplate(L2CharTemplate template) { + _template = template; + } + + /** + * @return the Title of the L2Character. + */ + public final String getTitle() { + return _title; + } + + /** + * Set the Title of the L2Character. + * @param value + */ + public final void setTitle(String value) { + if (value == null) { + _title = ""; + } else { + _title = value.length() > 21 ? value.substring(0, 20) : value; + } + } + + /** + * Set the L2Character movement type to walk and send Server->Client packet ChangeMoveType to all others L2PcInstance. + */ + public final void setWalking() { + if (isRunning()) { + setIsRunning(false); + } + } + + /** + * Gets the abnormal visual effects affecting this character. + * @return a map of 32 bits containing all abnormal visual effects in progress for this character + */ + public int getAbnormalVisualEffects() { + return _abnormalVisualEffects; + } + + /** + * Gets the special abnormal visual effects affecting this character. + * @return a map of 32 bits containing all special effect in progress for this character + */ + public int getAbnormalVisualEffectSpecial() { + return _abnormalVisualEffectsSpecial; + } + + /** + * Gets the event abnormal visual effects affecting this character. + * @return a map of 32 bits containing all event abnormal visual effects in progress for this character + */ + public int getAbnormalVisualEffectEvent() { + return _abnormalVisualEffectsEvent; + } + + /** + * Verify if this creature is affected by the given abnormal visual effect. + * @param ave the abnormal visual effect + * @return {@code true} if the creature is affected by the abnormal visual effect, {@code false} otherwise + */ + public boolean hasAbnormalVisualEffect(AbnormalVisualEffect ave) { + if (ave.isEvent()) { + return (getAbnormalVisualEffectEvent() & ave.getMask()) == ave.getMask(); + } + + if (ave.isSpecial()) { + return (getAbnormalVisualEffectSpecial() & ave.getMask()) == ave.getMask(); + } + + return (getAbnormalVisualEffects() & ave.getMask()) == ave.getMask(); + } + + /** + * Adds the abnormal visual effect flags in the binary mask and send Server->Client UserInfo/CharInfo packet. + * @param update if {@code true} update packets will be sent + * @param aves the abnormal visual effects + */ + public final void startAbnormalVisualEffect(boolean update, AbnormalVisualEffect... aves) { + for (AbnormalVisualEffect ave : aves) { + if (ave.isEvent()) { + _abnormalVisualEffectsEvent |= ave.getMask(); + } else if (ave.isSpecial()) { + _abnormalVisualEffectsSpecial |= ave.getMask(); + } else { + _abnormalVisualEffects |= ave.getMask(); + } + } + if (update) { + updateAbnormalEffect(); + } + } + + /** + * Removes the abnormal visual effect flags from the binary mask and send Server->Client UserInfo/CharInfo packet. + * @param update if {@code true} update packets will be sent + * @param aves the abnormal visual effects + */ + public final void stopAbnormalVisualEffect(boolean update, AbnormalVisualEffect... aves) { + for (AbnormalVisualEffect ave : aves) { + if (ave.isEvent()) { + _abnormalVisualEffectsEvent &= ~ave.getMask(); + } else if (ave.isSpecial()) { + _abnormalVisualEffectsSpecial &= ~ave.getMask(); + } else { + _abnormalVisualEffects &= ~ave.getMask(); + } + } + if (update) { + updateAbnormalEffect(); + } + } + + /** + * Active the abnormal effect Fake Death flag, notify the L2Character AI and send Server->Client UserInfo/CharInfo packet. + */ + public final void startFakeDeath() { + if (!isPlayer()) { + return; + } + + getActingPlayer().setIsFakeDeath(true); + // Aborts any attacks/casts if fake dead + abortAttack(); + abortCast(); + stopMove(null); + getAI().notifyEvent(CtrlEvent.EVT_FAKE_DEATH); + broadcastPacket(new ChangeWaitType(this, ChangeWaitType.WT_START_FAKE_DEATH)); + } + + /** + * Launch a Stun Abnormal Effect on the L2Character.
      + * Actions: + *
        + *
      • Calculate the success rate of the Stun Abnormal Effect on this L2Character
      • + *
      • If Stun succeed, active the abnormal effect Stun flag, notify the L2Character AI and send Server->Client UserInfo/CharInfo packet
      • + *
      • If Stun NOT succeed, send a system message Failed to the L2PcInstance attacker
      • + *
      + */ + public final void startStunning() { + // Aborts any attacks/casts if stuned + abortAttack(); + abortCast(); + stopMove(null); + getAI().notifyEvent(CtrlEvent.EVT_STUNNED); + if (!isSummon()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + } + updateAbnormalEffect(); + } + + /** + * Stop all active skills effects in progress on the L2Character. + */ + public void stopAllEffects() { + _effectList.stopAllEffects(); + } + + /** + * Stops all effects, except those that last through death. + */ + public void stopAllEffectsExceptThoseThatLastThroughDeath() { + _effectList.stopAllEffectsExceptThoseThatLastThroughDeath(); + } + + /** + * Stop and remove the effects corresponding to the skill ID. + * @param removed if {@code true} the effect will be set as removed, and a system message will be sent + * @param skillId the skill Id + */ + public void stopSkillEffects(boolean removed, int skillId) { + _effectList.stopSkillEffects(removed, skillId); + } + + public final void stopEffects(L2EffectType type) { + _effectList.stopEffects(type); + } + + /** + * Exits all buffs effects of the skills with "removedOnAnyAction" set.
      + * Called on any action except movement (attack, cast). + */ + public final void stopEffectsOnAction() { + _effectList.stopEffectsOnAction(); + } + + /** + * Exits all buffs effects of the skills with "removedOnDamage" set.
      + * Called on decreasing HP and mana burn. + * @param awake + */ + public final void stopEffectsOnDamage(boolean awake) { + _effectList.stopEffectsOnDamage(awake); + } + + /** + * Stop a specified/all Fake Death abnormal L2Effect.
      + * Actions: + *
        + *
      • Delete a specified/all (if effect=null) Fake Death abnormal L2Effect from L2Character and update client magic icon
      • + *
      • Set the abnormal effect flag _fake_death to False
      • + *
      • Notify the L2Character AI
      • + *
      + * @param removeEffects + */ + public final void stopFakeDeath(boolean removeEffects) { + if (removeEffects) { + stopEffects(L2EffectType.FAKE_DEATH); + } + + // if this is a player instance, start the grace period for this character (grace from mobs only)! + if (isPlayer()) { + getActingPlayer().setIsFakeDeath(false); + getActingPlayer().setRecentFakeDeath(true); + } + + broadcastPacket(new ChangeWaitType(this, ChangeWaitType.WT_STOP_FAKE_DEATH)); + // TODO: Temp hack: players see FD on ppl that are moving: Teleport to someone who uses FD - if he gets up he will fall down again for that client - + // even tho he is actually standing... Probably bad info in CharInfo packet? + broadcastPacket(new Revive(this)); + } + + /** + * Stop a specified/all Stun abnormal L2Effect.
      + * Actions: + *
        + *
      • Delete a specified/all (if effect=null) Stun abnormal L2Effect from L2Character and update client magic icon
      • + *
      • Set the abnormal effect flag _stuned to False
      • + *
      • Notify the L2Character AI
      • + *
      • Send Server->Client UserInfo/CharInfo packet
      • + *
      + * @param removeEffects + */ + public final void stopStunning(boolean removeEffects) { + if (removeEffects) { + stopEffects(L2EffectType.STUN); + } + + if (!isPlayer()) { + getAI().notifyEvent(CtrlEvent.EVT_THINK); + } + updateAbnormalEffect(); + } + + /** + * Stop L2Effect: Transformation.
      + * Actions: + *
        + *
      • Remove Transformation Effect
      • + *
      • Notify the L2Character AI
      • + *
      • Send Server->Client UserInfo/CharInfo packet
      • + *
      + * @param removeEffects + */ + public final void stopTransformation(boolean removeEffects) { + if (removeEffects) { + getEffectList().stopSkillEffects(false, AbnormalType.TRANSFORM); + } + + // if this is a player instance, then untransform, also set the transform_id column equal to 0 if not cursed. + if (isPlayer()) { + if (getActingPlayer().getTransformation() != null) { + getActingPlayer().untransform(); + } + } + + if (!isPlayer()) { + getAI().notifyEvent(CtrlEvent.EVT_THINK); + } + updateAbnormalEffect(); + } + + public abstract void updateAbnormalEffect(); + + /** + * Update active skills in progress (In Use and Not In Use because stacked) icons on client.
      + * Concept:
      + * All active skills effects in progress (In Use and Not In Use because stacked) are represented by an icon on the client.
      + * Caution : This method ONLY UPDATE the client of the player and not clients of all players in the party. + */ + public final void updateEffectIcons() { + updateEffectIcons(false); + } + + /** + * Updates Effect Icons for this character(player/summon) and his party if any. + * @param partyOnly + */ + public void updateEffectIcons(boolean partyOnly) { + // overridden + } + + public boolean isAffectedBySkill(int skillId) { + return _effectList.isAffectedBySkill(skillId); + } + + /** + * This class group all movement data.
      + * Data : + *
        + *
      • _moveTimestamp : Last time position update
      • + *
      • _xDestination, _yDestination, _zDestination : Position of the destination
      • + *
      • _xMoveFrom, _yMoveFrom, _zMoveFrom : Position of the origin
      • + *
      • _moveStartTime : Start time of the movement
      • + *
      • _ticksToMove : Nb of ticks between the start and the destination
      • + *
      • _xSpeedTicks, _ySpeedTicks : Speed in unit/ticks
      • + *
      + */ + public static class MoveData { + // when we retrieve x/y/z we use GameTimeControl.getGameTicks() + // if we are moving, but move timestamp==gameticks, we don't need + // to recalculate position + public int _moveStartTime; + public int _moveTimestamp; // last update + public int _xDestination; + public int _yDestination; + public int _zDestination; + public double _xAccurate; // otherwise there would be rounding errors + public double _yAccurate; + public double _zAccurate; + public int _heading; + + public boolean disregardingGeodata; + public int onGeodataPathIndex; + public List geoPath; + public int geoPathAccurateTx; + public int geoPathAccurateTy; + public int geoPathGtx; + public int geoPathGty; + } + + /** + * Add a Func to the Calculator set of the L2Character.
      + * Concept: A L2Character owns a table of Calculators called _calculators.
      + * Each Calculator (a calculator per state) own a table of Func object.
      + * A Func object is a mathematical function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...).
      + * To reduce cache memory use, L2NPCInstances who don't have skills share the same Calculator set called NPC_STD_CALCULATOR.
      + * That's why, if a L2NPCInstance is under a skill/spell effect that modify one of its state, a copy of the NPC_STD_CALCULATOR must be create in its _calculators before adding new Func object.
      + * Actions: + *
        + *
      • If _calculators is linked to NPC_STD_CALCULATOR, create a copy of NPC_STD_CALCULATOR in _calculators
      • + *
      • Add the Func object to _calculators
      • + *
      + * @param function The Func object to add to the Calculator corresponding to the state affected + */ + private final void addStatFunc(AbstractFunction function) { + if (function == null) { + return; + } + + synchronized (this) { + // Check if Calculator set is linked to the standard Calculator set of NPC + if (_calculators == NPC_STD_CALCULATOR) { + // Create a copy of the standard NPC Calculator set + _calculators = new Calculator[Stats.NUM_STATS]; + + for (int i = 0; i < Stats.NUM_STATS; i++) { + if (NPC_STD_CALCULATOR[i] != null) { + _calculators[i] = new Calculator(NPC_STD_CALCULATOR[i]); + } + } + } + + // Select the Calculator of the affected state in the Calculator set + int stat = function.getStat().ordinal(); + + if (_calculators[stat] == null) { + _calculators[stat] = new Calculator(); + } + + // Add the Func to the calculator corresponding to the state + _calculators[stat].addFunc(function); + } + } + + /** + * Add a list of Funcs to the Calculator set of the L2Character.
      + * Concept:
      + * A L2Character owns a table of Calculators called _calculators.
      + * Each Calculator (a calculator per state) own a table of Func object.
      + * A Func object is a mathematic function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...).
      + * Caution : This method is ONLY for L2PcInstance
      + * Example of use: + *
        + *
      • Equip an item from inventory
      • + *
      • Learn a new passive skill
      • + *
      • Use an active skill
      • + *
      + * @param functions The list of Func objects to add to the Calculator corresponding to the state affected + */ + public final void addStatFuncs(List functions) { + if (!isPlayer() && getKnownList().getKnownPlayers().isEmpty()) { + for (AbstractFunction f : functions) { + addStatFunc(f); + } + } else { + final List modifiedStats = new ArrayList<>(); + for (AbstractFunction f : functions) { + modifiedStats.add(f.getStat()); + addStatFunc(f); + } + broadcastModifiedStats(modifiedStats); + } + } + + public final void addStatFuncs(AbstractFunction function) { + addStatFuncs(List.of(function)); + } + + /** + * Remove a Func from the Calculator set of the L2Character.
      + * Concept:
      + * A L2Character owns a table of Calculators called _calculators.
      + * Each Calculator (a calculator per state) own a table of Func object.
      + * A Func object is a mathematic function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...).
      + * To reduce cache memory use, L2NPCInstances who don't have skills share the same Calculator set called NPC_STD_CALCULATOR.
      + * That's why, if a L2NPCInstance is under a skill/spell effect that modify one of its state, a copy of the NPC_STD_CALCULATOR must be create in its _calculators before addind new Func object.
      + * Actions: + *
        + *
      • Remove the Func object from _calculators
      • + *
      • If L2Character is a L2NPCInstance and _calculators is equal to NPC_STD_CALCULATOR, free cache memory and just create a link on NPC_STD_CALCULATOR in _calculators
      • + *
      + * @param function The Func object to remove from the Calculator corresponding to the state affected + */ + private final void removeStatFunc(AbstractFunction function) { + if (function == null) { + return; + } + + // Select the Calculator of the affected state in the Calculator set + int stat = function.getStat().ordinal(); + + synchronized (this) { + if (_calculators[stat] == null) { + return; + } + + // Remove the Func object from the Calculator + _calculators[stat].removeFunc(function); + + if (_calculators[stat].size() == 0) { + _calculators[stat] = null; + } + + // If possible, free the memory and just create a link on NPC_STD_CALCULATOR + if (isNpc()) { + int i = 0; + for (; i < Stats.NUM_STATS; i++) { + if (!Calculator.equalsCals(_calculators[i], NPC_STD_CALCULATOR[i])) { + break; + } + } + + if (i >= Stats.NUM_STATS) { + _calculators = NPC_STD_CALCULATOR; + } + } + } + } + + /** + * Remove a list of Funcs from the Calculator set of the L2PcInstance.
      + * Concept:
      + * A L2Character owns a table of Calculators called _calculators.
      + * Each Calculator (a calculator per state) own a table of Func object.
      + * A Func object is a mathematic function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...).
      + * Caution : This method is ONLY for L2PcInstance
      + * Example of use: + *
        + *
      • Unequip an item from inventory
      • + *
      • Stop an active skill
      • + *
      + * @param functions The list of Func objects to add to the Calculator corresponding to the state affected + */ + public final void removeStatFuncs(AbstractFunction[] functions) { + if (!isPlayer() && getKnownList().getKnownPlayers().isEmpty()) { + for (AbstractFunction f : functions) { + removeStatFunc(f); + } + } else { + final List modifiedStats = new ArrayList<>(); + for (AbstractFunction f : functions) { + modifiedStats.add(f.getStat()); + removeStatFunc(f); + } + + broadcastModifiedStats(modifiedStats); + } + } + + /** + * Remove all Func objects with the selected owner from the Calculator set of the L2Character.
      + * Concept:
      + * A L2Character owns a table of Calculators called _calculators.
      + * Each Calculator (a calculator per state) own a table of Func object.
      + * A Func object is a mathematic function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...).
      + * To reduce cache memory use, L2NPCInstances who don't have skills share the same Calculator set called NPC_STD_CALCULATOR.
      + * That's why, if a L2NPCInstance is under a skill/spell effect that modify one of its state, a copy of the NPC_STD_CALCULATOR must be create in its _calculators before addind new Func object.
      + * Actions: + *
        + *
      • Remove all Func objects of the selected owner from _calculators
      • + *
      • If L2Character is a L2NPCInstance and _calculators is equal to NPC_STD_CALCULATOR, free cache memory and just create a link on NPC_STD_CALCULATOR in _calculators
      • + *
      + * Example of use: + *
        + *
      • Unequip an item from inventory
      • + *
      • Stop an active skill
      • + *
      + * @param owner The Object(Skill, Item...) that has created the effect + */ + public final void removeStatsOwner(Object owner) { + List modifiedStats = null; + int i = 0; + // Go through the Calculator set + synchronized (this) { + for (Calculator calc : _calculators) { + if (calc != null) { + // Delete all Func objects of the selected owner + if (modifiedStats != null) { + modifiedStats.addAll(calc.removeOwner(owner)); + } else { + modifiedStats = calc.removeOwner(owner); + } + + if (calc.size() == 0) { + _calculators[i] = null; + } + } + i++; + } + + // If possible, free the memory and just create a link on NPC_STD_CALCULATOR + if (isNpc()) { + i = 0; + for (; i < Stats.NUM_STATS; i++) { + if (!Calculator.equalsCals(_calculators[i], NPC_STD_CALCULATOR[i])) { + break; + } + } + + if (i >= Stats.NUM_STATS) { + _calculators = NPC_STD_CALCULATOR; + } + } + + broadcastModifiedStats(modifiedStats); + } + } + + protected void broadcastModifiedStats(List stats) { + if ((stats == null) || stats.isEmpty()) { + return; + } + + if (isSummon()) { + L2Summon summon = (L2Summon) this; + if (summon.getOwner() != null) { + summon.updateAndBroadcastStatus(1); + } + } else { + boolean broadcastFull = false; + StatusUpdate su = new StatusUpdate(this); + + for (Stats stat : stats) { + if (stat == Stats.POWER_ATTACK_SPEED) { + su.addAttribute(StatusUpdate.ATK_SPD, (int) getPAtkSpd()); + } else if (stat == Stats.MAGIC_ATTACK_SPEED) { + su.addAttribute(StatusUpdate.CAST_SPD, getMAtkSpd()); + } else if (stat == Stats.MOVE_SPEED) { + broadcastFull = true; + } + } + + if (isPlayer()) { + if (broadcastFull) { + getActingPlayer().updateAndBroadcastStatus(2); + } else { + getActingPlayer().updateAndBroadcastStatus(1); + if (su.hasAttributes()) { + broadcastPacket(su); + } + } + if ((getSummon() != null) && isAffected(EffectFlag.SERVITOR_SHARE)) { + getSummon().broadcastStatusUpdate(); + } + } else if (isNpc()) { + if (broadcastFull) { + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if ((player == null) || !isVisibleFor(player)) { + continue; + } + if (getRunSpeed() == 0) { + player.sendPacket(new ServerObjectInfo((L2Npc) this, player)); + } else { + player.sendPacket(new AbstractNpcInfo.NpcInfo((L2Npc) this, player)); + } + } + } else if (su.hasAttributes()) { + broadcastPacket(su); + } + } else if (su.hasAttributes()) { + broadcastPacket(su); + } + } + } + + public final int getXdestination() { + MoveData m = _move; + + if (m != null) { + return m._xDestination; + } + + return getX(); + } + + /** + * @return the Y destination of the L2Character or the Y position if not in movement. + */ + public final int getYdestination() { + MoveData m = _move; + + if (m != null) { + return m._yDestination; + } + + return getY(); + } + + /** + * @return the Z destination of the L2Character or the Z position if not in movement. + */ + public final int getZdestination() { + MoveData m = _move; + + if (m != null) { + return m._zDestination; + } + + return getZ(); + } + + /** + * @return True if the L2Character is in combat. + */ + public boolean isInCombat() { + return hasAI() && ((getAI().getAttackTarget() != null) || getAI().isAutoAttacking()); + } + + /** + * @return True if the L2Character is moving. + */ + public final boolean isMoving() { + return _move != null; + } + + /** + * @return True if the L2Character is travelling a calculated path. + */ + public final boolean isOnGeodataPath() { + MoveData m = _move; + if (m == null) { + return false; + } + if (m.onGeodataPathIndex == -1) { + return false; + } + return m.onGeodataPathIndex != (m.geoPath.size() - 1); + } + + public final boolean isCastingNow() { + return _isCastingNow; + } + + public void setIsCastingNow(boolean value) { + _isCastingNow = value; + } + + public final boolean isCastingSimultaneouslyNow() { + return _isCastingSimultaneouslyNow; + } + + public void setIsCastingSimultaneouslyNow(boolean value) { + _isCastingSimultaneouslyNow = value; + } + + public final boolean canAbortCast() { + return _castInterruptTime > GameTimeController.getInstance().getGameTicks(); + } + + public int getCastInterruptTime() { + return _castInterruptTime; + } + + /** + * Verifies if the creature is attacking now. + * @return {@code true} if the creature is attacking now, {@code false} otherwise + */ + public final boolean isAttackingNow() { + return _attackEndTime > System.nanoTime(); + } + + /** + * Abort the attack of the L2Character and send Server->Client ActionFailed packet. + */ + public final void abortAttack() { + if (isAttackingNow()) { + sendPacket(ActionFailed.STATIC_PACKET); + } + } + + /** + * Abort the cast of the L2Character and send Server->Client MagicSkillCanceld/ActionFailed packet. + */ + public final void abortCast() { + if (isCastingNow() || isCastingSimultaneouslyNow()) { + Future future = _skillCast; + // cancels the skill hit scheduled task + if (future != null) { + future.cancel(true); + _skillCast = null; + } + future = _skillCast2; + if (future != null) { + future.cancel(true); + _skillCast2 = null; + } + + // TODO: Handle removing spawned npc. + + if (isChanneling()) { + getSkillChannelizer().stopChanneling(); + } + + if (_allSkillsDisabled) { + enableAllSkills(); // this remains for forced skill use, e.g. scroll of escape + } + setIsCastingNow(false); + setIsCastingSimultaneouslyNow(false); + // safeguard for cannot be interrupt any more + _castInterruptTime = 0; + if (isPlayer()) { + getAI().notifyEvent(CtrlEvent.EVT_FINISH_CASTING); // setting back previous intention + } + broadcastPacket(new MagicSkillCanceled(getObjectId())); // broadcast packet to stop animations client-side + sendPacket(ActionFailed.STATIC_PACKET); // send an "action failed" packet to the caster + } + } + + /** + * Update the position of the L2Character during a movement and return True if the movement is finished.
      + * Concept:
      + * At the beginning of the move action, all properties of the movement are stored in the MoveData object called _move of the L2Character.
      + * The position of the start point and of the destination permit to estimated in function of the movement speed the time to achieve the destination.
      + * When the movement is started (ex : by MovetoLocation), this method will be called each 0.1 sec to estimate and update the L2Character position on the server.
      + * Note, that the current server position can differe from the current client position even if each movement is straight foward.
      + * That's why, client send regularly a Client->Server ValidatePosition packet to eventually correct the gap on the server.
      + * But, it's always the server position that is used in range calculation. At the end of the estimated movement time,
      + * the L2Character position is automatically set to the destination position even if the movement is not finished.
      + * Caution: The current Z position is obtained FROM THE CLIENT by the Client->Server ValidatePosition Packet.
      + * But x and y positions must be calculated to avoid that players try to modify their movement speed.
      + * @return True if the movement is finished + */ + public boolean updatePosition() { + // Get movement data + MoveData m = _move; + + if (m == null) { + return true; + } + + if (!isVisible()) { + _move = null; + return true; + } + + // Check if this is the first update + if (m._moveTimestamp == 0) { + m._moveTimestamp = m._moveStartTime; + m._xAccurate = getX(); + m._yAccurate = getY(); + } + + int gameTicks = GameTimeController.getInstance().getGameTicks(); + + // Check if the position has already been calculated + if (m._moveTimestamp == gameTicks) { + return false; + } + + int xPrev = getX(); + int yPrev = getY(); + int zPrev = getZ(); // the z coordinate may be modified by coordinate synchronizations + + double dx, dy, dz; + if (geodata().getCoordSynchronize() == 1) { + // the only method that can modify x,y while moving (otherwise _move would/should be set null) + dx = m._xDestination - xPrev; + dy = m._yDestination - yPrev; + } else { + // otherwise we need saved temporary values to avoid rounding errors + dx = m._xDestination - m._xAccurate; + dy = m._yDestination - m._yAccurate; + } + + final boolean isFloating = isFlying() || isInsideZone(ZoneId.WATER); + + // Z coordinate will follow geodata or client values + // once a second to reduce possible cpu load + if ((geodata().getCoordSynchronize() == 2) && // + !isFloating && !m.disregardingGeodata && // + ((GameTimeController.getInstance().getGameTicks() % 10) == 0) // + && GeoData.getInstance().hasGeo(xPrev, yPrev)) { + int geoHeight = GeoData.getInstance().getSpawnHeight(xPrev, yPrev, zPrev); + dz = m._zDestination - geoHeight; + // quite a big difference, compare to validatePosition packet + if (isPlayer() && (Math.abs(getActingPlayer().getClientZ() - geoHeight) > 200) && (Math.abs(getActingPlayer().getClientZ() - geoHeight) < 1500)) { + dz = m._zDestination - zPrev; // allow diff + } else if (isInCombat() && (Math.abs(dz) > 200) && (((dx * dx) + (dy * dy)) < 40000)) // allow mob to climb up to pcinstance + { + dz = m._zDestination - zPrev; // climbing + } else { + zPrev = geoHeight; + } + } else { + dz = m._zDestination - zPrev; + } + + double delta = (dx * dx) + (dy * dy); + // close enough, allows error between client and server geodata if it cannot be avoided + if ((delta < 10000) && ((dz * dz) > 2500) && !isFloating) { + delta = Math.sqrt(delta); + } else { + delta = Math.sqrt(delta + (dz * dz)); + } + + double distFraction = Double.MAX_VALUE; + if (delta > 1) { + final double distPassed = (getMoveSpeed() * (gameTicks - m._moveTimestamp)) / GameTimeController.TICKS_PER_SECOND; + distFraction = distPassed / delta; + } + + // if (general().developer()) _log.warning("Move Ticks:" + (gameTicks - m._moveTimestamp) + ", distPassed:" + distPassed + ", distFraction:" + distFraction); + + if (distFraction > 1) { + // Set the position of the L2Character to the destination + super.setXYZ(m._xDestination, m._yDestination, m._zDestination); + } else { + m._xAccurate += dx * distFraction; + m._yAccurate += dy * distFraction; + + // Set the position of the L2Character to estimated after parcial move + super.setXYZ((int) (m._xAccurate), (int) (m._yAccurate), zPrev + (int) ((dz * distFraction) + 0.5)); + } + revalidateZone(false); + + // Set the timer of last position update to now + m._moveTimestamp = gameTicks; + + if (distFraction > 1) { + ThreadPoolManager.getInstance().executeAi(() -> { + try { + if (general().moveBasedKnownList()) { + getKnownList().findObjects(); + } + + getAI().notifyEvent(CtrlEvent.EVT_ARRIVED); + } catch (final Throwable e) { + LOG.warn("{}", e); + } + }); + return true; + } + return false; + } + + public void revalidateZone(boolean force) { + if (getWorldRegion() == null) { + return; + } + + // This function is called too often from movement code + if (force) { + _zoneValidateCounter = 4; + } else { + _zoneValidateCounter--; + if (_zoneValidateCounter < 0) { + _zoneValidateCounter = 4; + } else { + return; + } + } + + getWorldRegion().revalidateZones(this); + } + + /** + * Stop movement of the L2Character (Called by AI Accessor only).
      + * Actions: + *
        + *
      • Delete movement data of the L2Character
      • + *
      • Set the current position (x,y,z), its current L2WorldRegion if necessary and its heading
      • + *
      • Remove the L2Object object from _gmList of GmListTable
      • + *
      • Remove object from _knownObjects and _knownPlayer of all surrounding L2WorldRegion L2Characters
      • + *
      + * Caution: This method DOESN'T send Server->Client packet StopMove/StopRotation + * @param loc + */ + public void stopMove(Location loc) { + stopMove(loc, false); + } + + public void stopMove(Location loc, boolean updateKnownObjects) { + // Delete movement data of the L2Character + _move = null; + + // if (getAI() != null) + // getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + + // Set the current position (x,y,z), its current L2WorldRegion if necessary and its heading + // All data are contained in a Location object + if (loc != null) { + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + setHeading(loc.getHeading()); + revalidateZone(true); + } + broadcastPacket(new StopMove(this)); + if (general().moveBasedKnownList() && updateKnownObjects) { + getKnownList().findObjects(); + } + } + + /** + * @return Returns the showSummonAnimation. + */ + public boolean isShowSummonAnimation() { + return _showSummonAnimation; + } + + /** + * @param showSummonAnimation The showSummonAnimation to set. + */ + public void setShowSummonAnimation(boolean showSummonAnimation) { + _showSummonAnimation = showSummonAnimation; + } + + /** + * Target a L2Object (add the target to the L2Character _target, _knownObject and L2Character to _KnownObject of the L2Object).
      + * Concept:
      + * The L2Object (including L2Character) targeted is identified in _target of the L2Character.
      + * Actions: + *
        + *
      • Set the _target of L2Character to L2Object
      • + *
      • If necessary, add L2Object to _knownObject of the L2Character
      • + *
      • If necessary, add L2Character to _KnownObject of the L2Object
      • + *
      • If object==null, cancel Attak or Cast
      • + *
      + * @param object L2object to target + */ + public void setTarget(L2Object object) { + if ((object != null) && !object.isVisible()) { + object = null; + } + + if ((object != null) && (object != _target)) { + getKnownList().addKnownObject(object); + object.getKnownList().addKnownObject(this); + } + + _target = object; + } + + /** + * @return the identifier of the L2Object targeted or -1. + */ + public final int getTargetId() { + if (_target != null) { + return _target.getObjectId(); + } + return 0; + } + + /** + * @return the L2Object targeted or null. + */ + public final L2Object getTarget() { + return _target; + } + + /** + * Calculate movement data for a move to location action and add the L2Character to movingObjects of GameTimeController (only called by AI Accessor).
      + * Concept:
      + * At the beginning of the move action, all properties of the movement are stored in the MoveData object called _move of the L2Character.
      + * The position of the start point and of the destination permit to estimated in function of the movement speed the time to achieve the destination.
      + * All L2Character in movement are identified in movingObjects of GameTimeController that will call the updatePosition method of those L2Character each 0.1s.
      + * Actions: + *
        + *
      • Get current position of the L2Character
      • + *
      • Calculate distance (dx,dy) between current position and destination including offset
      • + *
      • Create and Init a MoveData object
      • + *
      • Set the L2Character _move object to MoveData object
      • + *
      • Add the L2Character to movingObjects of the GameTimeController
      • + *
      • Create a task to notify the AI that L2Character arrives at a check point of the movement
      • + *
      + * Caution: This method DOESN'T send Server->Client packet MoveToPawn/CharMoveToLocation.
      + * Example of use: + *
        + *
      • AI : onIntentionMoveTo(Location), onIntentionPickUp(L2Object), onIntentionInteract(L2Object)
      • + *
      • FollowTask
      • + *
      + * @param x The X position of the destination + * @param y The Y position of the destination + * @param z The Y position of the destination + * @param offset The size of the interaction area of the L2Character targeted + */ + public void moveToLocation(int x, int y, int z, int offset) { + // Get the Move Speed of the L2Charcater + double speed = getMoveSpeed(); + if ((speed <= 0) || isMovementDisabled()) { + return; + } + + // Get current position of the L2Character + final int curX = super.getX(); + final int curY = super.getY(); + final int curZ = super.getZ(); + + // Calculate distance (dx,dy) between current position and destination + // TODO: improve Z axis move/follow support when dx,dy are small compared to dz + double dx = (x - curX); + double dy = (y - curY); + double dz = (z - curZ); + double distance = Math.hypot(dx, dy); + + final boolean verticalMovementOnly = isFlying() && (distance == 0) && (dz != 0); + if (verticalMovementOnly) { + distance = Math.abs(dz); + } + + // make water move short and use no geodata checks for swimming chars + // distance in a click can easily be over 3000 + if (isInsideZone(ZoneId.WATER) && (distance > 700)) { + double divider = 700 / distance; + x = curX + (int) (divider * dx); + y = curY + (int) (divider * dy); + z = curZ + (int) (divider * dz); + dx = (x - curX); + dy = (y - curY); + dz = (z - curZ); + distance = Math.hypot(dx, dy); + } + + // @formatter:off + // Define movement angles needed + // ^ + // | X (x,y) + // | / + // | / distance + // | / + // |/ angle + // X ----------> + // (curx,cury) + // @formatter:on + + double cos; + double sin; + + // Check if a movement offset is defined or no distance to go through + if ((offset > 0) || (distance < 1)) { + // approximation for moving closer when z coordinates are different + // TODO: handle Z axis movement better + offset -= Math.abs(dz); + if (offset < 5) { + offset = 5; + } + + // If no distance to go through, the movement is canceled + if ((distance < 1) || ((distance - offset) <= 0)) { + // Notify the AI that the L2Character is arrived at destination + getAI().notifyEvent(CtrlEvent.EVT_ARRIVED); + + return; + } + // Calculate movement angles needed + sin = dy / distance; + cos = dx / distance; + + distance -= (offset - 5); // due to rounding error, we have to move a bit closer to be in range + + // Calculate the new destination with offset included + x = curX + (int) (distance * cos); + y = curY + (int) (distance * sin); + } else { + // Calculate movement angles needed + sin = dy / distance; + cos = dx / distance; + } + + // Create and Init a MoveData object + MoveData m = new MoveData(); + + // GEODATA MOVEMENT CHECKS AND PATHFINDING + m.onGeodataPathIndex = -1; // Initialize not on geodata path + m.disregardingGeodata = false; + + if (!isFlying() // flying chars not checked - even canSeeTarget doesn't work yet + && (!isInsideZone(ZoneId.WATER) || isInsideZone(ZoneId.SIEGE))) // swimming also not checked unless in siege zone - but distance is limited + { + final boolean isInVehicle = isPlayer() && (getActingPlayer().getVehicle() != null); + if (isInVehicle) { + m.disregardingGeodata = true; + } + + double originalDistance = distance; + int originalX = x; + int originalY = y; + int originalZ = z; + int gtx = (originalX - L2World.MAP_MIN_X) >> 4; + int gty = (originalY - L2World.MAP_MIN_Y) >> 4; + + // Movement checks: + // When pathfinding enabled, for all characters except monsters returning home (could be changed later to teleport if pathfinding fails) + if (((geodata().getPathFinding() > 0) && (!(isAttackable() && ((L2Attackable) this).isReturningToSpawnPoint()))) // + || (isPlayer() && !(isInVehicle && (distance > 1500))) // + || (this instanceof L2RiftInvaderInstance)) { + if (isOnGeodataPath()) { + try { + if ((gtx == _move.geoPathGtx) && (gty == _move.geoPathGty)) { + return; + } + _move.onGeodataPathIndex = -1; // Set not on geodata path + } catch (NullPointerException e) { + // nothing + } + } + + if ((curX < L2World.MAP_MIN_X) || (curX > L2World.MAP_MAX_X) || (curY < L2World.MAP_MIN_Y) || (curY > L2World.MAP_MAX_Y)) { + // Temporary fix for character outside world region errors + LOG.warn("{} is outside world area, in coordinates x:{} y:{}", this, curX, curY); + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + if (isPlayer()) { + getActingPlayer().logout(); + } else if (isSummon()) { + return; // preventation when summon get out of world coords, player will not loose him, unsummon handled from pcinstance + } else { + onDecay(); + } + return; + } + Location destiny = GeoData.getInstance().moveCheck(curX, curY, curZ, x, y, z, getInstanceId()); + // location different if destination wasn't reached (or just z coord is different) + x = destiny.getX(); + y = destiny.getY(); + z = destiny.getZ(); + dx = x - curX; + dy = y - curY; + dz = z - curZ; + distance = verticalMovementOnly ? Math.pow(dz, 2) : Math.hypot(dx, dy); + } + // Pathfinding checks. Only when geodata setting is 2, the LoS check gives shorter result + // than the original movement was and the LoS gives a shorter distance than 2000 + // This way of detecting need for pathfinding could be changed. + if ((geodata().getPathFinding() > 0) && ((originalDistance - distance) > 30)) { + // Path calculation + // Overrides previous movement check + if ((isPlayable() && !isInVehicle) || isMinion() || isInCombat()) { + m.geoPath = PathFinding.getInstance().findPath(curX, curY, curZ, originalX, originalY, originalZ, getInstanceId(), isPlayable()); + if ((m.geoPath == null) || (m.geoPath.size() < 2)) // No path found + { + // Even though there's no path found (remember geonodes aren't perfect), + // the mob is attacking and right now we set it so that the mob will go + // after target anyway, is dz is small enough. + // With cellpathfinding this approach could be changed but would require taking + // off the geonodes and some more checks. + // Summons will follow their masters no matter what. + // Currently minions also must move freely since L2AttackableAI commands them to move along with their leader + if (isPlayer() || (!isPlayable() && !isMinion() && (Math.abs(z - curZ) > 140)) || (isSummon() && !((L2Summon) this).getFollowStatus())) { + final Location destination = GeoData.getInstance().moveCheck(curX, curY, curZ, x, y, z, getInstanceId()); + getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, destination); + return; + } + + m.disregardingGeodata = true; + x = originalX; + y = originalY; + z = originalZ; + distance = originalDistance; + } else { + m.onGeodataPathIndex = 0; // on first segment + m.geoPathGtx = gtx; + m.geoPathGty = gty; + m.geoPathAccurateTx = originalX; + m.geoPathAccurateTy = originalY; + + x = m.geoPath.get(m.onGeodataPathIndex).getX(); + y = m.geoPath.get(m.onGeodataPathIndex).getY(); + z = m.geoPath.get(m.onGeodataPathIndex).getZ(); + + // check for doors in the route + if (DoorData.getInstance().checkIfDoorsBetween(curX, curY, curZ, x, y, z, getInstanceId())) { + m.geoPath = null; + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + return; + } + for (int i = 0; i < (m.geoPath.size() - 1); i++) { + if (DoorData.getInstance().checkIfDoorsBetween(m.geoPath.get(i), m.geoPath.get(i + 1), getInstanceId())) { + m.geoPath = null; + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + return; + } + } + + dx = x - curX; + dy = y - curY; + dz = z - curZ; + distance = verticalMovementOnly ? Math.pow(dz, 2) : Math.hypot(dx, dy); + sin = dy / distance; + cos = dx / distance; + } + } + } + // If no distance to go through, the movement is canceled + if ((distance < 1) && ((geodata().getPathFinding() > 0) || isPlayable() || (this instanceof L2RiftInvaderInstance) || isAfraid())) { + if (isSummon()) { + ((L2Summon) this).setFollowStatus(false); + } + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + return; + } + } + + // Apply Z distance for flying or swimming for correct timing calculations + if ((isFlying() || isInsideZone(ZoneId.WATER)) && !verticalMovementOnly) { + distance = Math.hypot(distance, dz); + } + + // Caclulate the Nb of ticks between the current position and the destination + // One tick added for rounding reasons + int ticksToMove = 1 + (int) ((GameTimeController.TICKS_PER_SECOND * distance) / speed); + m._xDestination = x; + m._yDestination = y; + m._zDestination = z; // this is what was requested from client + + // Calculate and set the heading of the L2Character + m._heading = 0; // initial value for coordinate sync + // Does not broke heading on vertical movements + if (!verticalMovementOnly) { + setHeading(Util.calculateHeadingFrom(cos, sin)); + } + + m._moveStartTime = GameTimeController.getInstance().getGameTicks(); + + // Set the L2Character _move object to MoveData object + _move = m; + + // Add the L2Character to movingObjects of the GameTimeController + // The GameTimeController manage objects movement + GameTimeController.getInstance().registerMovingObject(this); + + // Create a task to notify the AI that L2Character arrives at a check point of the movement + if ((ticksToMove * GameTimeController.MILLIS_IN_TICK) > 3000) { + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_ARRIVED_REVALIDATE), 2000); + } + + // the CtrlEvent.EVT_ARRIVED will be sent when the character will actually arrive + // to destination by GameTimeController + } + + public boolean moveToNextRoutePoint() { + if (!isOnGeodataPath()) { + // Cancel the move action + _move = null; + return false; + } + + // Get the Move Speed of the L2Charcater + double speed = getMoveSpeed(); + if ((speed <= 0) || isMovementDisabled()) { + // Cancel the move action + _move = null; + return false; + } + + MoveData md = _move; + if (md == null) { + return false; + } + + // Create and Init a MoveData object + MoveData m = new MoveData(); + + // Update MoveData object + m.onGeodataPathIndex = md.onGeodataPathIndex + 1; // next segment + m.geoPath = md.geoPath; + m.geoPathGtx = md.geoPathGtx; + m.geoPathGty = md.geoPathGty; + m.geoPathAccurateTx = md.geoPathAccurateTx; + m.geoPathAccurateTy = md.geoPathAccurateTy; + + if (md.onGeodataPathIndex == (md.geoPath.size() - 2)) { + m._xDestination = md.geoPathAccurateTx; + m._yDestination = md.geoPathAccurateTy; + m._zDestination = md.geoPath.get(m.onGeodataPathIndex).getZ(); + } else { + m._xDestination = md.geoPath.get(m.onGeodataPathIndex).getX(); + m._yDestination = md.geoPath.get(m.onGeodataPathIndex).getY(); + m._zDestination = md.geoPath.get(m.onGeodataPathIndex).getZ(); + } + + double distance = Math.hypot(m._xDestination - super.getX(), m._yDestination - super.getY()); + // Calculate and set the heading of the L2Character + if (distance != 0) { + setHeading(Util.calculateHeadingFrom(getX(), getY(), m._xDestination, m._yDestination)); + } + + // Caclulate the Nb of ticks between the current position and the destination + // One tick added for rounding reasons + int ticksToMove = 1 + (int) ((GameTimeController.TICKS_PER_SECOND * distance) / speed); + + m._heading = 0; // initial value for coordinate sync + + m._moveStartTime = GameTimeController.getInstance().getGameTicks(); + + // Set the L2Character _move object to MoveData object + _move = m; + + // Add the L2Character to movingObjects of the GameTimeController + // The GameTimeController manage objects movement + GameTimeController.getInstance().registerMovingObject(this); + + // Create a task to notify the AI that L2Character arrives at a check point of the movement + if ((ticksToMove * GameTimeController.MILLIS_IN_TICK) > 3000) { + ThreadPoolManager.getInstance().scheduleAi(new NotifyAITask(this, CtrlEvent.EVT_ARRIVED_REVALIDATE), 2000); + } + + // the CtrlEvent.EVT_ARRIVED will be sent when the character will actually arrive + // to destination by GameTimeController + + // Send a Server->Client packet CharMoveToLocation to the actor and all L2PcInstance in its _knownPlayers + MoveToLocation msg = new MoveToLocation(this); + broadcastPacket(msg); + + return true; + } + + public boolean validateMovementHeading(int heading) { + MoveData m = _move; + + if (m == null) { + return true; + } + + boolean result = true; + if (m._heading != heading) { + result = (m._heading == 0); // initial value or false + m._heading = heading; + } + + return result; + } + + /** + * Check if this object is inside the given radius around the given point. + * @param loc Location of the target + * @param radius the radius around the target + * @param checkZAxis should we check Z axis also + * @param strictCheck true if (distance < radius), false if (distance <= radius) + * @return true if the L2Character is inside the radius. + */ + public final boolean isInsideRadius(ILocational loc, int radius, boolean checkZAxis, boolean strictCheck) { + return isInsideRadius(loc.getX(), loc.getY(), loc.getZ(), radius, checkZAxis, strictCheck); + } + + /** + * Check if this object is inside the given radius around the given point. + * @param x X position of the target + * @param y Y position of the target + * @param z Z position of the target + * @param radius the radius around the target + * @param checkZAxis should we check Z axis also + * @param strictCheck true if (distance < radius), false if (distance <= radius) + * @return true if the L2Character is inside the radius. + */ + public final boolean isInsideRadius(int x, int y, int z, int radius, boolean checkZAxis, boolean strictCheck) { + final double distance = calculateDistance(x, y, z, checkZAxis, true); + return (strictCheck) ? (distance < (radius * radius)) : (distance <= (radius * radius)); + } + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return True if arrows are available. + */ + protected boolean checkAndEquipArrows() { + return true; + } + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return True if bolts are available. + */ + protected boolean checkAndEquipBolts() { + return true; + } + + /** + * Add Exp and Sp to the L2Character.
      + * Overridden in : + *
    • L2PcInstance
    • + *
    • L2PetInstance
    • + * @param addToExp + * @param addToSp + */ + public void addExpAndSp(long addToExp, int addToSp) { + // Dummy method (overridden by players and pets) + } + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return the active weapon instance (always equiped in the right hand). + */ + public abstract L2ItemInstance getActiveWeaponInstance(); + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return the active weapon item (always equiped in the right hand). + */ + public abstract L2Weapon getActiveWeaponItem(); + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return the secondary weapon instance (always equiped in the left hand). + */ + public abstract L2ItemInstance getSecondaryWeaponInstance(); + + /** + * Overridden in : + *
    • L2PcInstance
    • + * @return the secondary {@link L2Item} item (always equiped in the left hand). + */ + public abstract L2Item getSecondaryWeaponItem(); + + /** + * Manage hit process (called by Hit Task).
      + * Actions: + *
        + *
      • If the attacker/target is dead or use fake death, notify the AI with EVT_CANCEL and send a Server->Client packet ActionFailed (if attacker is a L2PcInstance)
      • + *
      • If attack isn't aborted, send a message system (critical hit, missed...) to attacker/target if they are L2PcInstance
      • + *
      • If attack isn't aborted and hit isn't missed, reduce HP of the target and calculate reflection damage to reduce HP of attacker if necessary
      • + *
      • if attack isn't aborted and hit isn't missed, manage attack or cast break of the target (calculating rate, sending message...)
      • + *
      + * @param target The L2Character targeted + * @param damage Nb of HP to reduce + * @param crit True if hit is critical + * @param miss True if hit is missed + * @param soulshot True if SoulShot are charged + * @param shld True if shield is efficient + */ + public void onHitTimer(L2Character target, int damage, boolean crit, boolean miss, boolean soulshot, byte shld) { + // If the attacker/target is dead or use fake death, notify the AI with EVT_CANCEL + // and send a Server->Client packet ActionFailed (if attacker is a L2PcInstance) + if ((target == null) || isAlikeDead() || (isNpc() && ((L2Npc) this).isEventMob())) { + getAI().notifyEvent(CtrlEvent.EVT_CANCEL); + return; + } + + if (isStunned() || isSleeping() || isAfraid() || isPhysicalAttackMuted()) { + getAI().notifyEvent(CtrlEvent.EVT_CANCEL); + return; + } + + if ((isNpc() && target.isAlikeDead()) || target.isDead() || (!getKnownList().knowsObject(target) && !isDoor())) { + // getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + // Some times attack is processed but target die before the hit + // So we need to recharge shot for next attack + rechargeShots(true, false); + getAI().notifyEvent(CtrlEvent.EVT_CANCEL); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + if (miss) { + // Notify target AI + if (target.hasAI()) { + target.getAI().notifyEvent(CtrlEvent.EVT_EVADED, this); + } + notifyAttackAvoid(target, false); + } + + // Send message about damage/crit or miss + sendDamageMessage(target, damage, false, crit, miss); + + // Check Raidboss attack + // Character will be petrified if attacking a raid that's more + // than 8 levels lower + if (target.isRaid() && target.giveRaidCurse() && npc().raidCurse()) { + if (getLevel() > (target.getLevel() + 8)) { + Skill skill = CommonSkill.RAID_CURSE2.getSkill(); + + if (skill != null) { + abortAttack(); + abortCast(); + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + skill.applyEffects(target, this); + } else { + LOG.warn("Skill 4515 at level 1 is missing in DP."); + } + + damage = 0; // prevents messing up drop calculation + } + } + + // If L2Character target is a L2PcInstance, send a system message + if (target.isPlayer()) { + L2PcInstance enemy = target.getActingPlayer(); + enemy.getAI().clientStartAutoAttack(); + } + + if (!miss && (damage > 0)) { + L2Weapon weapon = getActiveWeaponItem(); + boolean isBow = ((weapon != null) && ((weapon.getItemType() == WeaponType.BOW) || (weapon.getItemType() == WeaponType.CROSSBOW))); + int reflectedDamage = 0; + + if (!isBow && !target.isInvul()) // Do not reflect if weapon is of type bow or target is invunlerable + { + // quick fix for no drop from raid if boss attack high-level char with damage reflection + if (!target.isRaid() || (getActingPlayer() == null) || (getActingPlayer().getLevel() <= (target.getLevel() + 8))) { + // Reduce HP of the target and calculate reflection damage to reduce HP of attacker if necessary + double reflectPercent = target.getStat().calcStat(Stats.REFLECT_DAMAGE_PERCENT, 0, null, null); + + if (reflectPercent > 0) { + reflectedDamage = (int) ((reflectPercent / 100.) * damage); + + if (reflectedDamage > target.getMaxHp()) { + reflectedDamage = target.getMaxHp(); + } + } + } + } + + // reduce targets HP + target.reduceCurrentHp(damage, this, null); + target.notifyDamageReceived(damage, this, null, crit, false, false); + + if (reflectedDamage > 0) { + if (target.isPlayable()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DONE_S3_DAMAGE_TO_C2); + sm.addCharName(target).addCharName(this).addInt(reflectedDamage); + target.sendPacket(sm); + } + + if (isSummon()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_RECEIVED_DAMAGE_OF_S3_FROM_C2); + sm.addCharName(this).addCharName(target).addInt(reflectedDamage); + getActingPlayer().sendPacket(sm); + } + + reduceCurrentHp(reflectedDamage, target, true, false, null); + notifyDamageReceived(reflectedDamage, target, null, crit, false, true); + } + + if (!isBow) // Do not absorb if weapon is of type bow + { + // Absorb HP from the damage inflicted + double absorbPercent = getStat().calcStat(Stats.ABSORB_DAMAGE_PERCENT, 0, null, null); + + if (absorbPercent > 0) { + int maxCanAbsorb = (int) (getMaxRecoverableHp() - getCurrentHp()); + int absorbDamage = (int) ((absorbPercent / 100.) * damage); + + if (absorbDamage > maxCanAbsorb) { + absorbDamage = maxCanAbsorb; // Can't absord more than max hp + } + + if (absorbDamage > 0) { + setCurrentHp(getCurrentHp() + absorbDamage); + } + } + + // Absorb MP from the damage inflicted + absorbPercent = getStat().calcStat(Stats.ABSORB_MANA_DAMAGE_PERCENT, 0, null, null); + + if (absorbPercent > 0) { + int maxCanAbsorb = (int) (getMaxRecoverableMp() - getCurrentMp()); + int absorbDamage = (int) ((absorbPercent / 100.) * damage); + + if (absorbDamage > maxCanAbsorb) { + absorbDamage = maxCanAbsorb; // Can't absorb more than max hp + } + + if (absorbDamage > 0) { + setCurrentMp(getCurrentMp() + absorbDamage); + } + } + + } + + // Notify AI with EVT_ATTACKED + if (target.hasAI()) { + target.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this); + } + getAI().clientStartAutoAttack(); + if (isSummon()) { + L2PcInstance owner = ((L2Summon) this).getOwner(); + if (owner != null) { + owner.getAI().clientStartAutoAttack(); + } + } + + // Manage attack or cast break of the target (calculating rate, sending message...) + if (!target.isRaid() && Formulas.calcAtkBreak(target, damage)) { + target.breakAttack(); + target.breakCast(); + } + + if (_triggerSkills != null) { + for (OptionsSkillHolder holder : _triggerSkills.values()) { + if ((!crit && (holder.getSkillType() == OptionsSkillType.ATTACK)) || ((holder.getSkillType() == OptionsSkillType.CRITICAL) && crit)) { + if (Rnd.get(100) < holder.getChance()) { + makeTriggerCast(holder.getSkill(), target); + } + } + } + } + // Launch weapon onCritical Special ability effect if available + if (crit && (weapon != null)) { + weapon.castOnCriticalSkill(this, target); + } + } + // Recharge any active auto-soulshot tasks for current creature. + rechargeShots(true, false); + } + + /** + * Break an attack and send Server->Client ActionFailed packet and a System Message to the L2Character. + */ + public void breakAttack() { + if (isAttackingNow()) { + // Abort the attack of the L2Character and send Server->Client ActionFailed packet + abortAttack(); + if (isPlayer()) { + // Send a system message + sendPacket(SystemMessageId.ATTACK_FAILED); + } + } + } + + /** + * Break a cast and send Server->Client ActionFailed packet and a System Message to the L2Character. + */ + public void breakCast() { + // damage can only cancel magical & static skills + if (isCastingNow() && canAbortCast() && (getLastSkillCast() != null) && (getLastSkillCast().isMagic() || getLastSkillCast().isStatic())) { + // Abort the cast of the L2Character and send Server->Client MagicSkillCanceld/ActionFailed packet. + abortCast(); + + if (isPlayer()) { + // Send a system message + sendPacket(SystemMessageId.CASTING_INTERRUPTED); + } + } + } + + /** + * Reduce the arrow number of the L2Character.
      + * Overridden in : + *
    • L2PcInstance
    • + * @param bolts + */ + protected void reduceArrowCount(boolean bolts) { + // default is to do nothing + } + + /** + * Manage Forced attack (shift + select target).
      + * Actions: + *
        + *
      • If L2Character or target is in a town area, send a system message TARGET_IN_PEACEZONE a Server->Client packet ActionFailed
      • + *
      • If target is confused, send a Server->Client packet ActionFailed
      • + *
      • If L2Character is a L2ArtefactInstance, send a Server->Client packet ActionFailed
      • + *
      • Send a Server->Client packet MyTargetSelected to start attack and Notify AI with AI_INTENTION_ATTACK
      • + *
      + * @param player The L2PcInstance to attack + */ + @Override + public void onForcedAttack(L2PcInstance player) { + if (isInsidePeaceZone(player)) { + // If L2Character or target is in a peace zone, send a system message TARGET_IN_PEACEZONE a Server->Client packet ActionFailed + player.sendPacket(SystemMessageId.TARGET_IN_PEACEZONE); + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + if (player.isInOlympiadMode() && (player.getTarget() != null) && player.getTarget().isPlayable()) { + L2PcInstance target = null; + L2Object object = player.getTarget(); + if ((object != null) && object.isPlayable()) { + target = object.getActingPlayer(); + } + + if ((target == null) || (target.isInOlympiadMode() && (!player.isOlympiadStart() || (player.getOlympiadGameId() != target.getOlympiadGameId())))) { + // if L2PcInstance is in Olympia and the match isn't already start, send a Server->Client packet ActionFailed + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + } + if ((player.getTarget() != null) && !player.getTarget().canBeAttacked() && !player.getAccessLevel().allowPeaceAttack()) { + // If target is not attackable, send a Server->Client packet ActionFailed + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + if (player.isConfused()) { + // If target is confused, send a Server->Client packet ActionFailed + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + // GeoData Los Check or dz > 1000 + if (!GeoData.getInstance().canSeeTarget(player, this)) { + player.sendPacket(SystemMessageId.CANT_SEE_TARGET); + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + if (player.getBlockCheckerArena() != -1) { + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + // Notify AI with AI_INTENTION_ATTACK + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } + + /** + * @param attacker + * @return True if inside peace zone. + */ + public boolean isInsidePeaceZone(L2PcInstance attacker) { + return isInsidePeaceZone(attacker, this); + } + + public boolean isInsidePeaceZone(L2PcInstance attacker, L2Object target) { + return (!attacker.getAccessLevel().allowPeaceAttack() && isInsidePeaceZone((L2Object) attacker, target)); + } + + public boolean isInsidePeaceZone(L2Object attacker, L2Object target) { + if (target == null) { + return false; + } + if (!(target.isPlayable() && attacker.isPlayable())) { + return false; + } + if (InstanceManager.getInstance().getInstance(getInstanceId()).isPvPInstance()) { + return false; + } + + if (territoryWar().playerWithWardCanBeKilledInPeaceZone() && TerritoryWarManager.getInstance().isTWInProgress()) { + if (target.isPlayer() && target.getActingPlayer().isCombatFlagEquipped()) { + return false; + } + } + + if (character().karmaPlayerCanBeKilledInPeaceZone()) { + // allows red to be attacked and red to attack flagged players + if ((target.getActingPlayer() != null) && (target.getActingPlayer().getKarma() > 0)) { + return false; + } + if ((attacker.getActingPlayer() != null) && (attacker.getActingPlayer().getKarma() > 0) && (target.getActingPlayer() != null) && (target.getActingPlayer().getPvpFlag() > 0)) { + return false; + } + } + + return (target.isInsideZone(ZoneId.PEACE) || attacker.isInsideZone(ZoneId.PEACE)); + } + + /** + * @return true if this character is inside an active grid. + */ + public boolean isInActiveRegion() { + L2WorldRegion region = getWorldRegion(); + return ((region != null) && (region.isActive())); + } + + /** + * @return True if the L2Character has a Party in progress. + */ + public boolean isInParty() { + return false; + } + + /** + * @return the L2Party object of the L2Character. + */ + public L2Party getParty() { + return null; + } + + /** + * @return the Attack Speed of the L2Character (delay (in milliseconds) before next attack). + */ + public int calculateTimeBetweenAttacks() { + return (int) (500000 / getPAtkSpd()); + } + + /** + * @param weapon + * @return the Reuse Time of Attack (used for bow delay) + */ + public int calculateReuseTime(final L2Weapon weapon) { + if (isTransformed()) { + switch (getAttackType()) { + case BOW: + return (int) ((1500 * 333 * getStat().getWeaponReuseModifier(null)) / getStat().getPAtkSpd()); + case CROSSBOW: + return (int) ((1200 * 333 * getStat().getWeaponReuseModifier(null)) / getStat().getPAtkSpd()); + } + } + + if ((weapon == null) || (weapon.getReuseDelay() == 0)) { + return 0; + } + + return (int) ((weapon.getReuseDelay() * 333) / getPAtkSpd()); + } + + /** + * Add a skill to the L2Character _skills and its Func objects to the calculator set of the L2Character.
      + * Concept:
      + * All skills own by a L2Character are identified in _skills
      + * Actions: + *
        + *
      • Replace oldSkill by newSkill or Add the newSkill
      • + *
      • If an old skill has been replaced, remove all its Func objects of L2Character calculator set
      • + *
      • Add Func objects of newSkill to the calculator set of the L2Character
      • + *
      + * Overridden in: + *
        + *
      • L2PcInstance : Save update in the character_skills table of the database
      • + *
      + * @param newSkill The L2Skill to add to the L2Character + * @return The L2Skill replaced or null if just added a new L2Skill + */ + @Override + public Skill addSkill(Skill newSkill) { + Skill oldSkill = null; + if (newSkill != null) { + // Replace oldSkill by newSkill or Add the newSkill + oldSkill = _skills.put(newSkill.getId(), newSkill); + // If an old skill has been replaced, remove all its Func objects + if (oldSkill != null) { + removeStatsOwner(oldSkill); + + if (oldSkill.isPassive()) { + stopSkillEffects(false, oldSkill.getId()); + } + } + // Add Func objects of newSkill to the calculator set of the L2Character + addStatFuncs(newSkill.getStatFuncs(null, this)); + + if (newSkill.isPassive()) { + newSkill.applyEffects(this, this, -1, false, true, false, 0); + } + } + return oldSkill; + } + + public Skill removeSkill(Skill skill, boolean cancelEffect) { + return (skill != null) ? removeSkill(skill.getId(), cancelEffect) : null; + } + + public Skill removeSkill(int skillId) { + return removeSkill(skillId, true); + } + + public Skill removeSkill(int skillId, boolean cancelEffect) { + // Remove the skill from the L2Character _skills + Skill oldSkill = _skills.remove(skillId); + // Remove all its Func objects from the L2Character calculator set + if (oldSkill != null) { + // Stop casting if this skill is used right now + if ((getLastSkillCast() != null) && isCastingNow()) { + if (oldSkill.getId() == getLastSkillCast().getId()) { + abortCast(); + } + } + if ((getLastSimultaneousSkillCast() != null) && isCastingSimultaneouslyNow()) { + if (oldSkill.getId() == getLastSimultaneousSkillCast().getId()) { + abortCast(); + } + } + + // Stop effects. + if (cancelEffect || oldSkill.isToggle() || oldSkill.isPassive()) { + removeStatsOwner(oldSkill); + stopSkillEffects(false, oldSkill.getId()); + } + } + + return oldSkill; + } + + /** + * Concept:
      + * All skills own by a L2Character are identified in _skills the L2Character + * @return all skills own by the L2Character in a table of L2Skill. + */ + public final Collection getAllSkills() { + return _skills.values(); + } + + /** + * @return the map containing this character skills. + */ + @Override + public Map getSkills() { + return _skills; + } + + /** + * Return the level of a skill owned by the L2Character. + * @param skillId The identifier of the L2Skill whose level must be returned + * @return The level of the L2Skill identified by skillId + */ + @Override + public int getSkillLevel(int skillId) { + final Skill skill = getKnownSkill(skillId); + return (skill == null) ? -1 : skill.getLevel(); + } + + /** + * @param skillId The identifier of the L2Skill to check the knowledge + * @return the skill from the known skill. + */ + @Override + public final Skill getKnownSkill(int skillId) { + return _skills.get(skillId); + } + + /** + * Return the number of buffs affecting this L2Character. + * @return The number of Buffs affecting this L2Character + */ + public int getBuffCount() { + return _effectList.getBuffCount(); + } + + public int getDanceCount() { + return _effectList.getDanceCount(); + } + + /** + * Manage the magic skill launching task (MP, HP, Item consumation...) and display the magic skill animation on client.
      + * Actions: + *
        + *
      • Send a Server->Client packet MagicSkillLaunched (to display magic skill animation) to all L2PcInstance of L2Character _knownPlayers
      • + *
      • Consume MP, HP and Item if necessary
      • + *
      • Send a Server->Client packet StatusUpdate with MP modification to the L2PcInstance
      • + *
      • Launch the magic skill in order to calculate its effects
      • + *
      • If the skill type is PDAM, notify the AI of the target with AI_INTENTION_ATTACK
      • + *
      • Notify the AI of the L2Character with EVT_FINISH_CASTING
      • + *
      + * Caution: A magic skill casting MUST BE in progress + * @param mut + */ + public void onMagicLaunchedTimer(MagicUseTask mut) { + // TODO(Zoey76): The target list should be managed by the target type and affect scope and should not be modified here. + // TODO(Zoey76): The best approach would be to include the effect range and LOS validation in AffectScope. + + // Escaping from under skill's radius and peace zone check. First version, not perfect in AoE skills. + final var skill = mut.getSkill(); + int escapeRange = 0; + if (skill.getEffectRange() > escapeRange) { + escapeRange = skill.getEffectRange(); + } else if ((skill.getCastRange() < 0) && (skill.getAffectRange() > 80)) { + escapeRange = skill.getAffectRange(); + } + + final var targets = mut.getTargets(); + if (!targets.isEmpty() && (escapeRange > 0)) { + int skipRange = 0; + int skipLOS = 0; + int skipPeaceZone = 0; + final List targetList = new ArrayList<>(); + for (L2Object target : targets) { + if (target instanceof L2Character creature) { + if (!isInsideRadius(target.getX(), target.getY(), target.getZ(), escapeRange + getTemplate().getCollisionRadius(), true, false)) { + skipRange++; + continue; + } + + // Healing party members should ignore LOS. + if (((skill.getAffectScope() != PARTY) || !skill.hasEffectType(L2EffectType.HP)) // + && !GeoData.getInstance().canSeeTarget(this, target)) { + skipLOS++; + continue; + } + + if (skill.isBad()) { + if (isPlayer()) { + if (creature.isInsidePeaceZone(getActingPlayer())) { + skipPeaceZone++; + continue; + } + } else { + if (creature.isInsidePeaceZone(this, target)) { + skipPeaceZone++; + continue; + } + } + } + targetList.add(target); + } + } + if (targetList.isEmpty()) { + if (isPlayer()) { + if (skipRange > 0) { + sendPacket(SystemMessageId.DIST_TOO_FAR_CASTING_STOPPED); + } else if (skipLOS > 0) { + sendPacket(SystemMessageId.CANT_SEE_TARGET); + } else if (skipPeaceZone > 0) { + sendPacket(SystemMessageId.A_MALICIOUS_SKILL_CANNOT_BE_USED_IN_PEACE_ZONE); + } + } + abortCast(); + return; + } + mut.setTargets(targetList); + } + + // Ensure that a cast is in progress + // Check if player is using fake death. + // Static skills can be used while faking death. + if ((mut.isSimultaneous() && !isCastingSimultaneouslyNow()) || (!mut.isSimultaneous() && !isCastingNow()) || (isAlikeDead() && !skill.isStatic())) { + // now cancels both, simultaneous and normal + getAI().notifyEvent(CtrlEvent.EVT_CANCEL); + return; + } + + if (!skill.isToggle()) { + broadcastPacket(new MagicSkillLaunched(this, skill.getDisplayId(), skill.getDisplayLevel(), targets)); + } + + mut.setPhase(2); + if (mut.getSkillTime() == 0) { + onMagicHitTimer(mut); + } else { + _skillCast = ThreadPoolManager.getInstance().scheduleEffect(mut, 400); + } + } + + // Runs in the end of skill casting + public void onMagicHitTimer(MagicUseTask mut) { + final Skill skill = mut.getSkill(); + final var targets = mut.getTargets(); + + if ((skill == null) || (targets == null)) { + abortCast(); + return; + } + + // Go through targets table + for (L2Object tgt : targets) { + if (tgt.isPlayable()) { + if (isPlayer() && tgt.isSummon()) { + ((L2Summon) tgt).updateAndBroadcastStatus(1); + } + } else if (isPlayable() && tgt.isAttackable()) { + L2Character target = (L2Character) tgt; + if (skill.getEffectPoint() > 0) { + ((L2Attackable) target).reduceHate(this, skill.getEffectPoint()); + } else if (skill.getEffectPoint() < 0) { + ((L2Attackable) target).addDamageHate(this, 0, -skill.getEffectPoint()); + } + } + } + + rechargeShots(skill.useSoulShot(), skill.useSpiritShot()); + + final StatusUpdate su = new StatusUpdate(this); + boolean isSendStatus = false; + + // Consume MP of the L2Character and Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + double mpConsume2 = getStat().getMpConsume2(skill); + + if (mpConsume2 > 0) { + if (mpConsume2 > getCurrentMp()) { + sendPacket(SystemMessageId.NOT_ENOUGH_MP); + abortCast(); + return; + } + + getStatus().reduceMp(mpConsume2); + su.addAttribute(StatusUpdate.CUR_MP, (int) getCurrentMp()); + isSendStatus = true; + } + + // Consume HP if necessary and Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + if (skill.getHpConsume() > 0) { + double consumeHp = skill.getHpConsume(); + + if (consumeHp >= getCurrentHp()) { + sendPacket(SystemMessageId.NOT_ENOUGH_HP); + abortCast(); + return; + } + + getStatus().reduceHp(consumeHp, this, true); + + su.addAttribute(StatusUpdate.CUR_HP, (int) getCurrentHp()); + isSendStatus = true; + } + + // Send a Server->Client packet StatusUpdate with MP modification to the L2PcInstance + if (isSendStatus) { + sendPacket(su); + } + + if (isPlayer()) { + // Consume Charges + if (skill.getChargeConsume() > 0) { + getActingPlayer().decreaseCharges(skill.getChargeConsume()); + } + } + + // Launch the magic skill in order to calculate its effects + callSkill(mut.getSkill(), mut.getTargets()); + + if (mut.getSkillTime() > 0) { + mut.setCount(mut.getCount() + 1); + } + + mut.setPhase(3); + if (mut.getSkillTime() == 0) { + onMagicFinalizer(mut); + } else { + if (mut.isSimultaneous()) { + _skillCast2 = ThreadPoolManager.getInstance().scheduleEffect(mut, 0); + } else { + _skillCast = ThreadPoolManager.getInstance().scheduleEffect(mut, 0); + } + } + } + + // Runs after skillTime + public void onMagicFinalizer(MagicUseTask mut) { + if (mut.isSimultaneous()) { + _skillCast2 = null; + setIsCastingSimultaneouslyNow(false); + return; + } + + // Cleanup + _skillCast = null; + _castInterruptTime = 0; + + // Stop casting + setIsCastingNow(false); + setIsCastingSimultaneouslyNow(false); + + final L2PcInstance player = getActingPlayer(); + final Skill skill = mut.getSkill(); + final L2Object target = !mut.getTargets().isEmpty() ? mut.getTargets().get(0) : null; + + // On each repeat recharge shots before cast. + if (mut.getCount() > 0) { + rechargeShots(mut.getSkill().useSoulShot(), mut.getSkill().useSpiritShot()); + } + + // Attack target after skill use + if ((skill.nextActionIsAttack()) && (getTarget() instanceof L2Character) && (getTarget() != this) && (target != null) && (getTarget() == target) && target.canBeAttacked()) { + if ((getAI().getNextIntention() == null) || (getAI().getNextIntention().getCtrlIntention() != CtrlIntention.AI_INTENTION_MOVE_TO)) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, target); + } + } + + if (skill.isBad() && (skill.getTargetType() != TargetType.DOOR_TREASURE)) { + getAI().clientStartAutoAttack(); + } + + // Notify the AI of the L2Character with EVT_FINISH_CASTING + getAI().notifyEvent(CtrlEvent.EVT_FINISH_CASTING); + + // Notify DP Scripts + notifyQuestEventSkillFinished(skill, target); + + // If character is a player, then wipe their current cast state and check if a skill is queued. + // If there is a queued skill, launch it and wipe the queue. + if (isPlayer()) { + SkillUseHolder queuedSkill = player.getQueuedSkill(); + + player.setCurrentSkill(null, false, false); + + if (queuedSkill != null) { + player.setQueuedSkill(null, false, false); + + // DON'T USE : Recursive call to useMagic() method + // player.useMagic(queuedSkill.getSkill(), queuedSkill.isCtrlPressed(), queuedSkill.isShiftPressed()); + ThreadPoolManager.getInstance().executeGeneral(new QueuedMagicUseTask(player, queuedSkill.getSkill(), queuedSkill.isCtrlPressed(), queuedSkill.isShiftPressed())); + } + } + + if (isChanneling()) { + getSkillChannelizer().stopChanneling(); + } + } + + // Quest event ON_SPELL_FNISHED + protected void notifyQuestEventSkillFinished(Skill skill, L2Object target) { + + } + + /** + * Launch the magic skill and calculate its effects on each target contained in the targets table. + * @param skill the skill + * @param targets the targets + */ + public void callSkill(Skill skill, List targets) { + try { + L2Weapon activeWeapon = getActiveWeaponItem(); + + // Check if the toggle skill effects are already in progress on the L2Character + if (skill.isToggle() && isAffectedBySkill(skill.getId())) { + return; + } + + // Initial checks + for (L2Object obj : targets) { + if ((obj == null) || !obj.isCharacter()) { + continue; + } + + final L2Character target = (L2Character) obj; + // Check raid monster attack and check buffing characters who attack raid monsters. + L2Character targetsAttackTarget = null; + L2Character targetsCastTarget = null; + if (target.hasAI()) { + targetsAttackTarget = target.getAI().getAttackTarget(); + targetsCastTarget = target.getAI().getCastTarget(); + } + + if (npc().raidCurse() && ((target.isRaid() && target.giveRaidCurse() && (getLevel() > (target.getLevel() + 8))) + || (!skill.isBad() && (targetsAttackTarget != null) && targetsAttackTarget.isRaid() && targetsAttackTarget.giveRaidCurse() && targetsAttackTarget.getAttackByList().contains(target) && (getLevel() > (targetsAttackTarget.getLevel() + 8))) + || (!skill.isBad() && (targetsCastTarget != null) && targetsCastTarget.isRaid() && targetsCastTarget.giveRaidCurse() && targetsCastTarget.getAttackByList().contains(target) && (getLevel() > (targetsCastTarget.getLevel() + 8))))) { + final CommonSkill curse = skill.isMagic() ? CommonSkill.RAID_CURSE : CommonSkill.RAID_CURSE2; + Skill curseSkill = curse.getSkill(); + if (curseSkill != null) { + abortAttack(); + abortCast(); + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + curseSkill.applyEffects(target, this); + } else { + LOG.warn("Skill ID {} level {} is missing in DP!", curse.getId(), curse.getLevel()); + } + return; + } + + // Check if over-hit is possible + if (skill.isOverhit()) { + if (target.isAttackable()) { + ((L2Attackable) target).overhitEnabled(true); + } + } + + // Static skills not trigger any chance skills + if (!skill.isStatic()) { + // Launch weapon Special ability skill effect if available + if ((activeWeapon != null) && !target.isDead()) { + activeWeapon.castOnMagicSkill(this, target, skill); + } + + if (_triggerSkills != null) { + for (OptionsSkillHolder holder : _triggerSkills.values()) { + if ((skill.isMagic() && (holder.getSkillType() == OptionsSkillType.MAGIC)) || (skill.isPhysical() && (holder.getSkillType() == OptionsSkillType.ATTACK))) { + if (Rnd.get(100) < holder.getChance()) { + makeTriggerCast(holder.getSkill(), target); + } + } + } + } + } + } + + // Launch the magic skill and calculate its effects + skill.activateSkill(this, targets); + + L2PcInstance player = getActingPlayer(); + if (player != null) { + for (L2Object target : targets) { + // EVT_ATTACKED and PvPStatus + if (target instanceof L2Character creature) { + if (skill.getEffectPoint() <= 0) { + if (target.isPlayable() || target.isTrap()) { + // Casted on target_self but don't harm self + if (!target.equals(this)) { + // Combat-mode check + if (target.isPlayer()) { + target.getActingPlayer().getAI().clientStartAutoAttack(); + } else if (target.isSummon() && creature.hasAI()) { + L2PcInstance owner = ((L2Summon) target).getOwner(); + if (owner != null) { + owner.getAI().clientStartAutoAttack(); + } + } + + // attack of the own pet does not flag player + // triggering trap not flag trap owner + if ((player.getSummon() != target) && !isTrap() && skill.isBad()) { + player.updatePvPStatus(creature); + } + } + } else if (target.isAttackable()) { + switch (skill.getId()) { + case 51: // Lure + case 511: // Temptation + break; + default: + // add attacker into list + creature.addAttackerToAttackByList(this); + } + } + // notify target AI about the attack + if (creature.hasAI() && !skill.hasEffectType(L2EffectType.HATE)) { + creature.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this); + } + } else { + if (target.isPlayer()) { + // Casting non offensive skill on player with pvp flag set or with karma + if (!(target.equals(this) || target.equals(player)) && ((target.getActingPlayer().getPvpFlag() > 0) || (target.getActingPlayer().getKarma() > 0))) { + player.updatePvPStatus(); + } + } else if (target.isAttackable()) { + player.updatePvPStatus(); + } + } + } + } + + // Mobs in range 1000 see spell + Collection objs = player.getKnownList().getKnownObjects().values(); + for (L2Object spMob : objs) { + if ((spMob != null) && spMob.isNpc()) { + final L2Npc npcMob = (L2Npc) spMob; + if ((npcMob.isInsideRadius(player, 1000, true, true))) { + EventDispatcher.getInstance().notifyEventAsync(new NpcSkillSee(npcMob, player, skill, targets, isSummon()), npcMob); + + // On Skill See logic + if (npcMob.isAttackable()) { + final L2Attackable attackable = (L2Attackable) npcMob; + + int skillEffectPoint = skill.getEffectPoint(); + + if (player.hasSummon()) { + if ((targets.size() == 1) && targets.contains(player.getSummon())) { + skillEffectPoint = 0; + } + } + + if (skillEffectPoint > 0) { + if (attackable.hasAI() && (attackable.getAI().getIntention() == AI_INTENTION_ATTACK)) { + L2Object npcTarget = attackable.getTarget(); + for (L2Object skillTarget : targets) { + if ((npcTarget == skillTarget) || (npcMob == skillTarget)) { + L2Character originalCaster = isSummon() ? getSummon() : player; + attackable.addDamageHate(originalCaster, 0, (skillEffectPoint * 150) / (attackable.getLevel() + 7)); + } + } + } + } + } + } + } + } + } + + // Notify AI + if (skill.isBad() && !skill.hasEffectType(L2EffectType.HATE)) { + for (L2Object target : targets) { + if (target.isCharacter()) { + final L2Character creature = (L2Character) target; + if (creature.hasAI()) { + // Notify target AI about the attack + creature.getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, this); + } + } + } + } + } catch (Exception e) { + LOG.warn("callSkill() failed. skill={} actor(id={}, name={})", skill.getId(), getObjectId(), getName(), e); + } + } + + /** + * @param target + * @return True if the L2Character is behind the target and can't be seen. + */ + public boolean isBehind(L2Object target) { + double angleChar, angleTarget, angleDiff, maxAngleDiff = 60; + if (target instanceof L2Character creature) { + angleChar = Util.calculateAngleFrom(this, creature); + angleTarget = Util.convertHeadingToDegree(creature.getHeading()); + angleDiff = angleChar - angleTarget; + if (angleDiff <= (-360 + maxAngleDiff)) { + angleDiff += 360; + } + if (angleDiff >= (360 - maxAngleDiff)) { + angleDiff -= 360; + } + return Math.abs(angleDiff) <= maxAngleDiff; + } + return false; + } + + public boolean isBehindTarget() { + return isBehind(getTarget()); + } + + /** + * @param target + * @return True if the target is facing the L2Character. + */ + public boolean isInFrontOf(L2Character target) { + double angleChar, angleTarget, angleDiff, maxAngleDiff = 60; + if (target == null) { + return false; + } + + angleTarget = Util.calculateAngleFrom(target, this); + angleChar = Util.convertHeadingToDegree(target.getHeading()); + angleDiff = angleChar - angleTarget; + if (angleDiff <= (-360 + maxAngleDiff)) { + angleDiff += 360; + } + if (angleDiff >= (360 - maxAngleDiff)) { + angleDiff -= 360; + } + return Math.abs(angleDiff) <= maxAngleDiff; + } + + /** + * @param target + * @param maxAngle + * @return true if target is in front of L2Character (shield def etc) + */ + public boolean isFacing(L2Object target, int maxAngle) { + double angleChar, angleTarget, angleDiff, maxAngleDiff; + if (target == null) { + return false; + } + maxAngleDiff = maxAngle / 2.; + angleTarget = Util.calculateAngleFrom(this, target); + angleChar = Util.convertHeadingToDegree(getHeading()); + angleDiff = angleChar - angleTarget; + if (angleDiff <= (-360 + maxAngleDiff)) { + angleDiff += 360; + } + if (angleDiff >= (360 - maxAngleDiff)) { + angleDiff -= 360; + } + return Math.abs(angleDiff) <= maxAngleDiff; + } + + public boolean isInFrontOfTarget() { + L2Object target = getTarget(); + if (target instanceof L2Character creature) { + return isInFrontOf(creature); + } + return false; + } + + /** + * @return the Level Modifier ((level + 89) / 100). + */ + public double getLevelMod() { + return ((getLevel() + 89) / 100d); + } + + public final void setSkillCast(Future newSkillCast) { + _skillCast = newSkillCast; + } + + /** + * Sets _isCastingNow to true and _castInterruptTime is calculated from end time (ticks) + * @param newSkillCastEndTick + */ + public final void forceIsCasting(int newSkillCastEndTick) { + setIsCastingNow(true); + // for interrupt -400 ms + _castInterruptTime = newSkillCastEndTick - (400 / GameTimeController.MILLIS_IN_TICK); + } + + private boolean _AIdisabled = false; + + public void updatePvPFlag(int value) { + // Overridden in L2PcInstance + } + + /** + * @return a multiplier based on weapon random damage + */ + public final double getRandomDamageMultiplier() { + L2Weapon activeWeapon = getActiveWeaponItem(); + int random; + + if (activeWeapon != null) { + random = activeWeapon.getRandomDamage(); + } else { + random = 5 + (int) Math.sqrt(getLevel()); + } + + return (1 + ((double) Rnd.get(0 - random, random) / 100)); + } + + public final long getAttackEndTime() { + return _attackEndTime; + } + + /** + * Not Implemented. + * @return + */ + public abstract int getLevel(); + + /** + * Check minimum level what creature can be.
      + * {@link L2PetInstance} use it, some pet cannot drop under specific level. + * @return Minimum level what can be current Character. + */ + public int getMinLevel() { + return 1; + } + + public final double calcStat(Stats stat, double init) { + return getStat().calcStat(stat, init, null, null); + } + + // Stat - NEED TO REMOVE ONCE L2CHARSTAT IS COMPLETE + public final double calcStat(Stats stat, double init, L2Character target, Skill skill) { + return getStat().calcStat(stat, init, target, skill); + } + + public int getAccuracy() { + return getStat().getAccuracy(); + } + + public final float getAttackSpeedMultiplier() { + return getStat().getAttackSpeedMultiplier(); + } + + public final double getCriticalDmg(L2Character target, double init) { + return getStat().getCriticalDmg(target, init); + } + + public int getCriticalHit(L2Character target, Skill skill) { + return getStat().getCriticalHit(target, skill); + } + + public int getEvasionRate(L2Character target) { + return getStat().getEvasionRate(target); + } + + public final int getMagicalAttackRange(Skill skill) { + return getStat().getMagicalAttackRange(skill); + } + + public final int getMaxCp() { + return getStat().getMaxCp(); + } + + public final int getMaxRecoverableCp() { + return getStat().getMaxRecoverableCp(); + } + + public double getMAtk(L2Character target, Skill skill) { + return getStat().getMAtk(target, skill); + } + + public int getMAtkSpd() { + return getStat().getMAtkSpd(); + } + + public int getMaxMp() { + return getStat().getMaxMp(); + } + + public int getMaxRecoverableMp() { + return getStat().getMaxRecoverableMp(); + } + + public int getMaxHp() { + return (customs().championEnable() && isChampion()) ? getStat().getMaxHp() * customs().getChampionHp() : getStat().getMaxHp(); + } + + public int getMaxRecoverableHp() { + return getStat().getMaxRecoverableHp(); + } + + public final int getMCriticalHit(L2Character target, Skill skill) { + return getStat().getMCriticalHit(target, skill); + } + + public double getMDef(L2Character target, Skill skill) { + return getStat().getMDef(target, skill); + } + + public double getMReuseRate(Skill skill) { + return getStat().getMReuseRate(skill); + } + + public double getPAtk(L2Character target) { + return getStat().getPAtk(target); + } + + public double getPAtkSpd() { + return getStat().getPAtkSpd(); + } + + public double getPDef(L2Character target) { + return getStat().getPDef(target); + } + + public final int getPhysicalAttackRange() { + return getStat().getPhysicalAttackRange(); + } + + public double getMovementSpeedMultiplier() { + return getStat().getMovementSpeedMultiplier(); + } + + public double getRunSpeed() { + return getStat().getRunSpeed(); + } + + public double getWalkSpeed() { + return getStat().getWalkSpeed(); + } + + public final double getSwimRunSpeed() { + return getStat().getSwimRunSpeed(); + } + + public final double getSwimWalkSpeed() { + return getStat().getSwimWalkSpeed(); + } + + public double getMoveSpeed() { + return getStat().getMoveSpeed(); + } + + public final int getShldDef() { + return getStat().getShldDef(); + } + + public int getSTR() { + return getStat().getSTR(); + } + + public int getDEX() { + return getStat().getDEX(); + } + + public int getCON() { + return getStat().getCON(); + } + + public int getINT() { + return getStat().getINT(); + } + + public int getWIT() { + return getStat().getWIT(); + } + + public int getMEN() { + return getStat().getMEN(); + } + + // Status - NEED TO REMOVE ONCE L2CHARTATUS IS COMPLETE + public void addStatusListener(L2Character object) { + getStatus().addStatusListener(object); + } + + public void reduceCurrentHp(double damage, L2Character attacker, Skill skill) { + reduceCurrentHp(damage, attacker, true, false, skill); + } + + public void reduceCurrentHpByDOT(double damage, L2Character attacker, Skill skill) { + reduceCurrentHp(damage, attacker, !skill.isToggle(), true, skill); + } + + public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + getStatus().reduceHp(damage, attacker, awake, isDOT, false); + } + + public void reduceCurrentMp(double damage) { + getStatus().reduceMp(damage); + } + + @Override + public void removeStatusListener(L2Character object) { + getStatus().removeStatusListener(object); + } + + public void stopHpMpRegeneration() { + getStatus().stopHpMpRegeneration(); + } + + public final double getCurrentCp() { + return getStatus().getCurrentCp(); + } + + public final void setCurrentCp(double newCp) { + getStatus().setCurrentCp(newCp); + } + + public final double getCurrentHp() { + return getStatus().getCurrentHp(); + } + + public final void setCurrentHp(double newHp) { + getStatus().setCurrentHp(newHp); + } + + public final int getHpPercentage() { + return (int) ((getCurrentHp() / getMaxHp()) * 100.0); + } + + public final int getMpPercentage() { + return (int) ((getCurrentMp() / getMaxMp()) * 100.0); + } + + public final void setCurrentHpMp(double newHp, double newMp) { + getStatus().setCurrentHpMp(newHp, newMp); + } + + public final double getCurrentMp() { + return getStatus().getCurrentMp(); + } + + public final void setCurrentMp(double newMp) { + getStatus().setCurrentMp(newMp); + } + + /** + * @return the max weight that the L2Character can load. + */ + public int getMaxLoad() { + return 0; + } + + public int getBonusWeightPenalty() { + return 0; + } + + /** + * @return the current weight of the L2Character. + */ + public int getCurrentLoad() { + return 0; + } + + public boolean isChampion() { + return false; + } + + /** + * Send system message about damage. + * @param target + * @param damage + * @param mcrit + * @param pcrit + * @param miss + */ + public void sendDamageMessage(L2Character target, int damage, boolean mcrit, boolean pcrit, boolean miss) { + if (miss && target.isPlayer()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_EVADED_C2_ATTACK); + sm.addPcName(target.getActingPlayer()); + sm.addCharName(this); + target.sendPacket(sm); + } + } + + public byte getAttackElement() { + return getStat().getAttackElement(); + } + + public int getAttackElementValue(byte attackAttribute) { + return getStat().getAttackElementValue(attackAttribute); + } + + public int getDefenseElementValue(byte defenseAttribute) { + return getStat().getDefenseElementValue(defenseAttribute); + } + + public final void startPhysicalAttackMuted() { + abortAttack(); + } + + public void disableCoreAI(boolean val) { + _AIdisabled = val; + } + + public boolean isCoreAIDisabled() { + return _AIdisabled; + } + + /** + * @return true + */ + public boolean giveRaidCurse() { + return true; + } + + /** + * Check if target is affected with special buff + * @see CharEffectList#isAffected(EffectFlag) + * @param flag int + * @return boolean + */ + public boolean isAffected(EffectFlag flag) { + return _effectList.isAffected(flag); + } + + public void broadcastSocialAction(int id) { + broadcastPacket(new SocialAction(getObjectId(), id)); + } + + public Team getTeam() { + return _team; + } + + public void setTeam(Team team) { + _team = team; + } + + public void addOverrideCond(PcCondOverride... excs) { + for (PcCondOverride exc : excs) { + _exceptions |= exc.getMask(); + } + } + + public void removeOverridedCond(PcCondOverride... excs) { + for (PcCondOverride exc : excs) { + _exceptions &= ~exc.getMask(); + } + } + + public boolean canOverrideCond(PcCondOverride excs) { + return (_exceptions & excs.getMask()) == excs.getMask(); + } + + public void setOverrideCond(long masks) { + _exceptions = masks; + } + + public void setLethalable(boolean val) { + _lethalable = val; + } + + public boolean isLethalable() { + return _lethalable; + } + + public Map getTriggerSkills() { + if (_triggerSkills == null) { + synchronized (this) { + if (_triggerSkills == null) { + _triggerSkills = new ConcurrentHashMap<>(); + } + } + } + return _triggerSkills; + } + + public void addTriggerSkill(OptionsSkillHolder holder) { + getTriggerSkills().put(holder.getSkillId(), holder); + } + + public void removeTriggerSkill(OptionsSkillHolder holder) { + getTriggerSkills().remove(holder.getSkillId()); + } + + public void makeTriggerCast(Skill skill, L2Character target, boolean ignoreTargetType) { + try { + if ((skill == null)) { + return; + } + if (skill.checkCondition(this, target, false)) { + if (isSkillDisabled(skill)) { + return; + } + + if (skill.getReuseDelay() > 0) { + disableSkill(skill, skill.getReuseDelay()); + } + + final List targets = !ignoreTargetType ? skill.getTargets(this, target) : List.of(target); + if (targets.isEmpty()) { + return; + } + + for (L2Object obj : targets) { + if ((obj != null) && obj.isCharacter()) { + target = (L2Character) obj; + break; + } + } + + if (character().validateTriggerSkills() && isPlayable() && (target != null) && target.isPlayable()) { + final L2PcInstance player = getActingPlayer(); + if (!player.checkPvpSkill(target, skill)) { + return; + } + } + + broadcastPacket(new MagicSkillUse(this, target, skill.getDisplayId(), skill.getLevel(), 0, 0)); + broadcastPacket(new MagicSkillLaunched(this, skill.getDisplayId(), skill.getLevel(), targets)); + + // Launch the magic skill and calculate its effects + skill.activateSkill(this, targets); + } + } catch (Exception e) { + LOG.warn("{}", e); + } + } + + public void makeTriggerCast(Skill skill, L2Character target) { + makeTriggerCast(skill, target, false); + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return {@code true} if current player can revive and shows 'To Village' button upon death, {@code false} otherwise. + */ + public boolean canRevive() { + return true; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @param val + */ + public void setCanRevive(boolean val) { + } + + /** + * Dummy method overridden in {@link L2Attackable} + * @return {@code true} if there is a loot to sweep, {@code false} otherwise. + */ + public boolean isSweepActive() { + return false; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return {@code true} if player is on event, {@code false} otherwise. + */ + public boolean isOnEvent() { + return false; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return the clan id of current character. + */ + public int getClanId() { + return 0; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return the clan of current character. + */ + public L2Clan getClan() { + return null; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return {@code true} if player is in academy, {@code false} otherwise. + */ + public boolean isAcademyMember() { + return false; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return the pledge type of current character. + */ + public int getPledgeType() { + return 0; + } + + /** + * Dummy method overridden in {@link L2PcInstance} + * @return the alliance id of current character. + */ + public int getAllyId() { + return 0; + } + + /** + * Notifies to listeners that current character received damage. + * @param damage + * @param attacker + * @param skill + * @param critical + * @param damageOverTime + */ + public void notifyDamageReceived(double damage, L2Character attacker, Skill skill, boolean critical, boolean damageOverTime, boolean isReflect) { + EventDispatcher.getInstance().notifyEventAsync(new CreatureDamageReceived(attacker, this, damage, skill, critical, damageOverTime, isReflect), this); + EventDispatcher.getInstance().notifyEventAsync(new CreatureDamageDealt(attacker, this, damage, skill, critical, damageOverTime, isReflect), attacker); + } + + /** + * Notifies to listeners that current character avoid attack. + * @param target + * @param isDot + */ + public void notifyAttackAvoid(final L2Character target, final boolean isDot) { + EventDispatcher.getInstance().notifyEventAsync(new CreatureAttackAvoid(this, target, isDot), target); + } + + /** + * @return {@link WeaponType} of current character's weapon or basic weapon type. + */ + public final WeaponType getAttackType() { + if (isTransformed()) { + final TransformTemplate template = getTransformation().getTemplate(getActingPlayer()); + if (template != null) { + return template.getBaseAttackType(); + } + } + final L2Weapon weapon = getActiveWeaponItem(); + if (weapon != null) { + return weapon.getItemType(); + } + return getTemplate().getBaseAttackType(); + } + + public final boolean isInCategory(CategoryType type) { + return CategoryData.getInstance().isInCategory(type, getId()); + } + + /** + * @return the character that summoned this NPC. + */ + public L2Character getSummoner() { + return _summoner; + } + + /** + * @param summoner the summoner of this NPC. + */ + public void setSummoner(L2Character summoner) { + _summoner = summoner; + } + + @Override + public boolean isCharacter() { + return true; + } + + /** + * @return {@code true} if current character is casting channeling skill, {@code false} otherwise. + */ + public final boolean isChanneling() { + return (_channelizer != null) && _channelizer.isChanneling(); + } + + public final SkillChannelizer getSkillChannelizer() { + if (_channelizer == null) { + _channelizer = new SkillChannelizer(this); + } + return _channelizer; + } + + /** + * @return {@code true} if current character is affected by channeling skill, {@code false} otherwise. + */ + public final boolean isChannelized() { + return (_channelized != null) && !_channelized.isChannelized(); + } + + public final SkillChannelized getSkillChannelized() { + if (_channelized == null) { + _channelized = new SkillChannelized(); + } + return _channelized; + } + + public void addInvulAgainst(SkillHolder holder) { + final InvulSkillHolder invulHolder = getInvulAgainstSkills().get(holder.getSkillId()); + if (invulHolder != null) { + invulHolder.increaseInstances(); + return; + } + getInvulAgainstSkills().put(holder.getSkillId(), new InvulSkillHolder(holder)); + } + + public void removeInvulAgainst(SkillHolder holder) { + final InvulSkillHolder invulHolder = getInvulAgainstSkills().get(holder.getSkillId()); + if ((invulHolder != null) && (invulHolder.decreaseInstances() < 1)) { + getInvulAgainstSkills().remove(holder.getSkillId()); + } + } + + public boolean isInvulAgainst(int skillId, int skillLvl) { + if (_invulAgainst != null) { + final SkillHolder holder = getInvulAgainstSkills().get(skillId); + return ((holder != null) && ((holder.getSkillLvl() < 1) || (holder.getSkillLvl() == skillLvl))); + } + return false; + } + + private Map getInvulAgainstSkills() { + if (_invulAgainst == null) { + synchronized (this) { + if (_invulAgainst == null) { + _invulAgainst = new ConcurrentHashMap<>(); + } + } + } + return _invulAgainst; + } + + @Override + public Queue getListeners(EventType type) { + final Queue objectListenres = super.getListeners(type); + final Queue templateListeners = getTemplate().getListeners(type); + final Queue globalListeners = isNpc() && !isMonster() ? Containers.Npcs().getListeners(type) : isMonster() ? Containers.Monsters().getListeners(type) : isPlayer() ? Containers.Players().getListeners(type) : EmptyQueue.emptyQueue(); + + // Attempt to do not create collection + if (objectListenres.isEmpty() && templateListeners.isEmpty() && globalListeners.isEmpty()) { + return EmptyQueue.emptyQueue(); + } else if (!objectListenres.isEmpty() && templateListeners.isEmpty() && globalListeners.isEmpty()) { + return objectListenres; + } else if (!templateListeners.isEmpty() && objectListenres.isEmpty() && globalListeners.isEmpty()) { + return templateListeners; + } else if (!globalListeners.isEmpty() && objectListenres.isEmpty() && templateListeners.isEmpty()) { + return globalListeners; + } + + final Queue both = new LinkedBlockingDeque<>(objectListenres.size() + templateListeners.size() + globalListeners.size()); + both.addAll(objectListenres); + both.addAll(templateListeners); + both.addAll(globalListeners); + return both; + } + + public Race getRace() { + return getTemplate().getRace(); + } + + public boolean isInDuel() { + return false; + } + + public int getDuelId() { + return 0; + } + + public byte getSiegeState() { + return 0; + } + + public int getSiegeSide() { + return 0; + } + + /** + * Send a normal message to all L2PcInstance in the known list.
      + * @param msg String with message + */ + public void say(String msg) { + broadcastPacket(new CreatureSay(getObjectId(), isPlayer() ? Say2.ALL : Say2.NPC_ALL, getName(), msg)); + } + + /** + * Send a client message to all L2PcInstance in the known list.
      + * @param msg NpcString from client + */ + public void say(NpcStringId msg) { + broadcastPacket(new CreatureSay(getObjectId(), isPlayer() ? Say2.ALL : Say2.NPC_ALL, getName(), msg)); + } + + /** + * Send a shout message (orange chat) to all L2PcInstance in the known list.
      + * @param msg String with message + */ + public void shout(String msg) { + broadcastPacket(new CreatureSay(getObjectId(), isPlayer() ? Say2.SHOUT : Say2.NPC_SHOUT, getName(), msg)); + } + + /** + * Send a shout message (orange chat) to all L2PcInstance in the known list.
      + * @param msg NpcString from client + */ + public void shout(NpcStringId msg) { + broadcastPacket(new CreatureSay(getObjectId(), isPlayer() ? Say2.SHOUT : Say2.NPC_SHOUT, getName(), msg)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Decoy.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Decoy.java new file mode 100644 index 00000000..7f3f0b69 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Decoy.java @@ -0,0 +1,152 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.CharInfo; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; + +public abstract class L2Decoy extends L2Character { + private final L2PcInstance _owner; + + public L2Decoy(int objectId, L2CharTemplate template, L2PcInstance owner) { + super(objectId, template); + setInstanceType(InstanceType.L2Decoy); + _owner = owner; + setXYZInvisible(owner.getX(), owner.getY(), owner.getZ()); + setIsInvul(false); + } + + @Override + public void onSpawn() { + super.onSpawn(); + sendPacket(new CharInfo(this)); + } + + @Override + public void updateAbnormalEffect() { + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + if (player != null) { + player.sendPacket(new CharInfo(this)); + } + } + } + + public void stopDecay() { + DecayTaskManager.getInstance().cancel(this); + } + + @Override + public void onDecay() { + deleteMe(_owner); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return _owner.isAutoAttackable(attacker); + } + + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + @Override + public final int getId() { + return getTemplate().getId(); + } + + @Override + public int getLevel() { + return getTemplate().getLevel(); + } + + public void deleteMe(L2PcInstance owner) { + decayMe(); + getKnownList().removeAllKnownObjects(); + owner.setDecoy(null); + } + + public synchronized void unSummon(L2PcInstance owner) { + + if (isVisible() && !isDead()) { + if (getWorldRegion() != null) { + getWorldRegion().removeFromZones(this); + } + owner.setDecoy(null); + decayMe(); + getKnownList().removeAllKnownObjects(); + } + } + + public final L2PcInstance getOwner() { + return _owner; + } + + @Override + public L2PcInstance getActingPlayer() { + return _owner; + } + + @Override + public L2NpcTemplate getTemplate() { + return (L2NpcTemplate) super.getTemplate(); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + activeChar.sendPacket(new CharInfo(this)); + } + + @Override + public void sendPacket(L2GameServerPacket mov) { + if (getOwner() != null) { + getOwner().sendPacket(mov); + } + } + + @Override + public void sendPacket(SystemMessageId id) { + if (getOwner() != null) { + getOwner().sendPacket(id); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Npc.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Npc.java new file mode 100644 index 00000000..858336c9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Npc.java @@ -0,0 +1,1619 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_ACTIVE; +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.config.Configuration.olympiad; +import static com.l2jserver.gameserver.config.Configuration.rates; +import static com.l2jserver.gameserver.enums.InstanceType.L2Npc; +import static com.l2jserver.gameserver.model.events.EventType.NPC_EVENT_RECEIVED; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ItemsAutoDestroy; +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.SevenSignsFestival; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.cache.HtmCache; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.datatables.NpcPersonalAIData; +import com.l2jserver.gameserver.enums.AISkillScope; +import com.l2jserver.gameserver.enums.AIType; +import com.l2jserver.gameserver.enums.PrivateStoreType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.enums.ShotType; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.handler.BypassHandler; +import com.l2jserver.gameserver.handler.IBypassHandler; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.ClanHallSiegeManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.TownManager; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.instance.L2ClanHallManagerInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoormenInstance; +import com.l2jserver.gameserver.model.actor.instance.L2MerchantInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2TeleporterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2WarehouseInstance; +import com.l2jserver.gameserver.model.actor.knownlist.NpcKnownList; +import com.l2jserver.gameserver.model.actor.stat.NpcStat; +import com.l2jserver.gameserver.model.actor.status.NpcStatus; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.EventType; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcCanBeSeen; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcEventReceived; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcSkillFinished; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcSpawn; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcTeleport; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.olympiad.Olympiad; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.variables.NpcVariables; +import com.l2jserver.gameserver.model.zone.type.L2TownZone; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExChangeNpcState; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.ServerObjectInfo; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * This class represents a Non-Player-Character in the world.
      + * It can be a monster or a friendly character.
      + * It uses a template to fetch some static values. + */ +public class L2Npc extends L2Character { + private static final Logger LOG = LoggerFactory.getLogger(L2Npc.class); + /** The interaction distance of the L2NpcInstance(is used as offset in MovetoLocation method) */ + public static final int INTERACTION_DISTANCE = 150; + /** Maximum distance where the drop may appear given this NPC position. */ + public static final int RANDOM_ITEM_DROP_LIMIT = 70; + /** The L2Spawn object that manage this L2NpcInstance */ + private L2Spawn _spawn; + /** The flag to specify if this L2NpcInstance is busy */ + private boolean _isBusy = false; + /** The busy message for this L2NpcInstance */ + private String _busyMessage = ""; + /** True if endDecayTask has already been called */ + private volatile boolean _isDecayed = false; + /** The castle index in the array of L2Castle this L2NpcInstance belongs to */ + private int _castleIndex = -2; + /** The fortress index in the array of L2Fort this L2NpcInstance belongs to */ + private int _fortIndex = -2; + private boolean _eventMob = false; + private boolean _isInTown = false; + /** True if this L2Npc is autoattackable **/ + private boolean _isAutoAttackable = false; + /** Time of last social packet broadcast */ + private long _lastSocialBroadcast = 0; + /** Minimum interval between social packets */ + private static final int MINIMUM_SOCIAL_INTERVAL = 6000; + /** Support for random animation switching */ + private boolean _isRandomAnimationEnabled = true; + private boolean _isTalking = true; + + protected RandomAnimationTask _rAniTask = null; + private int _currentLHandId; // normally this shouldn't change from the template, but there exist exceptions + private int _currentRHandId; // normally this shouldn't change from the template, but there exist exceptions + private int _currentEnchant; // normally this shouldn't change from the template, but there exist exceptions + private double _currentCollisionHeight; // used for npc grow effect skills + private double _currentCollisionRadius; // used for npc grow effect skills + + private int _soulshotamount = 0; + private int _spiritshotamount = 0; + private int _displayEffect = 0; + + private int _shotsMask = 0; + private int _killingBlowWeaponId; + /** Map of summoned NPCs by this NPC. */ + private volatile Map _summonedNpcs = null; + + /** + * Creates a NPC. + * @param template the NPC template + */ + public L2Npc(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(L2Npc); + initCharStatusUpdateValues(); + + // initialize the "current" equipment + _currentLHandId = getTemplate().getLHandId(); + _currentRHandId = getTemplate().getRHandId(); + _currentEnchant = npc().randomEnchantEffect() ? Rnd.get(4, 21) : getTemplate().getWeaponEnchant(); + + // initialize the "current" collisions + _currentCollisionHeight = getTemplate().getfCollisionHeight(); + _currentCollisionRadius = getTemplate().getfCollisionRadius(); + + setIsFlying(template.isFlying()); + + // Copy the skills from template. + for (Skill skill : template.getSkills().values()) { + addSkill(skill); + } + } + + /** + * Creates a NPC. + * @param npcId the NPC ID + */ + public L2Npc(int objectId, int npcId) { + this(objectId, NpcData.getInstance().getTemplate(npcId)); + } + + public int getSoulShotChance() { + return getTemplate().getSoulShotChance(); + } + + public int getSpiritShotChance() { + return getTemplate().getSpiritShotChance(); + } + + /** + * Verifies if the NPC can cast a skill given the minimum and maximum skill chances. + * @return {@code true} if the NPC has chances of casting a skill + */ + public boolean hasSkillChance() { + return Rnd.get(100) < Rnd.get(getTemplate().getMinSkillChance(), getTemplate().getMaxSkillChance()); + } + + public boolean canMove() { + return getTemplate().canMove(); + } + + public boolean isChaos() { + return getTemplate().isChaos(); + } + + public List getLongRangeSkills() { + return getTemplate().getAISkills(AISkillScope.LONG_RANGE); + } + + public List getShortRangeSkills() { + return getTemplate().getAISkills(AISkillScope.SHORT_RANGE); + } + + /** Task launching the function onRandomAnimation() */ + protected static class RandomAnimationTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(RandomAnimationTask.class); + private final L2Npc _npc; + + protected RandomAnimationTask(L2Npc npc) { + _npc = npc; + } + + @Override + public void run() { + try { + if (_npc.isMob()) { + // Cancel further animation timers until intention is changed to ACTIVE again. + if (_npc.getAI().getIntention() != AI_INTENTION_ACTIVE) { + return; + } + } else { + if (!_npc.isInActiveRegion()) { + return; + } + } + + if (!(_npc.isDead() || _npc.isStunned() || _npc.isSleeping())) { + _npc.onRandomAnimation(Rnd.get(2, 3)); + } + + _npc.startRandomAnimationTimer(); + } catch (Exception e) { + LOG.error("There has been an error trying to perform a random animation for NPC {}!", _npc, e); + } + } + } + + /** + * Send a packet SocialAction to all L2PcInstance in the _KnownPlayers of the L2NpcInstance and create a new RandomAnimation Task. + * @param animationId + */ + public void onRandomAnimation(int animationId) { + // Send a packet SocialAction to all L2PcInstance in the _KnownPlayers of the L2NpcInstance + long now = System.currentTimeMillis(); + if ((now - _lastSocialBroadcast) > MINIMUM_SOCIAL_INTERVAL) { + _lastSocialBroadcast = now; + broadcastPacket(new SocialAction(getObjectId(), animationId)); + } + } + + /** + * Create a RandomAnimation Task that will be launched after the calculated delay. + */ + public void startRandomAnimationTimer() { + if (!hasRandomAnimation()) { + return; + } + + int minWait = isMob() ? general().getMinMonsterAnimation() : general().getMinNPCAnimation(); + int maxWait = isMob() ? general().getMaxMonsterAnimation() : general().getMaxNPCAnimation(); + + // Calculate the delay before the next animation + int interval = Rnd.get(minWait, maxWait) * 1000; + + // Create a RandomAnimation Task that will be launched after the calculated delay + _rAniTask = new RandomAnimationTask(this); + ThreadPoolManager.getInstance().scheduleGeneral(_rAniTask, interval); + } + + /** + * @return true if the server allows Random Animation. + */ + public boolean hasRandomAnimation() { + return ((general().getMaxNPCAnimation() > 0) && _isRandomAnimationEnabled && !getAiType().equals(AIType.CORPSE)); + } + + /** + * Switches random Animation state into val. + * @param val needed state of random animation + */ + public void setRandomAnimationEnabled(boolean val) { + _isRandomAnimationEnabled = val; + } + + /** + * @return {@code true}, if random animation is enabled, {@code false} otherwise. + */ + public boolean isRandomAnimationEnabled() { + return _isRandomAnimationEnabled; + } + + @Override + public NpcKnownList getKnownList() { + return (NpcKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new NpcKnownList(this)); + } + + @Override + public NpcStat getStat() { + return (NpcStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new NpcStat(this)); + } + + @Override + public NpcStatus getStatus() { + return (NpcStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new NpcStatus(this)); + } + + /** Return the L2NpcTemplate of the L2NpcInstance. */ + @Override + public final L2NpcTemplate getTemplate() { + return (L2NpcTemplate) super.getTemplate(); + } + + /** + * Gets the NPC ID. + * @return the NPC ID + */ + @Override + public int getId() { + return getTemplate().getId(); + } + + @Override + public boolean canBeAttacked() { + return npc().attackableNpcs(); + } + + /** + * Return the Level of this L2NpcInstance contained in the L2NpcTemplate. + */ + @Override + public final int getLevel() { + return getTemplate().getLevel(); + } + + /** + * Verifies if the NPC is aggressive. + * @return {@code true} if the NPC is aggressive, {@code false} otherwise + */ + public boolean isAggressive() { + return false; + } + + /** + * Gets the aggression range.
      + * Overridden by AI value, capped by MaxAggroRange. + * @return the aggression range + */ + public int getAggroRange() { + final int aiAggroRange = getAIValue("aggroRange"); + if (aiAggroRange >= 0) { + return aiAggroRange; + } + + final int maxAggroRange = npc().getMaxAggroRange(); + if (maxAggroRange > 0) { + return Math.min(getTemplate().getAggroRange(), maxAggroRange); + } + return getTemplate().getAggroRange(); + } + + public boolean isInMyClan(L2Npc npc) { + return getTemplate().isClan(npc.getTemplate().getClans()); + } + + /** + * Return True if this L2NpcInstance is undead in function of the L2NpcTemplate. + */ + @Override + public boolean isUndead() { + return getTemplate().getRace() == Race.UNDEAD; + } + + public boolean isSevenNpc() { + return getTemplate().isClan("C_DUNGEON") && !getTemplate().isClan("ALL"); + } + + /** + * Send a packet NpcInfo with state of abnormal effect to all L2PcInstance in the _KnownPlayers of the L2NpcInstance. + */ + @Override + public void updateAbnormalEffect() { + // Send a Server->Client packet NpcInfo with state of abnormal effect to all L2PcInstance in the _KnownPlayers of the L2NpcInstance + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if ((player == null) || !isVisibleFor(player)) { + continue; + } + if (getRunSpeed() == 0) { + player.sendPacket(new ServerObjectInfo(this, player)); + } else { + player.sendPacket(new AbstractNpcInfo.NpcInfo(this, player)); + } + } + } + + public boolean isEventMob() { + return _eventMob; + } + + public void setEventMob(boolean val) { + _eventMob = val; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return _isAutoAttackable; + } + + public void setAutoAttackable(boolean flag) { + _isAutoAttackable = flag; + } + + /** + * @return the Identifier of the item in the left hand of this L2NpcInstance contained in the L2NpcTemplate. + */ + public int getLeftHandItem() { + return _currentLHandId; + } + + /** + * @return the Identifier of the item in the right hand of this L2NpcInstance contained in the L2NpcTemplate. + */ + public int getRightHandItem() { + return _currentRHandId; + } + + public int getEnchantEffect() { + return _currentEnchant; + } + + /** + * @return the busy status of this L2NpcInstance. + */ + public final boolean isBusy() { + return _isBusy; + } + + /** + * @param isBusy the busy status of this L2Npc + */ + public void setBusy(boolean isBusy) { + _isBusy = isBusy; + } + + /** + * @return the busy message of this L2NpcInstance. + */ + public final String getBusyMessage() { + return _busyMessage; + } + + /** + * @param message the busy message of this L2Npc. + */ + public void setBusyMessage(String message) { + _busyMessage = message; + } + + /** + * @return true if this L2Npc instance can be warehouse manager. + */ + public boolean isWarehouse() { + return false; + } + + public boolean canTarget(L2PcInstance player) { + if (player.isOutOfControl()) { + player.sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + if (player.isLockedTarget() && (player.getLockedTarget() != this)) { + player.sendPacket(SystemMessageId.FAILED_CHANGE_TARGET); + player.sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + // TODO: More checks... + + return true; + } + + public boolean canInteract(L2PcInstance player) { + if (player.isCastingNow() || player.isCastingSimultaneouslyNow()) { + return false; + } else if (player.isDead() || player.isFakeDeath()) { + return false; + } else if (player.isSitting()) { + return false; + } else if (player.getPrivateStoreType() != PrivateStoreType.NONE) { + return false; + } else if (!isInsideRadius(player, INTERACTION_DISTANCE, true, false)) { + return false; + } else if ((player.getInstanceId() != getInstanceId()) && (player.getInstanceId() != -1)) { + return false; + } else if (isBusy()) { + return false; + } + return true; + } + + /** + * @return the L2Castle this L2NpcInstance belongs to. + */ + public final Castle getCastle() { + // Get castle this NPC belongs to (excluding L2Attackable) + if (_castleIndex < 0) { + L2TownZone town = TownManager.getTown(getX(), getY(), getZ()); + + if (town != null) { + _castleIndex = CastleManager.getInstance().getCastleIndex(town.getTaxById()); + } + + if (_castleIndex < 0) { + _castleIndex = CastleManager.getInstance().findNearestCastleIndex(this); + } else { + _isInTown = true; // Npc was spawned in town + } + } + + if (_castleIndex < 0) { + return null; + } + + return CastleManager.getInstance().getCastles().get(_castleIndex); + } + + /** + * Verify if the given player is this NPC's lord. + * @param player the player to check + * @return {@code true} if the player is clan leader and owner of a castle of fort that this NPC belongs to, {@code false} otherwise + */ + public boolean isMyLord(L2PcInstance player) { + if (player.isClanLeader()) { + final int castleId = getCastle() != null ? getCastle().getResidenceId() : -1; + final int fortId = getFort() != null ? getFort().getResidenceId() : -1; + return (player.getClan().getCastleId() == castleId) || (player.getClan().getFortId() == fortId); + } + return false; + } + + /** + * Verify if the give dominion Id is this NPC's dominion. + * @param dominionId the dominion Id to check + * @return {@code true} if the dominion Id matches the castle/fort Id, {@code false} otherwise + */ + public boolean isDominionOfLord(int dominionId) { + final int castleId = getCastle() != null ? getCastle().getResidenceId() : -1; + final int fortId = getFort() != null ? getFort().getResidenceId() : -1; + return (dominionId == castleId) || (dominionId == fortId); + } + + public final SiegableHall getConquerableHall() { + return ClanHallSiegeManager.getInstance().getNearbyClanHall(getX(), getY(), 10000); + } + + /** + * Return closest castle in defined distance + * @param maxDistance long + * @return Castle + */ + public final Castle getCastle(long maxDistance) { + int index = CastleManager.getInstance().findNearestCastleIndex(this, maxDistance); + + if (index < 0) { + return null; + } + + return CastleManager.getInstance().getCastles().get(index); + } + + /** + * @return the L2Fort this L2NpcInstance belongs to. + */ + public final Fort getFort() { + // Get Fort this NPC belongs to (excluding L2Attackable) + if (_fortIndex < 0) { + Fort fort = FortManager.getInstance().getFort(getX(), getY(), getZ()); + if (fort != null) { + _fortIndex = FortManager.getInstance().getFortIndex(fort.getResidenceId()); + } + + if (_fortIndex < 0) { + _fortIndex = FortManager.getInstance().findNearestFortIndex(this); + } + } + + if (_fortIndex < 0) { + return null; + } + + return FortManager.getInstance().getForts().get(_fortIndex); + } + + /** + * Return closest Fort in defined distance + * @param maxDistance long + * @return Fort + */ + public final Fort getFort(long maxDistance) { + int index = FortManager.getInstance().findNearestFortIndex(this, maxDistance); + + if (index < 0) { + return null; + } + + return FortManager.getInstance().getForts().get(index); + } + + public final boolean getIsInTown() { + if (_castleIndex < 0) { + getCastle(); + } + + return _isInTown; + } + + /** + * Open a quest or chat window on client with the text of the L2NpcInstance in function of the command.
      + * Example of use : + *
        + *
      • Client packet : RequestBypassToServer
      • + *
      + * @param player + * @param command The command string received from client + */ + public void onBypassFeedback(L2PcInstance player, String command) { + // if (canInteract(player)) + { + if (isBusy() && (getBusyMessage().length() > 0)) { + player.sendPacket(ActionFailed.STATIC_PACKET); + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), "data/html/npcbusy.htm"); + html.replace("%busymessage%", getBusyMessage()); + html.replace("%npcname%", getName()); + html.replace("%playername%", player.getName()); + player.sendPacket(html); + } else { + IBypassHandler handler = BypassHandler.getInstance().getHandler(command); + if (handler != null) { + handler.useBypass(command, player, this); + } else { + LOG.info("Unknown NPC bypass: \"{}\" NpcId: {}", command, getId()); + } + } + } + } + + /** + * Return null (regular NPCs don't have weapons instances). + */ + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + /** + * Return the weapon item equipped in the right hand of the L2NpcInstance or null. + */ + @Override + public L2Weapon getActiveWeaponItem() { + // Get the weapon identifier equipped in the right hand of the L2NpcInstance + int weaponId = getTemplate().getRHandId(); + + if (weaponId < 1) { + return null; + } + + // Get the weapon item equipped in the right hand of the L2NpcInstance + L2Item item = ItemTable.getInstance().getTemplate(getTemplate().getRHandId()); + + if (!(item instanceof L2Weapon)) { + return null; + } + + return (L2Weapon) item; + } + + /** + * Return null (regular NPCs don't have weapons instances). + */ + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + /** + * Return the weapon item equipped in the left hand of the L2NpcInstance or null. + */ + @Override + public L2Weapon getSecondaryWeaponItem() { + // Get the weapon identifier equipped in the right hand of the L2NpcInstance + int weaponId = getTemplate().getLHandId(); + if (weaponId < 1) { + return null; + } + + // Get the weapon item equipped in the right hand of the L2NpcInstance + L2Item item = ItemTable.getInstance().getTemplate(getTemplate().getLHandId()); + + if (!(item instanceof L2Weapon)) { + return null; + } + + return (L2Weapon) item; + } + + /** + * Send a Server->Client packet NpcHtmlMessage to the L2PcInstance in order to display the message of the L2NpcInstance. + * @param player The L2PcInstance who talks with the L2NpcInstance + * @param content The text of the L2NpcMessage + */ + public void insertObjectIdAndShowChatWindow(L2PcInstance player, String content) { + // Send a Server->Client packet NpcHtmlMessage to the L2PcInstance in order to display the message of the L2NpcInstance + content = content.replaceAll("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(new NpcHtmlMessage(getObjectId(), content)); + } + + /** + * : + *
        + *
      • if the file exists on the server (page number = 0) : data/html/default/12006.htm (npcId-page number)
      • + *
      • if the file exists on the server (page number > 0) : data/html/default/12006-1.htm (npcId-page number)
      • + *
      • if the file doesn't exist on the server : data/html/npcdefault.htm (message : "I have nothing to say to you")
      • + *
      + * @param npcId The Identifier of the L2NpcInstance whose text must be display + * @param val The number of the page to display + * @return the pathfile of the selected HTML file in function of the npcId and of the page number. + */ + public String getHtmlPath(int npcId, int val) { + String pom = ""; + + if (val == 0) { + pom = "" + npcId; + } else { + pom = npcId + "-" + val; + } + + String temp = "data/html/default/" + pom + ".htm"; + + if (!general().lazyCache()) { + // If not running lazy cache the file must be in the cache or it doesn't exist + if (HtmCache.getInstance().contains(temp)) { + return temp; + } + } else { + if (HtmCache.getInstance().isLoadable(temp)) { + return temp; + } + } + + // If the file is not found, the standard message "I have nothing to say to you" is returned + return "data/html/npcdefault.htm"; + } + + public void showChatWindow(L2PcInstance player) { + showChatWindow(player, 0); + } + + /** + * Returns true if html exists + * @param player + * @param type + * @return boolean + */ + private boolean showPkDenyChatWindow(L2PcInstance player, String type) { + final String html = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/" + type + "/" + getId() + "-pk.htm"); + if (html != null) { + insertObjectIdAndShowChatWindow(player, html); + player.sendPacket(ActionFailed.STATIC_PACKET); + return true; + } + return false; + } + + /** + * Open a chat window on client with the text of the L2NpcInstance.
      + * Actions: + *
        + *
      • Get the text of the selected HTML file in function of the npcId and of the page number
      • + *
      • Send a Server->Client NpcHtmlMessage containing the text of the L2NpcInstance to the L2PcInstance
      • + *
      • Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet
      • + *
      + * @param player The L2PcInstance that talk with the L2NpcInstance + * @param val The number of the page of the L2NpcInstance to display + */ + public void showChatWindow(L2PcInstance player, int val) { + if (!isTalking()) { + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + if (player.isCursedWeaponEquipped() && (!(player.getTarget() instanceof L2ClanHallManagerInstance) || !(player.getTarget() instanceof L2DoormenInstance))) { + player.setTarget(player); + return; + } + + if (player.getKarma() > 0) { + if (!character().karmaPlayerCanShop() && (this instanceof L2MerchantInstance)) { + if (showPkDenyChatWindow(player, "merchant")) { + return; + } + } else if (!character().karmaPlayerCanUseGK() && (this instanceof L2TeleporterInstance)) { + if (showPkDenyChatWindow(player, "teleporter")) { + return; + } + } else if (!character().karmaPlayerCanUseWareHouse() && (this instanceof L2WarehouseInstance)) { + if (showPkDenyChatWindow(player, "warehouse")) { + return; + } + } + } + + if (getTemplate().isType("L2Auctioneer") && (val == 0)) { + return; + } + + int npcId = getTemplate().getId(); + + /* For use with Seven Signs implementation */ + String filename = SevenSigns.SEVEN_SIGNS_HTML_PATH; + + switch (npcId) { + case 31127: // + case 31128: // + case 31129: // Dawn Festival Guides + case 31130: // + case 31131: // + filename += "festival/dawn_guide.htm"; + break; + case 31137: // + case 31138: // + case 31139: // Dusk Festival Guides + case 31140: // + case 31141: // + filename += "festival/dusk_guide.htm"; + break; + case 31132: + case 31133: + case 31134: + case 31135: + case 31136: // Festival Witches + case 31142: + case 31143: + case 31144: + case 31145: + case 31146: + filename += "festival/festival_witch.htm"; + break; + case 31688: + if (player.isNoble()) { + filename = Olympiad.OLYMPIAD_HTML_PATH + "noble_main.htm"; + } else { + filename = getHtmlPath(npcId, val); + } + break; + case 31690: + case 31769: + case 31770: + case 31771: + case 31772: + if (player.isHero() || player.isNoble()) { + filename = Olympiad.OLYMPIAD_HTML_PATH + "hero_main.htm"; + } else { + filename = getHtmlPath(npcId, val); + } + break; + case 36402: + if (player.getOlympiadBuffCount() > 0) { + filename = (player.getOlympiadBuffCount() == olympiad().getMaxBuffs() ? Olympiad.OLYMPIAD_HTML_PATH + "olympiad_buffs.htm" : Olympiad.OLYMPIAD_HTML_PATH + "olympiad_5buffs.htm"); + } else { + filename = Olympiad.OLYMPIAD_HTML_PATH + "olympiad_nobuffs.htm"; + } + break; + case 30298: // Blacksmith Pinter + if (player.isAcademyMember()) { + filename = getHtmlPath(npcId, 1); + } else { + filename = getHtmlPath(npcId, val); + } + break; + default: + if (((npcId >= 31093) && (npcId <= 31094)) || ((npcId >= 31172) && (npcId <= 31201)) || ((npcId >= 31239) && (npcId <= 31254))) { + return; + } + // Get the text of the selected HTML file in function of the npcId and of the page number + filename = getHtmlPath(npcId, val); + break; + } + + // Send a Server->Client NpcHtmlMessage containing the text of the L2NpcInstance to the L2PcInstance + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + + if (this instanceof L2MerchantInstance) { + if (npc().getPetRentNPCs().contains(npcId)) { + html.replace("_Quest", "_RentPet\">Rent Pet

      Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + /** + * Open a chat window on client with the text specified by the given file name and path, relative to the datapack root. + * @param player The L2PcInstance that talk with the L2NpcInstance + * @param filename The filename that contains the text to send + */ + public void showChatWindow(L2PcInstance player, String filename) { + // Send a Server->Client NpcHtmlMessage containing the text of the L2NpcInstance to the L2PcInstance + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + /** + * @return the Exp Reward of this L2Npc (modified by RATE_XP). + */ + public long getExpReward() { + return (long) (getLevel() * getLevel() * getTemplate().getExpRate() * rates().getRateXp()); + } + + /** + * @return the SP Reward of this L2Npc (modified by RATE_SP). + */ + public int getSpReward() { + return (int) (getTemplate().getSP() * rates().getRateSp()); + } + + /** + * Kill the L2NpcInstance (the corpse disappeared after 7 seconds).
      + * Actions: + *
        + *
      • Create a DecayTask to remove the corpse of the L2NpcInstance after 7 seconds
      • + *
      • Set target to null and cancel Attack or Cast
      • + *
      • Stop movement
      • + *
      • Stop HP/MP/CP Regeneration task
      • + *
      • Stop all active skills effects in progress on the L2Character
      • + *
      • Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform
      • + *
      • Notify L2Character AI
      • + *
      + * @param killer The L2Character who killed it + */ + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + // normally this wouldn't really be needed, but for those few exceptions, + // we do need to reset the weapons back to the initial template weapon. + _currentLHandId = getTemplate().getLHandId(); + _currentRHandId = getTemplate().getRHandId(); + _currentCollisionHeight = getTemplate().getfCollisionHeight(); + _currentCollisionRadius = getTemplate().getfCollisionRadius(); + + final L2Weapon weapon = (killer != null) ? killer.getActiveWeaponItem() : null; + _killingBlowWeaponId = (weapon != null) ? weapon.getId() : 0; + + DecayTaskManager.getInstance().add(this); + return true; + } + + /** + * Set the spawn of the L2NpcInstance. + * @param spawn The L2Spawn that manage the L2NpcInstance + */ + public void setSpawn(L2Spawn spawn) { + _spawn = spawn; + } + + @Override + public void onSpawn() { + super.onSpawn(); + + // Recharge shots + _soulshotamount = getTemplate().getSoulShot(); + _spiritshotamount = getTemplate().getSpiritShot(); + _killingBlowWeaponId = 0; + + if (isTeleporting()) { + EventDispatcher.getInstance().notifyEventAsync(new NpcTeleport(this), this); + } else { + WalkingManager.getInstance().onSpawn(this); + EventDispatcher.getInstance().notifyEventAsync(new NpcSpawn(this), this); + } + } + + /** + * Remove the L2NpcInstance from the world and update its spawn object (for a complete removal use the deleteMe method).
      + * Actions: + *
        + *
      • Remove the L2NpcInstance from the world when the decay task is launched
      • + *
      • Decrease its spawn counter
      • + *
      • Manage Siege task (killFlag, killCT)
      • + *
      + * Caution : This method DOESN'T REMOVE the object from _allObjects of L2World
      + * Caution : This method DOESN'T SEND Server->Client packets to players + */ + @Override + public void onDecay() { + if (isDecayed()) { + return; + } + setDecayed(true); + + // Remove the L2NpcInstance from the world when the decay task is launched + super.onDecay(); + + // Decrease its spawn counter + if (_spawn != null) { + _spawn.decreaseCount(this); + } + + // Notify Walking Manager + WalkingManager.getInstance().onDeath(this); + + // Removes itself from the summoned list. + final L2Character summoner = getSummoner(); + if ((summoner != null) && summoner.isNpc()) { + ((L2Npc) summoner).removeSummonedNpc(getObjectId()); + } + } + + /** + * Remove PROPERLY the L2NpcInstance from the world.
      + * Actions: + *
        + *
      • Remove the L2NpcInstance from the world and update its spawn object
      • + *
      • Remove all L2Object from _knownObjects and _knownPlayer of the L2NpcInstance then cancel Attack or Cast and notify AI
      • + *
      • Remove L2Object object from _allObjects of L2World
      • + *
      + * Caution: This method DOESN'T SEND Server->Client packets to players
      + * UnAfraid: TODO: Add Listener here + */ + @Override + public boolean deleteMe() { + try { + onDecay(); + } catch (Exception e) { + LOG.error("Failed decayMe(). {}", e); + } + + if (isChannelized()) { + getSkillChannelized().abortChannelization(); + } + + final L2WorldRegion oldRegion = getWorldRegion(); + if (oldRegion != null) { + oldRegion.removeFromZones(this); + } + + // Remove all L2Object from _knownObjects and _knownPlayer of the L2Character then cancel Attack or Cast and notify AI + try { + getKnownList().removeAllKnownObjects(); + } catch (Exception e) { + LOG.error("Failed removing cleaning knownlist. {}", e); + } + + // Remove L2Object object from _allObjects of L2World + L2World.getInstance().removeObject(this); + + return super.deleteMe(); + } + + /** + * @return the L2Spawn object that manage this L2NpcInstance. + */ + public L2Spawn getSpawn() { + return _spawn; + } + + @Override + public String toString() { + return getClass().getSimpleName() + ":" + getName() + "(" + getId() + ")" + "[" + getObjectId() + "]"; + } + + public boolean isDecayed() { + return _isDecayed; + } + + public void setDecayed(boolean decayed) { + _isDecayed = decayed; + } + + public void endDecayTask() { + if (!isDecayed()) { + DecayTaskManager.getInstance().cancel(this); + onDecay(); + } + } + + public boolean isMob() // rather delete this check + { + return false; // This means we use MAX_NPC_ANIMATION instead of MAX_MONSTER_ANIMATION + } + + // Two functions to change the appearance of the equipped weapons on the NPC + // This is only useful for a few NPCs and is most likely going to be called from AI + public void setLHandId(int newWeaponId) { + _currentLHandId = newWeaponId; + updateAbnormalEffect(); + } + + public void setRHandId(int newWeaponId) { + _currentRHandId = newWeaponId; + updateAbnormalEffect(); + } + + public void setLRHandId(int newLWeaponId, int newRWeaponId) { + _currentRHandId = newRWeaponId; + _currentLHandId = newLWeaponId; + updateAbnormalEffect(); + } + + public void setEnchant(int newEnchantValue) { + _currentEnchant = newEnchantValue; + updateAbnormalEffect(); + } + + public boolean isShowName() { + return getTemplate().isShowName(); + } + + @Override + public boolean isTargetable() { + return getTemplate().isTargetable(); + } + + public void setCollisionHeight(double height) { + _currentCollisionHeight = height; + } + + public void setCollisionRadius(double radius) { + _currentCollisionRadius = radius; + } + + public double getCollisionHeight() { + return _currentCollisionHeight; + } + + public double getCollisionRadius() { + return _currentCollisionRadius; + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + if (isVisibleFor(activeChar)) { + if (general().checkKnownList() && activeChar.isGM()) { + activeChar.sendMessage("Added NPC: " + getName()); + } + + if (getRunSpeed() == 0) { + activeChar.sendPacket(new ServerObjectInfo(this, activeChar)); + } else { + activeChar.sendPacket(new AbstractNpcInfo.NpcInfo(this, activeChar)); + } + } + } + + public L2Npc scheduleDespawn(long delay) { + ThreadPoolManager.getInstance().scheduleGeneral(() -> { + if (!isDecayed()) { + deleteMe(); + } + }, delay); + return this; + } + + @Override + protected final void notifyQuestEventSkillFinished(Skill skill, L2Object target) { + if ((target != null) && target.isPlayable()) { + EventDispatcher.getInstance().notifyEventAsync(new NpcSkillFinished(this, target.getActingPlayer(), skill), this); + } + } + + @Override + public boolean isMovementDisabled() { + return super.isMovementDisabled() || !canMove() || getAiType().equals(AIType.CORPSE); + } + + public AIType getAiType() { + return getTemplate().getAIType(); + } + + public void setDisplayEffect(int val) { + if (val != _displayEffect) { + _displayEffect = val; + broadcastPacket(new ExChangeNpcState(getObjectId(), val)); + } + } + + public int getDisplayEffect() { + return _displayEffect; + } + + public int getColorEffect() { + return 0; + } + + @Override + public boolean isNpc() { + return true; + } + + @Override + public void setTeam(Team team) { + super.setTeam(team); + broadcastInfo(); + } + + /** + * @return {@code true} if this L2Npc is registered in WalkingManager + */ + @Override + public boolean isWalker() { + return WalkingManager.getInstance().isRegistered(this); + } + + @Override + public boolean isChargedShot(ShotType type) { + return (_shotsMask & type.getMask()) == type.getMask(); + } + + @Override + public void setChargedShot(ShotType type, boolean charged) { + if (charged) { + _shotsMask |= type.getMask(); + } else { + _shotsMask &= ~type.getMask(); + } + } + + @Override + public void rechargeShots(boolean physical, boolean magic) { + if ((_soulshotamount > 0) || (_spiritshotamount > 0)) { + if (physical) { + if (_soulshotamount == 0) { + return; + } else if (Rnd.get(100) > getSoulShotChance()) { + return; + } + _soulshotamount--; + Broadcast.toSelfAndKnownPlayersInRadius(this, new MagicSkillUse(this, this, 2154, 1, 0, 0), 600); + setChargedShot(ShotType.SOULSHOTS, true); + } + if (magic) { + if (_spiritshotamount == 0) { + return; + } else if (Rnd.get(100) > getSpiritShotChance()) { + return; + } + _spiritshotamount--; + Broadcast.toSelfAndKnownPlayersInRadius(this, new MagicSkillUse(this, this, 2061, 1, 0, 0), 600); + setChargedShot(ShotType.SPIRITSHOTS, true); + } + } + } + + /** + * Short wrapper for backward compatibility + * @return stored script value + */ + public int getScriptValue() { + return getVariables().getInt("SCRIPT_VAL"); + } + + /** + * Short wrapper for backward compatibility. Stores script value + * @param val value to store + */ + public void setScriptValue(int val) { + getVariables().set("SCRIPT_VAL", val); + } + + /** + * Short wrapper for backward compatibility. + * @param val value to store + * @return {@code true} if stored script value equals given value, {@code false} otherwise + */ + public boolean isScriptValue(int val) { + return getVariables().getInt("SCRIPT_VAL") == val; + } + + /** + * @param paramName the parameter name to check + * @return given AI parameter value + */ + public int getAIValue(final String paramName) { + if ((getSpawn() == null) || (getSpawn().getName() == null)) { + return -1; + } + return NpcPersonalAIData.getInstance().getAIValue(getSpawn().getName(), paramName); + } + + /** + * @param paramName the parameter name to check + * @return {@code true} if given parameter is set for NPC, {@code false} otherwise + */ + public boolean hasAIValue(final String paramName) { + return (getSpawn() != null) && (getSpawn().getName() != null) && NpcPersonalAIData.getInstance().hasAIValue(getSpawn().getName(), paramName); + } + + /** + * @param npc NPC to check + * @return {@code true} if both given NPC and this NPC is in the same spawn group, {@code false} otherwise + */ + public boolean isInMySpawnGroup(L2Npc npc) { + return ((getSpawn() != null) && (npc.getSpawn() != null) && (getSpawn().getName() != null) && (getSpawn().getName().equals(npc.getSpawn().getName()))); + } + + /** + * @return {@code true} if NPC currently located in own spawn point, {@code false} otherwise + */ + public boolean staysInSpawnLoc() { + return ((getSpawn() != null) && (getSpawn().getX(this) == getX()) && (getSpawn().getY(this) == getY())); + } + + /** + * @return {@code true} if {@link NpcVariables} instance is attached to current player's scripts, {@code false} otherwise. + */ + public boolean hasVariables() { + return getScript(NpcVariables.class) != null; + } + + /** + * @return {@link NpcVariables} instance containing parameters regarding NPC. + */ + public NpcVariables getVariables() { + final NpcVariables vars = getScript(NpcVariables.class); + return vars != null ? vars : addScript(new NpcVariables()); + } + + /** + * Broadcasts a script event to all NPCs within a specified radius of the current object. + * @param eventName the name of the event to broadcast + * @param radius the radius within which to search for NPCs + */ + public void broadcastScriptEvent(String eventName, int radius) { + broadcastScriptEvent(eventName, radius, null, 0, 0); + } + + /** + * Broadcasts a script event to all NPCs within a specified radius of the current object. + * @param eventName the name of the event to broadcast + * @param radius the radius within which to search for NPCs + * @param reference an optional reference object to provide additional context for the event + */ + public void broadcastScriptEvent(String eventName, int radius, L2Object reference) { + broadcastScriptEvent(eventName, radius, reference, 0, 0); + } + + /** + * Broadcasts a script event to all NPCs within a specified radius of the current object. + * @param eventName the name of the event to broadcast + * @param radius the radius within which to search for NPCs + * @param arg1 an integer argument associated with the event + */ + public void broadcastScriptEvent(String eventName, int radius, int arg1) { + broadcastScriptEvent(eventName, radius, null, arg1, 0); + } + + /** + * Broadcasts a script event to all NPCs within a specified radius of the current object.
      + * NPCs that are within the radius and have registered a listener for the {@code NPC_EVENT_RECEIVED} event will receive the event asynchronously.
      + * This method also supports passing a reference object and additional arguments. + * @param eventName the name of the event to broadcast + * @param radius the radius within which to search for NPCs + * @param reference an optional reference object to provide additional context for the event + * @param arg1 an integer argument associated with the event + * @param arg2 an integer argument associated with the event + */ + public void broadcastScriptEvent(String eventName, int radius, L2Object reference, int arg1, int arg2) { + for (var obj : L2World.getInstance().getVisibleObjects(this, radius)) { + if (obj instanceof L2Npc npc && npc.hasListener(NPC_EVENT_RECEIVED)) { + EventDispatcher.getInstance().notifyEventAsync(new NpcEventReceived(eventName, this, npc, reference, arg1, arg2), npc); + } + } + } + + /** + * Sends a script event to a specific receiver asynchronously.
      + * This method triggers the {@code NPC_EVENT_RECEIVED} event for the specified receiver. + * @param eventName the name of the event to send + * @param receiver the {@link L2Object} that will receive the event + * @param reference an optional reference object to provide additional context for the event + */ + public void sendScriptEvent(String eventName, L2Object receiver, L2Object reference) { + EventDispatcher.getInstance().notifyEventAsync(new NpcEventReceived(eventName, this, (L2Npc) receiver, reference, 0, 0), receiver); + } + + /** + * Gets point in range between radiusMin and radiusMax from this NPC + * @param radiusMin miminal range from NPC (not closer than) + * @param radiusMax maximal range from NPC (not further than) + * @return Location in given range from this NPC + */ + public Location getPointInRange(int radiusMin, int radiusMax) { + if ((radiusMax == 0) || (radiusMax < radiusMin)) { + return new Location(getX(), getY(), getZ()); + } + + final int radius = Rnd.get(radiusMin, radiusMax); + final double angle = Rnd.nextDouble() * 2 * Math.PI; + + return new Location((int) (getX() + (radius * Math.cos(angle))), (int) (getY() + (radius * Math.sin(angle))), getZ()); + } + + /** + * Drops an item. + * @param player the last attacker or main damage dealer + * @param itemId the item ID + * @param itemCount the item count + * @return the dropped item + */ + public L2ItemInstance dropItem(L2PcInstance player, int itemId, long itemCount) { + L2ItemInstance item = null; + for (int i = 0; i < itemCount; i++) { + // Randomize drop position. + final int newX = (getX() + Rnd.get((RANDOM_ITEM_DROP_LIMIT * 2) + 1)) - RANDOM_ITEM_DROP_LIMIT; + final int newY = (getY() + Rnd.get((RANDOM_ITEM_DROP_LIMIT * 2) + 1)) - RANDOM_ITEM_DROP_LIMIT; + final int newZ = getZ() + 20; + + if (ItemTable.getInstance().getTemplate(itemId) == null) { + LOG.error("Item doesn't exist so cannot be dropped. Item ID: {} Quest: {}", itemId, getName()); + return null; + } + + item = ItemTable.getInstance().createItem("Loot", itemId, itemCount, player, this); + if (item == null) { + return null; + } + + if (player != null) { + item.getDropProtection().protect(player); + } + + item.dropMe(this, newX, newY, newZ); + + // Add drop to auto destroy item task. + if (!general().getProtectedItems().contains(itemId)) { + if (((general().getAutoDestroyDroppedItemAfter() > 0) && !item.getItem().hasExImmediateEffect()) || ((general().getAutoDestroyHerbTime() > 0) && item.getItem().hasExImmediateEffect())) { + ItemsAutoDestroy.getInstance().addItem(item); + } + } + item.setProtected(false); + + // If stackable, end loop as entire count is included in 1 instance of item. + if (item.isStackable() || !general().multipleItemDrop()) { + break; + } + } + return item; + } + + /** + * Method overload for {@link L2Attackable#dropItem(L2PcInstance, int, long)} + * @param player the last attacker or main damage dealer + * @param item the item holder + * @return the dropped item + */ + public L2ItemInstance dropItem(L2PcInstance player, ItemHolder item) { + return dropItem(player, item.getId(), item.getCount()); + } + + @Override + public final String getName() { + return getTemplate().getName(); + } + + @Override + public boolean isVisibleFor(L2Character character) { + if (hasListener(EventType.NPC_CAN_BE_SEEN)) { + final TerminateReturn term = EventDispatcher.getInstance().notifyEvent(new NpcCanBeSeen(this, character), this, TerminateReturn.class); + if (term != null) { + return term.terminate(); + } + } + return super.isVisibleFor(character); + } + + /** + * Sets if the players can talk with this npc or not + * @param val {@code true} if the players can talk, {@code false} otherwise + */ + public void setTalking(boolean val) { + _isTalking = val; + } + + /** + * Checks if the players can talk to this npc. + * @return {@code true} if the players can talk, {@code false} otherwise. + */ + public boolean isTalking() { + return _isTalking; + } + + /** + * Sets the weapon id with which this npc was killed. + * @param weaponId + */ + public void setKillingBlowWeapon(int weaponId) { + _killingBlowWeaponId = weaponId; + } + + /** + * @return the id of the weapon with which player killed this npc. + */ + public int getKillingBlowWeapon() { + return _killingBlowWeaponId; + } + + /** + * Adds a summoned NPC. + * @param npc the summoned NPC + */ + public final void addSummonedNpc(L2Npc npc) { + if (_summonedNpcs == null) { + synchronized (this) { + if (_summonedNpcs == null) { + _summonedNpcs = new ConcurrentHashMap<>(); + } + } + } + + _summonedNpcs.put(npc.getObjectId(), npc); + + npc.setSummoner(this); + } + + /** + * Removes a summoned NPC by object ID. + * @param objectId the summoned NPC object ID + */ + public final void removeSummonedNpc(int objectId) { + if (_summonedNpcs != null) { + _summonedNpcs.remove(objectId); + } + } + + /** + * Gets the summoned NPCs. + * @return the summoned NPCs + */ + public final Collection getSummonedNpcs() { + return _summonedNpcs != null ? _summonedNpcs.values() : Collections. emptyList(); + } + + /** + * Gets the summoned NPC by object ID. + * @param objectId the summoned NPC object ID + * @return the summoned NPC + */ + public final L2Npc getSummonedNpc(int objectId) { + if (_summonedNpcs != null) { + return _summonedNpcs.get(objectId); + } + return null; + } + + /** + * Gets the summoned NPC count. + * @return the summoned NPC count + */ + public final int getSummonedNpcCount() { + return _summonedNpcs != null ? _summonedNpcs.size() : 0; + } + + /** + * Resets the summoned NPCs list. + */ + public final void resetSummonedNpcs() { + if (_summonedNpcs != null) { + _summonedNpcs.clear(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Playable.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Playable.java new file mode 100644 index 00000000..ef6a7240 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Playable.java @@ -0,0 +1,346 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.knownlist.PlayableKnownList; +import com.l2jserver.gameserver.model.actor.stat.PlayableStat; +import com.l2jserver.gameserver.model.actor.status.PlayableStatus; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.model.effects.EffectFlag; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.CreatureKill; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.serverpackets.EtcStatusUpdate; + +/** + * This class represents all Playable characters in the world.
      + * L2Playable: + *
        + *
      • L2PcInstance
      • + *
      • L2Summon
      • + *
      + */ +public abstract class L2Playable extends L2Character { + private L2Character _lockedTarget = null; + private L2PcInstance transferDmgTo = null; + + public L2Playable(int objectId, L2CharTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2Playable); + setIsInvul(false); + } + + @Override + public PlayableKnownList getKnownList() { + return (PlayableKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new PlayableKnownList(this)); + } + + @Override + public PlayableStat getStat() { + return (PlayableStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new PlayableStat(this)); + } + + @Override + public PlayableStatus getStatus() { + return (PlayableStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new PlayableStatus(this)); + } + + @Override + public boolean doDie(L2Character killer) { + final TerminateReturn returnBack = EventDispatcher.getInstance().notifyEvent(new CreatureKill(killer, this), this, TerminateReturn.class); + if ((returnBack != null) && returnBack.terminate()) { + return false; + } + + // killing is only possible one time + synchronized (this) { + if (isDead()) { + return false; + } + // now reset currentHp to zero + setCurrentHp(0); + setIsDead(true); + } + + // Set target to null and cancel Attack or Cast + setTarget(null); + + // Stop movement + stopMove(null); + + // Stop HP/MP/CP Regeneration task + getStatus().stopHpMpRegeneration(); + + boolean deleteBuffs = true; + + if (isNoblesseBlessedAffected()) { + stopEffects(L2EffectType.NOBLESSE_BLESSING); + deleteBuffs = false; + } + if (isResurrectSpecialAffected()) { + stopEffects(L2EffectType.RESURRECTION_SPECIAL); + deleteBuffs = false; + } + if (isPlayer()) { + L2PcInstance activeChar = getActingPlayer(); + + if (activeChar.hasCharmOfCourage()) { + if (activeChar.isInSiege()) { + getActingPlayer().reviveRequest(getActingPlayer(), null, false, 0, 0); + } + activeChar.setCharmOfCourage(false); + activeChar.sendPacket(new EtcStatusUpdate(activeChar)); + } + } + + if (deleteBuffs) { + stopAllEffectsExceptThoseThatLastThroughDeath(); + } + + // Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + broadcastStatusUpdate(); + + if (getWorldRegion() != null) { + getWorldRegion().onDeath(this); + } + + // Notify Quest of L2Playable's death + L2PcInstance actingPlayer = getActingPlayer(); + + if (!actingPlayer.isNotifyQuestOfDeathEmpty()) { + for (var qs : actingPlayer.getNotifyQuestOfDeath()) { + qs.getQuest().onDeath((killer == null ? this : killer), this, qs); + } + } + // Notify instance + if (getInstanceId() > 0) { + final Instance instance = InstanceManager.getInstance().getInstance(getInstanceId()); + if (instance != null) { + instance.notifyDeath(killer, this); + } + } + + if (killer != null) { + L2PcInstance player = killer.getActingPlayer(); + + if (player != null) { + player.onKillUpdatePvPKarma(this); + } + } + + // Notify L2Character AI + getAI().notifyEvent(CtrlEvent.EVT_DEAD); + updateEffectIcons(); + return true; + } + + public boolean doesSkillNeedCtrl(L2Character target, Skill skill) { + if (target.isPlayable()) { + return !checkIfPvP(target) && (target.getActingPlayer().getKarma() == 0); + } + + return false; + } + + public boolean checkIfPvP(L2Character target) { + if (target == null) { + return false; // Target is null + } + if (target == this) { + return false; // Target is self + } + if (!target.isPlayable()) { + return false; // Target is not a L2Playable + } + + final L2PcInstance player = getActingPlayer(); + if (player == null) { + return false; // Active player is null + } + + final L2PcInstance targetPlayer = target.getActingPlayer(); + if (targetPlayer == null) { + return false; // Target player is null + } + + if (targetPlayer == this) { + return false; // Target player is self + } + + // pvp zone is always pvp + if (player.isInsideZone(ZoneId.PVP) && targetPlayer.isInsideZone(ZoneId.PVP)) { + return true; + } + + if ((player.getKarma() != 0) || (targetPlayer.getKarma() != 0)) { + return false; // Active player or target player has karma + } + + if ((player.getClan() != null) && (targetPlayer.getClan() != null) && targetPlayer.getClan().isAtWarWith(player.getClanId()) && player.getClan().isAtWarWith(targetPlayer.getClanId()) && (player.getWantsPeace() == 0) && (targetPlayer.getWantsPeace() == 0) && !player.isAcademyMember() + && !targetPlayer.isAcademyMember()) { + return true; // Target player and player has war + } + + return targetPlayer.getPvpFlag() != 0; + } + + /** + * Return True. + */ + @Override + public boolean canBeAttacked() { + return true; + } + + // Support for Noblesse Blessing skill, where buffs are retained after resurrect + public final boolean isNoblesseBlessedAffected() { + return isAffected(EffectFlag.NOBLESS_BLESSING); + } + + /** + * @return {@code true} if char can resurrect by himself, {@code false} otherwise + */ + public final boolean isResurrectSpecialAffected() { + return isAffected(EffectFlag.RESURRECTION_SPECIAL); + } + + /** + * @return {@code true} if the Silent Moving mode is active, {@code false} otherwise + */ + public boolean isSilentMovingAffected() { + return isAffected(EffectFlag.SILENT_MOVE); + } + + /** + * For Newbie Protection Blessing skill, keeps you safe from an attack by a chaotic character >= 10 levels apart from you. + * @return + */ + public final boolean isProtectionBlessingAffected() { + return isAffected(EffectFlag.PROTECTION_BLESSING); + } + + @Override + public void updateEffectIcons(boolean partyOnly) { + getEffectList().updateEffectIcons(partyOnly); + } + + public boolean isLockedTarget() { + return _lockedTarget != null; + } + + public L2Character getLockedTarget() { + return _lockedTarget; + } + + public void setLockedTarget(L2Character cha) { + _lockedTarget = cha; + } + + public void setTransferDamageTo(L2PcInstance val) { + transferDmgTo = val; + } + + public L2PcInstance getTransferringDamageTo() { + return transferDmgTo; + } + + /** + * Handle level add.
      + * Overridden in : + *
    • L2PcInstance
    • + *
    • L2PetInstance
    • + * @param levelAdd + */ + public boolean addLevel(int levelAdd) { + return false; + } + + /** + * Overridden in : + *
    • L2PcInstance
    • + *
    • L2PetInstance
    • + */ + public long getExp() { + return 0; + } + + /** + * Overridden in : + *
    • L2PcInstance
    • + *
    • L2PetInstance
    • + * @return the SP amount + */ + public int getSp() { + return 0; + } + + /** + * Handle packets send on level change.
      + * Overridden in : + *
    • L2PcInstance
    • + *
    • L2PetInstance
    • + * @param levelIncreased + */ + public void onLevelChange(boolean levelIncreased) { + + } + + public abstract void doPickupItem(L2Object object); + + public abstract int getKarma(); + + public abstract byte getPvpFlag(); + + public abstract boolean useMagic(Skill skill, boolean forceUse, boolean dontMove); + + public abstract void storeMe(); + + public abstract void storeEffect(boolean storeEffects); + + public abstract void restoreEffects(); + + @Override + public boolean isPlayable() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Summon.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Summon.java new file mode 100644 index 00000000..0401ec50 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Summon.java @@ -0,0 +1,1031 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.geodata; +import static com.l2jserver.gameserver.enums.InstanceType.L2Summon; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2SummonAI; +import com.l2jserver.gameserver.data.json.ExperienceData; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.enums.ShotType; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.handler.IItemHandler; +import com.l2jserver.gameserver.handler.ItemHandler; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.AggroInfo; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.actor.instance.L2NpcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.knownlist.SummonKnownList; +import com.l2jserver.gameserver.model.actor.stat.SummonStat; +import com.l2jserver.gameserver.model.actor.status.SummonStatus; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerSummonSpawn; +import com.l2jserver.gameserver.model.itemcontainer.PetInventory; +import com.l2jserver.gameserver.model.items.L2EtcItem; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.ActionType; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameManager; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.skills.targets.TargetType; +import com.l2jserver.gameserver.model.stats.Formulas; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo.SummonInfo; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExPartyPetWindowAdd; +import com.l2jserver.gameserver.network.serverpackets.ExPartyPetWindowDelete; +import com.l2jserver.gameserver.network.serverpackets.ExPartyPetWindowUpdate; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.PetDelete; +import com.l2jserver.gameserver.network.serverpackets.PetInfo; +import com.l2jserver.gameserver.network.serverpackets.PetItemList; +import com.l2jserver.gameserver.network.serverpackets.PetStatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.RelationChanged; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.TeleportToLocation; +import com.l2jserver.gameserver.pathfinding.PathFinding; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; +import com.l2jserver.gameserver.util.Util; + +public abstract class L2Summon extends L2Playable { + private L2PcInstance _owner; + private int _attackRange = 36; // Melee range + private boolean _follow = true; + private boolean _previousFollowStatus = true; + protected boolean _restoreSummon = true; + private int _shotsMask = 0; + + // @formatter:off + private static final int[] PASSIVE_SUMMONS = + { + 12564, 12621, 14702, 14703, 14704, 14705, 14706, 14707, 14708, 14709, 14710, 14711, + 14712, 14713, 14714, 14715, 14716, 14717, 14718, 14719, 14720, 14721, 14722, 14723, + 14724, 14725, 14726, 14727, 14728, 14729, 14730, 14731, 14732, 14733, 14734, 14735, 14736 + }; + // @formatter:on + + /** + * Creates an abstract summon. + * @param template the summon NPC template + * @param owner the owner + */ + public L2Summon(int objectId, L2NpcTemplate template, L2PcInstance owner) { + super(objectId, template); + setInstanceType(L2Summon); + setInstanceId(owner.getInstanceId()); + setShowSummonAnimation(true); + _owner = owner; + getAI(); + + setXYZInvisible(owner.getX() + Rnd.get(-100, 100), owner.getY() + Rnd.get(-100, 100), owner.getZ()); + + // Copy the skills from template. + for (Skill skill : template.getSkills().values()) { + addSkill(skill); + } + + Formulas.addFuncsToNewSummon(this); + } + + @Override + public void onSpawn() { + super.onSpawn(); + + if (character().summonStoreSkillCooltime() && !isTeleporting()) { + restoreEffects(); + } + + setFollowStatus(true); + updateAndBroadcastStatus(0); + sendPacket(new RelationChanged(this, getOwner().getRelation(getOwner()), false)); + for (L2PcInstance player : getOwner().getKnownList().getKnownPlayersInRadius(800)) { + player.sendPacket(new RelationChanged(this, getOwner().getRelation(player), isAutoAttackable(player))); + } + L2Party party = getOwner().getParty(); + if (party != null) { + party.broadcastToPartyMembers(getOwner(), new ExPartyPetWindowAdd(this)); + } + setShowSummonAnimation(false); // addVisibleObject created the info packets with summon animation + // if someone comes into range now, the animation shouldn't show any more + _restoreSummon = false; + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerSummonSpawn(this), this); + } + + @Override + public final SummonKnownList getKnownList() { + return (SummonKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new SummonKnownList(this)); + } + + @Override + public SummonStat getStat() { + return (SummonStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new SummonStat(this)); + } + + @Override + public SummonStatus getStatus() { + return (SummonStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new SummonStatus(this)); + } + + @Override + protected L2CharacterAI initAI() { + return new L2SummonAI(this); + } + + @Override + public L2NpcTemplate getTemplate() { + return (L2NpcTemplate) super.getTemplate(); + } + + // this defines the action buttons, 1 for Summon, 2 for Pets + public abstract int getSummonType(); + + @Override + public final void stopAllEffects() { + super.stopAllEffects(); + updateAndBroadcastStatus(1); + } + + @Override + public final void stopAllEffectsExceptThoseThatLastThroughDeath() { + super.stopAllEffectsExceptThoseThatLastThroughDeath(); + updateAndBroadcastStatus(1); + } + + @Override + public void updateAbnormalEffect() { + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + player.sendPacket(new SummonInfo(this, player, 1)); + } + } + + /** + * @return Returns the mountable. + */ + public boolean isMountable() { + return false; + } + + public long getExpForThisLevel() { + if (getLevel() >= (character().getMaxPetLevel() + 1)) { + return 0; + } + return ExperienceData.getInstance().getExpForLevel(getLevel()); + } + + public long getExpForNextLevel() { + if (getLevel() >= (character().getMaxPetLevel())) { + return 0; + } + return ExperienceData.getInstance().getExpForLevel(getLevel() + 1); + } + + @Override + public final int getKarma() { + return getOwner() != null ? getOwner().getKarma() : 0; + } + + @Override + public final byte getPvpFlag() { + return getOwner() != null ? getOwner().getPvpFlag() : 0; + } + + @Override + public final Team getTeam() { + return getOwner() != null ? getOwner().getTeam() : Team.NONE; + } + + public final L2PcInstance getOwner() { + return _owner; + } + + /** + * Gets the summon ID. + * @return the summon ID + */ + @Override + public final int getId() { + return getTemplate().getId(); + } + + public short getSoulShotsPerHit() { + if (getTemplate().getSoulShot() > 0) { + return (short) getTemplate().getSoulShot(); + } + return 1; + } + + public short getSpiritShotsPerHit() { + if (getTemplate().getSpiritShot() > 0) { + return (short) getTemplate().getSpiritShot(); + } + return 1; + } + + public void followOwner() { + setFollowStatus(true); + } + + @Override + public boolean doDie(L2Character killer) { + if (isNoblesseBlessedAffected()) { + stopEffects(L2EffectType.NOBLESSE_BLESSING); + storeEffect(true); + } else { + storeEffect(false); + } + + if (!super.doDie(killer)) { + return false; + } + + final L2PcInstance owner = getOwner(); + if (owner != null) { + for (L2Character TgMob : getKnownList().getKnownCharacters()) { + // get the mobs which have aggro on the this instance + if (TgMob instanceof L2Attackable) { + if (TgMob.isDead()) { + continue; + } + + AggroInfo info = ((L2Attackable) TgMob).getAggroList().get(this); + if (info != null) { + ((L2Attackable) TgMob).addDamageHate(owner, info.getDamage(), info.getHate()); + } + } + } + } + + DecayTaskManager.getInstance().add(this); + return true; + } + + public boolean doDie(L2Character killer, boolean decayed) { + if (!super.doDie(killer)) { + return false; + } + if (!decayed) { + DecayTaskManager.getInstance().add(this); + } + return true; + } + + public void stopDecay() { + DecayTaskManager.getInstance().cancel(this); + } + + @Override + public void onDecay() { + deleteMe(_owner); + } + + @Override + public void broadcastStatusUpdate() { + super.broadcastStatusUpdate(); + updateAndBroadcastStatus(1); + } + + public void deleteMe(L2PcInstance owner) { + if (owner != null) { + owner.sendPacket(new PetDelete(getSummonType(), getObjectId())); + final L2Party party = owner.getParty(); + if (party != null) { + party.broadcastToPartyMembers(owner, new ExPartyPetWindowDelete(this)); + } + + for (int itemId : owner.getAutoSoulShot()) { + String handler = ((L2EtcItem) ItemTable.getInstance().getTemplate(itemId)).getHandlerName(); + if ((handler != null) && handler.contains("Beast")) { + owner.disableAutoShot(itemId); + } + } + } + + // pet will be deleted along with all his items + if (getInventory() != null) { + getInventory().destroyAllItems("pet deleted", getOwner(), this); + } + decayMe(); + getKnownList().removeAllKnownObjects(); + if (owner != null) { + owner.setPet(null); + } + super.deleteMe(); + } + + public void unSummon(L2PcInstance owner) { + if (isVisible() && !isDead()) { + getAI().stopFollow(); + if (owner != null) { + owner.sendPacket(new PetDelete(getSummonType(), getObjectId())); + final L2Party party = owner.getParty(); + if (party != null) { + party.broadcastToPartyMembers(owner, new ExPartyPetWindowDelete(this)); + } + + if ((getInventory() != null) && (getInventory().getSize() > 0)) { + getOwner().setPetInvItems(true); + sendPacket(SystemMessageId.ITEMS_IN_PET_INVENTORY); + } else { + getOwner().setPetInvItems(false); + } + } + abortAttack(); + abortCast(); + storeMe(); + storeEffect(true); + if (owner != null) { + owner.setPet(null); + } + + // Stop AI tasks + if (hasAI()) { + getAI().stopAITask(); + } + + stopAllEffects(); + L2WorldRegion oldRegion = getWorldRegion(); + decayMe(); + if (oldRegion != null) { + oldRegion.removeFromZones(this); + } + getKnownList().removeAllKnownObjects(); + setTarget(null); + if (owner != null) { + for (int itemId : owner.getAutoSoulShot()) { + String handler = ((L2EtcItem) ItemTable.getInstance().getTemplate(itemId)).getHandlerName(); + if ((handler != null) && handler.contains("Beast")) { + owner.disableAutoShot(itemId); + } + } + } + } + } + + public int getAttackRange() { + return _attackRange; + } + + public void setAttackRange(int range) { + _attackRange = Math.max(range, 36); + } + + public void setFollowStatus(boolean state) { + _follow = state; + if (_follow) { + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, getOwner()); + } else { + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE, null); + } + } + + public boolean getFollowStatus() { + return _follow; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return (_owner != null) && _owner.isAutoAttackable(attacker); + } + + public int getControlObjectId() { + return 0; + } + + public L2Weapon getActiveWeapon() { + return null; + } + + @Override + public PetInventory getInventory() { + return null; + } + + public boolean isRestoreSummon() { + return _restoreSummon; + } + + public void setRestoreSummon(boolean val) { + _restoreSummon = val; + } + + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + /** + * Return True if the L2Summon is invulnerable or if the summoner is in spawn protection. + */ + @Override + public boolean isInvul() { + return super.isInvul() || getOwner().isSpawnProtected(); + } + + /** + * Return the L2Party object of its L2PcInstance owner or null. + */ + @Override + public L2Party getParty() { + if (_owner == null) { + return null; + } + + return _owner.getParty(); + } + + /** + * Return True if the L2Character has a Party in progress. + */ + @Override + public boolean isInParty() { + return (_owner != null) && _owner.isInParty(); + } + + /** + * Check if the active L2Skill can be casted.
      + * Actions: + *
        + *
      • Check if the target is correct
      • + *
      • Check if the target is in the skill cast range
      • + *
      • Check if the summon owns enough HP and MP to cast the skill
      • + *
      • Check if all skills are enabled and this skill is enabled
      • + *
      • Check if the skill is active
      • + *
      • Notify the AI with AI_INTENTION_CAST and target
      • + *
      + * @param skill The L2Skill to use + * @param forceUse used to force ATTACK on players + * @param dontMove used to prevent movement, if not in range + */ + @Override + public boolean useMagic(Skill skill, boolean forceUse, boolean dontMove) { + // Null skill, dead summon or null owner are reasons to prevent casting. + if ((skill == null) || isDead() || (getOwner() == null)) { + return false; + } + + // Check if the skill is active + if (skill.isPassive()) { + // just ignore the passive skill request. why does the client send it anyway ?? + return false; + } + + // If a skill is currently being used + if (isCastingNow()) { + return false; + } + + // Set current pet skill + getOwner().setCurrentPetSkill(skill, forceUse, dontMove); + + // Get the target for the skill + L2Object target = switch (skill.getTargetType()) { + // SELF should be cast even if no target has been found + case SELF -> this; + // Get the first target of the list + default -> { + final var targets = skill.getTargets(this); + yield targets.isEmpty() ? null : targets.get(0); + } + }; + + // Check the validity of the target + if (target == null) { + sendPacket(SystemMessageId.TARGET_CANT_FOUND); + return false; + } + + // Check if this skill is enabled (e.g. reuse time) + if (isSkillDisabled(skill)) { + sendPacket(SystemMessageId.PET_SKILL_CANNOT_BE_USED_RECHARCHING); + return false; + } + + // Check if the summon has enough MP + if (getCurrentMp() < (getStat().getMpConsume1(skill) + getStat().getMpConsume2(skill))) { + // Send a System Message to the caster + sendPacket(SystemMessageId.NOT_ENOUGH_MP); + return false; + } + + // Check if the summon has enough HP + if (getCurrentHp() <= skill.getHpConsume()) { + // Send a System Message to the caster + sendPacket(SystemMessageId.NOT_ENOUGH_HP); + return false; + } + + if ((this != target) && skill.isPhysical() && // + (geodata().getPathFinding() > 0) && // + (PathFinding.getInstance().findPath(getX(), getY(), getZ(), target.getX(), target.getY(), target.getZ(), getInstanceId(), true) == null)) { + sendPacket(SystemMessageId.CANT_SEE_TARGET); + return false; + } + + // Check if this is bad magic skill + if (skill.isBad()) { + if (getOwner() == target) { + return false; + } + + // Summons can cast skills on NPCs inside peace zones. + if (isInsidePeaceZone(this, target) && !getOwner().getAccessLevel().allowPeaceAttack()) { + // If summon or target is in a peace zone, send a system message: + sendPacket(SystemMessageId.TARGET_IN_PEACEZONE); + return false; + } + + // If L2PcInstance is in Olympiad and the match isn't already start, send a Server->Client packet ActionFailed + if (getOwner().isInOlympiadMode() && !getOwner().isOlympiadStart()) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if ((target.getActingPlayer() != null) && (getOwner().getSiegeState() > 0) && getOwner().isInsideZone(ZoneId.SIEGE) && (target.getActingPlayer().getSiegeState() == getOwner().getSiegeState()) && (target.getActingPlayer() != getOwner()) + && (target.getActingPlayer().getSiegeSide() == getOwner().getSiegeSide())) { + if (TerritoryWarManager.getInstance().isTWInProgress()) { + sendPacket(SystemMessageId.YOU_CANNOT_ATTACK_A_MEMBER_OF_THE_SAME_TERRITORY); + } else { + sendPacket(SystemMessageId.FORCED_ATTACK_IS_IMPOSSIBLE_AGAINST_SIEGE_SIDE_TEMPORARY_ALLIED_MEMBERS); + } + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the target is attackable + if (target.isDoor()) { + if (!target.isAutoAttackable(getOwner())) { + return false; + } + } else { + // Summons can cast skills on NPCs inside peace zones. + if (!target.canBeAttacked() && !getOwner().getAccessLevel().allowPeaceAttack()) { + return false; + } + + // Check if a Forced attack is in progress on non-attackable target + if (!target.isAutoAttackable(this) && !forceUse && !target.isNpc() && (skill.getTargetType() != TargetType.SELF)) { + return false; + } + } + } + // Notify the AI with AI_INTENTION_CAST and target + getAI().setIntention(CtrlIntention.AI_INTENTION_CAST, skill, target); + return true; + } + + @Override + public void setIsImmobilized(boolean value) { + super.setIsImmobilized(value); + + if (value) { + _previousFollowStatus = getFollowStatus(); + // if immobilized temporarily disable follow mode + if (_previousFollowStatus) { + setFollowStatus(false); + } + } else { + // if not more immobilized restore previous follow mode + setFollowStatus(_previousFollowStatus); + } + } + + public void setOwner(L2PcInstance newOwner) { + _owner = newOwner; + } + + @Override + public void sendDamageMessage(L2Character target, int damage, boolean mcrit, boolean pcrit, boolean miss) { + if (miss || (getOwner() == null)) { + return; + } + + // Prevents the double spam of system messages, if the target is the owning player. + if (target.getObjectId() != getOwner().getObjectId()) { + if (pcrit || mcrit) { + if (isServitor()) { + sendPacket(SystemMessageId.CRITICAL_HIT_BY_SUMMONED_MOB); + } else { + sendPacket(SystemMessageId.CRITICAL_HIT_BY_PET); + } + } + + if (getOwner().isInOlympiadMode() && (target instanceof L2PcInstance) && ((L2PcInstance) target).isInOlympiadMode() && (((L2PcInstance) target).getOlympiadGameId() == getOwner().getOlympiadGameId())) { + OlympiadGameManager.getInstance().notifyCompetitorDamage(getOwner(), damage); + } + + final SystemMessage sm; + + if (target.isInvul() && !(target instanceof L2NpcInstance)) { + sm = SystemMessage.getSystemMessage(SystemMessageId.ATTACK_WAS_BLOCKED); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DONE_S3_DAMAGE_TO_C2); + sm.addNpcName(this); + sm.addCharName(target); + sm.addInt(damage); + } + + sendPacket(sm); + } + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, Skill skill) { + super.reduceCurrentHp(damage, attacker, skill); + if ((getOwner() != null) && (attacker != null)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_RECEIVED_DAMAGE_OF_S3_FROM_C2); + sm.addNpcName(this); + sm.addCharName(attacker); + sm.addInt((int) damage); + sendPacket(sm); + } + } + + @Override + public void doCast(Skill skill) { + final L2PcInstance actingPlayer = getActingPlayer(); + if (!actingPlayer.checkPvpSkill(getTarget(), skill) && !actingPlayer.getAccessLevel().allowPeaceAttack()) { + // Send a System Message to the L2PcInstance + actingPlayer.sendPacket(SystemMessageId.TARGET_IS_INCORRECT); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + actingPlayer.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + super.doCast(skill); + } + + @Override + public boolean isInCombat() { + return (getOwner() != null) && getOwner().isInCombat(); + } + + @Override + public L2PcInstance getActingPlayer() { + return getOwner(); + } + + @Override + public final void broadcastPacket(L2GameServerPacket mov) { + if (getOwner() != null) { + mov.setInvisible(getOwner().isInvisible()); + } + super.broadcastPacket(mov); + } + + @Override + public final void broadcastPacket(L2GameServerPacket mov, int radiusInKnownlist) { + if (getOwner() != null) { + mov.setInvisible(getOwner().isInvisible()); + } + super.broadcastPacket(mov, radiusInKnownlist); + } + + public void updateAndBroadcastStatus(int val) { + if (getOwner() == null) { + return; + } + + sendPacket(new PetInfo(this, val)); + sendPacket(new PetStatusUpdate(this)); + if (isVisible()) { + broadcastNpcInfo(val); + } + L2Party party = getOwner().getParty(); + if (party != null) { + party.broadcastToPartyMembers(getOwner(), new ExPartyPetWindowUpdate(this)); + } + updateEffectIcons(true); + } + + public void broadcastNpcInfo(int val) { + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + if ((player == null) || (player == getOwner())) { + continue; + } + player.sendPacket(new SummonInfo(this, player, val)); + } + } + + public boolean isHungry() { + return false; + } + + public int getWeapon() { + return 0; + } + + public int getArmor() { + return 0; + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + // Check if the L2PcInstance is the owner of the Pet + if (activeChar == getOwner()) { + activeChar.sendPacket(new PetInfo(this, 0)); + // The PetInfo packet wipes the PartySpelled (list of active spells' icons). Re-add them + updateEffectIcons(true); + if (isPet()) { + activeChar.sendPacket(new PetItemList(getInventory().getItems())); + } + } else { + activeChar.sendPacket(new SummonInfo(this, activeChar, 0)); + } + } + + @Override + public void onTeleported() { + super.onTeleported(); + sendPacket(new TeleportToLocation(this, getX(), getY(), getZ(), getHeading())); + } + + @Override + public String toString() { + return super.toString() + "(" + getId() + ") Level: " + getLevel() + " Owner: " + getOwner(); + } + + @Override + public boolean isUndead() { + return getTemplate().getRace() == Race.UNDEAD; + } + + /** + * Change the summon's state. + */ + public void switchMode() { + // Do nothing. + } + + /** + * Cancel the summon's action. + */ + public void cancelAction() { + if (!isMovementDisabled()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + } + + /** + * Performs an attack to the owner's target. + */ + public void doAttack() { + if (getOwner() != null) { + final L2Object target = getOwner().getTarget(); + if (target != null) { + setTarget(target); + getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, target); + } + } + } + + /** + * Verify if the summon can perform an attack. + * @param ctrlPressed {@code true} if Ctrl key is pressed + * @return {@code true} if the summon can attack, {@code false} otherwise + */ + public final boolean canAttack(boolean ctrlPressed) { + if (getOwner() == null) { + return false; + } + + final L2Object target = getOwner().getTarget(); + if ((target == null) || (this == target) || (getOwner() == target)) { + return false; + } + + // Sin eater, Big Boom, Wyvern can't attack with attack button. + final int npcId = getId(); + if (Util.contains(PASSIVE_SUMMONS, npcId)) { + getOwner().sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (isBetrayed()) { + sendPacket(SystemMessageId.PET_REFUSING_ORDER); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (isAttackingDisabled()) { + if (!isAttackingNow()) { + return false; + } + getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, target); + } + + if (isPet() && ((getLevel() - getOwner().getLevel()) > 20)) { + sendPacket(SystemMessageId.PET_TOO_HIGH_TO_CONTROL); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (getOwner().isInOlympiadMode() && !getOwner().isOlympiadStart()) { + // If owner is in Olympiad and the match isn't already start, send a Server->Client packet ActionFailed + getOwner().sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if ((target.getActingPlayer() != null) && (getOwner().getSiegeState() > 0) && getOwner().isInsideZone(ZoneId.SIEGE) && (target.getActingPlayer().getSiegeSide() == getOwner().getSiegeSide())) { + if (TerritoryWarManager.getInstance().isTWInProgress()) { + sendPacket(SystemMessageId.YOU_CANNOT_ATTACK_A_MEMBER_OF_THE_SAME_TERRITORY); + } else { + sendPacket(SystemMessageId.FORCED_ATTACK_IS_IMPOSSIBLE_AGAINST_SIEGE_SIDE_TEMPORARY_ALLIED_MEMBERS); + } + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (!getOwner().getAccessLevel().allowPeaceAttack() && getOwner().isInsidePeaceZone(this, target)) { + sendPacket(SystemMessageId.TARGET_IN_PEACEZONE); + return false; + } + + if (isLockedTarget()) { + sendPacket(SystemMessageId.FAILED_CHANGE_TARGET); + return false; + } + + // Summons can attack NPCs even when the owner cannot. + if (!target.isAutoAttackable(getOwner()) && !ctrlPressed && !target.isNpc()) { + setFollowStatus(false); + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, target); + sendPacket(SystemMessageId.INCORRECT_TARGET); + return false; + } + + // Siege golems AI doesn't support attacking other than doors/walls at the moment. + if (target.isDoor() && (getTemplate().getRace() != Race.SIEGE_WEAPON)) { + return false; + } + + return true; + } + + @Override + public void sendPacket(L2GameServerPacket mov) { + if (getOwner() != null) { + getOwner().sendPacket(mov); + } + } + + @Override + public void sendPacket(SystemMessageId id) { + if (getOwner() != null) { + getOwner().sendPacket(id); + } + } + + @Override + public boolean isSummon() { + return true; + } + + @Override + public L2Summon getSummon() { + return this; + } + + @Override + public boolean isChargedShot(ShotType type) { + return (_shotsMask & type.getMask()) == type.getMask(); + } + + @Override + public void setChargedShot(ShotType type, boolean charged) { + if (charged) { + _shotsMask |= type.getMask(); + } else { + _shotsMask &= ~type.getMask(); + } + } + + @Override + public void rechargeShots(boolean physical, boolean magic) { + L2ItemInstance item; + IItemHandler handler; + + if ((getOwner().getAutoSoulShot() == null) || getOwner().getAutoSoulShot().isEmpty()) { + return; + } + + for (int itemId : getOwner().getAutoSoulShot()) { + item = getOwner().getInventory().getItemByItemId(itemId); + + if (item != null) { + if (magic) { + if (item.getItem().getDefaultAction() == ActionType.SUMMON_SPIRITSHOT) { + handler = ItemHandler.getInstance().getHandler(item.getEtcItem()); + if (handler != null) { + handler.useItem(getOwner(), item, false); + } + } + } + + if (physical) { + if (item.getItem().getDefaultAction() == ActionType.SUMMON_SOULSHOT) { + handler = ItemHandler.getInstance().getHandler(item.getEtcItem()); + if (handler != null) { + handler.useItem(getOwner(), item, false); + } + } + } + } else { + getOwner().removeAutoSoulShot(itemId); + } + } + } + + @Override + public int getClanId() { + return (getOwner() != null) ? getOwner().getClanId() : 0; + } + + @Override + public int getAllyId() { + return (getOwner() != null) ? getOwner().getAllyId() : 0; + } + + public int getFormId() { + int formId = 0; + final int npcId = getId(); + if ((npcId == 16041) || (npcId == 16042)) { + if (getLevel() > 69) { + formId = 3; + } else if (getLevel() > 64) { + formId = 2; + } else if (getLevel() > 59) { + formId = 1; + } + } else if ((npcId == 16025) || (npcId == 16037)) { + if (getLevel() > 69) { + formId = 3; + } else if (getLevel() > 64) { + formId = 2; + } else if (getLevel() > 59) { + formId = 1; + } + } + return formId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Tower.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Tower.java new file mode 100644 index 00000000..edfa7640 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Tower.java @@ -0,0 +1,73 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +/** + * This class is a super-class for L2ControlTowerInstance and L2FlameTowerInstance. + * @author Zoey76 + */ +public abstract class L2Tower extends L2Npc { + + public L2Tower(int objectId, L2NpcTemplate template) { + super(objectId, template); + setIsInvul(false); + } + + @Override + public boolean canBeAttacked() { + // Attackable during siege by attacker only + return ((getCastle() != null) && (getCastle().getResidenceId() > 0) && getCastle().getSiege().isInProgress()); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + // Attackable during siege by attacker only + return ((attacker != null) && attacker.isPlayer() && (getCastle() != null) && (getCastle().getResidenceId() > 0) && getCastle().getSiege().isInProgress() && getCastle().getSiege().checkIsAttacker(attacker.getClan())); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if (!canTarget(player)) { + return; + } + + if (this != player.getTarget()) { + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + if (isAutoAttackable(player) && (Math.abs(player.getZ() - getZ()) < 100) && GeoData.getInstance().canSeeTarget(player, this)) { + // Notify the L2PcInstance AI with AI_INTENTION_INTERACT + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } + } + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onForcedAttack(L2PcInstance player) { + onAction(player); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/L2Vehicle.java b/src/main/java/com/l2jserver/gameserver/model/actor/L2Vehicle.java new file mode 100644 index 00000000..f80d4f27 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/L2Vehicle.java @@ -0,0 +1,421 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.VehiclePathPoint; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.knownlist.VehicleKnownList; +import com.l2jserver.gameserver.model.actor.stat.VehicleStat; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.util.Util; + +/** + * @author DS + */ +public abstract class L2Vehicle extends L2Character { + private static final Logger LOG = LoggerFactory.getLogger(L2Vehicle.class); + + protected int _dockId = 0; + protected final List _passengers = new CopyOnWriteArrayList<>(); + protected Location _oustLoc = null; + private Runnable _engine = null; + + protected VehiclePathPoint[] _currentPath = null; + protected int _runState = 0; + + public L2Vehicle(int objectId, L2CharTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2Vehicle); + setIsFlying(true); + } + + public boolean isBoat() { + return false; + } + + public boolean isAirShip() { + return false; + } + + public boolean canBeControlled() { + return _engine == null; + } + + public void registerEngine(Runnable r) { + _engine = r; + } + + public void runEngine(int delay) { + if (_engine != null) { + ThreadPoolManager.getInstance().scheduleGeneral(_engine, delay); + } + } + + public void executePath(VehiclePathPoint[] path) { + _runState = 0; + _currentPath = path; + + if ((_currentPath != null) && (_currentPath.length > 0)) { + final VehiclePathPoint point = _currentPath[0]; + if (point.getMoveSpeed() > 0) { + getStat().setMoveSpeed(point.getMoveSpeed()); + } + if (point.getRotationSpeed() > 0) { + getStat().setRotationSpeed(point.getRotationSpeed()); + } + + getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, point); + return; + } + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + } + + @Override + public boolean moveToNextRoutePoint() { + _move = null; + + if (_currentPath != null) { + _runState++; + if (_runState < _currentPath.length) { + final VehiclePathPoint point = _currentPath[_runState]; + if (!isMovementDisabled()) { + if (point.getMoveSpeed() == 0) { + point.setHeading(point.getRotationSpeed()); + teleToLocation(point, false); + _currentPath = null; + } else { + if (point.getMoveSpeed() > 0) { + getStat().setMoveSpeed(point.getMoveSpeed()); + } + if (point.getRotationSpeed() > 0) { + getStat().setRotationSpeed(point.getRotationSpeed()); + } + + MoveData m = new MoveData(); + m.disregardingGeodata = false; + m.onGeodataPathIndex = -1; + m._xDestination = point.getX(); + m._yDestination = point.getY(); + m._zDestination = point.getZ(); + m._heading = 0; + + final double distance = Math.hypot(point.getX() - getX(), point.getY() - getY()); + if (distance > 1) { + setHeading(Util.calculateHeadingFrom(getX(), getY(), point.getX(), point.getY())); + } + + m._moveStartTime = GameTimeController.getInstance().getGameTicks(); + _move = m; + + GameTimeController.getInstance().registerMovingObject(this); + return true; + } + } + } else { + _currentPath = null; + } + } + + runEngine(10); + return false; + } + + @Override + public void initKnownList() { + setKnownList(new VehicleKnownList(this)); + } + + @Override + public VehicleStat getStat() { + return (VehicleStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new VehicleStat(this)); + } + + public boolean isInDock() { + return _dockId > 0; + } + + public int getDockId() { + return _dockId; + } + + public void setInDock(int d) { + _dockId = d; + } + + public void setOustLoc(Location loc) { + _oustLoc = loc; + } + + public Location getOustLoc() { + return _oustLoc != null ? _oustLoc : MapRegionManager.getInstance().getTeleToLocation(this, TeleportWhereType.TOWN); + } + + public void oustPlayers() { + _passengers.forEach(this::oustPlayer); + _passengers.clear(); + } + + public void oustPlayer(L2PcInstance player) { + player.setVehicle(null); + player.setInVehiclePosition(null); + removePassenger(player); + } + + public boolean addPassenger(L2PcInstance player) { + if ((player == null) || _passengers.contains(player)) { + return false; + } + + // already in other vehicle + if ((player.getVehicle() != null) && (player.getVehicle() != this)) { + return false; + } + + _passengers.add(player); + return true; + } + + public void removePassenger(L2PcInstance player) { + try { + _passengers.remove(player); + } catch (Exception e) { + } + } + + public boolean isEmpty() { + return _passengers.isEmpty(); + } + + public List getPassengers() { + return _passengers; + } + + public void broadcastToPassengers(L2GameServerPacket sm) { + for (L2PcInstance player : _passengers) { + if (player != null) { + player.sendPacket(sm); + } + } + } + + /** + * Consume ticket(s) and teleport player from boat if no correct ticket + * @param itemId Ticket itemId + * @param count Ticket count + * @param oustX + * @param oustY + * @param oustZ + */ + public void payForRide(int itemId, int count, int oustX, int oustY, int oustZ) { + final Collection passengers = getKnownList().getKnownPlayersInRadius(1000); + if (!passengers.isEmpty()) { + L2ItemInstance ticket; + InventoryUpdate iu; + for (L2PcInstance player : passengers) { + if (player == null) { + continue; + } + if (player.isInBoat() && (player.getBoat() == this)) { + if (itemId > 0) { + ticket = player.getInventory().getItemByItemId(itemId); + if ((ticket == null) || (player.getInventory().destroyItem("Boat", ticket, count, player, this) == null)) { + player.sendPacket(SystemMessageId.NOT_CORRECT_BOAT_TICKET); + player.teleToLocation(new Location(oustX, oustY, oustZ), true); + continue; + } + iu = new InventoryUpdate(); + iu.addModifiedItem(ticket); + player.sendPacket(iu); + } + addPassenger(player); + } + } + } + } + + @Override + public boolean updatePosition() { + final boolean result = super.updatePosition(); + + for (L2PcInstance player : _passengers) { + if ((player != null) && (player.getVehicle() == this)) { + player.setXYZ(getX(), getY(), getZ()); + player.revalidateZone(false); + } + } + + return result; + } + + @Override + public void teleToLocation(ILocational loc, boolean allowRandomOffset) { + if (isMoving()) { + stopMove(null, false); + } + + setIsTeleporting(true); + + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + + for (L2PcInstance player : _passengers) { + if (player != null) { + player.teleToLocation(loc, false); + } + } + + decayMe(); + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + + // temporary fix for heading on teleports + if (loc.getHeading() != 0) { + setHeading(loc.getHeading()); + } + + onTeleported(); + revalidateZone(true); + } + + @Override + public void stopMove(Location loc, boolean updateKnownObjects) { + _move = null; + if (loc != null) { + setXYZ(loc.getX(), loc.getY(), loc.getZ()); + setHeading(loc.getHeading()); + revalidateZone(true); + } + + if (general().moveBasedKnownList() && updateKnownObjects) { + getKnownList().findObjects(); + } + } + + @Override + public boolean deleteMe() { + _engine = null; + + try { + if (isMoving()) { + stopMove(null); + } + } catch (Exception ex) { + LOG.warn("Failed stopMove().", ex); + } + + try { + oustPlayers(); + } catch (Exception ex) { + LOG.warn("Failed oustPlayers().", ex); + } + + final L2WorldRegion oldRegion = getWorldRegion(); + + try { + decayMe(); + } catch (Exception ex) { + LOG.warn("Failed decayMe().", ex); + } + + if (oldRegion != null) { + oldRegion.removeFromZones(this); + } + + try { + getKnownList().removeAllKnownObjects(); + } catch (Exception ex) { + LOG.warn("Failed cleaning knownlist.", ex); + } + + // Remove L2Object object from _allObjects of L2World + L2World.getInstance().removeObject(this); + + return super.deleteMe(); + } + + @Override + public void updateAbnormalEffect() { + } + + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + @Override + public int getLevel() { + return 0; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return false; + } + + @Override + public void detachAI() { + } + + @Override + public boolean isVehicle() { + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/appearance/PcAppearance.java b/src/main/java/com/l2jserver/gameserver/model/actor/appearance/PcAppearance.java new file mode 100644 index 00000000..c1e980c4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/appearance/PcAppearance.java @@ -0,0 +1,189 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.appearance; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public class PcAppearance { + public static final int DEFAULT_TITLE_COLOR = 0xECF9A2; + + private L2PcInstance _owner; + + private byte _face; + + private byte _hairColor; + + private byte _hairStyle; + + private boolean _sex; // Female true(1) + + /** true if the player is invisible */ + private boolean _ghostMode = false; + + /** The current visible name of this player, not necessarily the real one */ + private String _visibleName; + + /** The current visible title of this player, not necessarily the real one */ + private String _visibleTitle; + + /** The default name color is 0xFFFFFF. */ + private int _nameColor = 0xFFFFFF; + + /** The default title color is 0xECF9A2. */ + private int _titleColor = DEFAULT_TITLE_COLOR; + + public PcAppearance(byte face, byte hColor, byte hStyle, boolean sex) { + _face = face; + _hairColor = hColor; + _hairStyle = hStyle; + _sex = sex; + } + + /** + * @param visibleName The visibleName to set. + */ + public final void setVisibleName(String visibleName) { + _visibleName = visibleName; + } + + /** + * @return Returns the visibleName. + */ + public final String getVisibleName() { + if (_visibleName == null) { + return getOwner().getName(); + } + return _visibleName; + } + + /** + * @param visibleTitle The visibleTitle to set. + */ + public final void setVisibleTitle(String visibleTitle) { + _visibleTitle = visibleTitle; + } + + /** + * @return Returns the visibleTitle. + */ + public final String getVisibleTitle() { + if (_visibleTitle == null) { + return getOwner().getTitle(); + } + return _visibleTitle; + } + + public final byte getFace() { + return _face; + } + + /** + * @param value + */ + public final void setFace(int value) { + _face = (byte) value; + } + + public final byte getHairColor() { + return _hairColor; + } + + /** + * @param value + */ + public final void setHairColor(int value) { + _hairColor = (byte) value; + } + + public final byte getHairStyle() { + return _hairStyle; + } + + /** + * @param value + */ + public final void setHairStyle(int value) { + _hairStyle = (byte) value; + } + + /** + * @return true if char is female + */ + public final boolean getSex() { + return _sex; + } + + public final void setSex(boolean female) { + _sex = female; + } + + public void setGhostMode(boolean b) { + _ghostMode = b; + } + + public boolean isGhost() { + return _ghostMode; + } + + public int getNameColor() { + return _nameColor; + } + + public void setNameColor(int nameColor) { + if (nameColor < 0) { + return; + } + + _nameColor = nameColor; + } + + public void setNameColor(int red, int green, int blue) { + _nameColor = (red & 0xFF) + ((green & 0xFF) << 8) + ((blue & 0xFF) << 16); + } + + public int getTitleColor() { + return _titleColor; + } + + public void setTitleColor(int titleColor) { + if (titleColor < 0) { + return; + } + + _titleColor = titleColor; + } + + public void setTitleColor(int red, int green, int blue) { + _titleColor = (red & 0xFF) + ((green & 0xFF) << 8) + ((blue & 0xFF) << 16); + } + + /** + * @param owner The owner to set. + */ + public void setOwner(L2PcInstance owner) { + _owner = owner; + } + + /** + * @return Returns the owner. + */ + public L2PcInstance getOwner() { + return _owner; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AirShipInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AirShipInstance.java new file mode 100644 index 00000000..d3fe52f8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AirShipInstance.java @@ -0,0 +1,166 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.ai.L2AirShipAI; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.AirShipManager; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Vehicle; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.network.serverpackets.ExAirShipInfo; +import com.l2jserver.gameserver.network.serverpackets.ExGetOffAirShip; +import com.l2jserver.gameserver.network.serverpackets.ExGetOnAirShip; +import com.l2jserver.gameserver.network.serverpackets.ExMoveToLocationAirShip; +import com.l2jserver.gameserver.network.serverpackets.ExStopMoveAirShip; + +/** + * Flying airships. + * @author DrHouse + * @author DS + */ +public class L2AirShipInstance extends L2Vehicle { + public L2AirShipInstance(int objectId, L2CharTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2AirShipInstance); + setAI(new L2AirShipAI(this)); + } + + @Override + public boolean isAirShip() { + return true; + } + + public boolean isOwner(L2PcInstance player) { + return false; + } + + public int getOwnerId() { + return 0; + } + + public boolean isCaptain(L2PcInstance player) { + return false; + } + + public int getCaptainId() { + return 0; + } + + public int getHelmObjectId() { + return 0; + } + + public int getHelmItemId() { + return 0; + } + + public boolean setCaptain(L2PcInstance player) { + return false; + } + + public int getFuel() { + return 0; + } + + public void setFuel(int f) { + + } + + public int getMaxFuel() { + return 0; + } + + public void setMaxFuel(int mf) { + + } + + @Override + public int getId() { + return 0; + } + + @Override + public boolean moveToNextRoutePoint() { + final boolean result = super.moveToNextRoutePoint(); + if (result) { + broadcastPacket(new ExMoveToLocationAirShip(this)); + } + + return result; + } + + @Override + public boolean addPassenger(L2PcInstance player) { + if (!super.addPassenger(player)) { + return false; + } + + player.setVehicle(this); + player.setInVehiclePosition(new Location(0, 0, 0)); + player.broadcastPacket(new ExGetOnAirShip(player, this)); + player.getKnownList().removeAllKnownObjects(); + player.setXYZ(getX(), getY(), getZ()); + player.revalidateZone(true); + return true; + } + + @Override + public void oustPlayer(L2PcInstance player) { + super.oustPlayer(player); + final Location loc = getOustLoc(); + if (player.isOnline()) { + player.broadcastPacket(new ExGetOffAirShip(player, this, loc.getX(), loc.getY(), loc.getZ())); + player.getKnownList().removeAllKnownObjects(); + player.setXYZ(loc.getX(), loc.getY(), loc.getZ()); + player.revalidateZone(true); + } else { + player.setXYZInvisible(loc.getX(), loc.getY(), loc.getZ()); + } + } + + @Override + public boolean deleteMe() { + if (!super.deleteMe()) { + return false; + } + + AirShipManager.getInstance().removeAirShip(this); + return true; + } + + @Override + public void stopMove(Location loc, boolean updateKnownObjects) { + super.stopMove(loc, updateKnownObjects); + + broadcastPacket(new ExStopMoveAirShip(this)); + } + + @Override + public void updateAbnormalEffect() { + broadcastPacket(new ExAirShipInfo(this)); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + if (isVisibleFor(activeChar)) { + activeChar.sendPacket(new ExAirShipInfo(this)); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ArtefactInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ArtefactInstance.java new file mode 100644 index 00000000..4cad9f3a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ArtefactInstance.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +public final class L2ArtefactInstance extends L2Npc { + + public L2ArtefactInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ArtefactInstance); + } + + @Override + public void onSpawn() { + super.onSpawn(); + getCastle().registerArtefact(this); + } + + /** + * Return False. + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + return false; + } + + @Override + public boolean canBeAttacked() { + return false; + } + + @Override + public void onForcedAttack(L2PcInstance player) { + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, Skill skill) { + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AuctioneerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AuctioneerInstance.java new file mode 100644 index 00000000..364a50c6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2AuctioneerInstance.java @@ -0,0 +1,607 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static java.util.concurrent.TimeUnit.DAYS; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.AuctionManager; +import com.l2jserver.gameserver.instancemanager.ClanHallManager; +import com.l2jserver.gameserver.instancemanager.MapRegionManager; +import com.l2jserver.gameserver.model.ClanPrivilege; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Auction; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; + +public final class L2AuctioneerInstance extends L2Npc { + private static final Logger LOG = LoggerFactory.getLogger(L2AuctioneerInstance.class); + + private static final int COND_ALL_FALSE = 0; + private static final int COND_BUSY_BECAUSE_OF_SIEGE = 1; + private static final int COND_REGULAR = 3; + + private int _currentPage = 1; + + private final Map _pendingAuctions = new ConcurrentHashMap<>(); + + public L2AuctioneerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2AuctioneerInstance); + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + int condition = validateCondition(player); + if (condition <= COND_ALL_FALSE) { + // TODO: html + player.sendMessage("Wrong conditions."); + return; + } else if (condition == COND_BUSY_BECAUSE_OF_SIEGE) { + String filename = "data/html/auction/auction-busy.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } else if (condition == COND_REGULAR) { + StringTokenizer st = new StringTokenizer(command, " "); + String actualCommand = st.nextToken(); // Get actual command + + String val = ""; + if (st.hasMoreTokens()) { + val = st.nextToken(); + } + + if (actualCommand.equalsIgnoreCase("auction")) { + if (val.isEmpty()) { + return; + } + + try { + int days = Integer.parseInt(val); + try { + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm"); + long bid = 0; + if (st.hasMoreTokens()) { + bid = Math.min(Long.parseLong(st.nextToken()), character().getMaxAdena()); + } + + Auction a = new Auction(player.getClan().getHideoutId(), player.getClan(), DAYS.toMillis(days), bid, ClanHallManager.getInstance().getClanHallByOwner(player.getClan()).getName()); + if (_pendingAuctions.get(a.getId()) != null) { + _pendingAuctions.remove(a.getId()); + } + + _pendingAuctions.put(a.getId(), a); + + String filename = "data/html/auction/AgitSale3.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%x%", val); + html.replace("%AGIT_AUCTION_END%", format.format(a.getEndDate())); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_MIN%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_DESC%", ClanHallManager.getInstance().getClanHallByOwner(player.getClan()).getDesc()); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_sale2"); + html.replace("%objectId%", String.valueOf((getObjectId()))); + player.sendPacket(html); + } catch (Exception e) { + player.sendMessage("Invalid bid!"); + } + } catch (Exception e) { + player.sendMessage("Invalid auction duration!"); + } + return; + } else if (actualCommand.equalsIgnoreCase("confirmAuction")) { + try { + Auction a = _pendingAuctions.get(player.getClan().getHideoutId()); + a.confirmAuction(); + _pendingAuctions.remove(player.getClan().getHideoutId()); + } catch (Exception e) { + player.sendMessage("Invalid auction"); + } + return; + } else if (actualCommand.equalsIgnoreCase("bidding")) { + if (val.isEmpty()) { + return; + } + + if (general().debug()) { + LOG.debug("bidding show successful"); + } + + try { + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm"); + int auctionId = Integer.parseInt(val); + + if (general().debug()) { + LOG.debug("auction test started"); + } + + String filename = "data/html/auction/AgitAuctionInfo.htm"; + Auction a = AuctionManager.getInstance().getAuction(auctionId); + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + if (a != null) { + html.replace("%AGIT_NAME%", a.getItemName()); + html.replace("%OWNER_PLEDGE_NAME%", a.getSellerClanName()); + html.replace("%OWNER_PLEDGE_MASTER%", a.getSellerName()); + html.replace("%AGIT_SIZE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getGrade() * 10)); + html.replace("%AGIT_LEASE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLease())); + html.replace("%AGIT_LOCATION%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLocation()); + html.replace("%AGIT_AUCTION_END%", format.format(a.getEndDate())); + html.replace("%AGIT_AUCTION_REMAIN%", ((a.getEndDate() - System.currentTimeMillis()) / 3600000) + " hours " + (((a.getEndDate() - System.currentTimeMillis()) / 60000) % 60) + " minutes"); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_COUNT%", String.valueOf(a.getBidders().size())); + html.replace("%AGIT_AUCTION_DESC%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getDesc()); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_list"); + html.replace("%AGIT_LINK_BIDLIST%", "bypass -h npc_" + getObjectId() + "_bidlist " + a.getId()); + html.replace("%AGIT_LINK_RE%", "bypass -h npc_" + getObjectId() + "_bid1 " + a.getId()); + } else { + LOG.debug("Auctioneer Auction null for AuctionId : {}", auctionId); + } + + player.sendPacket(html); + } catch (Exception e) { + player.sendMessage("Invalid auction!"); + } + return; + } else if (actualCommand.equalsIgnoreCase("bid")) { + if (val.isEmpty()) { + return; + } + + try { + int auctionId = Integer.parseInt(val); + try { + long bid = 0; + if (st.hasMoreTokens()) { + bid = Math.min(Long.parseLong(st.nextToken()), character().getMaxAdena()); + } + + AuctionManager.getInstance().getAuction(auctionId).setBid(player, bid); + } catch (Exception e) { + player.sendMessage("Invalid bid!"); + } + } catch (Exception e) { + player.sendMessage("Invalid auction!"); + } + return; + } else if (actualCommand.equalsIgnoreCase("bid1")) { + if ((player.getClan() == null) || (player.getClan().getLevel() < 2)) { + player.sendPacket(SystemMessageId.AUCTION_ONLY_CLAN_LEVEL_2_HIGHER); + return; + } + + if (val.isEmpty()) { + return; + } + + if (((player.getClan().getAuctionBidAt() > 0) && (player.getClan().getAuctionBidAt() != Integer.parseInt(val))) || (player.getClan().getHideoutId() > 0)) { + player.sendPacket(SystemMessageId.ALREADY_SUBMITTED_BID); + return; + } + + try { + String filename = "data/html/auction/AgitBid1.htm"; + + long minimumBid = AuctionManager.getInstance().getAuction(Integer.parseInt(val)).getHighestBidderMaxBid(); + if (minimumBid == 0) { + minimumBid = AuctionManager.getInstance().getAuction(Integer.parseInt(val)).getStartingBid(); + } + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_bidding " + val); + html.replace("%PLEDGE_ADENA%", String.valueOf(player.getClan().getWarehouse().getAdena())); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(minimumBid)); + html.replace("npc_%objectId%_bid", "npc_" + getObjectId() + "_bid " + val); + player.sendPacket(html); + return; + } catch (Exception e) { + player.sendMessage("Invalid auction!"); + } + return; + } else if (actualCommand.equalsIgnoreCase("list")) { + List auctions = AuctionManager.getInstance().getAuctions(); + SimpleDateFormat format = new SimpleDateFormat("yy/MM/dd"); + // Limit for make new page, prevent client crash. + + int limit = 10; + int start; + int i = 1; + + if (val.isEmpty()) { + start = 1; + _currentPage = 1; + } else { + start = (limit * (Integer.parseInt(val) - 1)) + 1; + limit *= Integer.parseInt(val); + _currentPage = Integer.parseInt(val); + } + + if (general().debug()) { + LOG.debug("cmd list: auction test started"); + } + + StringBuilder items = new StringBuilder(); + items.append(""); + + for (Auction a : auctions) { + if (a == null) { + continue; + } + + if (i > limit) { + break; + } else if (i < start) { + i++; + continue; + } else { + i++; + } + + items.append(""); + items.append(""); + items.append(""); + items.append(""); + items.append(""); + items.append(""); + items.append(""); + } + items.append("
      "); + items.append(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLocation()); + items.append(""); + items.append(a.getItemName()); + items.append("["); + items.append(AuctionManager.getInstance().getAuction(a.getId()).getBidders().size()); + items.append("]"); + items.append(format.format(a.getEndDate())); + items.append(""); + items.append(a.getStartingBid()); + items.append("
      "); + + items.append(""); + items.append(""); + if (_currentPage > 1) { + items.append(""); + } + items.append(""); + items.append(""); + items.append(""); + items.append("
      "); + items.append(""); + items.append("
      "); + + String filename = "data/html/auction/AgitAuctionList.htm"; + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_start"); + html.replace("%itemsField%", items.toString()); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("bidlist")) { + int auctionId; + if (val.isEmpty()) { + if (player.getClan().getAuctionBidAt() <= 0) { + return; + } + auctionId = player.getClan().getAuctionBidAt(); + } else { + auctionId = Integer.parseInt(val); + } + + if (general().debug()) { + LOG.debug("cmd bidlist: auction test started"); + } + + final var bidders = new StringBuilder(); + for (var b : AuctionManager.getInstance().getAuction(auctionId).getBidders().values()) { + bidders.append("") + .append(b.getClanName()) + .append("") + .append(b.getName()) + .append("") + .append(b.getTimeBid().get(Calendar.YEAR)) + .append("/") + .append(b.getTimeBid().get(Calendar.MONTH) + 1) + .append("/") + .append(b.getTimeBid().get(Calendar.DATE)) + .append("") + .append(b.getBid()) + .append("") + .append(""); + } + + final var fileName = "data/html/auction/AgitBidderList.htm"; + final var html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), fileName); + html.replace("%AGIT_LIST%", bidders.toString()); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_bidding " + auctionId); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("selectedItems")) { + if ((player.getClan() != null) && (player.getClan().getHideoutId() == 0) && (player.getClan().getAuctionBidAt() > 0)) { + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm"); + String filename = "data/html/auction/AgitBidInfo.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + Auction a = AuctionManager.getInstance().getAuction(player.getClan().getAuctionBidAt()); + if (a != null) { + html.replace("%AGIT_NAME%", a.getItemName()); + html.replace("%OWNER_PLEDGE_NAME%", a.getSellerClanName()); + html.replace("%OWNER_PLEDGE_MASTER%", a.getSellerName()); + html.replace("%AGIT_SIZE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getGrade() * 10)); + html.replace("%AGIT_LEASE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLease())); + html.replace("%AGIT_LOCATION%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLocation()); + html.replace("%AGIT_AUCTION_END%", format.format(a.getEndDate())); + html.replace("%AGIT_AUCTION_REMAIN%", ((a.getEndDate() - System.currentTimeMillis()) / 3600000) + " hours " + (((a.getEndDate() - System.currentTimeMillis()) / 60000) % 60) + " minutes"); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_MYBID%", String.valueOf(a.getBidders().get(player.getClanId()).getBid())); + html.replace("%AGIT_AUCTION_DESC%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getDesc()); + html.replace("%objectId%", String.valueOf(getObjectId())); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_start"); + } else { + LOG.warn("Auctioneer Auction null for AuctionBiddedAt : {}", player.getClan().getAuctionBidAt()); + } + + player.sendPacket(html); + return; + } else if ((player.getClan() != null) && (AuctionManager.getInstance().getAuction(player.getClan().getHideoutId()) != null)) { + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm"); + String filename = "data/html/auction/AgitSaleInfo.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + Auction a = AuctionManager.getInstance().getAuction(player.getClan().getHideoutId()); + if (a != null) { + html.replace("%AGIT_NAME%", a.getItemName()); + html.replace("%AGIT_OWNER_PLEDGE_NAME%", a.getSellerClanName()); + html.replace("%OWNER_PLEDGE_MASTER%", a.getSellerName()); + html.replace("%AGIT_SIZE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getGrade() * 10)); + html.replace("%AGIT_LEASE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLease())); + html.replace("%AGIT_LOCATION%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getLocation()); + html.replace("%AGIT_AUCTION_END%", format.format(a.getEndDate())); + html.replace("%AGIT_AUCTION_REMAIN%", ((a.getEndDate() - System.currentTimeMillis()) / 3600000) + " hours " + (((a.getEndDate() - System.currentTimeMillis()) / 60000) % 60) + " minutes"); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_BIDCOUNT%", String.valueOf(a.getBidders().size())); + html.replace("%AGIT_AUCTION_DESC%", ClanHallManager.getInstance().getAuctionableHallById(a.getItemId()).getDesc()); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_start"); + html.replace("%id%", String.valueOf(a.getId())); + html.replace("%objectId%", String.valueOf(getObjectId())); + } else { + LOG.warn("Auctioneer Auction null for getHasHideout : {}", player.getClan().getHideoutId()); + } + + player.sendPacket(html); + return; + } else if ((player.getClan() != null) && (player.getClan().getHideoutId() != 0)) { + int ItemId = player.getClan().getHideoutId(); + String filename = "data/html/auction/AgitInfo.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + if (ClanHallManager.getInstance().getAuctionableHallById(ItemId) != null) { + html.replace("%AGIT_NAME%", ClanHallManager.getInstance().getAuctionableHallById(ItemId).getName()); + html.replace("%AGIT_OWNER_PLEDGE_NAME%", player.getClan().getName()); + html.replace("%OWNER_PLEDGE_MASTER%", player.getClan().getLeaderName()); + html.replace("%AGIT_SIZE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(ItemId).getGrade() * 10)); + html.replace("%AGIT_LEASE%", String.valueOf(ClanHallManager.getInstance().getAuctionableHallById(ItemId).getLease())); + html.replace("%AGIT_LOCATION%", ClanHallManager.getInstance().getAuctionableHallById(ItemId).getLocation()); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_start"); + html.replace("%objectId%", String.valueOf(getObjectId())); + } else { + LOG.warn("Clan Hall ID NULL : {} Can be caused by concurrent write in ClanHallManager", ItemId); + } + + player.sendPacket(html); + return; + } else if ((player.getClan() != null) && (player.getClan().getHideoutId() == 0)) { + player.sendPacket(SystemMessageId.NO_OFFERINGS_OWN_OR_MADE_BID_FOR); + return; + } else if (player.getClan() == null) { + player.sendPacket(SystemMessageId.CANNOT_PARTICIPATE_IN_AN_AUCTION); + return; + } + } else if (actualCommand.equalsIgnoreCase("cancelBid")) { + long bid = AuctionManager.getInstance().getAuction(player.getClan().getAuctionBidAt()).getBidders().get(player.getClanId()).getBid(); + String filename = "data/html/auction/AgitBidCancel.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_BID%", String.valueOf(bid)); + html.replace("%AGIT_BID_REMAIN%", String.valueOf((long) (bid * 0.9))); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_selectedItems"); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("doCancelBid")) { + if (AuctionManager.getInstance().getAuction(player.getClan().getAuctionBidAt()) != null) { + AuctionManager.getInstance().getAuction(player.getClan().getAuctionBidAt()).cancelBid(player.getClanId()); + player.sendPacket(SystemMessageId.CANCELED_BID); + } + return; + } else if (actualCommand.equalsIgnoreCase("cancelAuction")) { + if (!player.hasClanPrivilege(ClanPrivilege.CH_AUCTION)) { + String filename = "data/html/auction/not_authorized.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } + String filename = "data/html/auction/AgitSaleCancel.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_DEPOSIT%", String.valueOf(ClanHallManager.getInstance().getClanHallByOwner(player.getClan()).getLease())); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_selectedItems"); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("doCancelAuction")) { + if (AuctionManager.getInstance().getAuction(player.getClan().getHideoutId()) != null) { + AuctionManager.getInstance().getAuction(player.getClan().getHideoutId()).cancelAuction(); + player.sendMessage("Your auction has been canceled"); + } + return; + } else if (actualCommand.equalsIgnoreCase("sale2")) { + String filename = "data/html/auction/AgitSale2.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_LAST_PRICE%", String.valueOf(ClanHallManager.getInstance().getClanHallByOwner(player.getClan()).getLease())); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_sale"); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("sale")) { + if (!player.hasClanPrivilege(ClanPrivilege.CH_AUCTION)) { + String filename = "data/html/auction/not_authorized.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } + String filename = "data/html/auction/AgitSale1.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%AGIT_DEPOSIT%", String.valueOf(ClanHallManager.getInstance().getClanHallByOwner(player.getClan()).getLease())); + html.replace("%AGIT_PLEDGE_ADENA%", String.valueOf(player.getClan().getWarehouse().getAdena())); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_selectedItems"); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("rebid")) { + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy HH:mm"); + if (!player.hasClanPrivilege(ClanPrivilege.CH_AUCTION)) { + String filename = "data/html/auction/not_authorized.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + return; + } + try { + String filename = "data/html/auction/AgitBid2.htm"; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + Auction a = AuctionManager.getInstance().getAuction(player.getClan().getAuctionBidAt()); + if (a != null) { + html.replace("%AGIT_AUCTION_BID%", String.valueOf(a.getBidders().get(player.getClanId()).getBid())); + html.replace("%AGIT_AUCTION_MINBID%", String.valueOf(a.getStartingBid())); + html.replace("%AGIT_AUCTION_END%", format.format(a.getEndDate())); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_selectedItems"); + html.replace("npc_%objectId%_bid1", "npc_" + getObjectId() + "_bid1 " + a.getId()); + } else { + LOG.warn("Auctioneer Auction null for AuctionBiddedAt : {}", player.getClan().getAuctionBidAt()); + } + + player.sendPacket(html); + } catch (Exception e) { + player.sendMessage("Invalid auction!"); + } + return; + } else if (actualCommand.equalsIgnoreCase("location")) { + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), "data/html/auction/location.htm"); + html.replace("%location%", MapRegionManager.getInstance().getClosestTownName(player)); + html.replace("%LOCATION%", getPictureName(player)); + html.replace("%AGIT_LINK_BACK%", "bypass -h npc_" + getObjectId() + "_start"); + player.sendPacket(html); + return; + } else if (actualCommand.equalsIgnoreCase("start")) { + showChatWindow(player); + return; + } + } + + super.onBypassFeedback(player, command); + } + + @Override + public void showChatWindow(L2PcInstance player) { + String filename; + int condition = validateCondition(player); + if (condition == COND_BUSY_BECAUSE_OF_SIEGE) { + filename = "data/html/auction/auction-busy.htm"; // Busy because of siege + } else { + filename = "data/html/auction/auction.htm"; + } + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + html.replace("%npcId%", String.valueOf(getId())); + html.replace("%npcname%", getName()); + player.sendPacket(html); + } + + private int validateCondition(L2PcInstance player) { + if ((getCastle() != null) && (getCastle().getResidenceId() > 0)) { + if (getCastle().getSiege().isInProgress()) { + return COND_BUSY_BECAUSE_OF_SIEGE; // Busy because of siege + } + return COND_REGULAR; + } + + return COND_ALL_FALSE; + } + + private String getPictureName(L2PcInstance plyr) { + int nearestTownId = MapRegionManager.getInstance().getMapRegionLocId(plyr); + return switch (nearestTownId) { + case 911 -> "GLUDIN"; + case 912 -> "GLUDIO"; + case 916 -> "DION"; + case 918 -> "GIRAN"; + case 1537 -> "RUNE"; + case 1538 -> "GODARD"; + case 1714 -> "SCHUTTGART"; + default -> "ADEN"; + }; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BlockInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BlockInstance.java new file mode 100644 index 00000000..dbf63842 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BlockInstance.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.ArenaParticipantsHolder; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.BlockCheckerEngine; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameChangePoints; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameExtendedChangePoints; + +/** + * @author BiggBoss + */ +public class L2BlockInstance extends L2MonsterInstance { + private int _colorEffect; + + public L2BlockInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + /** + * Will change the color of the block and update the appearance in the known players clients + * @param attacker + * @param holder + * @param team + */ + public void changeColor(L2PcInstance attacker, ArenaParticipantsHolder holder, int team) { + // Do not update color while sending old info + synchronized (this) { + final BlockCheckerEngine event = holder.getEvent(); + if (_colorEffect == 0x53) { + // Change color + _colorEffect = 0x00; + // BroadCast to all known players + this.broadcastPacket(new AbstractNpcInfo.NpcInfo(this, attacker)); + increaseTeamPointsAndSend(attacker, team, event); + } else { + // Change color + _colorEffect = 0x53; + // BroadCast to all known players + this.broadcastPacket(new AbstractNpcInfo.NpcInfo(this, attacker)); + increaseTeamPointsAndSend(attacker, team, event); + } + // 30% chance to drop the event items + int random = Rnd.get(100); + // Bond + if ((random > 69) && (random <= 84)) { + dropItem(13787, event, attacker); + } else if (random > 84) { + dropItem(13788, event, attacker); + } + } + } + + /** + * Sets if the block is red or blue. Mainly used in block spawn + * @param isRed + */ + public void setRed(boolean isRed) { + _colorEffect = isRed ? 0x53 : 0x00; + } + + /** + * @return {@code true} if the block is red at this moment, {@code false} otherwise + */ + @Override + public int getColorEffect() { + return _colorEffect; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + if (attacker instanceof L2PcInstance) { + return (attacker.getActingPlayer() != null) && (attacker.getActingPlayer().getBlockCheckerArena() > -1); + } + return true; + } + + @Override + public boolean doDie(L2Character killer) { + return false; + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if (!canTarget(player)) { + return; + } + + player.setLastFolkNPC(this); + + if (player.getTarget() != this) { + player.setTarget(this); + getAI(); // wake up ai + } else if (interact) { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + + private void increaseTeamPointsAndSend(L2PcInstance player, int team, BlockCheckerEngine eng) { + eng.increasePlayerPoints(player, team); + + int timeLeft = (int) ((eng.getStarterTime() - System.currentTimeMillis()) / 1000); + boolean isRed = eng.getHolder().getRedPlayers().contains(player); + + ExCubeGameChangePoints changePoints = new ExCubeGameChangePoints(timeLeft, eng.getBluePoints(), eng.getRedPoints()); + ExCubeGameExtendedChangePoints secretPoints = new ExCubeGameExtendedChangePoints(timeLeft, eng.getBluePoints(), eng.getRedPoints(), isRed, player, eng.getPlayerPoints(player, isRed)); + + eng.getHolder().broadCastPacketToTeam(changePoints); + eng.getHolder().broadCastPacketToTeam(secretPoints); + } + + private void dropItem(int id, BlockCheckerEngine eng, L2PcInstance player) { + L2ItemInstance drop = ItemTable.getInstance().createItem("Loot", id, 1, player, this); + int x = getX() + Rnd.get(50); + int y = getY() + Rnd.get(50); + int z = getZ(); + + drop.dropMe(this, x, y, z); + + eng.addNewDrop(drop); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BoatInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BoatInstance.java new file mode 100644 index 00000000..72e4897b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2BoatInstance.java @@ -0,0 +1,86 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.ai.L2BoatAI; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Vehicle; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.network.serverpackets.VehicleDeparture; +import com.l2jserver.gameserver.network.serverpackets.VehicleInfo; +import com.l2jserver.gameserver.network.serverpackets.VehicleStarted; + +/** + * @author Maktakien + * @author DS + */ +public class L2BoatInstance extends L2Vehicle { + + public L2BoatInstance(int objectId, L2CharTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2BoatInstance); + setAI(new L2BoatAI(this)); + } + + @Override + public boolean isBoat() { + return true; + } + + @Override + public int getId() { + return 0; + } + + @Override + public boolean moveToNextRoutePoint() { + final boolean result = super.moveToNextRoutePoint(); + if (result) { + broadcastPacket(new VehicleDeparture(this)); + } + + return result; + } + + @Override + public void oustPlayer(L2PcInstance player) { + super.oustPlayer(player); + + final Location loc = getOustLoc(); + if (player.isOnline()) { + player.teleToLocation(loc.getX(), loc.getY(), loc.getZ()); + } else { + player.setXYZInvisible(loc.getX(), loc.getY(), loc.getZ()); // disconnects handling + } + } + + @Override + public void stopMove(Location loc, boolean updateKnownObjects) { + super.stopMove(loc, updateKnownObjects); + + broadcastPacket(new VehicleStarted(this, 0)); + broadcastPacket(new VehicleInfo(this)); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + activeChar.sendPacket(new VehicleInfo(this)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CastleDoormenInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CastleDoormenInstance.java new file mode 100644 index 00000000..c7450a11 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CastleDoormenInstance.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2CastleDoormenInstance extends L2DoormenInstance { + public L2CastleDoormenInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2CastleDoormenInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ChestInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ChestInstance.java new file mode 100644 index 00000000..a956549b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ChestInstance.java @@ -0,0 +1,84 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * This class manages all chest. + * @author Julian + */ +public final class L2ChestInstance extends L2MonsterInstance { + private volatile boolean _specialDrop; + + public L2ChestInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ChestInstance); + setIsNoRndWalk(true); + _specialDrop = false; + } + + @Override + public void onSpawn() { + super.onSpawn(); + _specialDrop = false; + setMustRewardExpSp(true); + } + + public synchronized void setSpecialDrop() { + _specialDrop = true; + } + + @Override + public void doItemDrop(L2NpcTemplate npcTemplate, L2Character lastAttacker) { + int id = getTemplate().getId(); + + if (!_specialDrop) { + if ((id >= 18265) && (id <= 18286)) { + id += 3536; + } else if ((id == 18287) || (id == 18288)) { + id = 21671; + } else if ((id == 18289) || (id == 18290)) { + id = 21694; + } else if ((id == 18291) || (id == 18292)) { + id = 21717; + } else if ((id == 18293) || (id == 18294)) { + id = 21740; + } else if ((id == 18295) || (id == 18296)) { + id = 21763; + } else if ((id == 18297) || (id == 18298)) { + id = 21786; + } + } + super.doItemDrop(NpcData.getInstance().getTemplate(id), lastAttacker); + } + + @Override + public boolean isMovementDisabled() { + return true; + } + + @Override + public boolean hasRandomAnimation() { + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallDoormenInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallDoormenInstance.java new file mode 100644 index 00000000..994031e0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallDoormenInstance.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2ClanHallDoormenInstance extends L2DoormenInstance { + /** + * Creates a clan hall doorman. + * @param template the doorman NPC template + */ + public L2ClanHallDoormenInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ClanHallDoormenInstance); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallManagerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallManagerInstance.java new file mode 100644 index 00000000..8c363afe --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ClanHallManagerInstance.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2ClanHallManagerInstance extends L2MerchantInstance { + + public L2ClanHallManagerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ClanHallManagerInstance); + } + + @Override + public boolean isWarehouse() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ColosseumFence.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ColosseumFence.java new file mode 100644 index 00000000..8b434b8a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ColosseumFence.java @@ -0,0 +1,99 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.awt.Rectangle; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.network.serverpackets.ExColosseumFenceInfo; + +/** + * Colosseum Fence. + * @author HorridoJoho + */ +public final class L2ColosseumFence extends L2Object { + public enum FenceState { + HIDDEN, // the fene isn't shown at all + OPEN, // the 4 edges of the fence is shown only + CLOSED // full fence + } + + private final int _minZ; + private final int _maxZ; + private final FenceState _state; + private final Rectangle _bounds; + + private L2ColosseumFence(int objectId, int instanceId, int x, int y, int z, int minZ, int maxZ, int width, int height, FenceState state) { + super(objectId); + setInstanceId(instanceId); + setXYZ(x, y, z); + _minZ = minZ; + _maxZ = maxZ; + _state = state; + _bounds = new Rectangle(x - (width / 2), y - (height / 2), width, height); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + activeChar.sendPacket(new ExColosseumFenceInfo(this)); + } + + public int getFenceX() { + return _bounds.x; + } + + public int getFenceY() { + return _bounds.y; + } + + public int getFenceMinZ() { + return _minZ; + } + + public int getFenceMaxZ() { + return _maxZ; + } + + public int getFenceWidth() { + return _bounds.width; + } + + public int getFenceHeight() { + return _bounds.height; + } + + public FenceState getFenceState() { + return _state; + } + + @Override + public int getId() { + return getObjectId(); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return false; + } + + public boolean isInsideFence(int x, int y, int z) { + return (x >= _bounds.x) && (y >= _bounds.y) && (z >= _minZ) && (z <= _maxZ) && (x <= (_bounds.x + _bounds.width)) && (y <= (_bounds.y + _bounds.height)); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControlTowerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControlTowerInstance.java new file mode 100644 index 00000000..25b4231d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControlTowerInstance.java @@ -0,0 +1,84 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Tower; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * Class for Control Tower instance. + */ +public class L2ControlTowerInstance extends L2Tower { + private static final Logger LOG = LoggerFactory.getLogger(L2ControlTowerInstance.class); + + private volatile List _guards; + + /** + * Creates a control tower. + * @param template the control tower NPC template + */ + public L2ControlTowerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ControlTowerInstance); + } + + @Override + public boolean doDie(L2Character killer) { + if (getCastle().getSiege().isInProgress()) { + getCastle().getSiege().killedCT(this); + + if ((_guards != null) && !_guards.isEmpty()) { + for (L2Spawn spawn : _guards) { + try { + spawn.stopRespawn(); + // spawn.getLastSpawn().doDie(spawn.getLastSpawn()); + } catch (Exception e) { + LOG.warn("Error at L2ControlTowerInstance {}", e); + } + } + _guards.clear(); + } + } + return super.doDie(killer); + } + + public void registerGuard(L2Spawn guard) { + getGuards().add(guard); + } + + private final List getGuards() { + if (_guards == null) { + synchronized (this) { + if (_guards == null) { + _guards = new CopyOnWriteArrayList<>(); + } + } + } + return _guards; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableAirShipInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableAirShipInstance.java new file mode 100644 index 00000000..27055adb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableAirShipInstance.java @@ -0,0 +1,271 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.concurrent.Future; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.stat.ControllableAirShipStat; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.DeleteObject; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class L2ControllableAirShipInstance extends L2AirShipInstance { + private static final int HELM = 13556; + private static final int LOW_FUEL = 40; + + private int _fuel = 0; + private int _maxFuel = 0; + + private final int _ownerId; + private int _helmId; + private L2PcInstance _captain = null; + + private Future _consumeFuelTask; + private Future _checkTask; + + /** + * Creates a controllable air ship. + * @param template the controllable air ship template + * @param ownerId the owner ID + */ + public L2ControllableAirShipInstance(int objectId, L2CharTemplate template, int ownerId) { + super(objectId, template); + setInstanceType(InstanceType.L2ControllableAirShipInstance); + _ownerId = ownerId; + _helmId = IdFactory.getInstance().getNextId(); // not forget to release ! + } + + @Override + public ControllableAirShipStat getStat() { + return (ControllableAirShipStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new ControllableAirShipStat(this)); + } + + @Override + public boolean canBeControlled() { + return super.canBeControlled() && !isInDock(); + } + + @Override + public boolean isOwner(L2PcInstance player) { + if (_ownerId == 0) { + return false; + } + + return (player.getClanId() == _ownerId) || (player.getObjectId() == _ownerId); + } + + @Override + public int getOwnerId() { + return _ownerId; + } + + @Override + public boolean isCaptain(L2PcInstance player) { + return (_captain != null) && (player == _captain); + } + + @Override + public int getCaptainId() { + return _captain != null ? _captain.getObjectId() : 0; + } + + @Override + public int getHelmObjectId() { + return _helmId; + } + + @Override + public int getHelmItemId() { + return HELM; + } + + @Override + public boolean setCaptain(L2PcInstance player) { + if (player == null) { + _captain = null; + } else { + if ((_captain == null) && (player.getAirShip() == this)) { + final int x = player.getInVehiclePosition().getX() - 0x16e; + final int y = player.getInVehiclePosition().getY(); + final int z = player.getInVehiclePosition().getZ() - 0x6b; + if (((x * x) + (y * y) + (z * z)) > 2500) { + player.sendPacket(SystemMessageId.CANT_CONTROL_TOO_FAR); + return false; + } + // TODO: Missing message ID: 2739 Message: You cannot control the helm because you do not meet the requirements. + else if (player.isInCombat()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_IN_A_BATTLE); + return false; + } else if (player.isSitting()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_IN_A_SITTING_POSITION); + return false; + } else if (player.isStunned()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_YOU_ARE_PETRIFIED); + return false; + } else if (player.isCursedWeaponEquipped()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_A_CURSED_WEAPON_IS_EQUIPPED); + return false; + } else if (player.isFishing()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_FISHING); + return false; + } else if (player.isDead() || player.isFakeDeath()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHEN_YOU_ARE_DEAD); + return false; + } else if (player.isCastingNow()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_USING_A_SKILL); + return false; + } else if (player.isTransformed()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_TRANSFORMED); + return false; + } else if (player.isCombatFlagEquipped()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_HOLDING_A_FLAG); + return false; + } else if (player.isInDuel()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_CONTROL_THE_HELM_WHILE_IN_A_DUEL); + return false; + } + _captain = player; + player.broadcastUserInfo(); + } else { + return false; + } + } + updateAbnormalEffect(); + return true; + } + + @Override + public int getFuel() { + return _fuel; + } + + @Override + public void setFuel(int f) { + final int old = _fuel; + _fuel = Math.min(Math.max(f, 0), _maxFuel); + + if ((_fuel == 0) && (old > 0)) { + broadcastToPassengers(SystemMessage.getSystemMessage(SystemMessageId.THE_AIRSHIP_FUEL_RUN_OUT)); + } else if (_fuel < LOW_FUEL) { + broadcastToPassengers(SystemMessage.getSystemMessage(SystemMessageId.THE_AIRSHIP_FUEL_SOON_RUN_OUT)); + } + } + + @Override + public int getMaxFuel() { + return _maxFuel; + } + + @Override + public void setMaxFuel(int mf) { + _maxFuel = mf; + } + + @Override + public void oustPlayer(L2PcInstance player) { + if (player == _captain) { + setCaptain(null); // no need to broadcast userinfo here + } + + super.oustPlayer(player); + } + + @Override + public void onSpawn() { + super.onSpawn(); + _checkTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new CheckTask(), 60000, 10000); + _consumeFuelTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new ConsumeFuelTask(), 60000, 60000); + } + + @Override + public boolean deleteMe() { + if (!super.deleteMe()) { + return false; + } + + if (_checkTask != null) { + _checkTask.cancel(false); + _checkTask = null; + } + if (_consumeFuelTask != null) { + _consumeFuelTask.cancel(false); + _consumeFuelTask = null; + } + + broadcastPacket(new DeleteObject(_helmId)); + return true; + } + + @Override + public void refreshID() { + super.refreshID(); + IdFactory.getInstance().releaseId(_helmId); + _helmId = IdFactory.getInstance().getNextId(); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + super.sendInfo(activeChar); + if (_captain != null) { + _captain.sendInfo(activeChar); + } + } + + protected final class ConsumeFuelTask implements Runnable { + @Override + public void run() { + int fuel = getFuel(); + if (fuel > 0) { + fuel -= 10; + if (fuel < 0) { + fuel = 0; + } + + setFuel(fuel); + updateAbnormalEffect(); + } + } + } + + protected final class CheckTask implements Runnable { + @Override + public void run() { + if (isVisible() && isEmpty() && !isInDock()) { + // deleteMe() can't be called from CheckTask because task should not cancel itself + ThreadPoolManager.getInstance().executeGeneral(new DecayTask()); + } + } + } + + protected final class DecayTask implements Runnable { + @Override + public void run() { + deleteMe(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableMobInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableMobInstance.java new file mode 100644 index 00000000..890d3034 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ControllableMobInstance.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2ControllableMobAI; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @author littlecrow + */ +public class L2ControllableMobInstance extends L2MonsterInstance { + private boolean isInvulnerable; + + public L2ControllableMobInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ControllableMobInstance); + } + + @Override + public boolean isAggressive() { + return true; + } + + @Override + public int getAggroRange() { + // force mobs to be aggro + return 500; + } + + @Override + protected L2CharacterAI initAI() { + return new L2ControllableMobAI(this); + } + + @Override + public boolean isInvul() { + return isInvulnerable; + } + + public void setInvul(boolean isInvulnerable) { + this.isInvulnerable = isInvulnerable; + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + setAI(null); + return true; + } + + @Override + public void detachAI() { + // do nothing, AI of controllable mobs can't be detached automatically + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CubicInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CubicInstance.java new file mode 100644 index 00000000..d560629c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2CubicInstance.java @@ -0,0 +1,593 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.instancemanager.DuelManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.tasks.cubics.CubicAction; +import com.l2jserver.gameserver.model.actor.tasks.cubics.CubicDisappear; +import com.l2jserver.gameserver.model.actor.tasks.cubics.CubicHeal; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.entity.TvTEvent; +import com.l2jserver.gameserver.model.entity.TvTEventTeam; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Formulas; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; + +public final class L2CubicInstance implements IIdentifiable { + private static final Logger LOG = LoggerFactory.getLogger(L2CubicInstance.class); + + // Type of Cubics + public static final int STORM_CUBIC = 1; + public static final int VAMPIRIC_CUBIC = 2; + public static final int LIFE_CUBIC = 3; + public static final int VIPER_CUBIC = 4; + public static final int POLTERGEIST_CUBIC = 5; + public static final int BINDING_CUBIC = 6; + public static final int AQUA_CUBIC = 7; + public static final int SPARK_CUBIC = 8; + public static final int ATTRACT_CUBIC = 9; + public static final int SMART_CUBIC_EVATEMPLAR = 10; + public static final int SMART_CUBIC_SHILLIENTEMPLAR = 11; + public static final int SMART_CUBIC_ARCANALORD = 12; + public static final int SMART_CUBIC_ELEMENTALMASTER = 13; + public static final int SMART_CUBIC_SPECTRALMASTER = 14; + + // Max range of cubic skills + // TODO: Check/fix the max range + public static final int MAX_MAGIC_RANGE = 900; + + // Cubic skills + public static final int SKILL_CUBIC_HEAL = 4051; + public static final int SKILL_CUBIC_CURE = 5579; + + private final L2PcInstance _owner; + private L2Character _target; + + private final int _cubicId; + private final int _cubicPower; + private final int _cubicDelay; + private final int _cubicSkillChance; + private final int _cubicMaxCount; + private boolean _active; + private final boolean _givenByOther; + + private final List _skills = new ArrayList<>(); + + private Future _disappearTask; + private Future _actionTask; + + public L2CubicInstance(L2PcInstance owner, int cubicId, int level, int cubicPower, int cubicDelay, int cubicSkillChance, int cubicMaxCount, int cubicDuration, boolean givenByOther) { + _owner = owner; + _cubicId = cubicId; + _cubicPower = cubicPower; + _cubicDelay = cubicDelay * 1000; + _cubicSkillChance = cubicSkillChance; + _cubicMaxCount = cubicMaxCount; + _active = false; + _givenByOther = givenByOther; + + switch (_cubicId) { + case STORM_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4049, level)); + case VAMPIRIC_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4050, level)); + case LIFE_CUBIC -> { + _skills.add(SkillData.getInstance().getSkill(4051, level)); + doAction(); + } + case VIPER_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4052, level)); + case POLTERGEIST_CUBIC -> { + _skills.add(SkillData.getInstance().getSkill(4053, level)); + _skills.add(SkillData.getInstance().getSkill(4054, level)); + _skills.add(SkillData.getInstance().getSkill(4055, level)); + } + case BINDING_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4164, level)); + case AQUA_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4165, level)); + case SPARK_CUBIC -> _skills.add(SkillData.getInstance().getSkill(4166, level)); + case ATTRACT_CUBIC -> { + _skills.add(SkillData.getInstance().getSkill(5115, level)); + _skills.add(SkillData.getInstance().getSkill(5116, level)); + } + case SMART_CUBIC_ARCANALORD -> { + _skills.add(SkillData.getInstance().getSkill(4051, 7)); + _skills.add(SkillData.getInstance().getSkill(4165, 9)); + } + case SMART_CUBIC_ELEMENTALMASTER -> { + _skills.add(SkillData.getInstance().getSkill(4049, 8)); + _skills.add(SkillData.getInstance().getSkill(4166, 9)); + } + case SMART_CUBIC_SPECTRALMASTER -> { + _skills.add(SkillData.getInstance().getSkill(4049, 8)); + _skills.add(SkillData.getInstance().getSkill(4052, 6)); + } + case SMART_CUBIC_EVATEMPLAR -> { + _skills.add(SkillData.getInstance().getSkill(4053, 8)); + _skills.add(SkillData.getInstance().getSkill(4165, 9)); + } + case SMART_CUBIC_SHILLIENTEMPLAR -> { + _skills.add(SkillData.getInstance().getSkill(4049, 8)); + _skills.add(SkillData.getInstance().getSkill(5115, 4)); + } + } + _disappearTask = ThreadPoolManager.getInstance().scheduleGeneral(new CubicDisappear(this), cubicDuration * 1000); + } + + public synchronized void doAction() { + if (_active) { + return; + } + _active = true; + + switch (_cubicId) { + case AQUA_CUBIC, BINDING_CUBIC, SPARK_CUBIC, STORM_CUBIC, POLTERGEIST_CUBIC, VAMPIRIC_CUBIC, VIPER_CUBIC, // + ATTRACT_CUBIC, SMART_CUBIC_ARCANALORD, SMART_CUBIC_ELEMENTALMASTER, SMART_CUBIC_SPECTRALMASTER, SMART_CUBIC_EVATEMPLAR, // + SMART_CUBIC_SHILLIENTEMPLAR -> _actionTask = ThreadPoolManager.getInstance() + .scheduleEffectAtFixedRate(new CubicAction(this, _cubicSkillChance), 0, _cubicDelay); + case LIFE_CUBIC -> _actionTask = ThreadPoolManager.getInstance().scheduleEffectAtFixedRate(new CubicHeal(this), 0, _cubicDelay); + } + } + + @Override + public int getId() { + return _cubicId; + } + + public L2PcInstance getOwner() { + return _owner; + } + + public int getCubicPower() { + return _cubicPower; + } + + public L2Character getTarget() { + return _target; + } + + public void setTarget(L2Character target) { + _target = target; + } + + public List getSkills() { + return _skills; + } + + public int getCubicMaxCount() { + return _cubicMaxCount; + } + + public void stopAction() { + _target = null; + if (_actionTask != null) { + _actionTask.cancel(true); + _actionTask = null; + } + _active = false; + } + + public void cancelDisappear() { + if (_disappearTask != null) { + _disappearTask.cancel(true); + _disappearTask = null; + } + } + + /** this sets the enemy target for a cubic */ + public void getCubicTarget() { + try { + _target = null; + L2Object ownerTarget = _owner.getTarget(); + if (ownerTarget == null) { + return; + } + // TvT event targeting + if (TvTEvent.isStarted() && TvTEvent.isPlayerParticipant(_owner.getObjectId())) { + TvTEventTeam enemyTeam = TvTEvent.getParticipantEnemyTeam(_owner.getObjectId()); + + if (ownerTarget.getActingPlayer() != null) { + L2PcInstance target = ownerTarget.getActingPlayer(); + if (enemyTeam.containsPlayer(target.getObjectId()) && !(target.isDead())) { + _target = (L2Character) ownerTarget; + } + } + return; + } + // Duel targeting + if (_owner.isInDuel()) { + L2PcInstance PlayerA = DuelManager.getInstance().getDuel(_owner.getDuelId()).getTeamLeaderA(); + L2PcInstance PlayerB = DuelManager.getInstance().getDuel(_owner.getDuelId()).getTeamLeaderB(); + + if (DuelManager.getInstance().getDuel(_owner.getDuelId()).isPartyDuel()) { + L2Party partyA = PlayerA.getParty(); + L2Party partyB = PlayerB.getParty(); + L2Party partyEnemy = null; + + if (partyA != null) { + if (partyA.getMembers().contains(_owner)) { + if (partyB != null) { + partyEnemy = partyB; + } else { + _target = PlayerB; + } + } else { + partyEnemy = partyA; + } + } else { + if (PlayerA == _owner) { + if (partyB != null) { + partyEnemy = partyB; + } else { + _target = PlayerB; + } + } else { + _target = PlayerA; + } + } + if ((_target == PlayerA) || (_target == PlayerB)) { + if (_target == ownerTarget) { + return; + } + } + if (partyEnemy != null) { + if (partyEnemy.getMembers().contains(ownerTarget)) { + _target = (L2Character) ownerTarget; + } + return; + } + } + if ((PlayerA != _owner) && (ownerTarget == PlayerA)) { + _target = PlayerA; + return; + } + if ((PlayerB != _owner) && (ownerTarget == PlayerB)) { + _target = PlayerB; + return; + } + _target = null; + return; + } + // Olympiad targeting + if (_owner.isInOlympiadMode()) { + if (_owner.isOlympiadStart()) { + if (ownerTarget.isPlayable()) { + final L2PcInstance targetPlayer = ownerTarget.getActingPlayer(); + if ((targetPlayer != null) && (targetPlayer.getOlympiadGameId() == _owner.getOlympiadGameId()) && (targetPlayer.getOlympiadSide() != _owner.getOlympiadSide())) { + _target = (L2Character) ownerTarget; + } + } + } + return; + } + // test owners target if it is valid then use it + if (ownerTarget.isCharacter() && (ownerTarget != _owner.getSummon()) && (ownerTarget != _owner)) { + // target mob which has aggro on you or your summon + if (ownerTarget.isAttackable()) { + final L2Attackable attackable = (L2Attackable) ownerTarget; + if (attackable.isInAggroList(_owner) && !attackable.isDead()) { + _target = (L2Character) ownerTarget; + return; + } + if (_owner.hasSummon()) { + if (attackable.isInAggroList(_owner.getSummon()) && !attackable.isDead()) { + _target = (L2Character) ownerTarget; + return; + } + } + } + + // get target in pvp or in siege + L2PcInstance enemy = null; + + if (((_owner.getPvpFlag() > 0) && !_owner.isInsideZone(ZoneId.PEACE)) || _owner.isInsideZone(ZoneId.PVP)) { + if (!((L2Character) ownerTarget).isDead()) { + enemy = ownerTarget.getActingPlayer(); + } + + if (enemy != null) { + boolean targetIt = true; + + if (_owner.getParty() != null) { + if (_owner.getParty().getMembers().contains(enemy)) { + targetIt = false; + } else if (_owner.getParty().getCommandChannel() != null) { + if (_owner.getParty().getCommandChannel().getMembers().contains(enemy)) { + targetIt = false; + } + } + } + if ((_owner.getClan() != null) && !_owner.isInsideZone(ZoneId.PVP)) { + if (_owner.getClan().isMember(enemy.getObjectId())) { + targetIt = false; + } + if ((_owner.getAllyId() > 0) && (enemy.getAllyId() > 0)) { + if (_owner.getAllyId() == enemy.getAllyId()) { + targetIt = false; + } + } + } + if ((enemy.getPvpFlag() == 0) && !enemy.isInsideZone(ZoneId.PVP)) { + targetIt = false; + } + if (enemy.isInsideZone(ZoneId.PEACE)) { + targetIt = false; + } + if ((_owner.getSiegeState() > 0) && (_owner.getSiegeState() == enemy.getSiegeState())) { + targetIt = false; + } + if (!enemy.isVisible()) { + targetIt = false; + } + + if (targetIt) { + _target = enemy; + } + } + } + } + } catch (Exception ex) { + LOG.warn("Error while get cubic target!", ex); + } + } + + public void useCubicContinuous(Skill skill, List targets) { + int index = 0; + for (var object : targets) { + if ((object == null) || !object.isCharacter()) { + continue; + } + + final var target = (L2Character) object; + if (target.isDead()) { + continue; + } + + if (skill.isBad()) { + byte shld = Formulas.calcShldUse(_owner, target, skill); + boolean acted = Formulas.calcCubicSkillSuccess(this, target, skill, shld); + if (!acted) { + _owner.sendPacket(SystemMessageId.ATTACK_FAILED); + continue; + } + + } + + // Apply effects + skill.applyEffects(_owner, target, index++, false, false, true, 0); + } + } + + public void useCubicMdam(Skill skill, List targets) { + for (var object : targets) { + if ((object == null) || !object.isCharacter()) { + continue; + } + + final var target = (L2Character) object; + if (target.isAlikeDead()) { + if (target.isPlayer()) { + target.stopFakeDeath(true); + } else { + continue; + } + } + + boolean mcrit = Formulas.calcMCrit(_owner.getMCriticalHit(target, skill)); + byte shld = Formulas.calcShldUse(_owner, target, skill); + double damage = Formulas.calcMagicDam(this, target, skill, mcrit, shld); + + if (general().debug()) { + LOG.debug("L2SkillMdam: useCubicSkill() -> damage = " + damage); + } + + if (damage > 0) { + // Manage attack or cast break of the target (calculating rate, sending message...) + if (!target.isRaid() && Formulas.calcAtkBreak(target, damage)) { + target.breakAttack(); + target.breakCast(); + } + + // Shield Deflect Magic: If target is reflecting the skill then no damage is done. + if (target.getStat().calcStat(Stats.VENGEANCE_SKILL_MAGIC_DAMAGE, 0, target, skill) > Rnd.get(100)) { + damage = 0; + } else { + _owner.sendDamageMessage(target, (int) damage, mcrit, false, false); + target.reduceCurrentHp(damage, _owner, skill); + } + } + } + } + + public void useCubicDrain(Skill skill, List targets) { + for (var object : targets) { + if ((object == null) || !object.isCharacter()) { + continue; + } + + final var target = (L2Character) object; + if (target.isAlikeDead()) { + continue; + } + + boolean mcrit = Formulas.calcMCrit(_owner.getMCriticalHit(target, skill)); + byte shld = Formulas.calcShldUse(_owner, target, skill); + + double damage = Formulas.calcMagicDam(this, target, skill, mcrit, shld); + if (general().debug()) { + LOG.debug("L2SkillDrain: useCubicSkill() -> damage = " + damage); + } + + // TODO: Unhardcode fixed value + double hpAdd = (0.4 * damage); + L2PcInstance owner = _owner; + double hp = ((owner.getCurrentHp() + hpAdd) > owner.getMaxHp() ? owner.getMaxHp() : (owner.getCurrentHp() + hpAdd)); + + owner.setCurrentHp(hp); + + // Check to see if we should damage the target + if ((damage > 0) && !target.isDead()) { + target.reduceCurrentHp(damage, _owner, skill); + + // Manage attack or cast break of the target (calculating rate, sending message...) + if (!target.isRaid() && Formulas.calcAtkBreak(target, damage)) { + target.breakAttack(); + target.breakCast(); + } + owner.sendDamageMessage(target, (int) damage, mcrit, false, false); + } + } + } + + public void useCubicDisabler(Skill skill, List targets) { + int index = 0; + for (var object : targets) { + if ((object == null) || !object.isCharacter()) { + continue; + } + + final var target = (L2Character) object; + if (target.isDead()) { + continue; + } + + byte shld = Formulas.calcShldUse(_owner, target, skill); + + if (skill.hasEffectType(L2EffectType.STUN, L2EffectType.ROOT, L2EffectType.AGGRESSION)) { + if (Formulas.calcCubicSkillSuccess(this, target, skill, shld)) { + // Apply effects + skill.applyEffects(_owner, target, index++, false, false, true, 0); + + if (general().debug()) { + LOG.debug("Disablers: useCubicSkill() -> success"); + } + } else { + if (general().debug()) { + LOG.debug("Disablers: useCubicSkill() -> failed"); + } + } + } + } + } + + /** + * @param owner + * @param target + * @return true if the target is inside of the owner's max Cubic range + */ + public static boolean isInCubicRange(L2Character owner, L2Character target) { + if ((owner == null) || (target == null)) { + return false; + } + + int x, y, z; + // temporary range check until real behavior of cubics is known/coded + int range = MAX_MAGIC_RANGE; + + x = (owner.getX() - target.getX()); + y = (owner.getY() - target.getY()); + z = (owner.getZ() - target.getZ()); + + return (((x * x) + (y * y) + (z * z)) <= (range * range)); + } + + /** this sets the friendly target for a cubic */ + public void cubicTargetForHeal() { + L2Character target = null; + double percentleft = 100.0; + L2Party party = _owner.getParty(); + + // if owner is in a duel but not in a party duel, then it is the same as he does not have a party + if (_owner.isInDuel()) { + if (!DuelManager.getInstance().getDuel(_owner.getDuelId()).isPartyDuel()) { + party = null; + } + } + + if ((party != null) && !_owner.isInOlympiadMode()) { + // Get all visible objects in a spheric area near the L2Character + // Get a list of Party Members + for (L2Character partyMember : party.getMembers()) { + if (!partyMember.isDead()) { + // if party member not dead, check if he is in cast range of heal cubic + if (isInCubicRange(_owner, partyMember)) { + // member is in cubic casting range, check if he need heal and if he have + // the lowest HP + if (partyMember.getCurrentHp() < partyMember.getMaxHp()) { + if (percentleft > (partyMember.getCurrentHp() / partyMember.getMaxHp())) { + percentleft = (partyMember.getCurrentHp() / partyMember.getMaxHp()); + target = partyMember; + } + } + } + } + if (partyMember.getSummon() != null) { + if (partyMember.getSummon().isDead()) { + continue; + } + + // If party member's pet not dead, check if it is in cast range of heal cubic. + if (!isInCubicRange(_owner, partyMember.getSummon())) { + continue; + } + + // member's pet is in cubic casting range, check if he need heal and if he have + // the lowest HP + if (partyMember.getSummon().getCurrentHp() < partyMember.getSummon().getMaxHp()) { + if (percentleft > (partyMember.getSummon().getCurrentHp() / partyMember.getSummon().getMaxHp())) { + percentleft = (partyMember.getSummon().getCurrentHp() / partyMember.getSummon().getMaxHp()); + target = partyMember.getSummon(); + } + } + } + } + } else { + if (_owner.getCurrentHp() < _owner.getMaxHp()) { + percentleft = (_owner.getCurrentHp() / _owner.getMaxHp()); + target = _owner; + } + if (_owner.hasSummon()) { + if (!_owner.getSummon().isDead() && (_owner.getSummon().getCurrentHp() < _owner.getSummon().getMaxHp()) && (percentleft > (_owner.getSummon().getCurrentHp() / _owner.getSummon().getMaxHp())) && isInCubicRange(_owner, _owner.getSummon())) { + target = _owner.getSummon(); + } + } + } + + _target = target; + } + + public boolean givenByOther() { + return _givenByOther; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DecoyInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DecoyInstance.java new file mode 100644 index 00000000..7c2e7f78 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DecoyInstance.java @@ -0,0 +1,145 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Decoy; +import com.l2jserver.gameserver.model.actor.knownlist.DecoyKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; + +public class L2DecoyInstance extends L2Decoy { + private int _totalLifeTime; + private int _timeRemaining; + private Future _DecoyLifeTask; + private Future _HateSpam; + + public L2DecoyInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, int totalLifeTime) { + super(objectId, template, owner); + setInstanceType(InstanceType.L2DecoyInstance); + _totalLifeTime = totalLifeTime; + _timeRemaining = _totalLifeTime; + int skillLevel = getTemplate().getDisplayId() - 13070; + _DecoyLifeTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new DecoyLifetime(getOwner(), this), 1000, 1000); + _HateSpam = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new HateSpam(this, SkillData.getInstance().getSkill(5272, skillLevel)), 2000, 5000); + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + if (_HateSpam != null) { + _HateSpam.cancel(true); + _HateSpam = null; + } + _totalLifeTime = 0; + DecayTaskManager.getInstance().add(this); + return true; + } + + @Override + public DecoyKnownList getKnownList() { + return (DecoyKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new DecoyKnownList(this)); + } + + static class DecoyLifetime implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(DecoyLifetime.class); + private final L2PcInstance _activeChar; + private final L2DecoyInstance _Decoy; + + DecoyLifetime(L2PcInstance activeChar, L2DecoyInstance Decoy) { + _activeChar = activeChar; + _Decoy = Decoy; + } + + @Override + public void run() { + try { + _Decoy.decTimeRemaining(1000); + double newTimeRemaining = _Decoy.getTimeRemaining(); + if (newTimeRemaining < 0) { + _Decoy.unSummon(_activeChar); + } + } catch (Exception ex) { + LOG.error("Error running decoy lifetime task!", ex); + } + } + } + + private static class HateSpam implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(HateSpam.class); + private final L2DecoyInstance _activeChar; + private final Skill _skill; + + HateSpam(L2DecoyInstance activeChar, Skill Hate) { + _activeChar = activeChar; + _skill = Hate; + } + + @Override + public void run() { + try { + _activeChar.setTarget(_activeChar); + _activeChar.doCast(_skill); + } catch (Throwable ex) { + LOG.error("Error running decoy hate spam task!", ex); + } + } + } + + @Override + public void unSummon(L2PcInstance owner) { + if (_DecoyLifeTask != null) { + _DecoyLifeTask.cancel(true); + _DecoyLifeTask = null; + } + if (_HateSpam != null) { + _HateSpam.cancel(true); + _HateSpam = null; + } + super.unSummon(owner); + } + + public void decTimeRemaining(int value) { + _timeRemaining -= value; + } + + public int getTimeRemaining() { + return _timeRemaining; + } + + public int getTotalLifeTime() { + return _totalLifeTime; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DefenderInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DefenderInstance.java new file mode 100644 index 00000000..2c3145de --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DefenderInstance.java @@ -0,0 +1,196 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2FortSiegeGuardAI; +import com.l2jserver.gameserver.ai.L2SiegeGuardAI; +import com.l2jserver.gameserver.ai.L2SpecialSiegeGuardAI; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.knownlist.DefenderKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +public class L2DefenderInstance extends L2Attackable { + private static final Logger LOG = LoggerFactory.getLogger(L2DefenderInstance.class); + private Castle _castle = null; // the castle which the instance should defend + private Fort _fort = null; // the fortress which the instance should defend + private SiegableHall _hall = null; // the siegable hall which the instance should defend + + public L2DefenderInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2DefenderInstance); + } + + @Override + public DefenderKnownList getKnownList() { + return (DefenderKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new DefenderKnownList(this)); + } + + @Override + protected L2CharacterAI initAI() { + if ((getConquerableHall() == null) && (getCastle(10000) == null)) { + return new L2FortSiegeGuardAI(this); + } else if (getCastle(10000) != null) { + return new L2SiegeGuardAI(this); + } + return new L2SpecialSiegeGuardAI(this); + } + + /** + * Return True if a siege is in progress and the L2Character attacker isn't a Defender. + * @param attacker The L2Character that the L2SiegeGuardInstance try to attack + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + // Attackable during siege by all except defenders + if (!(attacker instanceof L2Playable)) { + return false; + } + + L2PcInstance player = attacker.getActingPlayer(); + + // Check if siege is in progress + if (((_fort != null) && _fort.getZone().isActive()) || ((_castle != null) && _castle.getZone().isActive()) || ((_hall != null) && _hall.getSiegeZone().isActive())) { + int activeSiegeId = (_fort != null ? _fort.getResidenceId() : (_castle != null ? _castle.getResidenceId() : (_hall != null ? _hall.getId() : 0))); + + // Check if player is an enemy of this defender npc + return (player != null) && (((player.getSiegeState() == 2) && !player.isRegisteredOnThisSiegeField(activeSiegeId)) || ((player.getSiegeState() == 1) && !TerritoryWarManager.getInstance().isAllyField(player, activeSiegeId)) || (player.getSiegeState() == 0)); + } + return false; + } + + @Override + public boolean hasRandomAnimation() { + return false; + } + + /** + * This method forces guard to return to home location previously set + */ + @Override + public void returnHome() { + if (getWalkSpeed() <= 0) { + return; + } + if (getSpawn() == null) { + return; + } + if (!isInsideRadius(getSpawn(), 40, false, false)) { + if (general().debug()) { + LOG.debug("{} moving home", getObjectId()); + } + setisReturningToSpawnPoint(true); + clearAggroList(); + + if (hasAI()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, getSpawn().getLocation()); + } + } + } + + @Override + public void onSpawn() { + super.onSpawn(); + + _fort = FortManager.getInstance().getFort(getX(), getY(), getZ()); + _castle = CastleManager.getInstance().getCastle(getX(), getY(), getZ()); + _hall = getConquerableHall(); + if ((_fort == null) && (_castle == null) && (_hall == null)) { + LOG.warn("L2DefenderInstance spawned outside of Fortress, Castle or Siegable hall Zone! NpcId: {} x={} y={} z={}", getId(), getX(), getY(), getZ()); + } + } + + /** + * Custom onAction behaviour. Note that super() is not called because guards need extra check to see if a player should interact or ATTACK them when clicked. + */ + @Override + public void onAction(L2PcInstance player, boolean interact) { + if (!canTarget(player)) { + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + // Check if the L2PcInstance already target the L2NpcInstance + if (this != player.getTarget()) { + if (general().debug()) { + LOG.debug("new target selected: {}", getObjectId()); + } + + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + if (isAutoAttackable(player) && !isAlikeDead()) { + if (Math.abs(player.getZ() - getZ()) < 600) // this max height difference might need some tweaking + { + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } + } + if (!isAutoAttackable(player)) { + if (!canInteract(player)) { + // Notify the L2PcInstance AI with AI_INTENTION_INTERACT + player.getAI().setIntention(CtrlIntention.AI_INTENTION_INTERACT, this); + } + } + } + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void addDamageHate(L2Character attacker, int damage, long aggro) { + if (attacker == null) { + return; + } + + if (!(attacker instanceof L2DefenderInstance)) { + if ((damage == 0) && (aggro <= 1) && (attacker instanceof L2Playable)) { + L2PcInstance player = attacker.getActingPlayer(); + // Check if siege is in progress + if (((_fort != null) && _fort.getZone().isActive()) || ((_castle != null) && _castle.getZone().isActive()) || ((_hall != null) && _hall.getSiegeZone().isActive())) { + int activeSiegeId = (_fort != null ? _fort.getResidenceId() : (_castle != null ? _castle.getResidenceId() : (_hall != null ? _hall.getId() : 0))); + if ((player != null) && (((player.getSiegeState() == 2) && player.isRegisteredOnThisSiegeField(activeSiegeId)) || ((player.getSiegeState() == 1) && TerritoryWarManager.getInstance().isAllyField(player, activeSiegeId)))) { + return; + } + } + } + super.addDamageHate(attacker, damage, aggro); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoorInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoorInstance.java new file mode 100644 index 00000000..e168edc5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoorInstance.java @@ -0,0 +1,652 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2DoorAI; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.ClanHallManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.knownlist.DoorKnownList; +import com.l2jserver.gameserver.model.actor.stat.DoorStat; +import com.l2jserver.gameserver.model.actor.status.DoorStatus; +import com.l2jserver.gameserver.model.actor.templates.L2DoorTemplate; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.ClanHall; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.DoorStatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.OnEventTrigger; +import com.l2jserver.gameserver.network.serverpackets.StaticObject; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class L2DoorInstance extends L2Character { + private static final Logger LOG = LoggerFactory.getLogger(L2DoorInstance.class); + + public static final byte OPEN_BY_CLICK = 1; + public static final byte OPEN_BY_TIME = 2; + public static final byte OPEN_BY_ITEM = 4; + public static final byte OPEN_BY_SKILL = 8; + public static final byte OPEN_BY_CYCLE = 16; + + /** The castle index in the array of L2Castle this L2NpcInstance belongs to */ + private int _castleIndex = -2; + /** The fort index in the array of L2Fort this L2NpcInstance belongs to */ + private int _fortIndex = -2; + private ClanHall _clanHall; + private boolean _open; + private boolean _isAttackableDoor; + private boolean _isTargetable; + private int _meshindex = 1; + // used for autoclose on open + private Future _autoCloseTask; + + public L2DoorInstance(int objectId, L2DoorTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2DoorInstance); + setIsInvul(false); + setLethalable(false); + _open = template.isOpenByDefault(); + _isAttackableDoor = template.isAttackable(); + _isTargetable = template.isTargetable(); + + if (getGroupName() != null) { + DoorData.addDoorGroup(getGroupName(), getId()); + } + + if (isOpenableByTime()) { + startTimerOpen(); + } + + int clanhallId = template.getClanHallId(); + if (clanhallId > 0) { + ClanHall hall = ClanHallManager.getInstance().getAllClanHalls().get(clanhallId); + if (hall != null) { + setClanHall(hall); + hall.getDoors().add(this); + } + } + } + + @Override + protected L2CharacterAI initAI() { + return new L2DoorAI(this); + } + + private void startTimerOpen() { + int delay = _open ? getTemplate().getOpenTime() : getTemplate().getCloseTime(); + if (getTemplate().getRandomTime() > 0) { + delay += Rnd.get(getTemplate().getRandomTime()); + } + ThreadPoolManager.getInstance().scheduleGeneral(new TimerOpen(), delay * 1000); + } + + @Override + public final DoorKnownList getKnownList() { + return (DoorKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new DoorKnownList(this)); + } + + @Override + public L2DoorTemplate getTemplate() { + return (L2DoorTemplate) super.getTemplate(); + + } + + @Override + public final DoorStatus getStatus() { + return (DoorStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new DoorStatus(this)); + } + + @Override + public void initCharStat() { + setStat(new DoorStat(this)); + } + + @Override + public DoorStat getStat() { + return (DoorStat) super.getStat(); + } + + /** + * @return {@code true} if door is open-able by skill. + */ + public final boolean isOpenableBySkill() { + return (getTemplate().getOpenType() & OPEN_BY_SKILL) == OPEN_BY_SKILL; + } + + /** + * @return {@code true} if door is open-able by item. + */ + public final boolean isOpenableByItem() { + return (getTemplate().getOpenType() & OPEN_BY_ITEM) == OPEN_BY_ITEM; + } + + /** + * @return {@code true} if door is open-able by double-click. + */ + public final boolean isOpenableByClick() { + return (getTemplate().getOpenType() & OPEN_BY_CLICK) == OPEN_BY_CLICK; + } + + /** + * @return {@code true} if door is open-able by time. + */ + public final boolean isOpenableByTime() { + return (getTemplate().getOpenType() & OPEN_BY_TIME) == OPEN_BY_TIME; + } + + /** + * @return {@code true} if door is open-able by Field Cycle system. + */ + public final boolean isOpenableByCycle() { + return (getTemplate().getOpenType() & OPEN_BY_CYCLE) == OPEN_BY_CYCLE; + } + + @Override + public final int getLevel() { + return getTemplate().getLevel(); + } + + /** + * Gets the door ID. + * @return the door ID + */ + @Override + public int getId() { + return getTemplate().getId(); + } + + /** + * @return Returns the open. + */ + public boolean getOpen() { + return _open; + } + + /** + * @param open The open to set. + */ + public void setOpen(boolean open) { + _open = open; + if (getChildId() > 0) { + L2DoorInstance sibling = getSiblingDoor(getChildId()); + if (sibling != null) { + sibling.notifyChildEvent(open); + } else { + LOG.warn("Cannot find child id: {}", getChildId()); + } + } + } + + public boolean getIsAttackableDoor() { + return _isAttackableDoor; + } + + public boolean getIsShowHp() { + return getTemplate().isShowHp(); + } + + public void setIsAttackableDoor(boolean val) { + _isAttackableDoor = val; + } + + public int getDamage() { + int dmg = 6 - (int) Math.ceil((getCurrentHp() / getMaxHp()) * 6); + if (dmg > 6) { + return 6; + } + return Math.max(dmg, 0); + } + + // TODO: Replace index with the castle id itself. + public final Castle getCastle() { + if (_castleIndex < 0) { + _castleIndex = CastleManager.getInstance().getCastleIndex(this); + } + if (_castleIndex < 0) { + return null; + } + return CastleManager.getInstance().getCastles().get(_castleIndex); + } + + // TODO: Replace index with the fort id itself. + public final Fort getFort() { + if (_fortIndex < 0) { + _fortIndex = FortManager.getInstance().getFortIndex(this); + } + if (_fortIndex < 0) { + return null; + } + return FortManager.getInstance().getForts().get(_fortIndex); + } + + public void setClanHall(ClanHall clanhall) { + _clanHall = clanhall; + } + + public ClanHall getClanHall() { + return _clanHall; + } + + public boolean isEnemy() { + if ((getCastle() != null) && (getCastle().getResidenceId() > 0) && getCastle().getZone().isActive() && getIsShowHp()) { + return true; + } + if ((getFort() != null) && (getFort().getResidenceId() > 0) && getFort().getZone().isActive() && getIsShowHp()) { + return true; + } + return (getClanHall() != null) && getClanHall().isSiegableHall() && ((SiegableHall) getClanHall()).getSiegeZone().isActive() && getIsShowHp(); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + // Doors can`t be attacked by NPCs + if (!(attacker instanceof L2Playable)) { + return false; + } + + if (getIsAttackableDoor()) { + return true; + } + if (!getIsShowHp()) { + return false; + } + + L2PcInstance actingPlayer = attacker.getActingPlayer(); + + if (getClanHall() != null) { + SiegableHall hall = (SiegableHall) getClanHall(); + if (!hall.isSiegableHall()) { + return false; + } + return hall.isInSiege() && hall.getSiege().doorIsAutoAttackable() && hall.getSiege().checkIsAttacker(actingPlayer.getClan()); + } + // Attackable only during siege by everyone (not owner) + boolean isCastle = ((getCastle() != null) && (getCastle().getResidenceId() > 0) && getCastle().getZone().isActive()); + boolean isFort = ((getFort() != null) && (getFort().getResidenceId() > 0) && getFort().getZone().isActive()); + int activeSiegeId = (getFort() != null ? getFort().getResidenceId() : (getCastle() != null ? getCastle().getResidenceId() : 0)); + + if (TerritoryWarManager.getInstance().isTWInProgress()) { + return !TerritoryWarManager.getInstance().isAllyField(actingPlayer, activeSiegeId); + } else if (isFort) { + L2Clan clan = actingPlayer.getClan(); + if ((clan != null) && (clan == getFort().getOwnerClan())) { + return false; + } + } else if (isCastle) { + L2Clan clan = actingPlayer.getClan(); + if ((clan != null) && (clan.getId() == getCastle().getOwnerId())) { + return false; + } + } + return (isCastle || isFort); + } + + @Override + public void updateAbnormalEffect() { + } + + /** + * Return null. + */ + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + @Override + public void broadcastStatusUpdate() { + Collection knownPlayers = getKnownList().getKnownPlayers().values(); + if (knownPlayers.isEmpty()) { + return; + } + + StaticObject su = new StaticObject(this, false); + StaticObject targetableSu = new StaticObject(this, true); + DoorStatusUpdate dsu = new DoorStatusUpdate(this); + OnEventTrigger oe = null; + if (getEmitter() > 0) { + oe = new OnEventTrigger(this, !getOpen()); + } + + for (L2PcInstance player : knownPlayers) { + if ((player == null) || !isVisibleFor(player)) { + continue; + } + + if (player.isGM() || (((getCastle() != null) && (getCastle().getResidenceId() > 0)) || ((getFort() != null) && (getFort().getResidenceId() > 0)))) { + player.sendPacket(targetableSu); + } else { + player.sendPacket(su); + } + + player.sendPacket(dsu); + if (oe != null) { + player.sendPacket(oe); + } + } + } + + public final void openMe() { + if (getGroupName() != null) { + manageGroupOpen(true, getGroupName()); + return; + } + setOpen(true); + broadcastStatusUpdate(); + startAutoCloseTask(); + } + + public final void closeMe() { + // remove close task + Future oldTask = _autoCloseTask; + if (oldTask != null) { + _autoCloseTask = null; + oldTask.cancel(false); + } + if (getGroupName() != null) { + manageGroupOpen(false, getGroupName()); + return; + } + setOpen(false); + broadcastStatusUpdate(); + } + + private void manageGroupOpen(boolean open, String groupName) { + Set set = DoorData.getDoorsByGroup(groupName); + L2DoorInstance first = null; + for (Integer id : set) { + L2DoorInstance door = getSiblingDoor(id); + if (first == null) { + first = door; + } + + if (door.getOpen() != open) { + door.setOpen(open); + door.broadcastStatusUpdate(); + } + } + if ((first != null) && open) { + first.startAutoCloseTask(); // only one from group + } + } + + /** + * Door notify child about open state change + * @param open true if opened + */ + private void notifyChildEvent(boolean open) { + byte openThis = open ? getTemplate().getMasterDoorOpen() : getTemplate().getMasterDoorClose(); + if (openThis == 1) { + openMe(); + } else if (openThis == -1) { + closeMe(); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getTemplate().getId() + "](" + getObjectId() + ")"; + } + + public String getDoorName() { + return getTemplate().getName(); + } + + public int getX(int i) { + return getTemplate().getNodeX()[i]; + } + + public int getY(int i) { + return getTemplate().getNodeY()[i]; + } + + public int getZMin() { + return getTemplate().getNodeZ(); + } + + public int getZMax() { + return getTemplate().getNodeZ() + getTemplate().getHeight(); + } + + public List getKnownDefenders() { + final List result = new ArrayList<>(); + for (L2Object obj : getKnownList().getKnownObjects().values()) { + if (obj instanceof L2DefenderInstance) { + result.add((L2DefenderInstance) obj); + } + } + return result; + } + + public void setMeshIndex(int mesh) { + _meshindex = mesh; + } + + public int getMeshIndex() { + return _meshindex; + } + + public int getEmitter() { + return getTemplate().getEmmiter(); + } + + public boolean isWall() { + return getTemplate().isWall(); + } + + public String getGroupName() { + return getTemplate().getGroupName(); + } + + public int getChildId() { + return getTemplate().getChildDoorId(); + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + if (isWall() && (getInstanceId() == 0)) { + if (!attacker.isServitor()) { + return; + } + + final L2ServitorInstance servitor = (L2ServitorInstance) attacker; + if (servitor.getTemplate().getRace() != Race.SIEGE_WEAPON) { + return; + } + } + + super.reduceCurrentHp(damage, attacker, awake, isDOT, skill); + } + + @Override + public void reduceCurrentHpByDOT(double i, L2Character attacker, Skill skill) { + // doors can't be damaged by DOTs + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + boolean isFort = ((getFort() != null) && (getFort().getResidenceId() > 0) && getFort().getSiege().isInProgress()); + boolean isCastle = ((getCastle() != null) && (getCastle().getResidenceId() > 0) && getCastle().getSiege().isInProgress()); + boolean isHall = ((getClanHall() != null) && getClanHall().isSiegableHall() && ((SiegableHall) getClanHall()).isInSiege()); + + if (isFort || isCastle || isHall) { + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.CASTLE_GATE_BROKEN_DOWN)); + } + return true; + } + + @Override + public void moveToLocation(int x, int y, int z, int offset) { + } + + @Override + public void stopMove(Location loc) { + } + + @Override + public void doAttack(L2Character target) { + } + + @Override + public void doCast(Skill skill) { + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + if (isVisibleFor(activeChar)) { + if (getEmitter() > 0) { + activeChar.sendPacket(new OnEventTrigger(this, !getOpen())); + } + + activeChar.sendPacket(new StaticObject(this, activeChar.isGM())); + } + } + + public void setTargetable(boolean b) { + _isTargetable = b; + broadcastStatusUpdate(); + } + + @Override + public boolean isTargetable() { + return _isTargetable; + } + + public boolean checkCollision() { + return getTemplate().isCheckCollision(); + } + + /** + * All doors are stored at DoorTable except instance doors + * @param doorId + * @return + */ + private L2DoorInstance getSiblingDoor(int doorId) { + if (getInstanceId() == 0) { + return DoorData.getInstance().getDoor(doorId); + } + + Instance inst = InstanceManager.getInstance().getInstance(getInstanceId()); + if (inst != null) { + return inst.getDoor(doorId); + } + + return null; // 2 late + } + + private void startAutoCloseTask() { + if ((getTemplate().getCloseTime() < 0) || isOpenableByTime()) { + return; + } + Future oldTask = _autoCloseTask; + if (oldTask != null) { + _autoCloseTask = null; + oldTask.cancel(false); + } + _autoCloseTask = ThreadPoolManager.getInstance().scheduleGeneral(new AutoClose(), getTemplate().getCloseTime() * 1000); + } + + class AutoClose implements Runnable { + @Override + public void run() { + if (getOpen()) { + closeMe(); + } + } + } + + class TimerOpen implements Runnable { + @Override + public void run() { + boolean open = getOpen(); + if (open) { + closeMe(); + } else { + openMe(); + } + + int delay = open ? getTemplate().getCloseTime() : getTemplate().getOpenTime(); + if (getTemplate().getRandomTime() > 0) { + delay += Rnd.get(getTemplate().getRandomTime()); + } + ThreadPoolManager.getInstance().scheduleGeneral(this, delay * 1000); + } + } + + @Override + public boolean isDoor() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoormenInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoormenInstance.java new file mode 100644 index 00000000..31bf0dc5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2DoormenInstance.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2DoormenInstance extends L2NpcInstance { + public L2DoormenInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2DoormenInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EffectPointInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EffectPointInstance.java new file mode 100644 index 00000000..3c5ba2b2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EffectPointInstance.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +public class L2EffectPointInstance extends L2Npc { + private final L2PcInstance _owner; + + public L2EffectPointInstance(int objectId, L2NpcTemplate template, L2Character owner) { + super(objectId, template); + setInstanceType(InstanceType.L2EffectPointInstance); + setIsInvul(false); + _owner = owner == null ? null : owner.getActingPlayer(); + if (owner != null) { + setInstanceId(owner.getInstanceId()); + } + } + + @Override + public L2PcInstance getActingPlayer() { + return _owner; + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onActionShift(L2PcInstance player) { + if (player == null) { + return; + } + player.sendPacket(ActionFailed.STATIC_PACKET); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EventMonsterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EventMonsterInstance.java new file mode 100644 index 00000000..43b5f711 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2EventMonsterInstance.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @author Gnacik + */ +public class L2EventMonsterInstance extends L2MonsterInstance { + // Block offensive skills usage on event mobs + // mainly for AoE skills, disallow kill many event mobs + // with one skill + public boolean block_skill_attack = false; + + // Event mobs should drop items to ground + // but item pickup must be protected to killer + // TODO: Some mobs need protect drop for spawner + public boolean drop_on_ground = false; + + public L2EventMonsterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2EventMobInstance); + } + + public void eventSetBlockOffensiveSkills(boolean value) { + block_skill_attack = value; + } + + public void eventSetDropOnGround(boolean value) { + drop_on_ground = value; + } + + public boolean eventDropOnGround() { + return drop_on_ground; + } + + public boolean eventSkillAttackBlocked() { + return block_skill_attack; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FeedableBeastInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FeedableBeastInstance.java new file mode 100644 index 00000000..703cb333 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FeedableBeastInstance.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +// This class is here mostly for convenience and for avoidance of hardcoded IDs. +// It refers to Beast (mobs) that can be attacked but can also be fed +// For example, the Beast Farm's Alpen Buffalo. +// This class is only truly used by the handlers in order to check the correctness +// of the target. However, no additional tasks are needed, since they are all +// handled by scripted AI. +public class L2FeedableBeastInstance extends L2MonsterInstance { + public L2FeedableBeastInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FeedableBeastInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalGuideInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalGuideInstance.java new file mode 100644 index 00000000..2ac1b655 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalGuideInstance.java @@ -0,0 +1,221 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.sevenSigns; + +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.SevenSignsFestival; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Festival of Darkness Guide (Seven Signs) + * @author Tempy + */ +public final class L2FestivalGuideInstance extends L2Npc { + private final int _festivalType; + private final int _festivalOracle; + private final int _blueStonesNeeded; + private final int _greenStonesNeeded; + private final int _redStonesNeeded; + + /** + * Creates a festival guide. + * @param template the festival guide NPC template + */ + public L2FestivalGuideInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FestivalGiudeInstance); + + switch (getId()) { + case 31127, 31132 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_31; + _festivalOracle = SevenSigns.CABAL_DAWN; + _blueStonesNeeded = 900; + _greenStonesNeeded = 540; + _redStonesNeeded = 270; + } + case 31128, 31133 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_42; + _festivalOracle = SevenSigns.CABAL_DAWN; + _blueStonesNeeded = 1500; + _greenStonesNeeded = 900; + _redStonesNeeded = 450; + } + case 31129, 31134 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_53; + _festivalOracle = SevenSigns.CABAL_DAWN; + _blueStonesNeeded = 3000; + _greenStonesNeeded = 1800; + _redStonesNeeded = 900; + } + case 31130, 31135 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_64; + _festivalOracle = SevenSigns.CABAL_DAWN; + _blueStonesNeeded = 4500; + _greenStonesNeeded = 2700; + _redStonesNeeded = 1350; + } + case 31131, 31136 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_NONE; + _festivalOracle = SevenSigns.CABAL_DAWN; + _blueStonesNeeded = 6000; + _greenStonesNeeded = 3600; + _redStonesNeeded = 1800; + } + case 31137, 31142 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_31; + _festivalOracle = SevenSigns.CABAL_DUSK; + _blueStonesNeeded = 900; + _greenStonesNeeded = 540; + _redStonesNeeded = 270; + } + case 31138, 31143 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_42; + _festivalOracle = SevenSigns.CABAL_DUSK; + _blueStonesNeeded = 1500; + _greenStonesNeeded = 900; + _redStonesNeeded = 450; + } + case 31139, 31144 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_53; + _festivalOracle = SevenSigns.CABAL_DUSK; + _blueStonesNeeded = 3000; + _greenStonesNeeded = 1800; + _redStonesNeeded = 900; + } + case 31140, 31145 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_64; + _festivalOracle = SevenSigns.CABAL_DUSK; + _blueStonesNeeded = 4500; + _greenStonesNeeded = 2700; + _redStonesNeeded = 1350; + } + case 31141, 31146 -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_NONE; + _festivalOracle = SevenSigns.CABAL_DUSK; + _blueStonesNeeded = 6000; + _greenStonesNeeded = 3600; + _redStonesNeeded = 1800; + } + default -> { + _festivalType = SevenSignsFestival.FESTIVAL_LEVEL_MAX_NONE; + _festivalOracle = SevenSigns.CABAL_NULL; + _blueStonesNeeded = 0; + _greenStonesNeeded = 0; + _redStonesNeeded = 0; + } + } + } + + public int getFestivalType() { + return _festivalType; + } + + public int getFestivalOracle() { + return _festivalOracle; + } + + public int getStoneCount(int stoneType) { + return switch (stoneType) { + case SevenSigns.SEAL_STONE_BLUE_ID -> _blueStonesNeeded; + case SevenSigns.SEAL_STONE_GREEN_ID -> _greenStonesNeeded; + case SevenSigns.SEAL_STONE_RED_ID -> _redStonesNeeded; + default -> -1; + }; + } + + public void showChatWindow(L2PcInstance player, int val, String suffix, boolean isDescription) { + final var filename = new StringBuilder(SevenSigns.SEVEN_SIGNS_HTML_PATH) + .append("festival/") + .append((isDescription) ? "desc_" : "festival_") + .append((suffix != null) ? val + suffix + ".htm" : val + ".htm"); + + // Send a Server->Client NpcHtmlMessage containing the text of the L2NpcInstance to the L2PcInstance + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename.toString()); + html.replace("%objectId%", String.valueOf(getObjectId())); + html.replace("%festivalType%", SevenSignsFestival.getFestivalName(_festivalType)); + html.replace("%cycleMins%", String.valueOf(SevenSignsFestival.getInstance().getMinsToNextCycle())); + if (!isDescription && "2b".equals(val + suffix)) { + html.replace("%minFestivalPartyMembers%", String.valueOf(sevenSigns().getFestivalMinPlayer())); + } + + // If the stats or bonus table is required, construct them. + if (val == 5) { + html.replace("%statsTable%", getStatsTable()); + } + if (val == 6) { + html.replace("%bonusTable%", getBonusTable()); + } + + // festival's fee + if (val == 1) { + html.replace("%blueStoneNeeded%", String.valueOf(_blueStonesNeeded)); + html.replace("%greenStoneNeeded%", String.valueOf(_greenStonesNeeded)); + html.replace("%redStoneNeeded%", String.valueOf(_redStonesNeeded)); + } + + player.sendPacket(html); + + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + private static String getStatsTable() { + final StringBuilder tableHtml = new StringBuilder(1000); + + // Get the scores for each of the festival level ranges (types). + for (int i = 0; i < 5; i++) { + int dawnScore = SevenSignsFestival.getInstance().getHighestScore(SevenSigns.CABAL_DAWN, i); + int duskScore = SevenSignsFestival.getInstance().getHighestScore(SevenSigns.CABAL_DUSK, i); + String festivalName = SevenSignsFestival.getFestivalName(i); + String winningCabal = "Children of Dusk"; + + if (dawnScore > duskScore) { + winningCabal = "Children of Dawn"; + } else if (dawnScore == duskScore) { + winningCabal = "None"; + } + + StringUtil.append(tableHtml, "", festivalName, "", String.valueOf(duskScore), "", String.valueOf(dawnScore), "", winningCabal, ""); + } + + return tableHtml.toString(); + } + + private static String getBonusTable() { + final StringBuilder tableHtml = new StringBuilder(500); + + // Get the accumulated scores for each of the festival level ranges (types). + for (int i = 0; i < 5; i++) { + int accumScore = SevenSignsFestival.getInstance().getAccumulatedBonus(i); + String festivalName = SevenSignsFestival.getFestivalName(i); + + StringUtil.append(tableHtml, "", festivalName, "", String.valueOf(accumScore), ""); + } + + return tableHtml.toString(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalMonsterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalMonsterInstance.java new file mode 100644 index 00000000..7689a10f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FestivalMonsterInstance.java @@ -0,0 +1,103 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.SevenSignsFestival; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; + +/** + * This class manages all attackable festival NPCs, spawned during the Festival of Darkness. + * @author Tempy + */ +public class L2FestivalMonsterInstance extends L2MonsterInstance { + protected int _bonusMultiplier = 1; + + public L2FestivalMonsterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FestivalMonsterInstance); + } + + public void setOfferingBonus(int bonusMultiplier) { + _bonusMultiplier = bonusMultiplier; + } + + /** + * Return True if the attacker is not another L2FestivalMonsterInstance. + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + return !(attacker instanceof L2FestivalMonsterInstance); + } + + /** + * All mobs in the festival are aggressive, and have high aggro range. + */ + @Override + public boolean isAggressive() { + return true; + } + + /** + * All mobs in the festival really don't need random animation. + */ + @Override + public boolean hasRandomAnimation() { + return false; + } + + /** + * Actions: + *
        + *
      • Check if the killing object is a player, and then find the party they belong to.
      • + *
      • Add a blood offering item to the leader of the party.
      • + *
      • Update the party leader's inventory to show the new item addition.
      • + *
      + */ + @Override + public void doItemDrop(L2Character lastAttacker) { + if (!(lastAttacker instanceof L2PcInstance killingChar)) { + return; + } + + L2Party associatedParty = killingChar.getParty(); + if (associatedParty == null) { + return; + } + + L2PcInstance partyLeader = associatedParty.getLeader(); + L2ItemInstance addedOfferings = partyLeader.getInventory().addItem("Sign", SevenSignsFestival.FESTIVAL_OFFERING_ID, _bonusMultiplier, partyLeader, this); + + InventoryUpdate iu = new InventoryUpdate(); + + if (addedOfferings.getCount() != _bonusMultiplier) { + iu.addModifiedItem(addedOfferings); + } else { + iu.addNewItem(addedOfferings); + } + + partyLeader.sendPacket(iu); + + super.doItemDrop(lastAttacker); // Normal drop + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlameTowerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlameTowerInstance.java new file mode 100644 index 00000000..782c5004 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlameTowerInstance.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.List; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Tower; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.zone.L2ZoneType; + +/** + * Flame Control Tower instance. + * @author JIV + */ +public class L2FlameTowerInstance extends L2Tower { + private int _upgradeLevel = 0; + private List _zoneList; + + public L2FlameTowerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FlameTowerInstance); + } + + @Override + public boolean doDie(L2Character killer) { + enableZones(false); + return super.doDie(killer); + } + + @Override + public boolean deleteMe() { + enableZones(false); + return super.deleteMe(); + } + + public final void enableZones(boolean state) { + if ((_zoneList != null) && (_upgradeLevel != 0)) { + final int maxIndex = _upgradeLevel * 2; + for (int i = 0; i < maxIndex; i++) { + final L2ZoneType zone = ZoneManager.getInstance().getZoneById(_zoneList.get(i)); + if (zone != null) { + zone.setEnabled(state); + } + } + } + } + + public final void setUpgradeLevel(int level) { + _upgradeLevel = level; + } + + public final void setZoneList(List list) { + _zoneList = list; + enableZones(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlyTerrainObjectInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlyTerrainObjectInstance.java new file mode 100644 index 00000000..9b336c4a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FlyTerrainObjectInstance.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +public final class L2FlyTerrainObjectInstance extends L2Npc { + + public L2FlyTerrainObjectInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FlyTerrainObjectInstance); + setIsFlying(true); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onActionShift(L2PcInstance player) { + if (player.isGM()) { + super.onActionShift(player); + } else { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortCommanderInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortCommanderInstance.java new file mode 100644 index 00000000..2f24fe67 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortCommanderInstance.java @@ -0,0 +1,165 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.model.FortSiegeSpawn; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.NpcSay; + +public class L2FortCommanderInstance extends L2DefenderInstance { + private static final Logger LOG = LoggerFactory.getLogger(L2FortCommanderInstance.class); + private boolean _canTalk; + + public L2FortCommanderInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FortCommanderInstance); + _canTalk = true; + } + + /** + * Return True if a siege is in progress and the L2Character attacker isn't a Defender. + * @param attacker The L2Character that the L2CommanderInstance try to attack + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + if (!(attacker instanceof L2PcInstance)) { + return false; + } + // Attackable during siege by all except defenders + return ((getFort() != null) && (getFort().getResidenceId() > 0) && getFort().getSiege().isInProgress() && !getFort().getSiege().checkIsDefender(attacker.getClan())); + } + + @Override + public void addDamageHate(L2Character attacker, int damage, long aggro) { + if (attacker == null) { + return; + } + + if (!(attacker instanceof L2FortCommanderInstance)) { + super.addDamageHate(attacker, damage, aggro); + } + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + if (getFort().getSiege().isInProgress()) { + getFort().getSiege().killedCommander(this); + + } + + return true; + } + + /** + * This method forces guard to return to home location previously set + */ + @Override + public void returnHome() { + if (!isInsideRadius(getSpawn(), 200, false, false)) { + if (general().debug()) { + LOG.debug("{} moving home", getObjectId()); + } + setisReturningToSpawnPoint(true); + clearAggroList(); + + if (hasAI()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_MOVE_TO, getSpawn().getLocation()); + } + } + } + + @Override + public final void addDamage(L2Character attacker, int damage, Skill skill) { + L2Spawn spawn = getSpawn(); + if ((spawn != null) && canTalk()) { + List commanders = FortSiegeManager.getInstance().getCommanderSpawnList(getFort().getResidenceId()); + for (FortSiegeSpawn spawn2 : commanders) { + if (spawn2.getId() == spawn.getId()) { + NpcStringId npcString = null; + switch (spawn2.getMessageId()) { + case 1 -> npcString = NpcStringId.ATTACKING_THE_ENEMYS_REINFORCEMENTS_IS_NECESSARY_TIME_TO_DIE; + case 2 -> { + if (attacker instanceof L2Summon) { + attacker = ((L2Summon) attacker).getOwner(); + } + npcString = NpcStringId.EVERYONE_CONCENTRATE_YOUR_ATTACKS_ON_S1_SHOW_THE_ENEMY_YOUR_RESOLVE; + } + case 3 -> npcString = NpcStringId.SPIRIT_OF_FIRE_UNLEASH_YOUR_POWER_BURN_THE_ENEMY; + } + if (npcString != null) { + NpcSay ns = new NpcSay(getObjectId(), Say2.NPC_SHOUT, getId(), npcString); + if (npcString.getParamCount() == 1) { + ns.addStringParameter(attacker.getName()); + } + + broadcastPacket(ns); + setCanTalk(false); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleTalkTask(), 10000); + } + } + } + } + super.addDamage(attacker, damage, skill); + } + + private class ScheduleTalkTask implements Runnable { + + public ScheduleTalkTask() { + } + + @Override + public void run() { + setCanTalk(true); + } + } + + void setCanTalk(boolean val) { + _canTalk = val; + } + + private boolean canTalk() { + return _canTalk; + } + + @Override + public boolean hasRandomAnimation() { + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortDoormenInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortDoormenInstance.java new file mode 100644 index 00000000..27b24eed --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortDoormenInstance.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2FortDoormenInstance extends L2DoormenInstance { + + public L2FortDoormenInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FortDoormenInstance); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortLogisticsInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortLogisticsInstance.java new file mode 100644 index 00000000..203b645e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortLogisticsInstance.java @@ -0,0 +1,188 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.StringTokenizer; + +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; + +/** + * @author Vice + * @author Zoey76 + */ +public class L2FortLogisticsInstance extends L2MerchantInstance { + private static final int[] SUPPLY_BOX_IDS = { + 35665, + 35697, + 35734, + 35766, + 35803, + 35834, + 35866, + 35903, + 35935, + 35973, + 36010, + 36042, + 36080, + 36117, + 36148, + 36180, + 36218, + 36256, + 36293, + 36325, + 36363 + }; + + public L2FortLogisticsInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FortLogisticsInstance); + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + if (player.getLastFolkNPC().getObjectId() != getObjectId()) { + return; + } + + StringTokenizer st = new StringTokenizer(command, " "); + String actualCommand = st.nextToken(); // Get actual command + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + if (actualCommand.equalsIgnoreCase("rewards")) { + if (isMyLord(player)) { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-rewards.htm"); + html.replace("%bloodoath%", String.valueOf(player.getClan().getBloodOathCount())); + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-noprivs.htm"); + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } else if (actualCommand.equalsIgnoreCase("blood")) { + if (isMyLord(player)) { + final int blood = player.getClan().getBloodOathCount(); + if (blood > 0) { + player.addItem("Quest", 9910, blood, this, true); + player.getClan().resetBloodOathCount(); + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-blood.htm"); + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-noblood.htm"); + } + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-noprivs.htm"); + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } else if (actualCommand.equalsIgnoreCase("supplylvl")) { + if (getFort().getFortState() == 2) { + if (player.isClanLeader()) { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-supplylvl.htm"); + html.replace("%supplylvl%", String.valueOf(getFort().getSupplyLvL())); + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-noprivs.htm"); + } + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-1.htm"); // TODO: Missing HTML? + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } else if (actualCommand.equalsIgnoreCase("supply")) { + if (isMyLord(player)) { + if (getFort().getSiege().isInProgress()) { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-siege.htm"); + } else { + final int level = getFort().getSupplyLvL(); + if (level > 0) { + // spawn box + final var boxTemplate = NpcData.getInstance().getTemplate(SUPPLY_BOX_IDS[level - 1]); + final var objectId = IdFactory.getInstance().getNextId(); + final var box = new L2MonsterInstance(objectId, boxTemplate); + box.setCurrentHp(box.getMaxHp()); + box.setCurrentMp(box.getMaxMp()); + box.setHeading(0); + box.spawnMe(getX() - 23, getY() + 41, getZ()); + + getFort().setSupplyLvL(0); + getFort().saveFortVariables(); + + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-supply.htm"); + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-nosupply.htm"); + } + } + } else { + html.setFile(player.getHtmlPrefix(), "data/html/fortress/logistics-noprivs.htm"); + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } else { + super.onBypassFeedback(player, command); + } + } + + @Override + public void showChatWindow(L2PcInstance player) { + showMessageWindow(player, 0); + } + + private void showMessageWindow(L2PcInstance player, int val) { + player.sendPacket(ActionFailed.STATIC_PACKET); + + String filename; + + if (val == 0) { + filename = "data/html/fortress/logistics.htm"; + } else { + filename = "data/html/fortress/logistics-" + val + ".htm"; + } + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + html.replace("%npcId%", String.valueOf(getId())); + if (getFort().getOwnerClan() != null) { + html.replace("%clanname%", getFort().getOwnerClan().getName()); + } else { + html.replace("%clanname%", "NPC"); + } + player.sendPacket(html); + } + + @Override + public String getHtmlPath(int npcId, int val) { + String pom; + if (val == 0) { + pom = "logistics"; + } else { + pom = "logistics-" + val; + } + return "data/html/fortress/" + pom + ".htm"; + } + + @Override + public boolean hasRandomAnimation() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortManagerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortManagerInstance.java new file mode 100644 index 00000000..ded67a24 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FortManagerInstance.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * Fortress Foreman implementation used for: Area Teleports, Support Magic, Clan Warehouse, Exp Loss Reduction + */ +public class L2FortManagerInstance extends L2MerchantInstance { + + public L2FortManagerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FortManagerInstance); + } + + @Override + public boolean isWarehouse() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FriendlyMobInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FriendlyMobInstance.java new file mode 100644 index 00000000..15ab962e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2FriendlyMobInstance.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.knownlist.FriendlyMobKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * This class represents Friendly Mobs lying over the world.
      + * These friendly mobs should only attack players with karma > 0 and it is always aggro, since it just attacks players with karma. + */ +public class L2FriendlyMobInstance extends L2Attackable { + + public L2FriendlyMobInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2FriendlyMobInstance); + } + + @Override + public final FriendlyMobKnownList getKnownList() { + return (FriendlyMobKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new FriendlyMobKnownList(this)); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + if (attacker instanceof L2PcInstance) { + return ((L2PcInstance) attacker).getKarma() > 0; + } + return false; + } + + @Override + public boolean isAggressive() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GrandBossInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GrandBossInstance.java new file mode 100644 index 00000000..3b85e2a9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GrandBossInstance.java @@ -0,0 +1,107 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.RaidBossPointsManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Hero; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * This class manages all Grand Bosses. + * @since 2006/06/16 + */ +public final class L2GrandBossInstance extends L2MonsterInstance { + private static final int BOSS_MAINTENANCE_INTERVAL = 10000; + private boolean _useRaidCurse = true; + + public L2GrandBossInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2GrandBossInstance); + setIsRaid(true); + setLethalable(false); + } + + @Override + protected int getMaintenanceInterval() { + return BOSS_MAINTENANCE_INTERVAL; + } + + @Override + public void onSpawn() { + setIsNoRndWalk(true); + super.onSpawn(); + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + L2PcInstance player = null; + + if (killer instanceof L2PcInstance) { + player = (L2PcInstance) killer; + } else if (killer instanceof L2Summon) { + player = ((L2Summon) killer).getOwner(); + } + + if (player != null) { + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.RAID_WAS_SUCCESSFUL)); + if (player.getParty() != null) { + for (L2PcInstance member : player.getParty().getMembers()) { + RaidBossPointsManager.getInstance().addPoints(member, getId(), (getLevel() / 2) + Rnd.get(-5, 5)); + if (member.isNoble()) { + Hero.getInstance().setRBkilled(member.getObjectId(), getId()); + } + } + } else { + RaidBossPointsManager.getInstance().addPoints(player, getId(), (getLevel() / 2) + Rnd.get(-5, 5)); + if (player.isNoble()) { + Hero.getInstance().setRBkilled(player.getObjectId(), getId()); + } + } + } + return true; + } + + @Override + public float getVitalityPoints(int damage) { + return -super.getVitalityPoints(damage) / 100; + } + + @Override + public boolean useVitalityRate() { + return false; + } + + public void setUseRaidCurse(boolean val) { + _useRaidCurse = val; + } + + @Override + public boolean giveRaidCurse() { + return _useRaidCurse; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GuardInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GuardInstance.java new file mode 100644 index 00000000..805f31e6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2GuardInstance.java @@ -0,0 +1,173 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.knownlist.GuardKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.EventType; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcFirstTalk; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; + +/** + * This class manages all Guards in the world. It inherits all methods from L2Attackable and adds some more such as tracking PK and aggressive L2MonsterInstance. + */ +public class L2GuardInstance extends L2Attackable { + private static final Logger LOG = LoggerFactory.getLogger(L2GuardInstance.class); + + public L2GuardInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2GuardInstance); + } + + @Override + public final GuardKnownList getKnownList() { + return (GuardKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new GuardKnownList(this)); + } + + /** + * Return True if the attacker is a L2MonsterInstance. + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + return attacker instanceof L2MonsterInstance; + } + + /** + * Set the home location of its L2GuardInstance. + */ + @Override + public void onSpawn() { + setIsNoRndWalk(true); + super.onSpawn(); + + // check the region where this mob is, do not activate the AI if region is inactive. + L2WorldRegion region = L2World.getInstance().getRegion(getX(), getY()); + if ((region != null) && (!region.isActive())) { + getAI().stopAITask(); + } + } + + /** + * Return the pathfile of the selected HTML file in function of the L2GuardInstance Identifier and of the page number.
      + * Format of the pathfile : + *
        + *
      • if page number = 0 : data/html/guard/12006.htm (npcId-page number)
      • + *
      • if page number > 0 : data/html/guard/12006-1.htm (npcId-page number)
      • + *
      + * @param npcId The Identifier of the L2NpcInstance whose text must be display + * @param val The number of the page to display + */ + @Override + public String getHtmlPath(int npcId, int val) { + String pom; + if (val == 0) { + pom = "" + npcId; + } else { + pom = npcId + "-" + val; + } + return "data/html/guard/" + pom + ".htm"; + } + + /** + * Manage actions when a player click on the L2GuardInstance.
      + * Actions on first click on the L2GuardInstance (Select it) : + *
        + *
      • Set the L2GuardInstance as target of the L2PcInstance player (if necessary)
      • + *
      • Send a Server->Client packet MyTargetSelected to the L2PcInstance player (display the select window)
      • + *
      • Set the L2PcInstance Intention to AI_INTENTION_IDLE
      • + *
      • Send a Server->Client packet ValidateLocation to correct the L2GuardInstance position and heading on the client
      • + *
      + * Actions on second click on the L2GuardInstance (Attack it/Interact with it) : + *
        + *
      • If L2PcInstance is in the _aggroList of the L2GuardInstance, set the L2PcInstance Intention to AI_INTENTION_ATTACK
      • + *
      • If L2PcInstance is NOT in the _aggroList of the L2GuardInstance, set the L2PcInstance Intention to AI_INTENTION_INTERACT (after a distance verification) and show message
      • + *
      + * Example of use : + *
        + *
      • Client packet : Action, AttackRequest
      • + *
      + * @param player The L2PcInstance that start an action on the L2GuardInstance + */ + @Override + public void onAction(L2PcInstance player, boolean interact) { + if (!canTarget(player)) { + return; + } + + // Check if the L2PcInstance already target the L2GuardInstance + if (getObjectId() != player.getTargetId()) { + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + // Check if the L2PcInstance is in the _aggroList of the L2GuardInstance + if (isInAggroList(player)) { + if (general().debug()) { + LOG.debug("{}: Attacked guard {}", player.getObjectId(), getObjectId()); + } + + // Set the L2PcInstance Intention to AI_INTENTION_ATTACK + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } else { + // Calculate the distance between the L2PcInstance and the L2NpcInstance + if (!canInteract(player)) { + // Set the L2PcInstance Intention to AI_INTENTION_INTERACT + player.getAI().setIntention(CtrlIntention.AI_INTENTION_INTERACT, this); + } else { + // Send a Server->Client packet SocialAction to the all L2PcInstance on the _knownPlayer of the L2NpcInstance + // to display a social action of the L2GuardInstance on their client + broadcastPacket(new SocialAction(getObjectId(), Rnd.nextInt(8))); + + player.setLastFolkNPC(this); + + // Open a chat window on client with the text of the L2GuardInstance + if (hasListener(EventType.NPC_QUEST_START)) { + player.setLastQuestNpcObject(getObjectId()); + } + + if (hasListener(EventType.NPC_FIRST_TALK)) { + EventDispatcher.getInstance().notifyEventAsync(new NpcFirstTalk(this, player), this); + } else { + showChatWindow(player, 0); + } + } + } + } + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MerchantInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MerchantInstance.java new file mode 100644 index 00000000..429c3b28 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MerchantInstance.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @since 2005/04/11 10:06:08 + */ +public class L2MerchantInstance extends L2NpcInstance { + + public L2MerchantInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2MerchantInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MonsterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MonsterInstance.java new file mode 100644 index 00000000..ecf0e212 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2MonsterInstance.java @@ -0,0 +1,188 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.knownlist.MonsterKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.util.MinionList; + +/** + * This class manages all Monsters. L2MonsterInstance: + *
        + *
      • L2MinionInstance
      • + *
      • L2RaidBossInstance
      • + *
      • L2GrandBossInstance
      • + *
      + */ +public class L2MonsterInstance extends L2Attackable { + private static final int MONSTER_MAINTENANCE_INTERVAL = 1000; + + private L2MonsterInstance _master = null; + private volatile MinionList _minionList = null; + + protected ScheduledFuture _maintenanceTask = null; + + public L2MonsterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2MonsterInstance); + setAutoAttackable(true); + } + + @Override + public final MonsterKnownList getKnownList() { + return (MonsterKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new MonsterKnownList(this)); + } + + /** + * Return True if the attacker is not another L2MonsterInstance. + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + return super.isAutoAttackable(attacker) && ((attacker == null) || !attacker.isMonster()) && !isEventMob(); + } + + @Override + public boolean isAggressive() { + return getTemplate().isAggressive() && !isEventMob(); + } + + @Override + public void onSpawn() { + if (!isTeleporting()) { + if (getLeader() != null) { + setIsNoRndWalk(true); + setIsRaidMinion(getLeader().isRaid()); + getLeader().getMinionList().onMinionSpawn(this); + } + + // delete spawned minions before dynamic minions spawned by script + if (hasMinions()) { + getMinionList().onMasterSpawn(); + } + + startMaintenanceTask(); + } + + // dynamic script-based minions spawned here, after all preparations. + super.onSpawn(); + } + + @Override + public void onTeleported() { + super.onTeleported(); + + if (hasMinions()) { + getMinionList().onMasterTeleported(); + } + } + + protected int getMaintenanceInterval() { + return MONSTER_MAINTENANCE_INTERVAL; + } + + protected void startMaintenanceTask() { + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + if (_maintenanceTask != null) { + _maintenanceTask.cancel(false); // doesn't do it? + _maintenanceTask = null; + } + + return true; + } + + @Override + public boolean deleteMe() { + if (_maintenanceTask != null) { + _maintenanceTask.cancel(false); + _maintenanceTask = null; + } + + if (hasMinions()) { + getMinionList().onMasterDie(true); + } + + if (getLeader() != null) { + getLeader().getMinionList().onMinionDie(this, 0); + } + + return super.deleteMe(); + } + + @Override + public L2MonsterInstance getLeader() { + return _master; + } + + public void setLeader(L2MonsterInstance leader) { + _master = leader; + } + + public boolean hasMinions() { + return _minionList != null; + } + + public MinionList getMinionList() { + if (_minionList == null) { + synchronized (this) { + if (_minionList == null) { + _minionList = new MinionList(this); + } + } + } + return _minionList; + } + + @Override + public boolean isMonster() { + return true; + } + + /** + * @return true if this L2MonsterInstance (or its master) is registered in WalkingManager + */ + @Override + public boolean isWalker() { + return ((getLeader() == null) ? super.isWalker() : getLeader().isWalker()); + } + + /** + * @return {@code true} if this L2MonsterInstance is not raid minion, master state otherwise. + */ + @Override + public boolean giveRaidCurse() { + return (isRaidMinion() && (getLeader() != null)) ? getLeader().giveRaidCurse() : super.giveRaidCurse(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcBufferInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcBufferInstance.java new file mode 100644 index 00000000..7ac6fd56 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcBufferInstance.java @@ -0,0 +1,169 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.cache.HtmCache; +import com.l2jserver.gameserver.data.sql.impl.NpcBufferTable; +import com.l2jserver.gameserver.data.sql.impl.NpcBufferTable.NpcBufferData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; + +/** + * The Class L2NpcBufferInstance.
      + * Zoey76: TODO: Unhardcode as DP base script for NPC Buffers. + */ +public class L2NpcBufferInstance extends L2Npc { + private static final Logger LOG = LoggerFactory.getLogger(L2NpcBufferInstance.class); + + private static final Map pageVal = new HashMap<>(); + + public L2NpcBufferInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2NpcBufferInstance); + } + + @Override + public void showChatWindow(L2PcInstance player, int val) { + if (player == null) { + return; + } + + String htmContent = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/mods/NpcBuffer.htm"); + if (val > 0) { + htmContent = HtmCache.getInstance().getHtm(player.getHtmlPrefix(), "data/html/mods/NpcBuffer-" + val + ".htm"); + } + + if (htmContent != null) { + final NpcHtmlMessage npcHtmlMessage = new NpcHtmlMessage(getObjectId()); + npcHtmlMessage.setHtml(htmContent); + npcHtmlMessage.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(npcHtmlMessage); + } + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + // BypassValidation Exploit plug. + if ((player == null) || (player.getLastFolkNPC() == null) || (player.getLastFolkNPC().getObjectId() != getObjectId())) { + return; + } + + L2Character target = player; + if (command.startsWith("Pet")) { + if (!player.hasSummon()) // TODO: Should be hasPet() ? + { + player.sendPacket(SystemMessageId.DONT_HAVE_PET); + showChatWindow(player, 0); // 0 = main window + return; + } + target = player.getSummon(); + } + + int npcId = getId(); + if (command.startsWith("Chat")) { + int val = Integer.parseInt(command.substring(5)); + + pageVal.put(player.getObjectId(), val); + + showChatWindow(player, val); + } else if (command.startsWith("Buff") || command.startsWith("PetBuff")) { + String[] buffGroupArray = command.substring(command.indexOf("Buff") + 5).split(" "); + + for (String buffGroupList : buffGroupArray) { + if (buffGroupList == null) { + LOG.warn("npcId = {} has no buffGroup set in the bypass for the buff selected.", npcId); + return; + } + + int buffGroup = Integer.parseInt(buffGroupList); + + final NpcBufferData npcBuffGroupInfo = NpcBufferTable.getInstance().getSkillInfo(npcId, buffGroup); + if (npcBuffGroupInfo == null) { + LOG.warn("npcId = {} Location: {}, {}, {} Player: {} has tried to use skill group ({}) not assigned to the NPC Buffer!", npcId, getX(), getY(), getZ(), player.getName(), buffGroup); + return; + } + + if (npcBuffGroupInfo.getFee().getId() != 0) { + L2ItemInstance itemInstance = player.getInventory().getItemByItemId(npcBuffGroupInfo.getFee().getId()); + if ((itemInstance == null) || (!itemInstance.isStackable() && (player.getInventory().getInventoryItemCount(npcBuffGroupInfo.getFee().getId(), -1) < npcBuffGroupInfo.getFee().getCount()))) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THERE_ARE_NOT_ENOUGH_NECESSARY_ITEMS_TO_USE_THE_SKILL); + player.sendPacket(sm); + continue; + } + + if (itemInstance.isStackable()) { + if (!player.destroyItemByItemId("Npc Buffer", npcBuffGroupInfo.getFee().getId(), npcBuffGroupInfo.getFee().getCount(), player.getTarget(), true)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THERE_ARE_NOT_ENOUGH_NECESSARY_ITEMS_TO_USE_THE_SKILL); + player.sendPacket(sm); + continue; + } + } else { + for (int i = 0; i < npcBuffGroupInfo.getFee().getCount(); ++i) { + player.destroyItemByItemId("Npc Buffer", npcBuffGroupInfo.getFee().getId(), 1, player.getTarget(), true); + } + } + } + + final Skill skill = SkillData.getInstance().getSkill(npcBuffGroupInfo.getSkill().getSkillId(), npcBuffGroupInfo.getSkill().getSkillLvl()); + if (skill != null) { + skill.applyEffects(player, target); + } + } + + showChatWindow(player, pageVal.get(player.getObjectId())); + } else if (command.startsWith("Heal") || command.startsWith("PetHeal")) { + if (!target.isInCombat() && !AttackStanceTaskManager.getInstance().hasAttackStanceTask(target)) { + String[] healArray = command.substring(command.indexOf("Heal") + 5).split(" "); + + for (String healType : healArray) { + if (healType.equalsIgnoreCase("HP")) { + target.setCurrentHp(target.getMaxHp()); + } else if (healType.equalsIgnoreCase("MP")) { + target.setCurrentMp(target.getMaxMp()); + } else if (healType.equalsIgnoreCase("CP")) { + target.setCurrentCp(target.getMaxCp()); + } + } + } + showChatWindow(player, pageVal.get(player.getObjectId())); + } else if (command.startsWith("RemoveBuffs") || command.startsWith("PetRemoveBuffs")) { + target.stopAllEffectsExceptThoseThatLastThroughDeath(); + showChatWindow(player, pageVal.get(player.getObjectId())); + } else { + super.onBypassFeedback(player, command); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcInstance.java new file mode 100644 index 00000000..2c03e071 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2NpcInstance.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.status.FolkStatus; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2NpcInstance extends L2Npc { + + public L2NpcInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2NpcInstance); + setIsInvul(false); + } + + @Override + public FolkStatus getStatus() { + return (FolkStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new FolkStatus(this)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ObservationInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ObservationInstance.java new file mode 100644 index 00000000..a9d9137d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ObservationInstance.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; + +/** + * @author NightMarez + * @since 2005/03/27 15:29:32 + */ +public final class L2ObservationInstance extends L2Npc { + + public L2ObservationInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2ObservationInstance); + } + + @Override + public void showChatWindow(L2PcInstance player, int val) { + String filename; + if (isInsideRadius(-79884, 86529, 0, 50, false, true) || isInsideRadius(-78858, 111358, 0, 50, false, true) || isInsideRadius(-76973, 87136, 0, 50, false, true) || isInsideRadius(-75850, 111968, 0, 50, false, true)) { + if (val == 0) { + filename = "data/html/observation/" + getId() + "-Oracle.htm"; + } else { + filename = "data/html/observation/" + getId() + "-Oracle-" + val + ".htm"; + } + } else { + if (val == 0) { + filename = "data/html/observation/" + getId() + ".htm"; + } else { + filename = "data/html/observation/" + getId() + "-" + val + ".htm"; + } + } + + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2OlympiadManagerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2OlympiadManagerInstance.java new file mode 100644 index 00000000..4918c4e5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2OlympiadManagerInstance.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.olympiad.Olympiad; + +/** + * Olympiad Manager instance. + * @author godson + */ +public class L2OlympiadManagerInstance extends L2Npc { + + public L2OlympiadManagerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2OlympiadManagerInstance); + } + + public void showChatWindow(L2PcInstance player, int val, String suffix) { + String filename = Olympiad.OLYMPIAD_HTML_PATH; + + filename += "noble_desc" + val; + filename += (suffix != null) ? suffix + ".htm" : ".htm"; + + if (filename.equals(Olympiad.OLYMPIAD_HTML_PATH + "noble_desc0.htm")) { + filename = Olympiad.OLYMPIAD_HTML_PATH + "noble_main.htm"; + } + + showChatWindow(player, filename); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PcInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PcInstance.java new file mode 100644 index 00000000..be5dff13 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PcInstance.java @@ -0,0 +1,10944 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.bbs.model.ForumType.MAIL; +import static com.l2jserver.gameserver.bbs.model.ForumType.MEMO; +import static com.l2jserver.gameserver.bbs.model.ForumVisibility.OWNER_ONLY; +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.clan; +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.hunting; +import static com.l2jserver.gameserver.config.Configuration.pvp; +import static com.l2jserver.gameserver.config.Configuration.rates; +import static com.l2jserver.gameserver.config.Configuration.vitality; +import static com.l2jserver.gameserver.model.items.L2Item.SLOT_DECO; +import static com.l2jserver.gameserver.model.items.L2Item.SLOT_MULTI_ALLWEAPON; +import static com.l2jserver.gameserver.network.SystemMessageId.CANNOT_EQUIP_ITEM_DUE_TO_BAD_CONDITION; +import static com.l2jserver.gameserver.network.SystemMessageId.EQUIPMENT_S1_S2_REMOVED; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_DISARMED; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_EQUIPPED; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_S2_EQUIPPED; +import static com.l2jserver.gameserver.network.SystemMessageId.YOU_EARNED_S1_EXP_BONUS_S2_AND_S3_SP_BONUS_S4; +import static java.util.concurrent.TimeUnit.SECONDS; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.helpers.MessageFormatter; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.GeoData; +import com.l2jserver.gameserver.ItemsAutoDestroy; +import com.l2jserver.gameserver.LoginServerThread; +import com.l2jserver.gameserver.RecipeController; +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.SevenSignsFestival; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.agathion.repository.AgathionRepository; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.ai.L2PlayerAI; +import com.l2jserver.gameserver.ai.L2SummonAI; +import com.l2jserver.gameserver.bbs.model.Forum; +import com.l2jserver.gameserver.bbs.service.ForumsBBSManager; +import com.l2jserver.gameserver.cache.WarehouseCacheManager; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.sql.impl.CharSummonTable; +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.data.xml.impl.EnchantSkillGroupsData; +import com.l2jserver.gameserver.data.xml.impl.FishData; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.data.xml.impl.PlayerTemplateData; +import com.l2jserver.gameserver.data.xml.impl.PlayerXpPercentLostData; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.enums.HtmlActionScope; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.MountType; +import com.l2jserver.gameserver.enums.PartyDistributionType; +import com.l2jserver.gameserver.enums.PlayerAction; +import com.l2jserver.gameserver.enums.PrivateStoreType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.enums.ShortcutType; +import com.l2jserver.gameserver.enums.ShotType; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.enums.audio.Music; +import com.l2jserver.gameserver.handler.IItemHandler; +import com.l2jserver.gameserver.handler.ItemHandler; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.AntiFeedManager; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.CoupleManager; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.instancemanager.DimensionalRiftManager; +import com.l2jserver.gameserver.instancemanager.DuelManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.GrandBossManager; +import com.l2jserver.gameserver.instancemanager.HandysBlockCheckerManager; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.instancemanager.ItemsOnGroundManager; +import com.l2jserver.gameserver.instancemanager.PunishmentManager; +import com.l2jserver.gameserver.instancemanager.QuestManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.ArenaParticipantsHolder; +import com.l2jserver.gameserver.model.BlockList; +import com.l2jserver.gameserver.model.ClanPrivilege; +import com.l2jserver.gameserver.model.L2AccessLevel; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.L2ContactList; +import com.l2jserver.gameserver.model.L2EnchantSkillLearn; +import com.l2jserver.gameserver.model.L2ManufactureItem; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.L2Party.messageType; +import com.l2jserver.gameserver.model.L2PetLevelData; +import com.l2jserver.gameserver.model.L2PremiumItem; +import com.l2jserver.gameserver.model.L2Radar; +import com.l2jserver.gameserver.model.L2RecipeList; +import com.l2jserver.gameserver.model.L2Request; +import com.l2jserver.gameserver.model.L2SkillLearn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.Macro; +import com.l2jserver.gameserver.model.MacroList; +import com.l2jserver.gameserver.model.PartyMatchRoom; +import com.l2jserver.gameserver.model.PartyMatchRoomList; +import com.l2jserver.gameserver.model.PartyMatchWaitingList; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.ShortCuts; +import com.l2jserver.gameserver.model.Shortcut; +import com.l2jserver.gameserver.model.TeleportBookmark; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.TerritoryWard; +import com.l2jserver.gameserver.model.TradeList; +import com.l2jserver.gameserver.model.UIKeysSettings; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Decoy; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.L2Vehicle; +import com.l2jserver.gameserver.model.actor.appearance.PcAppearance; +import com.l2jserver.gameserver.model.actor.knownlist.PcKnownList; +import com.l2jserver.gameserver.model.actor.stat.PcStat; +import com.l2jserver.gameserver.model.actor.status.PcStatus; +import com.l2jserver.gameserver.model.actor.tasks.player.DismountTask; +import com.l2jserver.gameserver.model.actor.tasks.player.FameTask; +import com.l2jserver.gameserver.model.actor.tasks.player.GameGuardCheckTask; +import com.l2jserver.gameserver.model.actor.tasks.player.InventoryEnableTask; +import com.l2jserver.gameserver.model.actor.tasks.player.LookingForFishTask; +import com.l2jserver.gameserver.model.actor.tasks.player.PetFeedTask; +import com.l2jserver.gameserver.model.actor.tasks.player.PvPFlagTask; +import com.l2jserver.gameserver.model.actor.tasks.player.RentPetTask; +import com.l2jserver.gameserver.model.actor.tasks.player.ResetChargesTask; +import com.l2jserver.gameserver.model.actor.tasks.player.ResetSoulsTask; +import com.l2jserver.gameserver.model.actor.tasks.player.SitDownTask; +import com.l2jserver.gameserver.model.actor.tasks.player.StandUpTask; +import com.l2jserver.gameserver.model.actor.tasks.player.TeleportWatchdogTask; +import com.l2jserver.gameserver.model.actor.tasks.player.VitalityTask; +import com.l2jserver.gameserver.model.actor.tasks.player.WarnUserTakeBreakTask; +import com.l2jserver.gameserver.model.actor.tasks.player.WaterTask; +import com.l2jserver.gameserver.model.actor.templates.L2PcTemplate; +import com.l2jserver.gameserver.model.actor.transform.Transform; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.base.ClassLevel; +import com.l2jserver.gameserver.model.base.PlayerClass; +import com.l2jserver.gameserver.model.base.SubClass; +import com.l2jserver.gameserver.model.effects.EffectFlag; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Duel; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.HuntingSystem; +import com.l2jserver.gameserver.model.entity.Instance; +import com.l2jserver.gameserver.model.entity.L2Event; +import com.l2jserver.gameserver.model.entity.RecommendationSystem; +import com.l2jserver.gameserver.model.entity.Siege; +import com.l2jserver.gameserver.model.entity.TvTEvent; +import com.l2jserver.gameserver.model.events.Containers; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerEquipItem; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerFameChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerHennaRemove; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerKarmaChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerLevelChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerLogin; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerLogout; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerPKChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerProfessionCancel; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerProfessionChange; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerPvPChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerPvPKill; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerSit; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerStand; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerTransform; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.fishing.L2Fish; +import com.l2jserver.gameserver.model.fishing.L2Fishing; +import com.l2jserver.gameserver.model.holders.AdditionalSkillHolder; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.holders.PlayerEventHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.holders.SkillUseHolder; +import com.l2jserver.gameserver.model.interfaces.IEventListener; +import com.l2jserver.gameserver.model.interfaces.ILocational; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.itemcontainer.ItemContainer; +import com.l2jserver.gameserver.model.itemcontainer.PcFreight; +import com.l2jserver.gameserver.model.itemcontainer.PcInventory; +import com.l2jserver.gameserver.model.itemcontainer.PcRefund; +import com.l2jserver.gameserver.model.itemcontainer.PcWarehouse; +import com.l2jserver.gameserver.model.itemcontainer.PetInventory; +import com.l2jserver.gameserver.model.items.L2Armor; +import com.l2jserver.gameserver.model.items.L2EtcItem; +import com.l2jserver.gameserver.model.items.L2Henna; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.ActionType; +import com.l2jserver.gameserver.model.items.type.ArmorType; +import com.l2jserver.gameserver.model.items.type.EtcItemType; +import com.l2jserver.gameserver.model.items.type.ItemType2; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.multisell.PreparedListContainer; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameManager; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameTask; +import com.l2jserver.gameserver.model.olympiad.OlympiadManager; +import com.l2jserver.gameserver.model.punishment.PunishmentAffect; +import com.l2jserver.gameserver.model.punishment.PunishmentType; +import com.l2jserver.gameserver.model.quest.Quest; +import com.l2jserver.gameserver.model.quest.QuestState; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.CommonSkill; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.skills.targets.TargetType; +import com.l2jserver.gameserver.model.stats.BaseStats; +import com.l2jserver.gameserver.model.stats.Formulas; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.variables.AccountVariables; +import com.l2jserver.gameserver.model.variables.PlayerVariables; +import com.l2jserver.gameserver.model.zone.L2ZoneType; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.model.zone.type.L2BossZone; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.AbstractHtmlPacket; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ChangeWaitType; +import com.l2jserver.gameserver.network.serverpackets.CharInfo; +import com.l2jserver.gameserver.network.serverpackets.ConfirmDlg; +import com.l2jserver.gameserver.network.serverpackets.EtcStatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.ExAutoSoulShot; +import com.l2jserver.gameserver.network.serverpackets.ExBR_AgathionEnergyInfo; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.ExDominionWarStart; +import com.l2jserver.gameserver.network.serverpackets.ExDuelUpdateUserInfo; +import com.l2jserver.gameserver.network.serverpackets.ExFishingEnd; +import com.l2jserver.gameserver.network.serverpackets.ExFishingStart; +import com.l2jserver.gameserver.network.serverpackets.ExGetBookMarkInfoPacket; +import com.l2jserver.gameserver.network.serverpackets.ExGetOnAirShip; +import com.l2jserver.gameserver.network.serverpackets.ExOlympiadMode; +import com.l2jserver.gameserver.network.serverpackets.ExPrivateStoreSetWholeMsg; +import com.l2jserver.gameserver.network.serverpackets.ExSetCompassZoneCode; +import com.l2jserver.gameserver.network.serverpackets.ExStartScenePlayer; +import com.l2jserver.gameserver.network.serverpackets.ExStorageMaxCount; +import com.l2jserver.gameserver.network.serverpackets.ExVoteSystemInfo; +import com.l2jserver.gameserver.network.serverpackets.FriendStatusPacket; +import com.l2jserver.gameserver.network.serverpackets.GameGuardQuery; +import com.l2jserver.gameserver.network.serverpackets.GetOnVehicle; +import com.l2jserver.gameserver.network.serverpackets.HennaInfo; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.ItemList; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.LeaveWorld; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.MyTargetSelected; +import com.l2jserver.gameserver.network.serverpackets.NicknameChanged; +import com.l2jserver.gameserver.network.serverpackets.ObservationMode; +import com.l2jserver.gameserver.network.serverpackets.ObservationReturn; +import com.l2jserver.gameserver.network.serverpackets.PartySmallWindowUpdate; +import com.l2jserver.gameserver.network.serverpackets.PetInventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListDelete; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowMemberListUpdate; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreListBuy; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreListSell; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreManageListBuy; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreManageListSell; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreMsgBuy; +import com.l2jserver.gameserver.network.serverpackets.PrivateStoreMsgSell; +import com.l2jserver.gameserver.network.serverpackets.RecipeShopMsg; +import com.l2jserver.gameserver.network.serverpackets.RecipeShopSellList; +import com.l2jserver.gameserver.network.serverpackets.RelationChanged; +import com.l2jserver.gameserver.network.serverpackets.Ride; +import com.l2jserver.gameserver.network.serverpackets.ServerClose; +import com.l2jserver.gameserver.network.serverpackets.SetupGauge; +import com.l2jserver.gameserver.network.serverpackets.ShortCutInit; +import com.l2jserver.gameserver.network.serverpackets.SkillCoolTime; +import com.l2jserver.gameserver.network.serverpackets.SkillList; +import com.l2jserver.gameserver.network.serverpackets.Snoop; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.StopMove; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.TargetSelected; +import com.l2jserver.gameserver.network.serverpackets.TargetUnselected; +import com.l2jserver.gameserver.network.serverpackets.TradeDone; +import com.l2jserver.gameserver.network.serverpackets.TradeOtherDone; +import com.l2jserver.gameserver.network.serverpackets.TradeStart; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.network.serverpackets.ValidateLocation; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; +import com.l2jserver.gameserver.util.Broadcast; +import com.l2jserver.gameserver.util.EnumIntBitmask; +import com.l2jserver.gameserver.util.FloodProtectors; +import com.l2jserver.gameserver.util.Util; + +/** + * This class represents all player characters in the world.
      + * There is always a client-thread connected to this (except if a player-store is activated upon logout). + */ +public final class L2PcInstance extends L2Playable { + + public static final int ID_NONE = -1; + public static final int REQUEST_TIMEOUT = 15; + private static final Logger LOG = LoggerFactory.getLogger(L2PcInstance.class); + private static final String COND_OVERRIDE_KEY = "cond_override"; + // during fall validations will be disabled for 10 ms. + private static final int FALLING_VALIDATION_DELAY = 10000; + public final ReentrantLock soulShotLock = new ReentrantLock(); + private final Queue _eventListeners = new ConcurrentLinkedQueue<>(); + private final String _accountName; + private final ReentrantLock _subclassLock = new ReentrantLock(); + private final PcAppearance _appearance; + private final L2ContactList _contactList = new L2ContactList(this); + private final Map _tpbookmarks = new ConcurrentHashMap<>(); + /** The table containing all L2RecipeList of the L2PcInstance */ + private final Map _dwarvenRecipeBook = new ConcurrentHashMap<>(); + private final Map _commonRecipeBook = new ConcurrentHashMap<>(); + /** Premium Items */ + private final Map _premiumItems = new ConcurrentHashMap<>(); + /** Location before entering Observer Mode */ + private final Location _lastLoc = new Location(0, 0, 0); + /** Stored from last ValidatePosition **/ + private final Location _lastServerPosition = new Location(0, 0, 0); + private final PcInventory _inventory = new PcInventory(this); + private final PcFreight _freight = new PcFreight(this); + /** The table containing all Quests began by the L2PcInstance */ + private final Map _quests = new ConcurrentHashMap<>(); + /** The list containing all shortCuts of this player. */ + private final ShortCuts _shortCuts = new ShortCuts(this); + /** The list containing all macros of this player. */ + private final MacroList _macros = new MacroList(this); + private final Set _snoopListener = ConcurrentHashMap.newKeySet(1); + private final Set _snoopedPlayer = ConcurrentHashMap.newKeySet(1); + // TODO: This needs to be better integrated and saved/loaded + private final L2Radar _radar; + private final AtomicInteger _charges = new AtomicInteger(); + private final L2Request _request = new L2Request(this); + private final Map _chars = new LinkedHashMap<>(); + /** Player's cubics. */ + private final Map _cubics = new ConcurrentSkipListMap<>(); // TODO(Zoey76): This should be sorted in insert order. + /** Active shots. */ + private final Set _activeSoulShots = ConcurrentHashMap.newKeySet(1); + /** new loto ticket **/ + private final int _loto[] = new int[5]; + /** new race ticket **/ + private final int _race[] = new int[2]; + private final BlockList _blockList = new BlockList(this); + /** Last Html Npcs, 0 = last html was not bound to an npc */ + private final int[] _htmlActionOriginObjectIds = new int[HtmlActionScope.values().length]; + /** Bypass validations */ + @SuppressWarnings("unchecked") + private final LinkedList[] _htmlActionCaches = new LinkedList[HtmlActionScope.values().length]; + private volatile Set _friends; + private PartyDistributionType _partyDistributionType; + private L2GameClient _client; + private long _deleteTimer; + private Calendar _createDate = Calendar.getInstance(); + private String _lang = null; + private String _htmlPrefix = null; + private volatile boolean _isOnline = false; + private long _onlineTime; + private long _onlineBeginTime; + private long _lastAccess; + private long _uptime; + private int _baseClass; + private int _activeClass; + private int _classIndex = 0; + /** data for mounted pets */ + private int _controlItemId; + private L2PetLevelData _leveldata; + private int _curFeed; + private Future _mountFeedTask; + private ScheduledFuture _dismountTask; + private boolean _petItems = false; + /** The list of sub-classes this character has. */ + private Map _subClasses; + /** The Experience of the L2PcInstance before the last Death Penalty */ + private long _expBeforeDeath; + /** The Karma of the L2PcInstance (if higher than 0, the name of the L2PcInstance appears in red) */ + private int _karma; + /** The number of player killed during a PvP (the player killed was PvP Flagged) */ + private int _pvpKills; + /** The PK counter of the L2PcInstance (= Number of non PvP Flagged player killed) */ + private int _pkKills; + /** The PvP Flag state of the L2PcInstance (0=White, 1=Purple) */ + private byte _pvpFlag; + /** The Fame of this L2PcInstance */ + private int _fame; + private ScheduledFuture _fameTask; + /** Vitality recovery task */ + private ScheduledFuture _vitalityTask; + private volatile ScheduledFuture _teleportWatchdog; + /** The Siege state of the L2PcInstance */ + private byte _siegeState = 0; + /** The id of castle/fort which the L2PcInstance is registered for siege */ + private int _siegeSide = 0; + private int _curWeightPenalty = 0; + private int _lastCompassZone; // the last compass zone update send to the client + private boolean _isIn7sDungeon = false; + private int _bookmarkslot = 0; // The Teleport Bookmark Slot + private boolean _canFeed; + private boolean _isInSiege; + private boolean _isInHideoutSiege = false; + /** Olympiad */ + private boolean _inOlympiadMode = false; + private boolean _OlympiadStart = false; + private int _olympiadGameId = -1; + private int _olympiadSide = -1; + /** Olympiad buff count. */ + private int _olyBuffsCount = 0; + /** Duel */ + private DuelState _duelState = DuelState.NO_DUEL; + private int _duelId = 0; + /** Boat and AirShip */ + private L2Vehicle _vehicle = null; + private Location _inVehiclePosition; + private ScheduledFuture _taskforfish; + private MountType _mountType = MountType.NONE; + private int _mountNpcId; + private int _mountLevel; + /** Store object used to summon the strider you are mounting **/ + private int _mountObjectID = 0; + private int _telemode = 0; + private boolean _inCrystallize; + private boolean _inCraftMode; + private long _offlineShopStart = 0; + private Transform _transformation; + private volatile Map _transformSkills; + /** True if the L2PcInstance is sitting */ + private boolean _waitTypeSitting; + private boolean _observerMode = false; + private final HuntingSystem _huntingSystem = new HuntingSystem(this); + private final RecommendationSystem _recSystem = new RecommendationSystem(this); + private PcWarehouse _warehouse; + private PcRefund _refund; + private PrivateStoreType _privateStoreType = PrivateStoreType.NONE; + private TradeList _activeTradeList; + private ItemContainer _activeWarehouse; + private volatile Map _manufactureItems; + private String _storeName = ""; + private TradeList _sellList; + private TradeList _buyList; + // Multisell + private PreparedListContainer _currentMultiSell = null; + /** Bitmask used to keep track of one-time/newbie quest rewards */ + private int _newbie; + private boolean _noble = false; + private boolean _hero = false; + /** The L2FolkInstance corresponding to the last Folk which one the player talked. */ + private L2Npc _lastFolkNpc = null; + /** Last NPC Id talked on a quest */ + private int _questNpcObject = 0; + private L2Henna[] _henna = new L2Henna[3]; + private int _hennaSTR; + private int _hennaINT; + private int _hennaDEX; + private int _hennaMEN; + private int _hennaWIT; + private int _hennaCON; + /** The L2Summon of the L2PcInstance */ + private L2Summon _summon = null; + /** The L2Decoy of the L2PcInstance */ + private L2Decoy _decoy = null; + /** The L2Trap of the L2PcInstance */ + private L2TrapInstance _trap = null; + /** The L2Agathion of the L2PcInstance */ + private int _agathionId = 0; + // apparently, a L2PcInstance CAN have both a summon AND a tamed beast at the same time!! + // after Freya players can control more than one tamed beast + private volatile Set _tamedBeasts = null; + private boolean _minimapAllowed = false; + private int _partyroom = 0; + /** The Clan Identifier of the L2PcInstance */ + private int _clanId; + /** The Clan object of the L2PcInstance */ + private L2Clan _clan; + /** Apprentice and Sponsor IDs */ + private int _apprentice = 0; + private int _sponsor = 0; + private long _clanJoinExpiryTime; + private long _clanCreateExpiryTime; + private int _powerGrade = 0; + private volatile EnumIntBitmask _clanPrivileges = new EnumIntBitmask<>(ClanPrivilege.class, false); + /** L2PcInstance's pledge class (knight, Baron, etc.) */ + private int _pledgeClass = 0; + private int _pledgeType = 0; + /** Level at which the player joined the clan as an academy member */ + private int _lvlJoinedAcademy = 0; + private int _wantsPeace = 0; + // Death Penalty Buff Level + private int _deathPenaltyBuffLevel = 0; + private volatile ScheduledFuture _chargeTask = null; + // Absorbed Souls + private int _souls = 0; + private ScheduledFuture _soulTask = null; + // WorldPosition used by TARGET_SIGNET_GROUND + private Location _currentSkillWorldPosition; + private L2AccessLevel _accessLevel; + private boolean _messageRefusal = false; // message refusal mode + private boolean _silenceMode = false; // silence mode + private List _silenceModeExcluded; // silence mode + private boolean _dietMode = false; // ignore weight penalty + private boolean _tradeRefusal = false; // Trade refusal + private boolean _exchangeRefusal = false; // Exchange refusal + private L2Party _party; + // this is needed to find the inviting player for Party response + // there can only be one active party request at once + private L2PcInstance _activeRequester; + private long _requestExpireTime = 0; + private L2ItemInstance _arrowItem; + private L2ItemInstance _boltItem; + // Used for protection after teleport + private long _protectEndTime = 0; + private L2ItemInstance _lure = null; + private long _teleportProtectEndTime = 0; + // protects a char from aggro mobs when getting up from fake death + private long _recentFakeDeathEndTime = 0; + private boolean _isFakeDeath; + /** The fists L2Weapon of the L2PcInstance (used when no weapon is equipped) */ + private L2Weapon _fistsWeaponItem; + private int _expertiseArmorPenalty = 0; + private int _expertiseWeaponPenalty = 0; + private int _expertisePenaltyBonus = 0; + private boolean _isEnchanting = false; + private int _activeEnchantItemId = ID_NONE; + private int _activeEnchantSupportItemId = ID_NONE; + private int _activeEnchantAttrItemId = ID_NONE; + private long _activeEnchantTimestamp = 0; + private boolean _inventoryDisable = false; + /** Event parameters */ + private PlayerEventHolder eventStatus = null; + private byte _handysBlockCheckerEventArena = -1; + private L2Fishing _fishCombat; + private boolean _fishing = false; + private int _fishx = 0; + private int _fishy = 0; + private int _fishz = 0; + private ScheduledFuture _taskRentPet; + private ScheduledFuture _taskWater; + /** + * Origin of the last incoming html action request.
      + * This can be used for HTMLs continuing the conversation with an npc. + */ + private int _lastHtmlActionOriginObjId; + private Forum _forumMail; + private Forum _forumMemo; + + /** Current skill in use. Note that L2Character has _lastSkillCast, but this has the button presses */ + private SkillUseHolder _currentSkill; + private SkillUseHolder _currentPetSkill; + + /** Skills queued because a skill is already in progress */ + private SkillUseHolder _queuedSkill; + + private int _cursedWeaponEquippedId = 0; + private boolean _combatFlagEquippedId = false; + + private boolean _canRevive = true; + private int _reviveRequested = 0; + private double _revivePower = 0; + private int _reviveRecovery = 0; + private boolean _revivePet = false; + + private double _cpUpdateIncCheck = .0; + private double _cpUpdateDecCheck = .0; + private double _cpUpdateInterval = .0; + private double _mpUpdateIncCheck = .0; + private double _mpUpdateDecCheck = .0; + private double _mpUpdateInterval = .0; + + /** Char Coords from Client */ + private int _clientX; + private int _clientY; + private int _clientZ; + private int _clientHeading; + private volatile long _fallingTimestamp = 0; + private int _multiSocialTarget = 0; + private int _multiSociaAction = 0; + private int _movieId = 0; + private String _adminConfirmCmd = null; + private volatile long _lastItemAuctionInfoRequest = 0; + private Future _PvPRegTask; + private long _pvpFlagLasts; + private long _notMoveUntil = 0; + /** Map containing all custom skills of this player. */ + private Map _customSkills = null; + private volatile int _actionMask; + private Map _servitorShare; + // Character UI + private UIKeysSettings _uiKeySettings; + /** Auto Loot Extension. */ + private boolean _enableAutoLoot = false; + private boolean _enableAutoLootItem = false; + private boolean _enableAutoLootHerb = false; + // L2JMOD Wedding + private boolean _married = false; + private int _partnerId = 0; + private int _coupleId = 0; + private boolean _engagerequest = false; + private int _engageid = 0; + private boolean _marryrequest = false; + private boolean _marryaccepted = false; + // Save responder name for log it + private String _lastPetitionGmName = null; + private boolean _hasCharmOfCourage = false; + /** List of all QuestState instance that needs to be notified of this L2PcInstance's or its pet's death */ + private volatile Set _notifyQuestOfDeathList; + /** + * Used for AltGameSkillLearn to set a custom skill learning class Id. + */ + private ClassId _learningClass = getClassId(); + private ScheduledFuture _taskWarnUserTakeBreak; + private L2Fish _fish; + + /** + * Creates a player. + * @param objectId the object ID + * @param classId the player's class ID + * @param accountName the account name + * @param app the player appearance + */ + public L2PcInstance(int objectId, int classId, String accountName, PcAppearance app) { + super(objectId, PlayerTemplateData.getInstance().getTemplate(classId)); + setInstanceType(InstanceType.L2PcInstance); + super.initCharStatusUpdateValues(); + initPcStatusUpdateValues(); + + for (int i = 0; i < _htmlActionCaches.length; ++i) { + _htmlActionCaches[i] = new LinkedList<>(); + } + + _accountName = accountName; + app.setOwner(this); + _appearance = app; + + // Create an AI + getAI(); + + // Create a L2Radar object + _radar = new L2Radar(this); + + startVitalityTask(); + + Formulas.addFuncsToNewPlayer(this); + } + + /** + * Create a new L2PcInstance and add it in the characters table of the database.
      + * Actions : + *
        + *
      • Create a new L2PcInstance with an account name
      • + *
      • Set the name, the Hair Style, the Hair Color and the Face type of the L2PcInstance
      • + *
      • Add the player in the characters table of the database
      • + *
      + * @param classId the player class ID + * @param accountName The name of the L2PcInstance + * @param name The name of the L2PcInstance + * @param app the player's appearance + * @return The L2PcInstance added to the database or null + */ + public static L2PcInstance create(int classId, String accountName, String name, PcAppearance app) { + // Create a new L2PcInstance with an account name + final var objectId = IdFactory.getInstance().getNextId(); + final var player = new L2PcInstance(objectId, classId, accountName, app); + // Set the name of the L2PcInstance + player.setName(name); + // Set Character's create time + player.setCreateDate(Calendar.getInstance()); + // Set the base class ID to that of the actual class ID. + player.setBaseClass(player.getClassId()); + // Kept for backwards compatibility. + player.setNewbie(1); + // Give 20 recommendations + player.getRecSystem().setLeft(20); + // Give one hour bonus for new chars + player.getRecSystem().setBonusTime(3600); + // Add the player in the characters table of the database + return DAOFactory.getInstance().getPlayerDAO().insert(player) ? player : null; + } + + /** + * Restores a player from the database. + * @param objectId the player's object ID + * @return the player + */ + public static L2PcInstance load(int objectId) { + try { + final L2PcInstance player = DAOFactory.getInstance().getPlayerDAO().load(objectId); + if (player == null) { + return null; + } + + DAOFactory.getInstance().getPlayerDAO().loadCharacters(player); + + // Retrieve from the database all items of this L2PcInstance and add them to _inventory + player.getInventory().restore(); + player.getFreight().restore(); + if (!general().warehouseCache()) { + player.getWarehouse(); + } + + // Retrieve from the database all secondary data of this L2PcInstance + // Note that Clan, Noblesse and Hero skills are given separately and not here. + // Retrieve from the database all skills of this L2PcInstance and add them to _skills + DAOFactory.getInstance().getSkillDAO().load(player); + + player._macros.restoreMe(); + + player._shortCuts.restoreMe(); + + DAOFactory.getInstance().getHennaDAO().load(player); + + DAOFactory.getInstance().getTeleportBookmarkDAO().load(player); + + DAOFactory.getInstance().getRecipeBookDAO().load(player, true); + + if (character().storeRecipeShopList()) { + DAOFactory.getInstance().getRecipeShopListDAO().load(player); + } + + DAOFactory.getInstance().getPremiumItemDAO().load(player); + + DAOFactory.getInstance().getItemDAO().loadPetInventory(player); + + // Reward auto-get skills and all available skills if auto-learn skills is true. + player.rewardSkills(); + + DAOFactory.getInstance().getItemReuseDAO().load(player); + + // Buff and status icons + if (character().storeSkillCooltime()) { + player.restoreEffects(); + } + + // Restore current CP, HP and MP values + if (player.getCurrentHp() < 0.5) { + player.setIsDead(true); + player.stopHpMpRegeneration(); + } + + // Restore pet if exists in the world + player.setPet(L2World.getInstance().getPet(player.getObjectId())); + if (player.hasSummon()) { + player.getSummon().setOwner(player); + } + + // Update the overloaded status of the L2PcInstance + player.refreshOverloaded(); + // Update the expertise status of the L2PcInstance + player.refreshExpertisePenalty(); + + DAOFactory.getInstance().getFriendDAO().load(player); + + if (character().storeUISettings()) { + player.restoreUISettings(); + } + + if (player.isGM()) { + final long masks = player.getVariables().getLong(COND_OVERRIDE_KEY, PcCondOverride.getAllExceptionsMask()); + player.setOverrideCond(masks); + } + + // Starting recommendations give task, init Task give = 10 reco 2hs & 1 every 1hs. + player.getRecSystem().startGiveTask(); + + // Load player's recommendations and bonus time + DAOFactory.getInstance().getRecommendationBonusDAO().load(player); + + return player; + } catch (Exception e) { + LOG.error("Failed loading character.", e); + } + return null; + } + + public void restoreStatusFromLoadedValues() { + setCurrentCp(getClient().getCharSelection().getCurrentCp()); + setCurrentHp(getClient().getCharSelection().getCurrentHp()); + setCurrentMp(getClient().getCharSelection().getCurrentMp()); + } + + public boolean isSpawnProtected() { + return _protectEndTime > GameTimeController.getInstance().getGameTicks(); + } + + public boolean isTeleportProtected() { + return _teleportProtectEndTime > GameTimeController.getInstance().getGameTicks(); + } + + public long getPvpFlagLasts() { + return _pvpFlagLasts; + } + + public void setPvpFlagLasts(long time) { + _pvpFlagLasts = time; + } + + public void startPvPFlag() { + updatePvPFlag(1); + + if (_PvPRegTask == null) { + _PvPRegTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new PvPFlagTask(this), 1000, 1000); + } + } + + public void stopPvpRegTask() { + if (_PvPRegTask != null) { + _PvPRegTask.cancel(true); + _PvPRegTask = null; + } + } + + public void stopPvPFlag() { + stopPvpRegTask(); + + updatePvPFlag(0); + + _PvPRegTask = null; + } + + public String getAccountName() { + if (getClient() == null) { + return getAccountNamePlayer(); + } + return getClient().getAccountName(); + } + + public String getAccountNamePlayer() { + return _accountName; + } + + public Map getAccountChars() { + return _chars; + } + + public int getRelation(L2PcInstance target) { + int result = 0; + + if (getClan() != null) { + result |= RelationChanged.RELATION_CLAN_MEMBER; + if (getClan() == target.getClan()) { + result |= RelationChanged.RELATION_CLAN_MATE; + } + if (getAllyId() != 0) { + result |= RelationChanged.RELATION_ALLY_MEMBER; + } + } + if (isClanLeader()) { + result |= RelationChanged.RELATION_LEADER; + } + if ((getParty() != null) && (getParty() == target.getParty())) { + result |= RelationChanged.RELATION_HAS_PARTY; + for (int i = 0; i < getParty().getMembers().size(); i++) { + if (getParty().getMembers().get(i) != this) { + continue; + } + switch (i) { + case 0 -> result |= RelationChanged.RELATION_PARTYLEADER; // 0x10 + case 1 -> result |= RelationChanged.RELATION_PARTY4; // 0x8 + case 2 -> result |= RelationChanged.RELATION_PARTY3 + RelationChanged.RELATION_PARTY2 + RelationChanged.RELATION_PARTY1; // 0x7 + case 3 -> result |= RelationChanged.RELATION_PARTY3 + RelationChanged.RELATION_PARTY2; // 0x6 + case 4 -> result |= RelationChanged.RELATION_PARTY3 + RelationChanged.RELATION_PARTY1; // 0x5 + case 5 -> result |= RelationChanged.RELATION_PARTY3; // 0x4 + case 6 -> result |= RelationChanged.RELATION_PARTY2 + RelationChanged.RELATION_PARTY1; // 0x3 + case 7 -> result |= RelationChanged.RELATION_PARTY2; // 0x2 + case 8 -> result |= RelationChanged.RELATION_PARTY1; // 0x1 + } + } + } + if (getSiegeState() != 0) { + if (TerritoryWarManager.getInstance().getRegisteredTerritoryId(this) != 0) { + result |= RelationChanged.RELATION_TERRITORY_WAR; + } else { + result |= RelationChanged.RELATION_INSIEGE; + if (getSiegeState() != target.getSiegeState()) { + result |= RelationChanged.RELATION_ENEMY; + } else { + result |= RelationChanged.RELATION_ALLY; + } + if (getSiegeState() == 1) { + result |= RelationChanged.RELATION_ATTACKER; + } + } + } + if ((getClan() != null) && (target.getClan() != null)) { + if ((target.getPledgeType() != L2Clan.SUBUNIT_ACADEMY) && (getPledgeType() != L2Clan.SUBUNIT_ACADEMY) && target.getClan().isAtWarWith(getClan().getId())) { + result |= RelationChanged.RELATION_1SIDED_WAR; + if (getClan().isAtWarWith(target.getClan().getId())) { + result |= RelationChanged.RELATION_MUTUAL_WAR; + } + } + } + if (getBlockCheckerArena() != -1) { + result |= RelationChanged.RELATION_INSIEGE; + ArenaParticipantsHolder holder = HandysBlockCheckerManager.getInstance().getHolder(getBlockCheckerArena()); + if (holder.getPlayerTeam(this) == 0) { + result |= RelationChanged.RELATION_ENEMY; + } else { + result |= RelationChanged.RELATION_ALLY; + } + result |= RelationChanged.RELATION_ATTACKER; + } + return result; + } + + private void initPcStatusUpdateValues() { + _cpUpdateInterval = getMaxCp() / 352.0; + _cpUpdateIncCheck = getMaxCp(); + _cpUpdateDecCheck = getMaxCp() - _cpUpdateInterval; + _mpUpdateInterval = getMaxMp() / 352.0; + _mpUpdateIncCheck = getMaxMp(); + _mpUpdateDecCheck = getMaxMp() - _mpUpdateInterval; + } + + @Override + public PcKnownList getKnownList() { + return (PcKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new PcKnownList(this)); + } + + @Override + public PcStat getStat() { + return (PcStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new PcStat(this)); + } + + @Override + public PcStatus getStatus() { + return (PcStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new PcStatus(this)); + } + + public PcAppearance getAppearance() { + return _appearance; + } + + /** + * @return the base L2PcTemplate link to the L2PcInstance. + */ + public L2PcTemplate getBaseTemplate() { + return PlayerTemplateData.getInstance().getTemplate(_baseClass); + } + + /** + * @return the L2PcTemplate link to the L2PcInstance. + */ + @Override + public L2PcTemplate getTemplate() { + return (L2PcTemplate) super.getTemplate(); + } + + /** + * @param newclass + */ + public void setTemplate(ClassId newclass) { + super.setTemplate(PlayerTemplateData.getInstance().getTemplate(newclass)); + } + + @Override + protected L2CharacterAI initAI() { + return new L2PlayerAI(this); + } + + /** Return the Level of the L2PcInstance. */ + @Override + public int getLevel() { + if (isSubClassActive()) { + return getSubClasses().get(getClassIndex()).getStat().getLevel(); + } + return getStat().getLevel(); + } + + public int getBaseLevel() { + return getStat().getLevel(); + } + + public long getBaseExp() { + return getStat().getExp(); + } + + public int getBaseSp() { + return getStat().getSp(); + } + + @Override + public double getLevelMod() { + if (isTransformed()) { + double levelMod = getTransformation().getLevelMod(this); + if (levelMod > -1) { + return levelMod; + } + } + return super.getLevelMod(); + } + + /** + * @return the _newbie rewards state of the L2PcInstance. + */ + public int getNewbie() { + return _newbie; + } + + /** + * Set the _newbie rewards state of the L2PcInstance. + * @param newbieRewards The Identifier of the _newbie state + */ + public void setNewbie(int newbieRewards) { + _newbie = newbieRewards; + } + + public void setBaseClass(int baseClass) { + _baseClass = baseClass; + } + + public boolean isInStoreMode() { + return getPrivateStoreType() != PrivateStoreType.NONE; + } + + public boolean isInCraftMode() { + return _inCraftMode; + } + + public void isInCraftMode(boolean b) { + _inCraftMode = b; + } + + /** + * Manage Logout Task: + *
        + *
      • Remove player from world
      • + *
      • Save player data into DB
      • + *
      + */ + public void logout() { + logout(true); + } + + /** + * Manage Logout Task: + *
        + *
      • Remove player from world
      • + *
      • Save player data into DB
      • + *
      + * @param closeClient + */ + public void logout(boolean closeClient) { + try { + closeNetConnection(closeClient); + } catch (Exception ex) { + LOG.warn("Error trying to logout!", ex); + } + } + + /** + * @return a table containing all Common L2RecipeList of the L2PcInstance. + */ + public L2RecipeList[] getCommonRecipeBook() { + return _commonRecipeBook.values().toArray(new L2RecipeList[_commonRecipeBook.size()]); + } + + /** + * @return a table containing all Dwarf L2RecipeList of the L2PcInstance. + */ + public L2RecipeList[] getDwarvenRecipeBook() { + return _dwarvenRecipeBook.values().toArray(new L2RecipeList[_dwarvenRecipeBook.size()]); + } + + /** + * Add a new L2RecipList to the table _commonrecipebook containing all L2RecipeList of the L2PcInstance + * @param recipe The L2RecipeList to add to the _recipebook + * @param saveToDb + */ + public void registerCommonRecipeList(L2RecipeList recipe, boolean saveToDb) { + _commonRecipeBook.put(recipe.getId(), recipe); + + if (saveToDb) { + DAOFactory.getInstance().getRecipeBookDAO().insert(this, recipe.getId(), false); + } + } + + /** + * Add a new L2RecipList to the table _recipebook containing all L2RecipeList of the L2PcInstance + * @param recipe The L2RecipeList to add to the _recipebook + * @param saveToDb + */ + public void registerDwarvenRecipeList(L2RecipeList recipe, boolean saveToDb) { + _dwarvenRecipeBook.put(recipe.getId(), recipe); + + if (saveToDb) { + DAOFactory.getInstance().getRecipeBookDAO().insert(this, recipe.getId(), true); + } + } + + /** + * @param recipeId The Identifier of the L2RecipeList to check in the player's recipe books + * @return {@code true}if player has the recipe on Common or Dwarven Recipe book else returns {@code false} + */ + public boolean hasRecipeList(int recipeId) { + return _dwarvenRecipeBook.containsKey(recipeId) || _commonRecipeBook.containsKey(recipeId); + } + + /** + * Tries to remove a L2RecipList from the table _DwarvenRecipeBook or from table _CommonRecipeBook, those table contain all L2RecipeList of the L2PcInstance + * @param recipeId The Identifier of the L2RecipeList to remove from the _recipebook + */ + public void unregisterRecipeList(int recipeId) { + if (_dwarvenRecipeBook.remove(recipeId) != null) { + DAOFactory.getInstance().getRecipeBookDAO().delete(this, recipeId, true); + } else if (_commonRecipeBook.remove(recipeId) != null) { + DAOFactory.getInstance().getRecipeBookDAO().delete(this, recipeId, false); + } else { + LOG.warn("Attempted to remove unknown RecipeList: {}", recipeId); + } + + for (Shortcut sc : getAllShortCuts()) { + if ((sc != null) && (sc.getId() == recipeId) && (sc.getType() == ShortcutType.RECIPE)) { + deleteShortCut(sc.getSlot(), sc.getPage()); + } + } + } + + /** + * @return the Id for the last talked quest NPC. + */ + public int getLastQuestNpcObject() { + return _questNpcObject; + } + + public void setLastQuestNpcObject(int npcId) { + _questNpcObject = npcId; + } + + /** + * @param quest The name of the quest + * @return the QuestState object corresponding to the quest name. + */ + public QuestState getQuestState(String quest) { + return _quests.get(quest); + } + + /** + * Add a QuestState to the table _quest containing all quests began by the L2PcInstance. + * @param qs The QuestState to add to _quest + */ + public void setQuestState(QuestState qs) { + _quests.put(qs.getQuestName(), qs); + } + + /** + * Verify if the player has the quest state. + * @param quest the quest state to check + * @return {@code true} if the player has the quest state, {@code false} otherwise + */ + public boolean hasQuestState(String quest) { + return _quests.containsKey(quest); + } + + /** + * Verify if this player has completed the given quest. + * @param quest to check if its completed or not. + * @return {@code true} if the player has completed the given quest, {@code false} otherwise. + */ + public boolean hasQuestCompleted(String quest) { + final QuestState qs = _quests.get(quest); + return (qs != null) && qs.isCompleted(); + } + + /** + * Remove a QuestState from the table _quest containing all quests began by the L2PcInstance. + * @param quest The name of the quest + */ + public void delQuestState(String quest) { + _quests.remove(quest); + } + + /** + * Gets all the active quests. + * @return a list of active quests + */ + public List getAllActiveQuests() { + final List quests = new LinkedList<>(); + for (QuestState qs : _quests.values()) { + if ((qs == null) || (qs.getQuest() == null) || (!qs.isStarted() && !general().developer())) { + continue; + } + + // Ignore other scripts. + final int questId = qs.getQuest().getId(); + if ((questId > 19999) || (questId < 1)) { + continue; + } + quests.add(qs.getQuest()); + } + + return quests; + } + + public void processQuestEvent(String questName, String event) { + final Quest quest = QuestManager.getInstance().getQuest(questName); + if ((quest == null) || (event == null) || event.isEmpty()) { + return; + } + + if (getLastQuestNpcObject() > 0) { + final L2Object object = L2World.getInstance().findObject(getLastQuestNpcObject()); + if (object.isNpc() && isInsideRadius(object, L2Npc.INTERACTION_DISTANCE, false, false)) { + final L2Npc npc = (L2Npc) object; + quest.notifyEvent(event, npc, this); + } + } + } + + /** + * Add QuestState instance that is to be notified of L2PcInstance's death. + * @param qs The QuestState that subscribe to this event + */ + public void addNotifyQuestOfDeath(QuestState qs) { + if (qs == null) { + return; + } + + if (!getNotifyQuestOfDeath().contains(qs)) { + getNotifyQuestOfDeath().add(qs); + } + } + + /** + * Remove QuestState instance that is to be notified of L2PcInstance's death. + * @param qs The QuestState that subscribe to this event + */ + public void removeNotifyQuestOfDeath(QuestState qs) { + if ((qs == null) || (_notifyQuestOfDeathList == null)) { + return; + } + + _notifyQuestOfDeathList.remove(qs); + } + + /** + * Gets the quest states registered for notify of death of this player. + * @return the quest states + */ + public Set getNotifyQuestOfDeath() { + if (_notifyQuestOfDeathList == null) { + synchronized (this) { + if (_notifyQuestOfDeathList == null) { + _notifyQuestOfDeathList = ConcurrentHashMap.newKeySet(1); + } + } + } + + return _notifyQuestOfDeathList; + } + + public boolean isNotifyQuestOfDeathEmpty() { + return (_notifyQuestOfDeathList == null) || _notifyQuestOfDeathList.isEmpty(); + } + + /** + * @return a table containing all L2ShortCut of the L2PcInstance. + */ + public Shortcut[] getAllShortCuts() { + return _shortCuts.getAllShortCuts(); + } + + /** + * @param slot The slot in which the shortCuts is equipped + * @param page The page of shortCuts containing the slot + * @return the L2ShortCut of the L2PcInstance corresponding to the position (page-slot). + */ + public Shortcut getShortCut(int slot, int page) { + return _shortCuts.getShortCut(slot, page); + } + + /** + * Add a L2shortCut to the L2PcInstance _shortCuts + * @param shortcut + */ + public void registerShortCut(Shortcut shortcut) { + _shortCuts.registerShortCut(shortcut); + } + + /** + * Updates the shortcut bars with the new skill. + * @param skillId the skill Id to search and update. + * @param skillLevel the skill level to update. + */ + public void updateShortCuts(int skillId, int skillLevel) { + _shortCuts.updateShortCuts(skillId, skillLevel); + } + + /** + * Delete the L2ShortCut corresponding to the position (page-slot) from the L2PcInstance _shortCuts. + * @param slot + * @param page + */ + public void deleteShortCut(int slot, int page) { + _shortCuts.deleteShortCut(slot, page); + } + + /** + * @param macro the macro to add to this L2PcInstance. + */ + public void registerMacro(Macro macro) { + _macros.registerMacro(macro); + } + + /** + * @param id the macro Id to delete. + */ + public void deleteMacro(int id) { + _macros.deleteMacro(id); + } + + /** + * @return all L2Macro of the L2PcInstance. + */ + public MacroList getMacros() { + return _macros; + } + + /** + * Get the siege state of the L2PcInstance. + * @return 1 = attacker, 2 = defender, 0 = not involved + */ + @Override + public byte getSiegeState() { + return _siegeState; + } + + /** + * Set the siege state of the L2PcInstance. + * @param siegeState 1 = attacker, 2 = defender, 0 = not involved + */ + public void setSiegeState(byte siegeState) { + _siegeState = siegeState; + } + + public boolean isRegisteredOnThisSiegeField(int val) { + if ((_siegeSide != val) && ((_siegeSide < 81) || (_siegeSide > 89))) { + return false; + } + return true; + } + + @Override + public int getSiegeSide() { + return _siegeSide; + } + + /** + * Set the siege Side of the L2PcInstance. + * @param val + */ + public void setSiegeSide(int val) { + _siegeSide = val; + } + + @Override + public byte getPvpFlag() { + return _pvpFlag; + } + + /** + * Set the PvP Flag of the L2PcInstance. + * @param pvpFlag + */ + public void setPvpFlag(int pvpFlag) { + _pvpFlag = (byte) pvpFlag; + } + + @Override + public void updatePvPFlag(int value) { + if (getPvpFlag() == value) { + return; + } + setPvpFlag(value); + + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + + // If this player has a pet update the pets pvp flag as well + if (hasSummon()) { + sendPacket(new RelationChanged(getSummon(), getRelation(this), false)); + } + + Collection plrs = getKnownList().getKnownPlayers().values(); + + for (L2PcInstance target : plrs) { + target.sendPacket(new RelationChanged(this, getRelation(target), isAutoAttackable(target))); + if (hasSummon()) { + target.sendPacket(new RelationChanged(getSummon(), getRelation(target), isAutoAttackable(target))); + } + } + } + + @Override + public void revalidateZone(boolean force) { + // Cannot validate if not in a world region (happens during teleport) + if (getWorldRegion() == null) { + return; + } + + // This function is called too often from movement code + if (force) { + _zoneValidateCounter = 4; + } else { + _zoneValidateCounter--; + if (_zoneValidateCounter < 0) { + _zoneValidateCounter = 4; + } else { + return; + } + } + + getWorldRegion().revalidateZones(this); + + if (general().allowWater()) { + checkWaterState(); + } + + if (isInsideZone(ZoneId.ALTERED)) { + if (_lastCompassZone == ExSetCompassZoneCode.ALTERED_ZONE) { + return; + } + _lastCompassZone = ExSetCompassZoneCode.ALTERED_ZONE; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.ALTERED_ZONE); + sendPacket(cz); + } else if (isInsideZone(ZoneId.SIEGE)) { + if (_lastCompassZone == ExSetCompassZoneCode.SIEGE_WAR_ZONE_2) { + return; + } + _lastCompassZone = ExSetCompassZoneCode.SIEGE_WAR_ZONE_2; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.SIEGE_WAR_ZONE_2); + sendPacket(cz); + } else if (isInsideZone(ZoneId.PVP)) { + if (_lastCompassZone == ExSetCompassZoneCode.PVP_ZONE) { + return; + } + _lastCompassZone = ExSetCompassZoneCode.PVP_ZONE; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.PVP_ZONE); + sendPacket(cz); + } else if (isIn7sDungeon()) { + if (_lastCompassZone == ExSetCompassZoneCode.SEVEN_SIGNS_ZONE) { + return; + } + _lastCompassZone = ExSetCompassZoneCode.SEVEN_SIGNS_ZONE; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.SEVEN_SIGNS_ZONE); + sendPacket(cz); + } else if (isInsideZone(ZoneId.PEACE)) { + if (_lastCompassZone == ExSetCompassZoneCode.PEACE_ZONE) { + return; + } + _lastCompassZone = ExSetCompassZoneCode.PEACE_ZONE; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.PEACE_ZONE); + sendPacket(cz); + } else { + if (_lastCompassZone == ExSetCompassZoneCode.GENERAL_ZONE) { + return; + } + if (_lastCompassZone == ExSetCompassZoneCode.SIEGE_WAR_ZONE_2) { + updatePvPStatus(); + } + _lastCompassZone = ExSetCompassZoneCode.GENERAL_ZONE; + ExSetCompassZoneCode cz = new ExSetCompassZoneCode(ExSetCompassZoneCode.GENERAL_ZONE); + sendPacket(cz); + } + } + + /** + * @return True if the L2PcInstance can Craft Dwarven Recipes. + */ + public boolean hasDwarvenCraft() { + return getSkillLevel(CommonSkill.CREATE_DWARVEN.getId()) >= 1; + } + + public int getDwarvenCraft() { + return getSkillLevel(CommonSkill.CREATE_DWARVEN.getId()); + } + + /** + * @return True if the L2PcInstance can Craft Dwarven Recipes. + */ + public boolean hasCommonCraft() { + return getSkillLevel(CommonSkill.CREATE_COMMON.getId()) >= 1; + } + + public int getCommonCraft() { + return getSkillLevel(CommonSkill.CREATE_COMMON.getId()); + } + + public boolean canCrystallize() { + return getSkillLevel(CommonSkill.CRYSTALLIZE.getId()) >= 1; + } + + /** + * @return the PK counter of the L2PcInstance. + */ + public int getPkKills() { + return _pkKills; + } + + /** + * Set the PK counter of the L2PcInstance. + * @param pkKills + */ + public void setPkKills(int pkKills) { + EventDispatcher.getInstance().notifyEventAsync(new PlayerPKChanged(this, _pkKills, pkKills), this); + _pkKills = pkKills; + } + + /** + * @return the _deleteTimer of the L2PcInstance. + */ + public long getDeleteTimer() { + return _deleteTimer; + } + + /** + * Set the _deleteTimer of the L2PcInstance. + * @param deleteTimer + */ + public void setDeleteTimer(long deleteTimer) { + _deleteTimer = deleteTimer; + } + + public long getExpBeforeDeath() { + return _expBeforeDeath; + } + + /** + * Set the exp of the L2PcInstance before a death + * @param exp + */ + public void setExpBeforeDeath(long exp) { + _expBeforeDeath = exp; + } + + /** + * Return the Karma of the L2PcInstance. + */ + @Override + public int getKarma() { + return _karma; + } + + /** + * Set the Karma of the L2PcInstance and send a Server->Client packet StatusUpdate (broadcast). + * @param karma + */ + public void setKarma(int karma) { + // Notify to scripts. + EventDispatcher.getInstance().notifyEventAsync(new PlayerKarmaChanged(this, getKarma(), karma), this); + + if (karma < 0) { + karma = 0; + } + if ((_karma == 0) && (karma > 0)) { + Collection objs = getKnownList().getKnownObjects().values(); + + for (L2Object object : objs) { + if (!(object instanceof L2GuardInstance)) { + continue; + } + + if (((L2GuardInstance) object).getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) { + ((L2GuardInstance) object).getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + } + } else if ((_karma > 0) && (karma == 0)) { + // Send a Server->Client StatusUpdate packet with Karma and PvP Flag to the L2PcInstance and all L2PcInstance to inform (broadcast) + setKarmaFlag(0); + } + + _karma = karma; + broadcastKarma(); + } + + public int getExpertiseArmorPenalty() { + return _expertiseArmorPenalty; + } + + public int getExpertiseWeaponPenalty() { + return _expertiseWeaponPenalty; + } + + public int getExpertisePenaltyBonus() { + return _expertisePenaltyBonus; + } + + public void setExpertisePenaltyBonus(int bonus) { + _expertisePenaltyBonus = bonus; + } + + public int getWeightPenalty() { + if (_dietMode) { + return 0; + } + return _curWeightPenalty; + } + + /** + * Update the overloaded status of the L2PcInstance. + */ + public void refreshOverloaded() { + int maxLoad = getMaxLoad(); + if (maxLoad > 0) { + long weightproc = (((getCurrentLoad() - getBonusWeightPenalty()) * 1000L) / getMaxLoad()); + int newWeightPenalty; + if ((weightproc < 500) || _dietMode) { + newWeightPenalty = 0; + } else if (weightproc < 666) { + newWeightPenalty = 1; + } else if (weightproc < 800) { + newWeightPenalty = 2; + } else if (weightproc < 1000) { + newWeightPenalty = 3; + } else { + newWeightPenalty = 4; + } + + if (_curWeightPenalty != newWeightPenalty) { + _curWeightPenalty = newWeightPenalty; + if ((newWeightPenalty > 0) && !_dietMode) { + addSkill(SkillData.getInstance().getSkill(4270, newWeightPenalty)); + setIsOverloaded(getCurrentLoad() > maxLoad); + } else { + removeSkill(getKnownSkill(4270), false, true); + setIsOverloaded(false); + } + sendPacket(new UserInfo(this)); + sendPacket(new EtcStatusUpdate(this)); + broadcastPacket(new CharInfo(this)); + broadcastPacket(new ExBrExtraUserInfo(this)); + } + } + } + + public void refreshExpertisePenalty() { + if (!character().expertisePenalty()) { + return; + } + + final int expertiseLevel = getExpertiseLevel(); + + int armorPenalty = 0; + int weaponPenalty = 0; + int crystaltype; + + for (L2ItemInstance item : getInventory().getItems()) { + if ((item != null) && item.isEquipped() && ((item.getItemType() != EtcItemType.ARROW) && (item.getItemType() != EtcItemType.BOLT))) { + crystaltype = item.getItem().getCrystalType().getId(); + if (crystaltype > expertiseLevel) { + if (item.isWeapon() && (crystaltype > weaponPenalty)) { + weaponPenalty = crystaltype; + } else if (crystaltype > armorPenalty) { + armorPenalty = crystaltype; + } + } + } + } + + boolean changed = false; + final int bonus = getExpertisePenaltyBonus(); + + // calc weapon penalty + weaponPenalty = weaponPenalty - expertiseLevel - bonus; + weaponPenalty = Math.min(Math.max(weaponPenalty, 0), 4); + + if ((getExpertiseWeaponPenalty() != weaponPenalty) || (getSkillLevel(CommonSkill.WEAPON_GRADE_PENALTY.getId()) != weaponPenalty)) { + _expertiseWeaponPenalty = weaponPenalty; + if (_expertiseWeaponPenalty > 0) { + addSkill(SkillData.getInstance().getSkill(CommonSkill.WEAPON_GRADE_PENALTY.getId(), _expertiseWeaponPenalty)); + } else { + removeSkill(getKnownSkill(CommonSkill.WEAPON_GRADE_PENALTY.getId()), false, true); + } + changed = true; + } + + // calc armor penalty + armorPenalty = armorPenalty - expertiseLevel - bonus; + armorPenalty = Math.min(Math.max(armorPenalty, 0), 4); + + if ((getExpertiseArmorPenalty() != armorPenalty) || (getSkillLevel(CommonSkill.ARMOR_GRADE_PENALTY.getId()) != armorPenalty)) { + _expertiseArmorPenalty = armorPenalty; + if (_expertiseArmorPenalty > 0) { + addSkill(SkillData.getInstance().getSkill(CommonSkill.ARMOR_GRADE_PENALTY.getId(), _expertiseArmorPenalty)); + } else { + removeSkill(getKnownSkill(CommonSkill.ARMOR_GRADE_PENALTY.getId()), false, true); + } + changed = true; + } + + if (changed) { + sendPacket(new EtcStatusUpdate(this)); + } + } + + public void useEquippableItem(int objectId, boolean abortAttack) { + final var item = getInventory().getItemByObjectId(objectId); + if (item == null) { + return; + } + + L2ItemInstance[] items = null; + final boolean isEquiped = item.isEquipped(); + final int oldInvLimit = getInventoryLimit(); + SystemMessage sm = null; + + if (isEquiped) { + if (item.getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(EQUIPMENT_S1_S2_REMOVED); + sm.addInt(item.getEnchantLevel()); + sm.addItemName(item); + } else { + sm = SystemMessage.getSystemMessage(S1_DISARMED); + sm.addItemName(item); + } + sendPacket(sm); + + int slot = getInventory().getSlotFromItem(item); + // we can't unequip talisman by body slot + if (slot == SLOT_DECO) { + items = getInventory().unEquipItemInSlotAndRecord(item.getLocationSlot()); + } else { + items = getInventory().unEquipItemInBodySlotAndRecord(slot); + } + } else { + items = getInventory().equipItemAndRecord(item); + + if (item.isEquipped()) { + if (item.getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(S1_S2_EQUIPPED); + sm.addInt(item.getEnchantLevel()); + sm.addItemName(item); + } else { + sm = SystemMessage.getSystemMessage(S1_EQUIPPED); + sm.addItemName(item); + } + sendPacket(sm); + + // Consume mana - will start a task if required; returns if item is not a shadow item + item.decreaseMana(false); + + if ((item.getItem().getBodyPart() & SLOT_MULTI_ALLWEAPON) != 0) { + rechargeShots(true, true); + } + } else { + sendPacket(CANNOT_EQUIP_ITEM_DUE_TO_BAD_CONDITION); + } + } + + refreshExpertisePenalty(); + + broadcastUserInfo(); + + final InventoryUpdate iu = new InventoryUpdate(); + iu.addItems(Arrays.asList(items)); + sendPacket(iu); + + if (abortAttack) { + abortAttack(); + } + + if (getInventoryLimit() != oldInvLimit) { + sendPacket(new ExStorageMaxCount(this)); + } + + // If item is agathion with energy, then send info to client. + final var agathionInfo = AgathionRepository.getInstance().getByItemId(item.getId()); + if ((agathionInfo != null) && (agathionInfo.maxEnergy() > 0)) { + sendPacket(new ExBR_AgathionEnergyInfo(List.of(item))); + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerEquipItem(this, item), this); + } + + /** + * @return the the PvP Kills of the L2PcInstance (Number of player killed during a PvP). + */ + public int getPvpKills() { + return _pvpKills; + } + + /** + * Set the the PvP Kills of the L2PcInstance (Number of player killed during a PvP). + * @param pvpKills + */ + public void setPvpKills(int pvpKills) { + EventDispatcher.getInstance().notifyEventAsync(new PlayerPvPChanged(this, _pvpKills, pvpKills), this); + _pvpKills = pvpKills; + } + + /** + * @return the Fame of this L2PcInstance + */ + public int getFame() { + return _fame; + } + + /** + * Set the Fame of this L2PcInstane + * @param fame + */ + public void setFame(int fame) { + EventDispatcher.getInstance().notifyEventAsync(new PlayerFameChanged(this, _fame, fame), this); + _fame = (fame > character().getMaxPersonalFamePoints()) ? character().getMaxPersonalFamePoints() : fame; + } + + /** + * @return the ClassId object of the L2PcInstance contained in L2PcTemplate. + */ + public ClassId getClassId() { + return getTemplate().getClassId(); + } + + /** + * Set the template of the L2PcInstance. + * @param Id The Identifier of the L2PcTemplate to set to the L2PcInstance + */ + public void setClassId(int Id) { + if (!_subclassLock.tryLock()) { + return; + } + + try { + if ((getLvlJoinedAcademy() != 0) && (_clan != null) && (PlayerClass.values()[Id].getLevel() == ClassLevel.Third)) { + if (getLvlJoinedAcademy() <= 16) { + _clan.addReputationScore(clan().getCompleteAcademyMaxPoints(), true); + } else if (getLvlJoinedAcademy() >= 39) { + _clan.addReputationScore(clan().getCompleteAcademyMinPoints(), true); + } else { + _clan.addReputationScore((clan().getCompleteAcademyMaxPoints() - ((getLvlJoinedAcademy() - 16) * 20)), true); + } + setLvlJoinedAcademy(0); + // oust pledge member from the academy, cuz he has finished his 2nd class transfer + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.CLAN_MEMBER_S1_EXPELLED); + msg.addPcName(this); + _clan.broadcastToOnlineMembers(msg); + _clan.broadcastToOnlineMembers(new PledgeShowMemberListDelete(getName())); + _clan.removeClanMember(getObjectId(), 0); + sendPacket(SystemMessageId.ACADEMY_MEMBERSHIP_TERMINATED); + + // receive graduation gift + getInventory().addItem("Gift", 8181, 1, this, null); // give academy circlet + } + if (isSubClassActive()) { + getSubClasses().get(_classIndex).setClassId(Id); + } + setTarget(this); + broadcastPacket(new MagicSkillUse(this, 5103, 1, 1000, 0)); + setClassTemplate(Id); + if (getClassId().level() == 3) { + sendPacket(SystemMessageId.THIRD_CLASS_TRANSFER); + } else { + sendPacket(SystemMessageId.CLASS_TRANSFER); + } + + // Update class icon in party and clan + if (isInParty()) { + getParty().broadcastPacket(new PartySmallWindowUpdate(this)); + } + + if (getClan() != null) { + getClan().broadcastToOnlineMembers(new PledgeShowMemberListUpdate(this)); + } + + // Add AutoGet skills and normal skills and/or learnByFS depending on configurations. + rewardSkills(); + + if (!canOverrideCond(PcCondOverride.SKILL_CONDITIONS) && character().decreaseSkillOnDelevel()) { + checkPlayerSkills(); + } + } finally { + _subclassLock.unlock(); + } + } + + /** + * @return the custom skill learning class Id. + */ + public ClassId getLearningClass() { + return _learningClass; + } + + /** + * @param learningClass the custom skill learning class Id to set. + */ + public void setLearningClass(ClassId learningClass) { + _learningClass = learningClass; + } + + /** + * @return the Experience of the L2PcInstance. + */ + @Override + public long getExp() { + if (isSubClassActive()) { + return getSubClasses().get(getClassIndex()).getStat().getExp(); + } + return getStat().getExp(); + } + + /** + * Set the Experience value of the L2PcInstance. + * @param exp + */ + public void setExp(long exp) { + if (exp < 0) { + LOG.warn("For player {} is set negative amount of exp [{}]", this, exp, new IllegalArgumentException()); + exp = 0; + } + getSubStat().setExp(exp); + } + + public void setLevel(int level) { + getSubStat().setLevel(Math.min(level, getMaxLevel())); + } + + public int getMaxLevel() { + return getSubStat().getMaxLevel(); + } + + public int getMaxExpLevel() { + return getSubStat().getMaxExpLevel(); + } + + private PcStat getSubStat() { + return isSubClassActive() ? getSubClasses().get(getClassIndex()).getStat() : getStat(); + } + + @Override + public boolean addLevel(int value) { + if ((getLevel() + value) > getMaxLevel()) { + return false; + } + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerLevelChanged(this, getLevel(), getLevel() + value), this); + + boolean levelIncreased = getSubStat().addLevel(value); + onLevelChange(levelIncreased); + + return levelIncreased; + } + + @Override + public void onLevelChange(boolean levelIncreased) { + if (levelIncreased) { + setCurrentCp(getMaxCp()); + setCurrentHp(getMaxHp()); + setCurrentMp(getMaxMp()); + broadcastPacket(new SocialAction(getObjectId(), SocialAction.LEVEL_UP)); + sendPacket(SystemMessageId.YOU_INCREASED_YOUR_LEVEL); + } else { + if (!isGM() && character().decreaseSkillOnDelevel()) { + checkPlayerSkills(); + } + } + + // Give AutoGet skills and all normal skills if Auto-Learn is activated. + rewardSkills(); + + if (getClan() != null) { + getClan().updateClanMember(this); + getClan().broadcastToOnlineMembers(new PledgeShowMemberListUpdate(this)); + } + if (isInParty()) { + getParty().recalculatePartyLevel(); // Recalculate the party level + } + + if (isTransformed() || isInStance()) { + getTransformation().onLevelUp(this); + } + + // Synchronize level with pet if possible. + if (hasPet()) { + final L2PetInstance pet = (L2PetInstance) getSummon(); + if (pet.getPetData().isSyncLevel() && (pet.getLevel() != getLevel())) { + pet.getStat().setLevel(getLevel()); + pet.getStat().getExpForLevel(getLevel()); + pet.setCurrentHp(pet.getMaxHp()); + pet.setCurrentMp(pet.getMaxMp()); + pet.broadcastPacket(new SocialAction(getObjectId(), SocialAction.LEVEL_UP)); + pet.updateAndBroadcastStatus(1); + } + } + + // Get Nevit Blessing Points on level up + if (getActingPlayer().getHuntingSystem().isNevitBlessingActive()) { + getActingPlayer().getHuntingSystem().addPoints(hunting().getNevitLevelAcquirePoints2()); + } else { + getActingPlayer().getHuntingSystem().addPoints(hunting().getNevitLevelAcquirePoints()); + } + + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.LEVEL, getLevel()); + su.addAttribute(StatusUpdate.MAX_CP, getMaxCp()); + su.addAttribute(StatusUpdate.MAX_HP, getMaxHp()); + su.addAttribute(StatusUpdate.MAX_MP, getMaxMp()); + sendPacket(su); + + // Update the overloaded status of the L2PcInstance + refreshOverloaded(); + // Update the expertise status of the L2PcInstance + refreshExpertisePenalty(); + // Send a Server->Client packet UserInfo to the L2PcInstance + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + sendPacket(new ExVoteSystemInfo(this)); + } + + public int getActiveEnchantAttrItemId() { + return _activeEnchantAttrItemId; + } + + public void setActiveEnchantAttrItemId(int objectId) { + _activeEnchantAttrItemId = objectId; + } + + public int getActiveEnchantItemId() { + return _activeEnchantItemId; + } + + public void setActiveEnchantItemId(int objectId) { + // If we don't have a Enchant Item, we are not enchanting. + if (objectId == ID_NONE) { + setActiveEnchantSupportItemId(ID_NONE); + setActiveEnchantTimestamp(0); + setIsEnchanting(false); + } + _activeEnchantItemId = objectId; + } + + public int getActiveEnchantSupportItemId() { + return _activeEnchantSupportItemId; + } + + public void setActiveEnchantSupportItemId(int objectId) { + _activeEnchantSupportItemId = objectId; + } + + public long getActiveEnchantTimestamp() { + return _activeEnchantTimestamp; + } + + public void setActiveEnchantTimestamp(long val) { + _activeEnchantTimestamp = val; + } + + public void setIsEnchanting(boolean val) { + _isEnchanting = val; + } + + public boolean isEnchanting() { + return _isEnchanting; + } + + /** + * @return the fists weapon of the L2PcInstance (used when no weapon is equipped). + */ + public L2Weapon getFistsWeaponItem() { + return _fistsWeaponItem; + } + + /** + * Set the fists weapon of the L2PcInstance (used when no weapon is equiped). + * @param weaponItem The fists L2Weapon to set to the L2PcInstance + */ + public void setFistsWeaponItem(L2Weapon weaponItem) { + _fistsWeaponItem = weaponItem; + } + + /** + * @param classId + * @return the fists weapon of the L2PcInstance Class (used when no weapon is equipped). + */ + public L2Weapon findFistsWeaponItem(int classId) { + L2Weapon weaponItem = null; + if ((classId >= 0x00) && (classId <= 0x09)) { + // human fighter fists + L2Item temp = ItemTable.getInstance().getTemplate(246); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x0a) && (classId <= 0x11)) { + // human mage fists + L2Item temp = ItemTable.getInstance().getTemplate(251); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x12) && (classId <= 0x18)) { + // elven fighter fists + L2Item temp = ItemTable.getInstance().getTemplate(244); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x19) && (classId <= 0x1e)) { + // elven mage fists + L2Item temp = ItemTable.getInstance().getTemplate(249); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x1f) && (classId <= 0x25)) { + // dark elven fighter fists + L2Item temp = ItemTable.getInstance().getTemplate(245); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x26) && (classId <= 0x2b)) { + // dark elven mage fists + L2Item temp = ItemTable.getInstance().getTemplate(250); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x2c) && (classId <= 0x30)) { + // orc fighter fists + L2Item temp = ItemTable.getInstance().getTemplate(248); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x31) && (classId <= 0x34)) { + // orc mage fists + L2Item temp = ItemTable.getInstance().getTemplate(252); + weaponItem = (L2Weapon) temp; + } else if ((classId >= 0x35) && (classId <= 0x39)) { + // dwarven fists + L2Item temp = ItemTable.getInstance().getTemplate(247); + weaponItem = (L2Weapon) temp; + } + + return weaponItem; + } + + /** + * This method reward all AutoGet skills and Normal skills if Auto-Learn configuration is true. + */ + public void rewardSkills() { + // Give all normal skills if activated Auto-Learn is activated, included AutoGet skills. + if (character().autoLearnSkills()) { + giveAvailableSkills(character().autoLearnForgottenScrollSkills(), true); + } else { + giveAvailableAutoGetSkills(); + } + + checkPlayerSkills(); + checkItemRestriction(); + sendSkillList(); + } + + /** + * Re-give all skills which aren't saved to database, like Noble, Hero, Clan Skills.
      + */ + public void regiveTemporarySkills() { + // Do not call this on enterworld or char load + + // Add noble skills if noble + if (isNoble()) { + setNoble(true); + } + + // Add Hero skills if hero + if (isHero()) { + setHero(true); + } + + // Add clan skills + if (getClan() != null) { + L2Clan clan = getClan(); + clan.addSkillEffects(this); + + if ((clan.getLevel() >= SiegeManager.getInstance().getSiegeClanMinLevel()) && isClanLeader()) { + SiegeManager.getInstance().addSiegeSkills(this); + } + if (getClan().getCastleId() > 0) { + CastleManager.getInstance().getCastleByOwner(getClan()).giveResidentialSkills(this); + } + if (getClan().getFortId() > 0) { + FortManager.getInstance().getFortByOwner(getClan()).giveResidentialSkills(this); + } + } + + // Reload passive skills from armors / jewels / weapons + getInventory().reloadEquippedItems(); + + // Add Death Penalty Buff Level + restoreDeathPenaltyBuffLevel(); + } + + /** + * Give all available skills to the player. + * @param includedByFs if {@code true} forgotten scroll skills present in the skill tree will be added + * @param includeAutoGet if {@code true} auto-get skills present in the skill tree will be added + * @return the amount of new skills earned + */ + public int giveAvailableSkills(boolean includedByFs, boolean includeAutoGet) { + int skillCounter = 0; + // Get available skills + Collection skills = SkillTreesData.getInstance().getAllAvailableSkills(this, getClassId(), includedByFs, includeAutoGet); + List skillsForStore = new ArrayList<>(); + + for (Skill sk : skills) { + if (getKnownSkill(sk.getId()) == sk) { + continue; + } + + if (getSkillLevel(sk.getId()) == -1) { + skillCounter++; + } + + // fix when learning toggle skills + if (sk.isToggle() && isAffectedBySkill(sk.getId())) { + stopSkillEffects(true, sk.getId()); + } + + addSkill(sk, false); + skillsForStore.add(sk); + } + + DAOFactory.getInstance().getSkillDAO().insert(this, -1, skillsForStore); + + if (character().autoLearnSkills() && (skillCounter > 0)) { + sendMessage("You have learned " + skillCounter + " new skills."); + } + return skillCounter; + } + + /** + * Give all available auto-get skills to the player. + */ + public void giveAvailableAutoGetSkills() { + // Get available skills + final List autoGetSkills = SkillTreesData.getInstance().getAvailableAutoGetSkills(this); + final SkillData st = SkillData.getInstance(); + Skill skill; + for (L2SkillLearn s : autoGetSkills) { + skill = st.getSkill(s.getSkillId(), s.getSkillLevel()); + if (skill != null) { + addSkill(skill, true); + } else { + LOG.warn("Skipping null auto-get skill for player: {}", this); + } + } + } + + /** + * @return the Race object of the L2PcInstance. + */ + @Override + public Race getRace() { + if (!isSubClassActive()) { + return getTemplate().getRace(); + } + return PlayerTemplateData.getInstance().getTemplate(_baseClass).getRace(); + } + + public L2Radar getRadar() { + return _radar; + } + + /* Return true if Hellbound minimap allowed */ + public boolean isMinimapAllowed() { + return _minimapAllowed; + } + + /* Enable or disable minimap on Hellbound */ + public void setMinimapAllowed(boolean b) { + _minimapAllowed = b; + } + + public void addSp(int sp) { + getSubStat().addSp(sp); + } + + @Override + public int getSp() { + return isSubClassActive() ? getSubClasses().get(getClassIndex()).getStat().getSp() : getStat().getSp(); + } + + /** + * Set the SP amount of the L2PcInstance. + * @param sp + */ + public void setSp(int sp) { + if (sp < 0) { + sp = 0; + } + if (isSubClassActive()) { + getSubClasses().get(getClassIndex()).getStat().setSp(sp); + } else { + super.getStat().setSp(sp); + } + } + + /** + * @param castleId + * @return true if this L2PcInstance is a clan leader in ownership of the passed castle + */ + public boolean isCastleLord(int castleId) { + L2Clan clan = getClan(); + + // player has clan and is the clan leader, check the castle info + if ((clan != null) && (clan.getLeader().getPlayerInstance() == this)) { + // if the clan has a castle and it is actually the queried castle, return true + Castle castle = CastleManager.getInstance().getCastleByOwner(clan); + if ((castle != null) && (castle == CastleManager.getInstance().getCastleById(castleId))) { + return true; + } + } + + return false; + } + + /** + * @return the Clan Identifier of the L2PcInstance. + */ + @Override + public int getClanId() { + return _clanId; + } + + /** + * @return the Clan Crest Identifier of the L2PcInstance or 0. + */ + public int getClanCrestId() { + if (_clan != null) { + return _clan.getCrestId(); + } + + return 0; + } + + /** + * @return The Clan CrestLarge Identifier or 0 + */ + public int getClanCrestLargeId() { + if ((_clan != null) && ((_clan.getCastleId() != 0) || (_clan.getHideoutId() != 0))) { + return _clan.getCrestLargeId(); + } + return 0; + } + + public long getClanJoinExpiryTime() { + return _clanJoinExpiryTime; + } + + public void setClanJoinExpiryTime(long time) { + _clanJoinExpiryTime = time; + } + + public long getClanCreateExpiryTime() { + return _clanCreateExpiryTime; + } + + public void setClanCreateExpiryTime(long time) { + _clanCreateExpiryTime = time; + } + + /** + * Return the PcInventory Inventory of the L2PcInstance contained in _inventory. + */ + @Override + public PcInventory getInventory() { + return _inventory; + } + + /** + * Delete a ShortCut of the L2PcInstance _shortCuts. + * @param objectId + */ + public void removeItemFromShortCut(int objectId) { + _shortCuts.deleteShortCutByObjectId(objectId); + } + + /** + * @return True if the L2PcInstance is sitting. + */ + public boolean isSitting() { + return _waitTypeSitting; + } + + /** + * Set _waitTypeSitting to given value + * @param state + */ + public void setIsSitting(boolean state) { + _waitTypeSitting = state; + } + + /** + * Sit down the L2PcInstance, set the AI Intention to AI_INTENTION_REST and send a Server->Client ChangeWaitType packet (broadcast) + */ + public void sitDown() { + sitDown(true); + } + + public void sitDown(boolean checkCast) { + final TerminateReturn terminate = EventDispatcher.getInstance().notifyEvent(new PlayerSit(this), this, TerminateReturn.class); + if ((terminate != null) && terminate.terminate()) { + return; + } + + if (checkCast && isCastingNow()) { + return; + } + + if (!_waitTypeSitting && !isAttackingDisabled() && !isOutOfControl() && !isImmobilized() && !isMoving()) { + breakAttack(); + setIsSitting(true); + getAI().setIntention(CtrlIntention.AI_INTENTION_REST); + broadcastPacket(new ChangeWaitType(this, ChangeWaitType.WT_SITTING)); + // Schedule a sit down task to wait for the animation to finish + ThreadPoolManager.getInstance().scheduleGeneral(new SitDownTask(this), 2500); + startStunning(); + } + } + + /** + * Stand up the L2PcInstance, set the AI Intention to AI_INTENTION_IDLE and send a Server->Client ChangeWaitType packet (broadcast) + */ + public void standUp() { + final TerminateReturn terminate = EventDispatcher.getInstance().notifyEvent(new PlayerStand(this), Containers.Players(), TerminateReturn.class); + if ((terminate != null) && terminate.terminate()) { + return; + } + + if (_waitTypeSitting && !isInStoreMode() && !isAlikeDead()) { + if (getEffectList().isAffected(EffectFlag.RELAXING)) { + stopEffects(L2EffectType.RELAXING); + } + + broadcastPacket(new ChangeWaitType(this, ChangeWaitType.WT_STANDING)); + // Schedule a stand up task to wait for the animation to finish + ThreadPoolManager.getInstance().scheduleGeneral(new StandUpTask(this), 2500); + } + } + + /** + * @return the PcWarehouse object of the L2PcInstance. + */ + public PcWarehouse getWarehouse() { + if (_warehouse == null) { + _warehouse = new PcWarehouse(this); + _warehouse.restore(); + } + if (general().warehouseCache()) { + WarehouseCacheManager.getInstance().addCacheTask(this); + } + return _warehouse; + } + + /** + * Free memory used by Warehouse + */ + public void clearWarehouse() { + if (_warehouse != null) { + _warehouse.deleteMe(); + } + _warehouse = null; + } + + /** + * @return the PcFreight object of the L2PcInstance. + */ + public PcFreight getFreight() { + return _freight; + } + + /** + * @return true if refund list is not empty + */ + public boolean hasRefund() { + return (_refund != null) && (_refund.getSize() > 0) && general().allowRefund(); + } + + /** + * @return refund object or create new if not exist + */ + public PcRefund getRefund() { + if (_refund == null) { + _refund = new PcRefund(this); + } + return _refund; + } + + /** + * Clear refund + */ + public void clearRefund() { + if (_refund != null) { + _refund.deleteMe(); + } + _refund = null; + } + + /** + * @return the Adena amount of the L2PcInstance. + */ + public long getAdena() { + return _inventory.getAdena(); + } + + /** + * @return the Ancient Adena amount of the L2PcInstance. + */ + public long getAncientAdena() { + return _inventory.getAncientAdena(); + } + + /** + * Add adena to Inventory of the L2PcInstance and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param count : int Quantity of adena to be added + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + */ + public void addAdena(String process, long count, L2Object reference, boolean sendMessage) { + if (sendMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S1_ADENA); + sm.addLong(count); + sendPacket(sm); + } + + if (count > 0) { + _inventory.addAdena(process, count, this, reference); + + // Send update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(_inventory.getAdenaInstance()); + sendPacket(iu); + } else { + sendPacket(new ItemList(this, false)); + } + } + } + + /** + * Reduce adena in Inventory of the L2PcInstance and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param count : long Quantity of adena to be reduced + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + public boolean reduceAdena(String process, long count, L2Object reference, boolean sendMessage) { + if (count > getAdena()) { + if (sendMessage) { + sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + } + return false; + } + + if (count > 0) { + L2ItemInstance adenaItem = _inventory.getAdenaInstance(); + if (!_inventory.reduceAdena(process, count, this, reference)) { + return false; + } + + // Send update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(adenaItem); + sendPacket(iu); + } else { + sendPacket(new ItemList(this, false)); + } + + if (sendMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED_ADENA); + sm.addLong(count); + sendPacket(sm); + } + } + + return true; + } + + /** + * Add ancient adena to Inventory of the L2PcInstance and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param count : int Quantity of ancient adena to be added + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + */ + public void addAncientAdena(String process, long count, L2Object reference, boolean sendMessage) { + if (sendMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S); + sm.addItemName(Inventory.ANCIENT_ADENA_ID); + sm.addLong(count); + sendPacket(sm); + } + + if (count > 0) { + _inventory.addAncientAdena(process, count, this, reference); + + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(_inventory.getAncientAdenaInstance()); + sendPacket(iu); + } else { + sendPacket(new ItemList(this, false)); + } + } + } + + /** + * Reduce ancient adena in Inventory of the L2PcInstance and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param count : long Quantity of ancient adena to be reduced + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + public boolean reduceAncientAdena(String process, long count, L2Object reference, boolean sendMessage) { + if (count > getAncientAdena()) { + if (sendMessage) { + sendPacket(SystemMessageId.YOU_NOT_ENOUGH_ADENA); + } + + return false; + } + + if (count > 0) { + L2ItemInstance ancientAdenaItem = _inventory.getAncientAdenaInstance(); + if (!_inventory.reduceAncientAdena(process, count, this, reference)) { + return false; + } + + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(ancientAdenaItem); + sendPacket(iu); + } else { + sendPacket(new ItemList(this, false)); + } + + if (sendMessage) { + if (count > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(Inventory.ANCIENT_ADENA_ID); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(Inventory.ANCIENT_ADENA_ID); + sendPacket(sm); + } + } + } + + return true; + } + + /** + * Adds item to inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param item : L2ItemInstance to be added + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + */ + public void addItem(String process, L2ItemInstance item, L2Object reference, boolean sendMessage) { + if (item.getCount() > 0) { + // Sends message to client if requested + if (sendMessage) { + if (item.getCount() > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_PICKED_UP_S1_S2); + sm.addItemName(item); + sm.addLong(item.getCount()); + sendPacket(sm); + } else if (item.getEnchantLevel() > 0) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_PICKED_UP_A_S1_S2); + sm.addInt(item.getEnchantLevel()); + sm.addItemName(item); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_PICKED_UP_S1); + sm.addItemName(item); + sendPacket(sm); + } + } + + // Add the item to inventory + L2ItemInstance newitem = _inventory.addItem(process, item, this, reference); + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + playerIU.addItem(newitem); + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(su); + + // If over capacity, drop the item + if (!canOverrideCond(PcCondOverride.ITEM_CONDITIONS) && !_inventory.validateCapacity(0, item.isQuestItem()) && newitem.isDroppable() && (!newitem.isStackable() || (newitem.getLastChange() != L2ItemInstance.MODIFIED))) { + dropItem("InvDrop", newitem, null, true, true); + } else if (CursedWeaponsManager.getInstance().isCursed(newitem.getId())) { + CursedWeaponsManager.getInstance().activate(this, newitem); + } + + // Combat Flag + else if (FortSiegeManager.getInstance().isCombat(item.getId())) { + if (FortSiegeManager.getInstance().activateCombatFlag(this, item)) { + Fort fort = FortManager.getInstance().getFort(this); + fort.getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.C1_ACQUIRED_THE_FLAG), getName()); + } + } + // Territory Ward + else if ((item.getId() >= 13560) && (item.getId() <= 13568)) { + TerritoryWard ward = TerritoryWarManager.getInstance().getTerritoryWard(item.getId() - 13479); + if (ward != null) { + ward.activate(this, item); + } + } + } + } + + /** + * Equivalent to {@link #addItem(String, int, long, L2Object, boolean)} with parameters (process, itemId, 1, -1, reference, sendMessage) + * @param process + * @param itemId + * @param reference + * @param sendMessage + * @return the new or updated item + */ + public L2ItemInstance addItem(String process, int itemId, L2Object reference, boolean sendMessage) { + return addItem(process, itemId, 1, -1, reference, sendMessage); + } + + /** + * Equivalent to {@link #addItem(String, int, long, int, L2Object, boolean)} with parameters (process, itemId, count, -1, reference, sendMessage) + * @param process + * @param itemId + * @param count + * @param reference + * @param sendMessage + * @return the new or updated item + */ + public L2ItemInstance addItem(String process, int itemId, long count, L2Object reference, boolean sendMessage) { + return addItem(process, itemId, count, -1, reference, sendMessage); + } + + /** + * Adds item to Inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param itemId : int Item Identifier of the item to be added + * @param count : long Quantity of items to be added + * @param enchantLevel : int Enchant of the item; -1 to not modify on existing items, for new items use the default enchantLevel when -1 + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return the new or updated item + */ + public L2ItemInstance addItem(String process, int itemId, long count, int enchantLevel, L2Object reference, boolean sendMessage) { + if (count > 0) { + final L2Item item = ItemTable.getInstance().getTemplate(itemId); + if (item == null) { + LOG.error("Item doesn't exist so cannot be added. Item ID: {}", itemId); + return null; + } + // Sends message to client if requested + if (sendMessage) { + if (count > 1) { + if (process.equalsIgnoreCase("Sweeper") || process.equalsIgnoreCase("Quest")) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S); + sm.addItemName(itemId); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_PICKED_UP_S1_S2); + sm.addItemName(itemId); + sm.addLong(count); + sendPacket(sm); + } + } else { + if (process.equalsIgnoreCase("Sweeper") || process.equalsIgnoreCase("Quest")) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_ITEM_S1); + sm.addItemName(itemId); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_PICKED_UP_S1); + sm.addItemName(itemId); + sendPacket(sm); + } + } + } + + // Auto-use herbs. + if (item.hasExImmediateEffect()) { + final IItemHandler handler = ItemHandler.getInstance().getHandler(item instanceof L2EtcItem ? (L2EtcItem) item : null); + if (handler == null) { + LOG.warn("No item handler registered for Herb {}!", item); + } else { + final var objectId = IdFactory.getInstance().getNextId(); + handler.useItem(this, new L2ItemInstance(objectId, itemId), false); + } + } else { + // Add the item to inventory + L2ItemInstance createdItem = _inventory.addItem(process, itemId, count, enchantLevel, this, reference); + + // If over capacity, drop the item + if (!canOverrideCond(PcCondOverride.ITEM_CONDITIONS) && !_inventory.validateCapacity(0, item.isQuestItem()) && createdItem.isDroppable() && (!createdItem.isStackable() || (createdItem.getLastChange() != L2ItemInstance.MODIFIED))) { + dropItem("InvDrop", createdItem, null, true); + } else if (CursedWeaponsManager.getInstance().isCursed(createdItem.getId())) { + CursedWeaponsManager.getInstance().activate(this, createdItem); + } + // Territory Ward + else if ((createdItem.getId() >= 13560) && (createdItem.getId() <= 13568)) { + TerritoryWard ward = TerritoryWarManager.getInstance().getTerritoryWard(createdItem.getId() - 13479); + if (ward != null) { + ward.activate(this, createdItem); + } + } + return createdItem; + } + } + return null; + } + + /** + * @param process the process name + * @param item the item holder + * @param reference the reference object + * @param sendMessage if {@code true} a system message will be sent + */ + public void addItem(String process, ItemHolder item, L2Object reference, boolean sendMessage) { + addItem(process, item.getId(), item.getCount(), reference, sendMessage); + } + + /** + * Destroy item from inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param item : L2ItemInstance to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + public boolean destroyItem(String process, L2ItemInstance item, L2Object reference, boolean sendMessage) { + return destroyItem(process, item, item.getCount(), reference, sendMessage); + } + + /** + * Destroy item from inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param item : L2ItemInstance to be destroyed + * @param count + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + public boolean destroyItem(String process, L2ItemInstance item, long count, L2Object reference, boolean sendMessage) { + item = _inventory.destroyItem(process, item, count, this, reference); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + return false; + } + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + playerIU.addItem(item); + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(su); + + // Sends message to client if requested + if (sendMessage) { + if (count > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(item); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(item); + sendPacket(sm); + } + } + + return true; + } + + /** + * Destroys item from inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param objectId : int Item Instance identifier of the item to be destroyed + * @param count : int Quantity of items to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + @Override + public boolean destroyItem(String process, int objectId, long count, L2Object reference, boolean sendMessage) { + L2ItemInstance item = _inventory.getItemByObjectId(objectId); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return false; + } + return destroyItem(process, item, count, reference, sendMessage); + } + + /** + * Destroys shots from inventory without logging and only occasional saving to database. Sends a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param objectId : int Item Instance identifier of the item to be destroyed + * @param count : int Quantity of items to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + public boolean destroyItemWithoutTrace(String process, int objectId, long count, L2Object reference, boolean sendMessage) { + L2ItemInstance item = _inventory.getItemByObjectId(objectId); + + if ((item == null) || (item.getCount() < count)) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return false; + } + + return destroyItem(null, item, count, reference, sendMessage); + } + + /** + * Destroy item from inventory by using its itemId and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param itemId : int Item identifier of the item to be destroyed + * @param count : int Quantity of items to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + @Override + public boolean destroyItemByItemId(String process, int itemId, long count, L2Object reference, boolean sendMessage) { + if (itemId == Inventory.ADENA_ID) { + return reduceAdena(process, count, reference, sendMessage); + } + + L2ItemInstance item = _inventory.getItemByItemId(itemId); + + if ((item == null) || (item.getCount() < count) || (_inventory.destroyItemByItemId(process, itemId, count, this, reference) == null)) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return false; + } + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + playerIU.addItem(item); + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(su); + + // Sends message to client if requested + if (sendMessage) { + if (count > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(itemId); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(itemId); + sendPacket(sm); + } + } + + return true; + } + + /** + * Transfers item to another ItemContainer and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param objectId : int Item Identifier of the item to be transfered + * @param count : long Quantity of items to be transfered + * @param target + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @return L2ItemInstance corresponding to the new item or the updated item in inventory + */ + public L2ItemInstance transferItem(String process, int objectId, long count, Inventory target, L2Object reference) { + L2ItemInstance oldItem = checkItemManipulation(objectId, count, "transfer"); + if (oldItem == null) { + return null; + } + L2ItemInstance newItem = getInventory().transferItem(process, objectId, count, target, this, reference); + if (newItem == null) { + return null; + } + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + + if ((oldItem.getCount() > 0) && (oldItem != newItem)) { + playerIU.addModifiedItem(oldItem); + } else { + playerIU.addRemovedItem(oldItem); + } + + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate playerSU = new StatusUpdate(this); + playerSU.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(playerSU); + + // Send target update packet + if (target instanceof PcInventory) { + L2PcInstance targetPlayer = ((PcInventory) target).getOwner(); + + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + + if (newItem.getCount() > count) { + playerIU.addModifiedItem(newItem); + } else { + playerIU.addNewItem(newItem); + } + + targetPlayer.sendPacket(playerIU); + } else { + targetPlayer.sendPacket(new ItemList(targetPlayer, false)); + } + + // Update current load as well + playerSU = new StatusUpdate(targetPlayer); + playerSU.addAttribute(StatusUpdate.CUR_LOAD, targetPlayer.getCurrentLoad()); + targetPlayer.sendPacket(playerSU); + } else if (target instanceof PetInventory) { + PetInventoryUpdate petIU = new PetInventoryUpdate(); + + if (newItem.getCount() > count) { + petIU.addModifiedItem(newItem); + } else { + petIU.addNewItem(newItem); + } + + ((PetInventory) target).getOwner().sendPacket(petIU); + } + return newItem; + } + + /** + * Use instead of calling {@link #addItem(String, L2ItemInstance, L2Object, boolean)} and {@link #destroyItemByItemId(String, int, long, L2Object, boolean)}
      + * This method validates slots and weight limit, for stackable and non-stackable items. + * @param process a generic string representing the process that is exchanging this items + * @param reference the (probably NPC) reference, could be null + * @param coinId the item Id of the item given on the exchange + * @param cost the amount of items given on the exchange + * @param rewardId the item received on the exchange + * @param count the amount of items received on the exchange + * @param sendMessage if {@code true} it will send messages to the acting player + * @return {@code true} if the player successfully exchanged the items, {@code false} otherwise + */ + public boolean exchangeItemsById(String process, L2Object reference, int coinId, long cost, int rewardId, long count, boolean sendMessage) { + final PcInventory inv = getInventory(); + if (!inv.validateCapacityByItemId(rewardId, count)) { + if (sendMessage) { + sendPacket(SystemMessageId.SLOTS_FULL); + } + return false; + } + + if (!inv.validateWeightByItemId(rewardId, count)) { + if (sendMessage) { + sendPacket(SystemMessageId.WEIGHT_LIMIT_EXCEEDED); + } + return false; + } + + if (destroyItemByItemId(process, coinId, cost, reference, sendMessage)) { + addItem(process, rewardId, count, reference, sendMessage); + return true; + } + return false; + } + + /** + * Drop item from inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process String Identifier of process triggering this action + * @param item L2ItemInstance to be dropped + * @param reference L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage boolean Specifies whether to send message to Client about this action + * @param protectItem whether or not dropped item must be protected temporary against other players + * @return boolean informing if the action was successful + */ + public boolean dropItem(String process, L2ItemInstance item, L2Object reference, boolean sendMessage, boolean protectItem) { + item = _inventory.dropItem(process, item, this, reference); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return false; + } + + item.dropMe(this, (getX() + Rnd.get(50)) - 25, (getY() + Rnd.get(50)) - 25, getZ() + 20); + + if ((general().getAutoDestroyDroppedItemAfter() > 0) && general().destroyPlayerDroppedItem() && !general().getProtectedItems().contains(item.getId())) { + if ((item.isEquipable() && general().destroyEquipableItem()) || !item.isEquipable()) { + ItemsAutoDestroy.getInstance().addItem(item); + } + } + + // protection against auto destroy dropped item + if (general().destroyPlayerDroppedItem()) { + if (!item.isEquipable() || (item.isEquipable() && general().destroyEquipableItem())) { + item.setProtected(false); + } else { + item.setProtected(true); + } + } else { + item.setProtected(true); + } + + // retail drop protection + if (protectItem) { + item.getDropProtection().protect(this); + } + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + playerIU.addItem(item); + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(su); + + // Sends message to client if requested + if (sendMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_DROPPED_S1); + sm.addItemName(item); + sendPacket(sm); + } + + return true; + } + + public boolean dropItem(String process, L2ItemInstance item, L2Object reference, boolean sendMessage) { + return dropItem(process, item, reference, sendMessage, false); + } + + /** + * Drop item from inventory by using its objectID and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param objectId : int Item Instance identifier of the item to be dropped + * @param count : long Quantity of items to be dropped + * @param x : int coordinate for drop X + * @param y : int coordinate for drop Y + * @param z : int coordinate for drop Z + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @param protectItem + * @return L2ItemInstance corresponding to the new item or the updated item in inventory + */ + public L2ItemInstance dropItem(String process, int objectId, long count, int x, int y, int z, L2Object reference, boolean sendMessage, boolean protectItem) { + L2ItemInstance invitem = _inventory.getItemByObjectId(objectId); + L2ItemInstance item = _inventory.dropItem(process, objectId, count, this, reference); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return null; + } + + item.dropMe(this, x, y, z); + + if ((general().getAutoDestroyDroppedItemAfter() > 0) && general().destroyPlayerDroppedItem() && !general().getProtectedItems().contains(item.getId())) { + if ((item.isEquipable() && general().destroyEquipableItem()) || !item.isEquipable()) { + ItemsAutoDestroy.getInstance().addItem(item); + } + } + if (general().destroyPlayerDroppedItem()) { + if (!item.isEquipable() || (item.isEquipable() && general().destroyEquipableItem())) { + item.setProtected(false); + } else { + item.setProtected(true); + } + } else { + item.setProtected(true); + } + + // retail drop protection + if (protectItem) { + item.getDropProtection().protect(this); + } + + // Send inventory update packet + if (!general().forceInventoryUpdate()) { + InventoryUpdate playerIU = new InventoryUpdate(); + playerIU.addItem(invitem); + sendPacket(playerIU); + } else { + sendPacket(new ItemList(this, false)); + } + + // Update current load as well + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.CUR_LOAD, getCurrentLoad()); + sendPacket(su); + + // Sends message to client if requested + if (sendMessage) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_DROPPED_S1); + sm.addItemName(item); + sendPacket(sm); + } + + return item; + } + + public L2ItemInstance checkItemManipulation(int objectId, long count, String action) { + // TODO: if we remove objects that are not visible from the L2World, we'll have to remove this check + if (L2World.getInstance().findObject(objectId) == null) { + LOG.warn("{} tried to {} item not available in L2World", this, action); + return null; + } + + L2ItemInstance item = getInventory().getItemByObjectId(objectId); + + if ((item == null) || (item.getOwnerId() != getObjectId())) { + LOG.warn("{} tried to {} item he is not owner of", this, action); + return null; + } + + if ((count < 0) || ((count > 1) && !item.isStackable())) { + LOG.warn("{} tried to {} item with invalid count: {}", this, action, count); + return null; + } + + if (count > item.getCount()) { + LOG.warn("{} tried to {} more items than he owns", this, action); + return null; + } + + // Pet is summoned and not the item that summoned the pet AND not the buggle from strider you're mounting + if ((hasSummon() && (getSummon().getControlObjectId() == objectId)) || (getMountObjectID() == objectId)) { + if (general().debug()) { + LOG.debug("{} tried to {} item controling pet", this, action); + } + + return null; + } + + if (getActiveEnchantItemId() == objectId) { + if (general().debug()) { + LOG.debug("{} tried to {} an enchant scroll he was using", this, action); + } + return null; + } + + // We cannot put a Weapon with Augmention in WH while casting (Possible Exploit) + if (item.isAugmented() && (isCastingNow() || isCastingSimultaneouslyNow())) { + return null; + } + + return item; + } + + /** + * Set _protectEndTime according settings. + * @param protect + */ + public void setProtection(boolean protect) { + if (general().developer() && (protect || (_protectEndTime > 0))) { + LOG.debug("{}: Protection {} (currently {})", this, (protect ? "ON " + (GameTimeController.getInstance().getGameTicks() + // + (character().getPlayerSpawnProtection() * GameTimeController.TICKS_PER_SECOND)) : "OFF"), GameTimeController.getInstance().getGameTicks()); + } + + _protectEndTime = protect ? GameTimeController.getInstance().getGameTicks() + (character().getPlayerSpawnProtection() * GameTimeController.TICKS_PER_SECOND) : 0; + } + + public void setTeleportProtection(boolean protect) { + if (general().developer() && (protect || (_teleportProtectEndTime > 0))) { + LOG.debug("{}: Tele Protection {} (currently {})", this, (protect ? "ON " + (GameTimeController.getInstance().getGameTicks() + // + (character().getPlayerTeleportProtection() * GameTimeController.TICKS_PER_SECOND)) : "OFF"), GameTimeController.getInstance().getGameTicks()); + } + + _teleportProtectEndTime = protect ? GameTimeController.getInstance().getGameTicks() + (character().getPlayerTeleportProtection() * GameTimeController.TICKS_PER_SECOND) : 0; + } + + public boolean isRecentFakeDeath() { + return _recentFakeDeathEndTime > GameTimeController.getInstance().getGameTicks(); + } + + /** + * Set protection from agro mobs when getting up from fake death, according settings. + * @param protect + */ + public void setRecentFakeDeath(boolean protect) { + _recentFakeDeathEndTime = protect ? GameTimeController.getInstance().getGameTicks() + (character().getPlayerFakeDeathUpProtection() * GameTimeController.TICKS_PER_SECOND) : 0; + } + + public boolean isFakeDeath() { + return _isFakeDeath; + } + + public void setIsFakeDeath(boolean value) { + _isFakeDeath = value; + } + + @Override + public boolean isAlikeDead() { + return super.isAlikeDead() || isFakeDeath(); + } + + /** + * @return the client owner of this char. + */ + public L2GameClient getClient() { + return _client; + } + + public void setClient(L2GameClient client) { + _client = client; + } + + public String getIPAddress() { + String ip = "N/A"; + if ((_client != null) && (_client.getConnectionAddress() != null)) { + ip = _client.getConnectionAddress().getHostAddress(); + } + return ip; + } + + /** + * Close the active connection with the client. + * @param closeClient + */ + private void closeNetConnection(boolean closeClient) { + L2GameClient client = _client; + if (client != null) { + if (client.isDetached()) { + client.cleanMe(true); + } else { + if (!client.getConnection().isClosed()) { + if (closeClient) { + client.close(LeaveWorld.STATIC_PACKET); + } else { + client.close(ServerClose.STATIC_PACKET); + } + } + } + } + } + + public Location getCurrentSkillWorldPosition() { + return _currentSkillWorldPosition; + } + + public void setCurrentSkillWorldPosition(Location worldPosition) { + _currentSkillWorldPosition = worldPosition; + } + + @Override + public void enableSkill(Skill skill) { + super.enableSkill(skill); + removeTimeStamp(skill); + } + + @Override + public boolean checkDoCastConditions(Skill skill) { + if (!super.checkDoCastConditions(skill)) { + return false; + } + + if (inObserverMode()) { + return false; + } + + if (isInOlympiadMode() && skill.isBlockedInOlympiad()) { + sendPacket(SystemMessageId.THIS_SKILL_IS_NOT_AVAILABLE_FOR_THE_OLYMPIAD_EVENT); + return false; + } + + // Check if the spell using charges or not in AirShip + if (((getCharges() < skill.getChargeConsume())) || (isInAirShip() && !skill.hasEffectType(L2EffectType.REFUEL_AIRSHIP))) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + sendPacket(sm); + return false; + } + return true; + } + + /** + * Returns true if cp update should be done, false if not + * @return boolean + */ + private boolean needCpUpdate() { + double currentCp = getCurrentCp(); + + if ((currentCp <= 1.0) || (getMaxCp() < MAX_HP_BAR_PX)) { + return true; + } + + if ((currentCp <= _cpUpdateDecCheck) || (currentCp >= _cpUpdateIncCheck)) { + if (currentCp == getMaxCp()) { + _cpUpdateIncCheck = currentCp + 1; + _cpUpdateDecCheck = currentCp - _cpUpdateInterval; + } else { + double doubleMulti = currentCp / _cpUpdateInterval; + int intMulti = (int) doubleMulti; + + _cpUpdateDecCheck = _cpUpdateInterval * (doubleMulti < intMulti ? intMulti-- : intMulti); + _cpUpdateIncCheck = _cpUpdateDecCheck + _cpUpdateInterval; + } + + return true; + } + + return false; + } + + /** + * Returns true if mp update should be done, false if not + * @return boolean + */ + private boolean needMpUpdate() { + double currentMp = getCurrentMp(); + + if ((currentMp <= 1.0) || (getMaxMp() < MAX_HP_BAR_PX)) { + return true; + } + + if ((currentMp <= _mpUpdateDecCheck) || (currentMp >= _mpUpdateIncCheck)) { + if (currentMp == getMaxMp()) { + _mpUpdateIncCheck = currentMp + 1; + _mpUpdateDecCheck = currentMp - _mpUpdateInterval; + } else { + double doubleMulti = currentMp / _mpUpdateInterval; + int intMulti = (int) doubleMulti; + + _mpUpdateDecCheck = _mpUpdateInterval * (doubleMulti < intMulti ? intMulti-- : intMulti); + _mpUpdateIncCheck = _mpUpdateDecCheck + _mpUpdateInterval; + } + + return true; + } + + return false; + } + + /** + * Send packet StatusUpdate with current HP,MP and CP to the L2PcInstance and only current HP, MP and Level to all other L2PcInstance of the Party. Actions : + *
    • Send the Server->Client packet StatusUpdate with current HP, MP and CP to this L2PcInstance

    • + *
    • Send the Server->Client packet PartySmallWindowUpdate with current HP, MP and Level to all other L2PcInstance of the Party
    • Caution : This method DOESN'T SEND current HP and MP to all L2PcInstance of the _statusListener + */ + @Override + public void broadcastStatusUpdate() { + // TODO We mustn't send these informations to other players + // Send the Server->Client packet StatusUpdate with current HP and MP to all L2PcInstance that must be informed of HP/MP updates of this L2PcInstance + // super.broadcastStatusUpdate(); + + // Send the Server->Client packet StatusUpdate with current HP, MP and CP to this L2PcInstance + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.MAX_HP, getMaxHp()); + su.addAttribute(StatusUpdate.CUR_HP, (int) getCurrentHp()); + su.addAttribute(StatusUpdate.MAX_MP, getMaxMp()); + su.addAttribute(StatusUpdate.CUR_MP, (int) getCurrentMp()); + su.addAttribute(StatusUpdate.MAX_CP, getMaxCp()); + su.addAttribute(StatusUpdate.CUR_CP, (int) getCurrentCp()); + sendPacket(su); + + final boolean needCpUpdate = needCpUpdate(); + final boolean needHpUpdate = needHpUpdate(); + + // Check if a party is in progress and party window update is usefull + if (isInParty() && (needCpUpdate || needHpUpdate || needMpUpdate())) { + getParty().broadcastToPartyMembers(this, new PartySmallWindowUpdate(this)); + } + + if (isInOlympiadMode() && isOlympiadStart() && (needCpUpdate || needHpUpdate)) { + final OlympiadGameTask game = OlympiadGameManager.getInstance().getOlympiadTask(getOlympiadGameId()); + if ((game != null) && game.isBattleStarted()) { + game.getZone().broadcastStatusUpdate(this); + } + } + + // In duel MP updated only with CP or HP + if (isInDuel() && (needCpUpdate || needHpUpdate)) { + DuelManager.getInstance().broadcastToOpposingTeam(this, new ExDuelUpdateUserInfo(this)); + } + } + + /** + * Send a Server->Client packet UserInfo to this L2PcInstance and CharInfo to all L2PcInstance in its _KnownPlayers. Concept : Others L2PcInstance in the detection area of the L2PcInstance are identified in _knownPlayers. In order to inform other players of this + * L2PcInstance state modifications, server just need to go through _knownPlayers to send Server->Client Packet Actions : + *
    • Send a Server->Client packet UserInfo to this L2PcInstance (Public and Private Data)
    • + *
    • Send a Server->Client packet CharInfo to all L2PcInstance in _KnownPlayers of the L2PcInstance (Public data only)
    • Caution : DON'T SEND UserInfo packet to other players instead of CharInfo packet. Indeed, UserInfo packet contains PRIVATE DATA as MaxHP, + * STR, DEX... + */ + public void broadcastUserInfo() { + // Send a Server->Client packet UserInfo to this L2PcInstance + sendPacket(new UserInfo(this)); + + // Send a Server->Client packet CharInfo to all L2PcInstance in _KnownPlayers of the L2PcInstance + broadcastPacket(new CharInfo(this)); + broadcastPacket(new ExBrExtraUserInfo(this)); + if (TerritoryWarManager.getInstance().isTWInProgress() && (TerritoryWarManager.getInstance().checkIsRegistered(-1, getObjectId()) || TerritoryWarManager.getInstance().checkIsRegistered(-1, getClan()))) { + broadcastPacket(new ExDominionWarStart(this)); + } + } + + public void broadcastTitleInfo() { + // Send a Server->Client packet UserInfo to this L2PcInstance + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + + // Send a Server->Client packet TitleUpdate to all L2PcInstance in _KnownPlayers of the L2PcInstance + + broadcastPacket(new NicknameChanged(this)); + } + + @Override + public void broadcastPacket(L2GameServerPacket mov) { + if (!(mov instanceof CharInfo)) { + sendPacket(mov); + } + + mov.setInvisible(isInvisible()); + + final Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if ((player == null) || !isVisibleFor(player)) { + continue; + } + player.sendPacket(mov); + if (mov instanceof CharInfo) { + int relation = getRelation(player); + Integer oldrelation = getKnownList().getKnownRelations().get(player.getObjectId()); + if ((oldrelation != null) && (oldrelation != relation)) { + player.sendPacket(new RelationChanged(this, relation, isAutoAttackable(player))); + if (hasSummon()) { + player.sendPacket(new RelationChanged(getSummon(), relation, isAutoAttackable(player))); + } + } + } + } + } + + @Override + public void broadcastPacket(L2GameServerPacket mov, int radiusInKnownlist) { + if (!(mov instanceof CharInfo)) { + sendPacket(mov); + } + + mov.setInvisible(isInvisible()); + + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + if (player == null) { + continue; + } + if (isInsideRadius(player, radiusInKnownlist, false, false)) { + player.sendPacket(mov); + if (mov instanceof CharInfo) { + int relation = getRelation(player); + Integer oldrelation = getKnownList().getKnownRelations().get(player.getObjectId()); + if ((oldrelation != null) && (oldrelation != relation)) { + player.sendPacket(new RelationChanged(this, relation, isAutoAttackable(player))); + if (hasSummon()) { + player.sendPacket(new RelationChanged(getSummon(), relation, isAutoAttackable(player))); + } + } + } + } + } + } + + /** + * @return the Alliance Identifier of the L2PcInstance. + */ + @Override + public int getAllyId() { + if (_clan == null) { + return 0; + } + return _clan.getAllyId(); + } + + public int getAllyCrestId() { + if (getClanId() == 0) { + return 0; + } + if (getClan().getAllyId() == 0) { + return 0; + } + return getClan().getAllyCrestId(); + } + + public void queryGameGuard() { + if (getClient() != null) { + getClient().setGameGuardOk(false); + sendPacket(GameGuardQuery.STATIC_PACKET); + } + if (general().gameGuardEnforce()) { + ThreadPoolManager.getInstance().scheduleGeneral(new GameGuardCheckTask(this), 30 * 1000); + } + } + + /** + * Send a Server->Client packet StatusUpdate to the L2PcInstance. + */ + @Override + public void sendPacket(L2GameServerPacket packet) { + if (_client != null) { + _client.sendPacket(packet); + } + } + + /** + * Send SystemMessage packet. + * @param id SystemMessageId + */ + @Override + public void sendPacket(SystemMessageId id) { + sendPacket(SystemMessage.getSystemMessage(id)); + } + + /** + * Manage Interact Task with another L2PcInstance. Actions : + *
    • If the private store is a STORE_PRIVATE_SELL, send a Server->Client PrivateBuyListSell packet to the L2PcInstance
    • + *
    • If the private store is a STORE_PRIVATE_BUY, send a Server->Client PrivateBuyListBuy packet to the L2PcInstance
    • + *
    • If the private store is a STORE_PRIVATE_MANUFACTURE, send a Server->Client RecipeShopSellList packet to the L2PcInstance
    • + * @param target The L2Character targeted + */ + public void doInteract(L2Character target) { + if (target instanceof L2PcInstance temp) { + sendPacket(ActionFailed.STATIC_PACKET); + + if ((temp.getPrivateStoreType() == PrivateStoreType.SELL) || (temp.getPrivateStoreType() == PrivateStoreType.PACKAGE_SELL)) { + sendPacket(new PrivateStoreListSell(this, temp)); + } else if (temp.getPrivateStoreType() == PrivateStoreType.BUY) { + sendPacket(new PrivateStoreListBuy(this, temp)); + } else if (temp.getPrivateStoreType() == PrivateStoreType.MANUFACTURE) { + sendPacket(new RecipeShopSellList(this, temp)); + } + + } else { + target.onAction(this); + } + } + + /** + * Manages AutoLoot Task.
      + *
        + *
      • Send a system message to the player.
      • + *
      • Add the item to the player's inventory.
      • + *
      • Send a Server->Client packet InventoryUpdate to this player with NewItem (use a new slot) or ModifiedItem (increase amount).
      • + *
      • Send a Server->Client packet StatusUpdate to this player with current weight.
      • + *
      + * Caution: If a party is in progress, distribute the items between the party members! + * @param target the NPC dropping the item + * @param itemId the item ID + * @param itemCount the item count + */ + public void doAutoLoot(L2Attackable target, int itemId, long itemCount) { + if (isInParty() && !ItemTable.getInstance().getTemplate(itemId).hasExImmediateEffect()) { + getParty().distributeItem(this, itemId, itemCount, false, target); + } else if (itemId == Inventory.ADENA_ID) { + addAdena("Loot", itemCount, target, true); + } else { + addItem("Loot", itemId, itemCount, target, true); + } + } + + /** + * Method overload for {@link L2PcInstance#doAutoLoot(L2Attackable, int, long)} + * @param target the NPC dropping the item + * @param item the item holder + */ + public void doAutoLoot(L2Attackable target, ItemHolder item) { + doAutoLoot(target, item.getId(), item.getCount()); + } + + /** + * Manage Pickup Task. Actions : + *
    • Send a Server->Client packet StopMove to this L2PcInstance
    • + *
    • Remove the L2ItemInstance from the world and send server->client GetItem packets
    • + *
    • Send a System Message to the L2PcInstance : YOU_PICKED_UP_S1_ADENA or YOU_PICKED_UP_S1_S2
    • + *
    • Add the Item to the L2PcInstance inventory
    • + *
    • Send a Server->Client packet InventoryUpdate to this L2PcInstance with NewItem (use a new slot) or ModifiedItem (increase amount)
    • + *
    • Send a Server->Client packet StatusUpdate to this L2PcInstance with current weight
    • Caution : If a Party is in progress, distribute Items between party members + * @param object The L2ItemInstance to pick up + */ + @Override + public void doPickupItem(L2Object object) { + if (isAlikeDead() || isFakeDeath() || isInvisible()) { + return; + } + + // Set the AI Intention to AI_INTENTION_IDLE + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + + // Check if the L2Object to pick up is a L2ItemInstance + if (!(object.isItem())) { + // dont try to pickup anything that is not an item :) + LOG.warn("{} trying to pickup wrong target.", this, getTarget()); + return; + } + + L2ItemInstance target = (L2ItemInstance) object; + + sendPacket(new StopMove(this)); + + SystemMessage smsg = null; + synchronized (target) { + // Check if the target to pick up is visible + if (!target.isVisible()) { + // Send a Server->Client packet ActionFailed to this L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + if (!target.getDropProtection().tryPickUp(this)) { + sendPacket(ActionFailed.STATIC_PACKET); + smsg = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1); + smsg.addItemName(target); + sendPacket(smsg); + return; + } + + if (((isInParty() && (getParty().getDistributionType() == PartyDistributionType.FINDERS_KEEPERS)) || !isInParty()) && !_inventory.validateCapacity(target)) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.SLOTS_FULL); + return; + } + + if (isInvisible() && !canOverrideCond(PcCondOverride.ITEM_CONDITIONS)) { + return; + } + + if ((target.getOwnerId() != 0) && (target.getOwnerId() != getObjectId()) && !isInLooterParty(target.getOwnerId())) { + if (target.getId() == Inventory.ADENA_ID) { + smsg = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1_ADENA); + smsg.addLong(target.getCount()); + } else if (target.getCount() > 1) { + smsg = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S2_S1_S); + smsg.addItemName(target); + smsg.addLong(target.getCount()); + } else { + smsg = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1); + smsg.addItemName(target); + } + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(smsg); + return; + } + + // You can pickup only 1 combat flag + if (FortSiegeManager.getInstance().isCombat(target.getId())) { + if (!FortSiegeManager.getInstance().checkIfCanPickup(this)) { + return; + } + } + + if ((target.getItemLootSchedule() != null) && ((target.getOwnerId() == getObjectId()) || isInLooterParty(target.getOwnerId()))) { + target.resetOwnerTimer(); + } + + // Remove the L2ItemInstance from the world and send server->client GetItem packets + target.pickupMe(this); + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().removeObject(target); + } + } + + // Auto use herbs - pick up + if (target.getItem().hasExImmediateEffect()) { + IItemHandler handler = ItemHandler.getInstance().getHandler(target.getEtcItem()); + if (handler == null) { + LOG.warn("No item handler registered for item ID: {}.", target.getId()); + } else { + handler.useItem(this, target, false); + } + ItemTable.getInstance().destroyItem("Consume", target, this, null); + } + // Cursed Weapons are not distributed + else if (CursedWeaponsManager.getInstance().isCursed(target.getId())) { + addItem("Pickup", target, null, true); + } else if (FortSiegeManager.getInstance().isCombat(target.getId())) { + addItem("Pickup", target, null, true); + } else { + // if item is instance of ArmorType or WeaponType broadcast an "Attention" system message + if ((target.getItemType() instanceof ArmorType) || (target.getItemType() instanceof WeaponType)) { + if (target.getEnchantLevel() > 0) { + smsg = SystemMessage.getSystemMessage(SystemMessageId.ANNOUNCEMENT_C1_PICKED_UP_S2_S3); + smsg.addPcName(this); + smsg.addInt(target.getEnchantLevel()); + smsg.addItemName(target.getId()); + broadcastPacket(smsg, 1400); + } else { + smsg = SystemMessage.getSystemMessage(SystemMessageId.ANNOUNCEMENT_C1_PICKED_UP_S2); + smsg.addPcName(this); + smsg.addItemName(target.getId()); + broadcastPacket(smsg, 1400); + } + } + + // Check if a Party is in progress + if (isInParty()) { + getParty().distributeItem(this, target); + } else if ((target.getId() == Inventory.ADENA_ID) && (getInventory().getAdenaInstance() != null)) { + addAdena("Pickup", target.getCount(), null, true); + ItemTable.getInstance().destroyItem("Pickup", target, this, null); + } else { + addItem("Pickup", target, null, true); + // Auto-Equip arrows/bolts if player has a bow/crossbow and player picks up arrows/bolts. + final L2ItemInstance weapon = getInventory().getPaperdollItem(Inventory.PAPERDOLL_RHAND); + if (weapon != null) { + final L2EtcItem etcItem = target.getEtcItem(); + if (etcItem != null) { + final EtcItemType itemType = etcItem.getItemType(); + if ((weapon.getItemType() == WeaponType.BOW) && (itemType == EtcItemType.ARROW)) { + checkAndEquipArrows(); + } else if ((weapon.getItemType() == WeaponType.CROSSBOW) && (itemType == EtcItemType.BOLT)) { + checkAndEquipBolts(); + } + } + } + } + } + } + + @Override + public void doAttack(L2Character target) { + super.doAttack(target); + setRecentFakeDeath(false); + } + + @Override + public void doCast(Skill skill) { + if (getCurrentSkill() != null) { + if (!checkUseMagicConditions(skill, getCurrentSkill().isCtrlPressed(), getCurrentSkill().isShiftPressed())) { + setIsCastingNow(false); + return; + } + } + super.doCast(skill); + setRecentFakeDeath(false); + } + + public boolean canOpenPrivateStore() { + return !isAlikeDead() && !isInOlympiadMode() && !isMounted() && !isInsideZone(ZoneId.NO_STORE) && !isCastingNow(); + } + + public void tryOpenPrivateBuyStore() { + // Player shouldn't be able to set stores if he/she is alike dead (dead or fake death) + if (canOpenPrivateStore()) { + if ((getPrivateStoreType() == PrivateStoreType.BUY) || (getPrivateStoreType() == PrivateStoreType.BUY_MANAGE)) { + setPrivateStoreType(PrivateStoreType.NONE); + } + if (getPrivateStoreType() == PrivateStoreType.NONE) { + if (isSitting()) { + standUp(); + } + setPrivateStoreType(PrivateStoreType.BUY_MANAGE); + sendPacket(new PrivateStoreManageListBuy(this)); + } + } else { + if (isInsideZone(ZoneId.NO_STORE)) { + sendPacket(SystemMessageId.NO_PRIVATE_STORE_HERE); + } + sendPacket(ActionFailed.STATIC_PACKET); + } + } + + public void tryOpenPrivateSellStore(boolean isPackageSale) { + // Player shouldn't be able to set stores if he/she is alike dead (dead or fake death) + if (canOpenPrivateStore()) { + if ((getPrivateStoreType() == PrivateStoreType.SELL) || (getPrivateStoreType() == PrivateStoreType.SELL_MANAGE) || (getPrivateStoreType() == PrivateStoreType.PACKAGE_SELL)) { + setPrivateStoreType(PrivateStoreType.NONE); + } + + if (getPrivateStoreType() == PrivateStoreType.NONE) { + if (isSitting()) { + standUp(); + } + setPrivateStoreType(PrivateStoreType.SELL_MANAGE); + sendPacket(new PrivateStoreManageListSell(this, isPackageSale)); + } + } else { + if (isInsideZone(ZoneId.NO_STORE)) { + sendPacket(SystemMessageId.NO_PRIVATE_STORE_HERE); + } + sendPacket(ActionFailed.STATIC_PACKET); + } + } + + public PreparedListContainer getMultiSell() { + return _currentMultiSell; + } + + public void setMultiSell(PreparedListContainer list) { + _currentMultiSell = list; + } + + @Override + public boolean isTransformed() { + return (_transformation != null) && !_transformation.isStance(); + } + + public boolean isInStance() { + return (_transformation != null) && _transformation.isStance(); + } + + public void transform(Transform transformation) { + if (_transformation != null) { + // You already polymorphed and cannot polymorph again. + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.YOU_ALREADY_POLYMORPHED_AND_CANNOT_POLYMORPH_AGAIN); + sendPacket(msg); + return; + } + + setQueuedSkill(null, false, false); + if (isMounted()) { + // Get off the strider or something else if character is mounted + dismount(); + } + + _transformation = transformation; + getEffectList().stopAllToggles(); + transformation.onTransform(this); + sendSkillList(); + sendPacket(new SkillCoolTime(this)); + broadcastUserInfo(); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerTransform(this, transformation.getId()), this); + } + + @Override + public void untransform() { + if (_transformation != null) { + setQueuedSkill(null, false, false); + _transformation.onUntransform(this); + _transformation = null; + getEffectList().stopAllToggles(false); + getEffectList().stopSkillEffects(false, AbnormalType.TRANSFORM); + sendSkillList(); + sendPacket(new SkillCoolTime(this)); + broadcastUserInfo(); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerTransform(this, 0), this); + } + } + + @Override + public Transform getTransformation() { + return _transformation; + } + + /** + * This returns the transformation Id of the current transformation. For example, if a player is transformed as a Buffalo, and then picks up the Zariche, the transform Id returned will be that of the Zariche, and NOT the Buffalo. + * @return Transformation Id + */ + public int getTransformationId() { + return (isTransformed() ? getTransformation().getId() : 0); + } + + public int getTransformationDisplayId() { + return (isTransformed() ? getTransformation().getDisplayId() : 0); + } + + /** + * Set a target. Actions : + *
        + *
      • Remove the L2PcInstance from the _statusListener of the old target if it was a L2Character
      • + *
      • Add the L2PcInstance to the _statusListener of the new target if it's a L2Character
      • + *
      • Target the new L2Object (add the target to the L2PcInstance _target, _knownObject and L2PcInstance to _KnownObject of the L2Object)
      • + *
      + * @param newTarget The L2Object to target + */ + @Override + public void setTarget(L2Object newTarget) { + if (newTarget != null) { + boolean isInParty = (newTarget.isPlayer() && isInParty() && getParty().containsPlayer(newTarget.getActingPlayer())); + + // Prevents /target exploiting + if (!isInParty && (Math.abs(newTarget.getZ() - getZ()) > 1000)) { + newTarget = null; + } + + // Check if the new target is visible + if ((newTarget != null) && !isInParty && !newTarget.isVisible()) { + newTarget = null; + } + + // vehicles cant be targeted + if (!isGM() && (newTarget instanceof L2Vehicle)) { + newTarget = null; + } + } + + // Get the current target + L2Object oldTarget = getTarget(); + + if (oldTarget != null) { + if (oldTarget.equals(newTarget)) // no target change? + { + // Validate location of the target. + if ((newTarget != null) && (newTarget.getObjectId() != getObjectId())) { + sendPacket(new ValidateLocation(newTarget)); + } + return; + } + + // Remove the target from the status listener. + oldTarget.removeStatusListener(this); + } + + if (newTarget instanceof L2Character target) { + // Validate location of the new target. + if (newTarget.getObjectId() != getObjectId()) { + sendPacket(new ValidateLocation(target)); + } + + // Show the client his new target. + sendPacket(new MyTargetSelected(this, target)); + + // Register target to listen for hp changes. + target.addStatusListener(this); + + // Send max/current hp. + final StatusUpdate su = new StatusUpdate(target); + su.addAttribute(StatusUpdate.MAX_HP, target.getMaxHp()); + su.addAttribute(StatusUpdate.CUR_HP, (int) target.getCurrentHp()); + sendPacket(su); + + // To others the new target, and not yourself! + Broadcast.toKnownPlayers(this, new TargetSelected(getObjectId(), newTarget.getObjectId(), getX(), getY(), getZ())); + } + + // Target was removed? + if ((newTarget == null) && (getTarget() != null)) { + broadcastPacket(new TargetUnselected(this)); + } + + // Target the new L2Object (add the target to the L2PcInstance _target, _knownObject and L2PcInstance to _KnownObject of the L2Object) + super.setTarget(newTarget); + } + + /** + * Return the active weapon instance (always equiped in the right hand). + */ + @Override + public L2ItemInstance getActiveWeaponInstance() { + return getInventory().getPaperdollItem(Inventory.PAPERDOLL_RHAND); + } + + /** + * Return the active weapon item (always equiped in the right hand). + */ + @Override + public L2Weapon getActiveWeaponItem() { + L2ItemInstance weapon = getActiveWeaponInstance(); + + if (weapon == null) { + return getFistsWeaponItem(); + } + + return (L2Weapon) weapon.getItem(); + } + + public L2ItemInstance getChestArmorInstance() { + return getInventory().getPaperdollItem(Inventory.PAPERDOLL_CHEST); + } + + public L2ItemInstance getLegsArmorInstance() { + return getInventory().getPaperdollItem(Inventory.PAPERDOLL_LEGS); + } + + public L2Armor getActiveChestArmorItem() { + L2ItemInstance armor = getChestArmorInstance(); + + if (armor == null) { + return null; + } + + return (L2Armor) armor.getItem(); + } + + public L2Armor getActiveLegsArmorItem() { + L2ItemInstance legs = getLegsArmorInstance(); + + if (legs == null) { + return null; + } + + return (L2Armor) legs.getItem(); + } + + public boolean isWearingHeavyArmor() { + L2ItemInstance legs = getLegsArmorInstance(); + L2ItemInstance armor = getChestArmorInstance(); + + if ((armor != null) && (legs != null)) { + if ((legs.getItemType() == ArmorType.HEAVY) && (armor.getItemType() == ArmorType.HEAVY)) { + return true; + } + } + if (armor != null) { + if (((getInventory().getPaperdollItem(Inventory.PAPERDOLL_CHEST).getItem().getBodyPart() == L2Item.SLOT_FULL_ARMOR) && (armor.getItemType() == ArmorType.HEAVY))) { + return true; + } + } + return false; + } + + public boolean isWearingLightArmor() { + L2ItemInstance legs = getLegsArmorInstance(); + L2ItemInstance armor = getChestArmorInstance(); + + if ((armor != null) && (legs != null)) { + if ((legs.getItemType() == ArmorType.LIGHT) && (armor.getItemType() == ArmorType.LIGHT)) { + return true; + } + } + if (armor != null) { + if (((getInventory().getPaperdollItem(Inventory.PAPERDOLL_CHEST).getItem().getBodyPart() == L2Item.SLOT_FULL_ARMOR) && (armor.getItemType() == ArmorType.LIGHT))) { + return true; + } + } + return false; + } + + public boolean isWearingMagicArmor() { + L2ItemInstance legs = getLegsArmorInstance(); + L2ItemInstance armor = getChestArmorInstance(); + + if ((armor != null) && (legs != null)) { + if ((legs.getItemType() == ArmorType.MAGIC) && (armor.getItemType() == ArmorType.MAGIC)) { + return true; + } + } + if (armor != null) { + if (((getInventory().getPaperdollItem(Inventory.PAPERDOLL_CHEST).getItem().getBodyPart() == L2Item.SLOT_FULL_ARMOR) && (armor.getItemType() == ArmorType.MAGIC))) { + return true; + } + } + return false; + } + + public boolean isAutoLoot() { + return _enableAutoLoot; + } + + public void setAutoLoot(boolean val) { + _enableAutoLoot = val; + } + + public boolean isAutoLootItem() { + return _enableAutoLootItem; + } + + public void setAutoLootItem(boolean val) { + _enableAutoLootItem = val; + } + + public boolean isAutoLootHerb() { + return _enableAutoLootHerb; + } + + public void setAutoLootHerbs(boolean val) { + _enableAutoLootHerb = val; + } + + public boolean isMarried() { + return _married; + } + + public void setMarried(boolean state) { + _married = state; + } + + public boolean isEngageRequest() { + return _engagerequest; + } + + public void setEngageRequest(boolean state, int playerid) { + _engagerequest = state; + _engageid = playerid; + } + + public boolean isMarryRequest() { + return _marryrequest; + } + + public void setMarryRequest(boolean state) { + _marryrequest = state; + } + + public boolean isMarryAccepted() { + return _marryaccepted; + } + + public void setMarryAccepted(boolean state) { + _marryaccepted = state; + } + + public int getEngageId() { + return _engageid; + } + + public int getPartnerId() { + return _partnerId; + } + + public void setPartnerId(int partnerid) { + _partnerId = partnerid; + } + + public int getCoupleId() { + return _coupleId; + } + + public void setCoupleId(int coupleId) { + _coupleId = coupleId; + } + + public void engageAnswer(int answer) { + if (!_engagerequest) { + return; + } else if (_engageid == 0) { + return; + } else { + L2PcInstance ptarget = L2World.getInstance().getPlayer(_engageid); + setEngageRequest(false, 0); + if (ptarget != null) { + if (answer == 1) { + CoupleManager.getInstance().createCouple(ptarget, L2PcInstance.this); + ptarget.sendMessage("Request to Engage has been >ACCEPTED<"); + } else { + ptarget.sendMessage("Request to Engage has been >DENIED + * Arrows, Shield..
      + */ + @Override + public L2Item getSecondaryWeaponItem() { + L2ItemInstance item = getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); + if (item != null) { + return item.getItem(); + } + return null; + } + + /** + * Kill the L2Character, Apply Death Penalty, Manage gain/loss Karma and Item Drop. Actions : + *
    • Reduce the Experience of the L2PcInstance in function of the calculated Death Penalty
    • + *
    • If necessary, unsummon the Pet of the killed L2PcInstance
    • + *
    • Manage Karma gain for attacker and Karam loss for the killed L2PcInstance
    • + *
    • If the killed L2PcInstance has Karma, manage Drop Item
    • + *
    • Kill the L2PcInstance
    • + * @param killer + */ + @Override + public boolean doDie(L2Character killer) { + // Kill the L2PcInstance + if (!super.doDie(killer)) { + return false; + } + + if (killer != null) { + final L2PcInstance pk = killer.getActingPlayer(); + if (pk != null) { + EventDispatcher.getInstance().notifyEventAsync(new PlayerPvPKill(pk, this), this); + + TvTEvent.onKill(killer, this); + + if (L2Event.isParticipant(pk)) { + pk.getEventStatus().getKills().add(this); + } + } + + broadcastStatusUpdate(); + // Clear resurrect xp calculation + setExpBeforeDeath(0); + + // Issues drop of Cursed Weapon. + if (isCursedWeaponEquipped()) { + CursedWeaponsManager.getInstance().drop(_cursedWeaponEquippedId, killer); + } else if (isCombatFlagEquipped()) { + // TODO: Fort siege during TW?? + if (TerritoryWarManager.getInstance().isTWInProgress()) { + TerritoryWarManager.getInstance().dropCombatFlag(this, true, false); + } else { + Fort fort = FortManager.getInstance().getFort(this); + if (fort != null) { + FortSiegeManager.getInstance().dropCombatFlag(this, fort.getResidenceId()); + } else { + int slot = getInventory().getSlotFromItem(getInventory().getItemByItemId(9819)); + getInventory().unEquipItemInBodySlot(slot); + destroyItem("CombatFlag", getInventory().getItemByItemId(9819), null, true); + } + } + } else { + final boolean insidePvpZone = isInsideZone(ZoneId.PVP); + final boolean insideSiegeZone = isInsideZone(ZoneId.SIEGE); + if ((pk == null) || !pk.isCursedWeaponEquipped()) { + onDieDropItem(killer); // Check if any item should be dropped + + if (!insidePvpZone && !insideSiegeZone) { + if ((pk != null) && (pk.getClan() != null) && (getClan() != null) && !isAcademyMember() && !(pk.isAcademyMember())) { + if ((_clan.isAtWarWith(pk.getClanId()) && pk.getClan().isAtWarWith(_clan.getId())) || (isInSiege() && pk.isInSiege())) { + if (AntiFeedManager.getInstance().check(killer, this)) { + // when your reputation score is 0 or below, the other clan cannot acquire any reputation points + if (getClan().getReputationScore() > 0) { + pk.getClan().addReputationScore(clan().getReputationScorePerKill(), false); + } + // when the opposing sides reputation score is 0 or below, your clans reputation score does not decrease + if (pk.getClan().getReputationScore() > 0) { + _clan.takeReputationScore(clan().getReputationScorePerKill(), false); + } + } + } + } + } + // If player is Lucky shouldn't get penalized. + if (character().delevel() && !isLucky() && !(insideSiegeZone || insidePvpZone) && !getHuntingSystem().isNevitBlessingActive()) { + calculateDeathExpPenalty(killer, isAtWarWith(pk)); + } + } + } + } + + if (isMounted()) { + stopFeed(); + } + synchronized (this) { + if (isFakeDeath()) { + stopFakeDeath(true); + } + } + + // Unsummon Cubics + if (!_cubics.isEmpty()) { + for (L2CubicInstance cubic : _cubics.values()) { + cubic.stopAction(); + cubic.cancelDisappear(); + } + _cubics.clear(); + } + + if (isChannelized()) { + getSkillChannelized().abortChannelization(); + } + + if (isInParty() && getParty().isInDimensionalRift()) { + getParty().getDimensionalRift().getDeadPlayers().add(this); + } + + if (getAgathionId() != 0) { + setAgathionId(0); + } + + // calculate death penalty buff + calculateDeathPenaltyBuffLevel(killer); + + stopRentPet(); + stopWaterTask(); + + AntiFeedManager.getInstance().setLastDeathTime(getObjectId()); + + return true; + } + + private void onDieDropItem(L2Character killer) { + if (L2Event.isParticipant(this) || (killer == null)) { + return; + } + + L2PcInstance pk = killer.getActingPlayer(); + if ((getKarma() <= 0) && (pk != null) && (pk.getClan() != null) && (getClan() != null) && (pk.getClan().isAtWarWith(getClanId()) + // || getClan().isAtWarWith(((L2PcInstance)killer).getClanId()) + )) { + return; + } + + if ((!isInsideZone(ZoneId.PVP) || (pk == null)) && (!isGM() || pvp().canGMDropEquipment())) { + boolean isKarmaDrop = false; + boolean isKillerNpc = (killer instanceof L2Npc); + int pkLimit = pvp().getMinimumPKRequiredToDrop(); + + int dropEquip = 0; + int dropEquipWeapon = 0; + int dropItem = 0; + int dropLimit = 0; + int dropPercent = 0; + + if ((getKarma() > 0) && (getPkKills() >= pkLimit)) { + isKarmaDrop = true; + dropPercent = rates().getKarmaRateDrop(); + dropEquip = rates().getKarmaRateDropEquip(); + dropEquipWeapon = rates().getKarmaRateDropEquipWeapon(); + dropItem = rates().getKarmaRateDropItem(); + dropLimit = rates().getKarmaDropLimit(); + } else if (isKillerNpc && (getLevel() > 4) && !isFestivalParticipant()) { + dropPercent = rates().getPlayerRateDrop(); + dropEquip = rates().getPlayerRateDropEquip(); + dropEquipWeapon = rates().getPlayerRateDropEquipWeapon(); + dropItem = rates().getPlayerRateDropItem(); + dropLimit = rates().getPlayerDropLimit(); + } + + if ((dropPercent > 0) && (Rnd.get(100) < dropPercent)) { + int dropCount = 0; + + int itemDropPercent = 0; + + for (L2ItemInstance itemDrop : getInventory().getItems()) { + // Don't drop: + // Adena + // Shadow Items + // Time Limited Items + // Quest Items + // Control Item of active pet + // Item listed in the non droppable item list + // Item listed in the non droppable pet item list + if (itemDrop.isShadowItem() || itemDrop.isTimeLimitedItem() || !itemDrop.isDroppable() || (itemDrop.getId() == Inventory.ADENA_ID) || // + (itemDrop.getItem().getType2() == ItemType2.QUEST) || (hasSummon() && (getSummon().getControlObjectId() == itemDrop.getObjectId())) || // + pvp().getNonDroppableItems().contains(itemDrop.getId()) || pvp().getPetItems().contains(itemDrop.getId())) { + continue; + } + + if (itemDrop.isEquipped()) { + // Set proper chance according to Item type of equipped Item + itemDropPercent = itemDrop.getItem().getType2() == ItemType2.WEAPON ? dropEquipWeapon : dropEquip; + getInventory().unEquipItemInSlot(itemDrop.getLocationSlot()); + } else { + itemDropPercent = dropItem; // Item in inventory + } + + // NOTE: Each time an item is dropped, the chance of another item being dropped gets lesser (dropCount * 2) + if (Rnd.get(100) < itemDropPercent) { + dropItem("DieDrop", itemDrop, killer, true); + + if (isKarmaDrop) { + LOG.info("{} has karma and dropped id = {}, count = {}", this, itemDrop.getId(), itemDrop.getCount()); + } else { + LOG.info("{} dropped id = {}, count = {}", this, itemDrop.getId(), itemDrop.getCount()); + } + + if (++dropCount >= dropLimit) { + break; + } + } + } + } + } + } + + public void onKillUpdatePvPKarma(L2Character target) { + if ((target == null) || !target.isPlayable()) { + return; + } + + L2PcInstance targetPlayer = target.getActingPlayer(); + if ((targetPlayer == null) || (targetPlayer == this)) { + return; + } + + if (isCursedWeaponEquipped() && target.isPlayer()) { + CursedWeaponsManager.getInstance().increaseKills(_cursedWeaponEquippedId); + return; + } + + // If in duel and you kill (only can kill l2summon), do nothing + if (isInDuel() && targetPlayer.isInDuel()) { + return; + } + + // If in Arena, do nothing + if (isInsideZone(ZoneId.PVP) || targetPlayer.isInsideZone(ZoneId.PVP)) { + if ((getSiegeState() > 0) && (targetPlayer.getSiegeState() > 0) && (getSiegeState() != targetPlayer.getSiegeState())) { + final L2Clan killerClan = getClan(); + final L2Clan targetClan = targetPlayer.getClan(); + if ((killerClan != null) && (targetClan != null)) { + killerClan.addSiegeKill(); + targetClan.addSiegeDeath(); + } + } + return; + } + + // Check if it's pvp + if ((checkIfPvP(target) && (targetPlayer.getPvpFlag() != 0)) || (isInsideZone(ZoneId.PVP) && targetPlayer.isInsideZone(ZoneId.PVP))) { + increasePvpKills(target); + } else { + // Target player doesn't have pvp flag set check about wars + if ((targetPlayer.getClan() != null) && (getClan() != null) && getClan().isAtWarWith(targetPlayer.getClanId()) && targetPlayer.getClan().isAtWarWith(getClanId()) && (targetPlayer.getPledgeType() != L2Clan.SUBUNIT_ACADEMY) && (getPledgeType() != L2Clan.SUBUNIT_ACADEMY)) { + // 'Both way war' -> 'PvP Kill' + increasePvpKills(target); + return; + } + + // 'No war' or 'One way war' -> 'Normal PK' + if (targetPlayer.getKarma() > 0) { + // Target player has karma + if (pvp().awardPKKillPVPPoint()) { + increasePvpKills(target); + } + } else if (targetPlayer.getPvpFlag() == 0) { + // Target player doesn't have karma + increasePkKillsAndKarma(target); + stopPvPFlag(); + // Unequip adventurer items + checkItemRestriction(); + } + } + } + + /** + * Increase the pvp kills count and send the info to the player + * @param target + */ + public void increasePvpKills(L2Character target) { + if ((target instanceof L2PcInstance) && AntiFeedManager.getInstance().check(this, target)) { + setPvpKills(getPvpKills() + 1); + + // Send a Server->Client UserInfo packet to attacker with its Karma and PK Counter + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + } + + /** + * Increase pk count, karma and send the info to the player + * @param target + */ + public void increasePkKillsAndKarma(L2Character target) { + // Only playables can increase karma/pk + if ((target == null) || !target.isPlayable()) { + return; + } + + // Calculate new karma. (calculate karma before incrase pk count!) + setKarma(getKarma() + Formulas.calculateKarmaGain(getPkKills(), target.isSummon())); + + // PK Points are increased only if you kill a player. + if (target.isPlayer()) { + setPkKills(getPkKills() + 1); + } + + // Update player's UI. + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + + public void updatePvPStatus() { + if (isInsideZone(ZoneId.PVP)) { + return; + } + + setPvpFlagLasts(System.currentTimeMillis() + pvp().getPvPVsNormalTime()); + + if (getPvpFlag() == 0) { + startPvPFlag(); + } + } + + public void updatePvPStatus(L2Character target) { + L2PcInstance player_target = target.getActingPlayer(); + + if (player_target == null) { + return; + } + + if ((isInDuel() && (player_target.getDuelId() == getDuelId()))) { + return; + } + if ((!isInsideZone(ZoneId.PVP) || !player_target.isInsideZone(ZoneId.PVP)) && (player_target.getKarma() == 0)) { + if (checkIfPvP(player_target)) { + setPvpFlagLasts(System.currentTimeMillis() + pvp().getPvPVsPvPTime()); + } else { + setPvpFlagLasts(System.currentTimeMillis() + pvp().getPvPVsNormalTime()); + } + if (getPvpFlag() == 0) { + startPvPFlag(); + } + } + } + + /** + * @return {@code true} if player has Lucky effect and is level 9 or less + */ + public boolean isLucky() { + return (getLevel() <= 9) && isAffectedBySkill(CommonSkill.LUCKY.getId()); + } + + /** + * Restore the specified % of experience this L2PcInstance has lost and sends a Server->Client StatusUpdate packet. + * @param restorePercent + */ + public void restoreExp(double restorePercent) { + if (getExpBeforeDeath() > 0) { + getSubStat().addExp(Math.round(((getExpBeforeDeath() - getExp()) * restorePercent) / 100)); + setExpBeforeDeath(0); + } + } + + @Override + public void addExpAndSp(long addToExp, int addToSp) { + addExpAndSp(addToExp, addToSp, false); + } + + /** + * Used for quest no bonus and no pet consume + * @param addToExp + * @param addToSp + */ + public void addExpAndSpQuest(long addToExp, int addToSp) { + if (addToExp != 0) { + getSubStat().addExp(addToExp); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S1_EXPERIENCE); + sm.addLong(addToExp); + sendPacket(sm); + } + + if (addToSp != 0) { + getSubStat().addSp(addToSp); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.ACQUIRED_S1_SP); + sm.addInt(addToSp); + sendPacket(sm); + } + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + + public void removeExpAndSp(long removeFromExp, int removeFromSp) { + getSubStat().removeExp(removeFromExp); + getSubStat().removeSp(removeFromSp); + + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + + public void addExpAndSp(long addToExp, int addToSp, boolean useBonuses) { + // Allowed to gain exp? + if (!getAccessLevel().canGainExp()) { + return; + } + changeKarma(addToExp); + + long baseExp = addToExp; + int baseSp = addToSp; + + if (useBonuses) { + addToExp *= getStat().getExpBonusMultiplier(); + addToSp *= getStat().getSpBonusMultiplier(); + } + + float ratioTakenByPlayer = 0; + + // if this player has a pet and it is in his range he takes from the owner's Exp, give the pet Exp now + if (hasPet() && Util.checkIfInShortRadius(character().getPartyRange(), this, getSummon(), false)) { + L2PetInstance pet = (L2PetInstance) getSummon(); + ratioTakenByPlayer = pet.getPetLevelData().getOwnerExpTaken() / 100f; + + // only give exp/sp to the pet by taking from the owner if the pet has a non-zero, positive ratio + // allow possible customizations that would have the pet earning more than 100% of the owner's exp/sp + if (ratioTakenByPlayer > 1) { + ratioTakenByPlayer = 1; + } + + if (!pet.isDead()) { + pet.addExpAndSp((long) (addToExp * (1 - ratioTakenByPlayer)), (int) (addToSp * (1 - ratioTakenByPlayer))); + } + + // now adjust the max ratio to avoid the owner earning negative exp/sp + baseExp = (long) (addToExp * ratioTakenByPlayer); + baseSp = (int) (addToSp * ratioTakenByPlayer); + addToExp = (long) (addToExp * ratioTakenByPlayer); + addToSp = (int) (addToSp * ratioTakenByPlayer); + } + + final var gainedExp = getSubStat().addExp(addToExp); + getSubStat().addSp(addToSp); + + final var sm = SystemMessage.getSystemMessage(YOU_EARNED_S1_EXP_BONUS_S2_AND_S3_SP_BONUS_S4); + if (gainedExp) { + sm.addLong(addToExp); + sm.addLong(addToExp - baseExp); + } else { + sm.addLong(0); + sm.addLong(0); + } + sm.addInt(addToSp); + sm.addInt(addToSp - baseSp); + + sendPacket(sm); + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + + public void removeExp(long exp) { + changeKarma(exp); + getSubStat().removeExp(exp); + + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + + private void changeKarma(long exp) { + if (!isCursedWeaponEquipped() && (getKarma() > 0) && (isGM() || !isInsideZone(ZoneId.PVP))) { + int karmaLost = Formulas.calculateKarmaLost(this, exp); + if (karmaLost > 0) { + setKarma(getKarma() - karmaLost); + final SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.YOUR_KARMA_HAS_BEEN_CHANGED_TO_S1); + msg.addInt(getKarma()); + sendPacket(msg); + } + } + } + + public boolean removeSp(int sp) { + return getSubStat().removeSp(sp); + } + + // + /** + * Reduce the Experience (and level if necessary) of the L2PcInstance in function of the calculated Death Penalty.
      + * Actions : + *
    • Calculate the Experience loss
    • + *
    • Set the value of _expBeforeDeath
    • + *
    • Set the new Experience value of the L2PcInstance and Decrease its level if necessary
    • + *
    • Send a Server->Client StatusUpdate packet with its new Experience
    • + * @param killer + * @param atWar + */ + public void calculateDeathExpPenalty(L2Character killer, boolean atWar) { + final int lvl = getLevel(); + double percentLost = PlayerXpPercentLostData.getInstance().getXpPercent(getLevel()); + + if (killer != null) { + if (killer.isRaid()) { + percentLost *= calcStat(Stats.REDUCE_EXP_LOST_BY_RAID, 1); + } else if (killer.isMonster()) { + percentLost *= calcStat(Stats.REDUCE_EXP_LOST_BY_MOB, 1); + } else if (killer.isPlayable()) { + percentLost *= calcStat(Stats.REDUCE_EXP_LOST_BY_PVP, 1); + } + } + + if (getKarma() > 0) { + percentLost *= rates().getRateKarmaExpLost(); + } + + // Calculate the Experience loss + long lostExp = 0; + if (!L2Event.isParticipant(this)) { + if (getHuntingSystem().isNevitBlessingActive()) { + lostExp = 0; + } else if (lvl < character().getMaxPlayerLevel()) { + lostExp = Math.round(((getStat().getExpForLevel(lvl + 1) - getStat().getExpForLevel(lvl)) * percentLost) / 100); + } else { + lostExp = Math.round(((getStat().getExpForLevel(character().getMaxPlayerLevel() + 1) - getStat().getExpForLevel(character().getMaxPlayerLevel())) * percentLost) / 100); + } + } + + if (isFestivalParticipant() || atWar) { + lostExp /= 4.0; + } + + setExpBeforeDeath(getExp()); + + removeExp(lostExp); + } + + public boolean isPartyWaiting() { + return PartyMatchWaitingList.getInstance().getPlayers().contains(this); + } + + public int getPartyRoom() { + return _partyroom; + } + + public void setPartyRoom(int id) { + _partyroom = id; + } + + public boolean isInPartyMatchRoom() { + return _partyroom > 0; + } + + /** + * Stop the HP/MP/CP Regeneration task. Actions : + *
    • Set the RegenActive flag to False
    • + *
    • Stop the HP/MP/CP Regeneration task
    • + */ + public void stopAllTimers() { + stopHpMpRegeneration(); + stopWarnUserTakeBreak(); + stopWaterTask(); + stopFeed(); + DAOFactory.getInstance().getPetDAO().updateFood(this, _mountNpcId); + stopRentPet(); + stopPvpRegTask(); + stopSoulTask(); + stopChargeTask(); + stopFameTask(); + stopVitalityTask(); + getRecSystem().stopBonusTask(false); + getRecSystem().stopGiveTask(); + } + + @Override + public L2Summon getSummon() { + return _summon; + } + + /** + * @return the L2Decoy of the L2PcInstance or null. + */ + public L2Decoy getDecoy() { + return _decoy; + } + + /** + * Set the L2Decoy of the L2PcInstance. + * @param decoy + */ + public void setDecoy(L2Decoy decoy) { + _decoy = decoy; + } + + /** + * @return the L2Trap of the L2PcInstance or null. + */ + public L2TrapInstance getTrap() { + return _trap; + } + + /** + * Set the L2Trap of this L2PcInstance + * @param trap + */ + public void setTrap(L2TrapInstance trap) { + _trap = trap; + } + + /** + * Set the L2Summon of the L2PcInstance. + * @param summon + */ + public void setPet(L2Summon summon) { + _summon = summon; + } + + /** + * Gets the players tamed beasts. + * @return the tamed beasts + */ + public Set getTamedBeasts() { + if (_tamedBeasts == null) { + synchronized (this) { + if (_tamedBeasts == null) { + _tamedBeasts = ConcurrentHashMap.newKeySet(1); + } + } + } + return _tamedBeasts; + } + + /** + * Verifies if the player has tamed beasts. + * @return {@code true} if the player has tamed beasts + */ + public boolean hasTamedBeasts() { + return (_tamedBeasts != null) && !_tamedBeasts.isEmpty(); + } + + /** + * Adds a tamed beast to the player. + * @param tamedBeast the tamed beast + */ + public void addTamedBeast(L2TamedBeastInstance tamedBeast) { + getTamedBeasts().add(tamedBeast); + } + + public void removeTamedBeast(L2TamedBeastInstance tamedBeast) { + if (hasTamedBeasts()) { + _tamedBeasts.remove(tamedBeast); + } + } + + /** + * @return the L2PcInstance requester of a transaction (ex : FriendInvite, JoinAlly, JoinParty...). + */ + public L2Request getRequest() { + return _request; + } + + /** + * @return the L2PcInstance requester of a transaction (ex : FriendInvite, JoinAlly, JoinParty...). + */ + public L2PcInstance getActiveRequester() { + L2PcInstance requester = _activeRequester; + if (requester != null) { + if (requester.isRequestExpired() && (_activeTradeList == null)) { + _activeRequester = null; + } + } + return _activeRequester; + } + + /** + * Set the L2PcInstance requester of a transaction (ex : FriendInvite, JoinAlly, JoinParty...). + * @param requester + */ + public void setActiveRequester(L2PcInstance requester) { + _activeRequester = requester; + } + + /** + * @return True if a transaction is in progress. + */ + public boolean isProcessingRequest() { + return (getActiveRequester() != null) || (_requestExpireTime > GameTimeController.getInstance().getGameTicks()); + } + + /** + * @return True if a transaction is in progress. + */ + public boolean isProcessingTransaction() { + return (getActiveRequester() != null) || (_activeTradeList != null) || (_requestExpireTime > GameTimeController.getInstance().getGameTicks()); + } + + /** + * Select the Warehouse to be used in next activity. + * @param partner + */ + public void onTransactionRequest(L2PcInstance partner) { + _requestExpireTime = GameTimeController.getInstance().getGameTicks() + (REQUEST_TIMEOUT * GameTimeController.TICKS_PER_SECOND); + partner.setActiveRequester(this); + } + + /** + * Return true if last request is expired. + * @return + */ + public boolean isRequestExpired() { + return !(_requestExpireTime > GameTimeController.getInstance().getGameTicks()); + } + + /** + * Select the Warehouse to be used in next activity. + */ + public void onTransactionResponse() { + _requestExpireTime = 0; + } + + /** + * @return active Warehouse. + */ + public ItemContainer getActiveWarehouse() { + return _activeWarehouse; + } + + /** + * Select the Warehouse to be used in next activity. + * @param warehouse + */ + public void setActiveWarehouse(ItemContainer warehouse) { + _activeWarehouse = warehouse; + } + + /** + * @return active TradeList. + */ + public TradeList getActiveTradeList() { + return _activeTradeList; + } + + /** + * Select the TradeList to be used in next activity. + * @param tradeList + */ + public void setActiveTradeList(TradeList tradeList) { + _activeTradeList = tradeList; + } + + public void onTradeStart(L2PcInstance partner) { + _activeTradeList = new TradeList(this); + _activeTradeList.setPartner(partner); + + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.BEGIN_TRADE_WITH_C1); + msg.addPcName(partner); + sendPacket(msg); + sendPacket(new TradeStart(this)); + } + + public void onTradeConfirm(L2PcInstance partner) { + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_CONFIRMED_TRADE); + msg.addPcName(partner); + sendPacket(msg); + sendPacket(TradeOtherDone.STATIC_PACKET); + } + + public void onTradeCancel(L2PcInstance partner) { + if (_activeTradeList == null) { + return; + } + + _activeTradeList.lock(); + _activeTradeList = null; + + sendPacket(new TradeDone(0)); + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.C1_CANCELED_TRADE); + msg.addPcName(partner); + sendPacket(msg); + } + + public void onTradeFinish(boolean successfull) { + _activeTradeList = null; + sendPacket(new TradeDone(1)); + if (successfull) { + sendPacket(SystemMessageId.TRADE_SUCCESSFUL); + } + } + + public void startTrade(L2PcInstance partner) { + onTradeStart(partner); + partner.onTradeStart(this); + } + + public void cancelActiveTrade() { + if (_activeTradeList == null) { + return; + } + + L2PcInstance partner = _activeTradeList.getPartner(); + if (partner != null) { + partner.onTradeCancel(this); + } + onTradeCancel(this); + } + + public boolean hasManufactureShop() { + return (_manufactureItems != null) && !_manufactureItems.isEmpty(); + } + + /** + * Get the manufacture items map of this player. + * @return the the manufacture items map + */ + public Map getManufactureItems() { + if (_manufactureItems == null) { + synchronized (this) { + if (_manufactureItems == null) { + _manufactureItems = Collections.synchronizedMap(new LinkedHashMap<>()); + } + } + } + return _manufactureItems; + } + + /** + * Get the store name, if any. + * @return the store name + */ + public String getStoreName() { + return _storeName; + } + + /** + * Set the store name. + * @param name the store name to set + */ + public void setStoreName(String name) { + _storeName = name == null ? "" : name; + } + + /** + * @return the _buyList object of the L2PcInstance. + */ + public TradeList getSellList() { + if (_sellList == null) { + _sellList = new TradeList(this); + } + return _sellList; + } + + /** + * @return the _buyList object of the L2PcInstance. + */ + public TradeList getBuyList() { + if (_buyList == null) { + _buyList = new TradeList(this); + } + return _buyList; + } + + /** + * Values : + *
    • 0 : STORE_PRIVATE_NONE
    • + *
    • 1 : STORE_PRIVATE_SELL
    • + *
    • 2 : sellmanage

    • + *
    • 3 : STORE_PRIVATE_BUY

    • + *
    • 4 : buymanage

    • + *
    • 5 : STORE_PRIVATE_MANUFACTURE

    • + * @return the Private Store type of the L2PcInstance. + */ + public PrivateStoreType getPrivateStoreType() { + return _privateStoreType; + } + + /** + * Set the Private Store type of the L2PcInstance. Values : + *
    • 0 : STORE_PRIVATE_NONE
    • + *
    • 1 : STORE_PRIVATE_SELL
    • + *
    • 2 : sellmanage

    • + *
    • 3 : STORE_PRIVATE_BUY

    • + *
    • 4 : buymanage

    • + *
    • 5 : STORE_PRIVATE_MANUFACTURE

    • + * @param privateStoreType + */ + public void setPrivateStoreType(PrivateStoreType privateStoreType) { + _privateStoreType = privateStoreType; + + if (customs().offlineDisconnectFinished() && (privateStoreType == PrivateStoreType.NONE) && ((getClient() == null) || getClient().isDetached())) { + deleteMe(); + } + } + + /** + * @return the _clan object of the L2PcInstance. + */ + @Override + public L2Clan getClan() { + return _clan; + } + + /** + * Set the _clan object, _clanId, _clanLeader Flag and title of the L2PcInstance. + * @param clan + */ + public void setClan(L2Clan clan) { + _clan = clan; + if (clan == null) { + _clanId = 0; + _clanPrivileges = new EnumIntBitmask<>(ClanPrivilege.class, false); + _pledgeType = 0; + _powerGrade = 0; + _lvlJoinedAcademy = 0; + _apprentice = 0; + _sponsor = 0; + _activeWarehouse = null; + return; + } + + if (!clan.isMember(getObjectId())) { + // char has been kicked from clan + setClan(null); + return; + } + + _clanId = clan.getId(); + } + + /** + * @return True if the L2PcInstance is the leader of its clan. + */ + public boolean isClanLeader() { + if (getClan() == null) { + return false; + } + return getObjectId() == getClan().getLeaderId(); + } + + /** + * Reduce the number of arrows/bolts owned by the L2PcInstance and send it Server->Client Packet InventoryUpdate or ItemList (to unequip if the last arrow was consummed). + */ + @Override + protected void reduceArrowCount(boolean bolts) { + L2ItemInstance arrows = getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); + + if (arrows == null) { + getInventory().unEquipItemInSlot(Inventory.PAPERDOLL_LHAND); + if (bolts) { + _boltItem = null; + } else { + _arrowItem = null; + } + sendPacket(new ItemList(this, false)); + return; + } + + // Adjust item quantity + if (arrows.getCount() > 1) { + synchronized (arrows) { + arrows.changeCountWithoutTrace(-1, this, null); + arrows.setLastChange(L2ItemInstance.MODIFIED); + + // could do also without saving, but let's save approx once per second + if ((GameTimeController.getInstance().getGameTicks() % GameTimeController.TICKS_PER_SECOND) == 0) { + arrows.updateDatabase(); + } + _inventory.refreshWeight(); + } + } else { + // Destroy entire item and save to database + _inventory.destroyItem("Consume", arrows, this, null); + + getInventory().unEquipItemInSlot(Inventory.PAPERDOLL_LHAND); + if (bolts) { + _boltItem = null; + } else { + _arrowItem = null; + } + + sendPacket(new ItemList(this, false)); + return; + } + + if (!general().forceInventoryUpdate()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addModifiedItem(arrows); + sendPacket(iu); + } else { + sendPacket(new ItemList(this, false)); + } + } + + /** + * Equip arrows needed in left hand and send a Server->Client packet ItemList to the L2PcINstance then return True. + */ + @Override + protected boolean checkAndEquipArrows() { + // Check if nothing is equiped in left hand + if (getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND) == null) { + // Get the L2ItemInstance of the arrows needed for this bow + _arrowItem = getInventory().findArrowForBow(getActiveWeaponItem()); + if (_arrowItem != null) { + // Equip arrows needed in left hand + getInventory().setPaperdollItem(Inventory.PAPERDOLL_LHAND, _arrowItem); + + // Send a Server->Client packet ItemList to this L2PcINstance to update left hand equipement + ItemList il = new ItemList(this, false); + sendPacket(il); + } + } else { + // Get the L2ItemInstance of arrows equiped in left hand + _arrowItem = getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); + } + return _arrowItem != null; + } + + /** + * Equip bolts needed in left hand and send a Server->Client packet ItemList to the L2PcINstance then return True. + */ + @Override + protected boolean checkAndEquipBolts() { + // Check if nothing is equiped in left hand + if (getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND) == null) { + // Get the L2ItemInstance of the arrows needed for this bow + _boltItem = getInventory().findBoltForCrossBow(getActiveWeaponItem()); + if (_boltItem != null) { + // Equip arrows needed in left hand + getInventory().setPaperdollItem(Inventory.PAPERDOLL_LHAND, _boltItem); + + // Send a Server->Client packet ItemList to this L2PcINstance to update left hand equipement + ItemList il = new ItemList(this, false); + sendPacket(il); + } + } else { + // Get the L2ItemInstance of arrows equiped in left hand + _boltItem = getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); + } + return _boltItem != null; + } + + /** + * Disarm the player's weapon. + * @return {@code true} if the player was disarmed or doesn't have a weapon to disarm, {@code false} otherwise. + */ + public boolean disarmWeapons() { + // If there is no weapon to disarm then return true. + final L2ItemInstance wpn = getInventory().getPaperdollItem(Inventory.PAPERDOLL_RHAND); + if (wpn == null) { + return true; + } + + // Don't allow disarming a cursed weapon + if (isCursedWeaponEquipped()) { + return false; + } + + // Don't allow disarming a Combat Flag or Territory Ward. + if (isCombatFlagEquipped()) { + return false; + } + + // Don't allow disarming if the weapon is force equip. + if (wpn.getWeaponItem().isForceEquip()) { + return false; + } + + final L2ItemInstance[] unequiped = getInventory().unEquipItemInBodySlotAndRecord(wpn.getItem().getBodyPart()); + final InventoryUpdate iu = new InventoryUpdate(); + for (L2ItemInstance itm : unequiped) { + iu.addModifiedItem(itm); + } + + sendPacket(iu); + abortAttack(); + broadcastUserInfo(); + + // This can be 0 if the user pressed the right mousebutton twice very fast. + if (unequiped.length > 0) { + final SystemMessage sm; + if (unequiped[0].getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.EQUIPMENT_S1_S2_REMOVED); + sm.addInt(unequiped[0].getEnchantLevel()); + sm.addItemName(unequiped[0]); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISARMED); + sm.addItemName(unequiped[0]); + } + sendPacket(sm); + } + return true; + } + + /** + * Disarm the player's shield. + * @return {@code true}. + */ + public boolean disarmShield() { + L2ItemInstance sld = getInventory().getPaperdollItem(Inventory.PAPERDOLL_LHAND); + if (sld != null) { + L2ItemInstance[] unequiped = getInventory().unEquipItemInBodySlotAndRecord(sld.getItem().getBodyPart()); + InventoryUpdate iu = new InventoryUpdate(); + for (L2ItemInstance itm : unequiped) { + iu.addModifiedItem(itm); + } + sendPacket(iu); + + abortAttack(); + broadcastUserInfo(); + + // this can be 0 if the user pressed the right mousebutton twice very fast + if (unequiped.length > 0) { + SystemMessage sm = null; + if (unequiped[0].getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.EQUIPMENT_S1_S2_REMOVED); + sm.addInt(unequiped[0].getEnchantLevel()); + sm.addItemName(unequiped[0]); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISARMED); + sm.addItemName(unequiped[0]); + } + sendPacket(sm); + } + } + return true; + } + + public boolean mount(L2Summon pet) { + if (!disarmWeapons() || !disarmShield() || isTransformed()) { + return false; + } + + getEffectList().stopAllToggles(); + setMount(pet.getId(), pet.getLevel()); + setMountObjectID(pet.getControlObjectId()); + startFeed(pet.getId()); + broadcastPacket(new Ride(this)); + + // Notify self and others about speed change + broadcastUserInfo(); + + pet.unSummon(this); + return true; + } + + public boolean mount(int npcId, int controlItemObjId, boolean useFood) { + if (!disarmWeapons() || !disarmShield() || isTransformed()) { + return false; + } + + getEffectList().stopAllToggles(); + setMount(npcId, getLevel()); + setMountObjectID(controlItemObjId); + broadcastPacket(new Ride(this)); + + // Notify self and others about speed change + broadcastUserInfo(); + if (useFood) { + startFeed(npcId); + } + return true; + } + + public boolean mountPlayer(L2Summon pet) { + if ((pet != null) && pet.isMountable() && !isMounted() && !isBetrayed()) { + if (isDead()) { + // A strider cannot be ridden when dead + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.STRIDER_CANT_BE_RIDDEN_WHILE_DEAD); + return false; + } else if (pet.isDead()) { + // A dead strider cannot be ridden. + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.DEAD_STRIDER_CANT_BE_RIDDEN); + return false; + } else if (pet.isInCombat() || pet.isRooted()) { + // A strider in battle cannot be ridden + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.STRIDER_IN_BATLLE_CANT_BE_RIDDEN); + return false; + + } else if (isInCombat()) { + // A strider cannot be ridden while in battle + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.STRIDER_CANT_BE_RIDDEN_WHILE_IN_BATTLE); + return false; + } else if (isSitting()) { + // A strider can be ridden only when standing + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.STRIDER_CAN_BE_RIDDEN_ONLY_WHILE_STANDING); + return false; + } else if (isFishing()) { + // You can't mount, dismount, break and drop items while fishing + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.CANNOT_DO_WHILE_FISHING_2); + return false; + } else if (isTransformed() || isCursedWeaponEquipped()) { + // no message needed, player while transformed doesn't have mount action + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } else if (getInventory().getItemByItemId(9819) != null) { + sendPacket(ActionFailed.STATIC_PACKET); + // FIXME: Wrong Message + sendMessage("You cannot mount a steed while holding a flag."); + return false; + } else if (pet.isHungry()) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.HUNGRY_STRIDER_NOT_MOUNT); + return false; + } else if (!Util.checkIfInRange(200, this, pet, true)) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.TOO_FAR_AWAY_FROM_FENRIR_TO_MOUNT); + return false; + } else if (!pet.isDead() && !isMounted()) { + mount(pet); + } + } else if (isRentedPet()) { + stopRentPet(); + } else if (isMounted()) { + if ((getMountType() == MountType.WYVERN) && isInsideZone(ZoneId.NO_LANDING)) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.NO_DISMOUNT_HERE); + return false; + } else if (isHungry()) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.HUNGRY_STRIDER_NOT_MOUNT); + return false; + } else { + dismount(); + } + } + return true; + } + + public boolean dismount() { + boolean wasFlying = isFlying(); + + sendPacket(new SetupGauge(3, 0, 0)); + int petId = _mountNpcId; + setMount(0, 0); + stopFeed(); + if (wasFlying) { + removeSkill(CommonSkill.WYVERN_BREATH.getSkill()); + } + broadcastPacket(new Ride(this)); + setMountObjectID(0); + DAOFactory.getInstance().getPetDAO().updateFood(this, petId); + // Notify self and others about speed change + broadcastUserInfo(); + return true; + } + + public long getUptime() { + return System.currentTimeMillis() - _uptime; + } + + public void setUptime(long time) { + _uptime = time; + } + + /** + * Return True if the L2PcInstance is invulnerable. + */ + @Override + public boolean isInvul() { + return super.isInvul() || _isTeleporting; + } + + /** + * Return True if the L2PcInstance has a Party in progress. + */ + @Override + public boolean isInParty() { + return _party != null; + } + + /** + * Set the _party object of the L2PcInstance AND join it. + * @param party + */ + public void joinParty(L2Party party) { + if (party != null) { + // First set the party otherwise this wouldn't be considered + // as in a party into the L2Character.updateEffectIcons() call. + _party = party; + party.addPartyMember(this); + } + } + + /** + * Manage the Leave Party task of the L2PcInstance. + */ + public void leaveParty() { + if (isInParty()) { + _party.removePartyMember(this, messageType.Disconnected); + _party = null; + } + } + + /** + * Return the _party object of the L2PcInstance. + */ + @Override + public L2Party getParty() { + return _party; + } + + /** + * Set the _party object of the L2PcInstance (without joining it). + * @param party + */ + public void setParty(L2Party party) { + _party = party; + } + + public PartyDistributionType getPartyDistributionType() { + return _partyDistributionType; + } + + public void setPartyDistributionType(PartyDistributionType pdt) { + _partyDistributionType = pdt; + } + + /** + * Return True if the L2PcInstance is a GM. + */ + @Override + public boolean isGM() { + return getAccessLevel().isGm(); + } + + public void setAccountAccesslevel(int level) { + LoginServerThread.getInstance().sendAccessLevel(getAccountName(), level); + } + + /** + * @return the _accessLevel of the L2PcInstance. + */ + @Override + public L2AccessLevel getAccessLevel() { + if (general().everybodyHasAdminRights()) { + return AdminData.getInstance().getMasterAccessLevel(); + } else if (_accessLevel == null) { + setAccessLevel(0); + } + return _accessLevel; + } + + /** + * Set the access level for this player. + * @param level the access level + */ + public void setAccessLevel(int level) { + _accessLevel = AdminData.getInstance().getAccessLevel(level); + + getAppearance().setNameColor(_accessLevel.getNameColor()); + getAppearance().setTitleColor(_accessLevel.getTitleColor()); + broadcastUserInfo(); + + CharNameTable.getInstance().addName(this); + + if (!AdminData.getInstance().hasAccessLevel(level)) { + LOG.warn("Tried to set unregistered access level {} for {}. Setting access level without privileges!", level, this); + } else if (level > 0) { + LOG.info("{} access level set for character {}.", _accessLevel.getName(), this); + } + } + + /** + * Update Stats of the L2PcInstance client side by sending Server->Client packet UserInfo/StatusUpdate to this L2PcInstance and CharInfo/StatusUpdate to all L2PcInstance in its _KnownPlayers (broadcast). + * @param broadcastType + */ + public void updateAndBroadcastStatus(int broadcastType) { + refreshOverloaded(); + refreshExpertisePenalty(); + // Send a Server->Client packet UserInfo to this L2PcInstance and CharInfo to all L2PcInstance in its _KnownPlayers (broadcast) + if (broadcastType == 1) { + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + } + if (broadcastType == 2) { + broadcastUserInfo(); + } + } + + /** + * Send a Server->Client StatusUpdate packet with Karma and PvP Flag to the L2PcInstance and all L2PcInstance to inform (broadcast). + * @param flag + */ + public void setKarmaFlag(int flag) { + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + Collection plrs = getKnownList().getKnownPlayers().values(); + for (L2PcInstance player : plrs) { + player.sendPacket(new RelationChanged(this, getRelation(player), isAutoAttackable(player))); + if (hasSummon()) { + player.sendPacket(new RelationChanged(getSummon(), getRelation(player), isAutoAttackable(player))); + } + } + } + + /** + * Send a Server->Client StatusUpdate packet with Karma to the L2PcInstance and all L2PcInstance to inform (broadcast). + */ + public void broadcastKarma() { + StatusUpdate su = new StatusUpdate(this); + su.addAttribute(StatusUpdate.KARMA, getKarma()); + sendPacket(su); + + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + player.sendPacket(new RelationChanged(this, getRelation(player), isAutoAttackable(player))); + if (hasSummon()) { + player.sendPacket(new RelationChanged(getSummon(), getRelation(player), isAutoAttackable(player))); + } + } + } + + /** + * Set the online Flag to True or False and update the characters table of the database with online status and lastAccess (called when login and logout). + * @param isOnline + * @param updateInDb + */ + public void setOnlineStatus(boolean isOnline, boolean updateInDb) { + if (_isOnline != isOnline) { + _isOnline = isOnline; + } + + // Update the characters table of the database with online status and lastAccess (called when login and logout) + if (updateInDb) { + DAOFactory.getInstance().getPlayerDAO().updateOnlineStatus(this); + } + } + + public void setIsIn7sDungeon(boolean isIn7sDungeon) { + _isIn7sDungeon = isIn7sDungeon; + } + + public Forum getMail() { + if (_forumMail == null) { + final var mailRootForum = ForumsBBSManager.getInstance().getForumByName("MailRoot"); + setMail(mailRootForum.getChildByName(getName())); + if (_forumMail == null) { + ForumsBBSManager.getInstance().create(getName(), mailRootForum, MAIL, OWNER_ONLY, getObjectId()); + setMail(mailRootForum.getChildByName(getName())); + } + } + return _forumMail; + } + + public void setMail(Forum forum) { + _forumMail = forum; + } + + public Forum getMemo() { + if (_forumMemo == null) { + final var memoRootForum = ForumsBBSManager.getInstance().getForumByName("MemoRoot"); + setMemo(memoRootForum.getChildByName(_accountName)); + if (_forumMemo == null) { + ForumsBBSManager.getInstance().create(_accountName, memoRootForum, MEMO, OWNER_ONLY, getObjectId()); + setMemo(memoRootForum.getChildByName(_accountName)); + } + } + return _forumMemo; + } + + public void setMemo(Forum forum) { + _forumMemo = forum; + } + + public Map getPremiumItemList() { + return _premiumItems; + } + + /** + * Update L2PcInstance stats in the characters table of the database. + * @param storeActiveEffects + */ + public synchronized void store(boolean storeActiveEffects) { + DAOFactory.getInstance().getPlayerDAO().storeCharBase(this); + + DAOFactory.getInstance().getSubclassDAO().update(this); + + storeEffect(storeActiveEffects); + + DAOFactory.getInstance().getItemReuseDAO().insert(this); + + if (character().storeRecipeShopList()) { + DAOFactory.getInstance().getRecipeShopListDAO().delete(this); + DAOFactory.getInstance().getRecipeShopListDAO().insert(this); + } + + if (character().storeUISettings()) { + storeUISettings(); + } + + SevenSigns.getInstance().saveSevenSignsData(getObjectId()); + + final PlayerVariables vars = getScript(PlayerVariables.class); + if (vars != null) { + vars.storeMe(); + } + + final AccountVariables aVars = getScript(AccountVariables.class); + if (aVars != null) { + aVars.storeMe(); + } + } + + @Override + public void storeMe() { + store(true); + } + + @Override + public void storeEffect(boolean storeEffects) { + if (!character().storeSkillCooltime()) { + return; + } + + DAOFactory.getInstance().getPlayerSkillSaveDAO().delete(this); + + DAOFactory.getInstance().getPlayerSkillSaveDAO().insert(this, storeEffects); + } + + /** + * @return True if the L2PcInstance is on line. + */ + public boolean isOnline() { + return _isOnline; + } + + public int isOnlineInt() { + if (_isOnline && (_client != null)) { + return _client.isDetached() ? 2 : 1; + } + return 0; + } + + /** + * Verifies if the player is in offline mode.
      + * The offline mode may happen for different reasons:
      + * Abnormally: Player gets abruptly disconnected from server.
      + * Normally: The player gets into offline shop mode, only available by enabling the offline shop mod. + * @return {@code true} if the player is in offline mode, {@code false} otherwise + */ + public boolean isInOfflineMode() { + return (_client == null) || _client.isDetached(); + } + + public boolean isIn7sDungeon() { + return _isIn7sDungeon; + } + + @Override + public Skill addSkill(Skill newSkill) { + addCustomSkill(newSkill); + return super.addSkill(newSkill); + } + + /** + * Add a skill to the L2PcInstance _skills and its Func objects to the calculator set of the L2PcInstance and save update in the character_skills table of the database. Concept : All skills own by a L2PcInstance are identified in _skills Actions : + *
    • Replace oldSkill by newSkill or Add the newSkill
    • + *
    • If an old skill has been replaced, remove all its Func objects of L2Character calculator set
    • + *
    • Add Func objects of newSkill to the calculator set of the L2Character
    • + * @param newSkill The L2Skill to add to the L2Character + * @param store + * @return The L2Skill replaced or null if just added a new L2Skill + */ + public Skill addSkill(Skill newSkill, boolean store) { + // Add a skill to the L2PcInstance _skills and its Func objects to the calculator set of the L2PcInstance + final Skill oldSkill = addSkill(newSkill); + // Add or update a L2PcInstance skill in the character_skills table of the database + if (store) { + storeSkill(newSkill, oldSkill, -1); + } + return oldSkill; + } + + @Override + public Skill removeSkill(Skill skill, boolean store) { + removeCustomSkill(skill); + return store ? removeSkill(skill) : super.removeSkill(skill, true); + } + + public Skill removeSkill(Skill skill, boolean store, boolean cancelEffect) { + removeCustomSkill(skill); + return store ? removeSkill(skill) : super.removeSkill(skill, cancelEffect); + } + + /** + * Remove a skill from the L2Character and its Func objects from calculator set of the L2Character and save update in the character_skills table of the database. Concept : All skills own by a L2Character are identified in _skills Actions : + *
    • Remove the skill from the L2Character _skills
    • + *
    • Remove all its Func objects from the L2Character calculator set
    • Overridden in : + *
    • L2PcInstance : Save update in the character_skills table of the database
    • + * @param skill The L2Skill to remove from the L2Character + * @return The L2Skill removed + */ + public Skill removeSkill(Skill skill) { + removeCustomSkill(skill); + // Remove a skill from the L2Character and its Func objects from calculator set of the L2Character + final Skill oldSkill = super.removeSkill(skill, true); + if (oldSkill != null) { + DAOFactory.getInstance().getSkillDAO().delete(this, oldSkill); + } + + if ((getTransformationId() > 0) || isCursedWeaponEquipped()) { + return oldSkill; + } + + if (skill != null) { + for (Shortcut sc : getAllShortCuts()) { + if ((sc != null) && (sc.getId() == skill.getId()) && (sc.getType() == ShortcutType.SKILL) && !((skill.getId() >= 3080) && (skill.getId() <= 3259))) { + deleteShortCut(sc.getSlot(), sc.getPage()); + } + } + } + return oldSkill; + } + + /** + * Add or update a L2PcInstance skill in the character_skills table of the database.
      + * If newClassIndex > -1, the skill will be stored with that class index, not the current one. + * @param newSkill + * @param oldSkill + * @param newClassIndex + */ + private void storeSkill(Skill newSkill, Skill oldSkill, int newClassIndex) { + final int classIndex = (newClassIndex > -1) ? newClassIndex : _classIndex; + if ((oldSkill != null) && (newSkill != null)) { + DAOFactory.getInstance().getSkillDAO().update(this, classIndex, newSkill, oldSkill); + } else if (newSkill != null) { + DAOFactory.getInstance().getSkillDAO().insert(this, classIndex, newSkill); + } else { + LOG.warn("Could not store new skill, it's null!"); + } + } + + /** + * Retrieve from the database all skill effects of this L2PcInstance and add them to the player. + */ + @Override + public void restoreEffects() { + DAOFactory.getInstance().getPlayerSkillSaveDAO().load(this); + + DAOFactory.getInstance().getPlayerSkillSaveDAO().delete(this); + } + + /** + * @return the number of Henna empty slot of the L2PcInstance. + */ + public int getHennaEmptySlots() { + int totalSlots = 0; + if (getClassId().level() == 1) { + totalSlots = 2; + } else { + totalSlots = 3; + } + + for (int i = 0; i < 3; i++) { + if (_henna[i] != null) { + totalSlots--; + } + } + + if (totalSlots <= 0) { + return 0; + } + + return totalSlots; + } + + /** + * Remove a henna from the player and updates the database. + * @param slot + * @return + */ + public boolean removeHenna(int slot) { + if ((slot < 1) || (slot > 3)) { + return false; + } + + slot--; + + L2Henna henna = _henna[slot]; + if (henna == null) { + return false; + } + + _henna[slot] = null; + + DAOFactory.getInstance().getHennaDAO().delete(this, slot + 1); + + // Calculate Henna modifiers of this L2PcInstance + recalcHennaStats(); + + // Send Server->Client HennaInfo packet to this L2PcInstance + sendPacket(new HennaInfo(this)); + + // Send Server->Client UserInfo packet to this L2PcInstance + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + // Add the recovered dyes to the player's inventory and notify them. + getInventory().addItem("Henna", henna.getDyeItemId(), henna.getCancelCount(), this, null); + reduceAdena("Henna", henna.getCancelFee(), this, false); + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.EARNED_S2_S1_S); + sm.addItemName(henna.getDyeItemId()); + sm.addLong(henna.getCancelCount()); + sendPacket(sm); + sendPacket(SystemMessageId.SYMBOL_DELETED); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerHennaRemove(this, henna), this); + return true; + } + + /** + * Adds a henna to the player and stores it into the database. + * @param henna the henna to add to the player + * @return {@code true} if the henna is added to the player, {@code false} otherwise + */ + public boolean addHenna(L2Henna henna) { + for (int i = 0; i < 3; i++) { + if (_henna[i] == null) { + _henna[i] = henna; + + // Calculate Henna modifiers of this L2PcInstance + recalcHennaStats(); + + DAOFactory.getInstance().getHennaDAO().insert(this, henna, i + 1); + + // Send Server->Client HennaInfo packet to this L2PcInstance + sendPacket(new HennaInfo(this)); + + // Send Server->Client UserInfo packet to this L2PcInstance + sendPacket(new UserInfo(this)); + sendPacket(new ExBrExtraUserInfo(this)); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerHennaRemove(this, henna), this); + return true; + } + } + return false; + } + + /** + * Calculate Henna modifiers of this L2PcInstance. + */ + public void recalcHennaStats() { + _hennaINT = 0; + _hennaSTR = 0; + _hennaCON = 0; + _hennaMEN = 0; + _hennaWIT = 0; + _hennaDEX = 0; + + for (L2Henna h : _henna) { + if (h == null) { + continue; + } + + _hennaINT += ((_hennaINT + h.getStatINT()) > 5) ? 5 - _hennaINT : h.getStatINT(); + _hennaSTR += ((_hennaSTR + h.getStatSTR()) > 5) ? 5 - _hennaSTR : h.getStatSTR(); + _hennaMEN += ((_hennaMEN + h.getStatMEN()) > 5) ? 5 - _hennaMEN : h.getStatMEN(); + _hennaCON += ((_hennaCON + h.getStatCON()) > 5) ? 5 - _hennaCON : h.getStatCON(); + _hennaWIT += ((_hennaWIT + h.getStatWIT()) > 5) ? 5 - _hennaWIT : h.getStatWIT(); + _hennaDEX += ((_hennaDEX + h.getStatDEX()) > 5) ? 5 - _hennaDEX : h.getStatDEX(); + } + } + + /** + * @param slot the character inventory henna slot. + * @return the Henna of this L2PcInstance corresponding to the selected slot. + */ + public L2Henna getHenna(int slot) { + if ((slot < 1) || (slot > 3)) { + return null; + } + return _henna[slot - 1]; + } + + /** + * @return {@code true} if player has at least 1 henna symbol, {@code false} otherwise. + */ + public boolean hasHennas() { + for (L2Henna henna : _henna) { + if (henna != null) { + return true; + } + } + return false; + } + + /** + * @return the henna holder for this player. + */ + public L2Henna[] getHennaList() { + return _henna; + } + + /** + * @return the INT Henna modifier of this L2PcInstance. + */ + public int getHennaStatINT() { + return _hennaINT; + } + + /** + * @return the STR Henna modifier of this L2PcInstance. + */ + public int getHennaStatSTR() { + return _hennaSTR; + } + + /** + * @return the CON Henna modifier of this L2PcInstance. + */ + public int getHennaStatCON() { + return _hennaCON; + } + + /** + * @return the MEN Henna modifier of this L2PcInstance. + */ + public int getHennaStatMEN() { + return _hennaMEN; + } + + /** + * @return the WIT Henna modifier of this L2PcInstance. + */ + public int getHennaStatWIT() { + return _hennaWIT; + } + + /** + * @return the DEX Henna modifier of this L2PcInstance. + */ + public int getHennaStatDEX() { + return _hennaDEX; + } + + /** + * Return True if the L2PcInstance is autoAttackable.
      + * Actions: + *
        + *
      • Check if the attacker isn't the L2PcInstance Pet
      • + *
      • Check if the attacker is L2MonsterInstance
      • + *
      • If the attacker is a L2PcInstance, check if it is not in the same party
      • + *
      • Check if the L2PcInstance has Karma
      • + *
      • If the attacker is a L2PcInstance, check if it is not in the same siege clan (Attacker, Defender)
      • + *
      + */ + @Override + public boolean isAutoAttackable(L2Character attacker) { + if (attacker == null) { + return false; + } + + // Check if the attacker isn't the L2PcInstance Pet + if ((attacker == this) || (attacker == getSummon())) { + return false; + } + + // Friendly mobs doesnt attack players + if (attacker instanceof L2FriendlyMobInstance) { + return false; + } + + // Check if the attacker is a L2MonsterInstance + if (attacker.isMonster()) { + return true; + } + + // is AutoAttackable if both players are in the same duel and the duel is still going on + if (attacker.isPlayable() && (getDuelState() == DuelState.DUELLING) && (getDuelId() == attacker.getActingPlayer().getDuelId())) { + Duel duel = DuelManager.getInstance().getDuel(getDuelId()); + if (duel.getTeamA().contains(this) && duel.getTeamA().contains(attacker)) { + return false; + } else if (duel.getTeamB().contains(this) && duel.getTeamB().contains(attacker)) { + return false; + } + return true; + } + + // Check if the attacker is not in the same party. NOTE: Party checks goes before oly checks in order to prevent patry member autoattack at oly. + if (isInParty() && getParty().getMembers().contains(attacker)) { + return false; + } + + // Check if the attacker is in olympia and olympia start + if (attacker.isPlayer() && attacker.getActingPlayer().isInOlympiadMode()) { + if (isInOlympiadMode() && isOlympiadStart() && (((L2PcInstance) attacker).getOlympiadGameId() == getOlympiadGameId())) { + return true; + } + return false; + } + + // Check if the attacker is in TvT and TvT is started + if (isOnEvent()) { + return true; + } + + // Check if the attacker is a L2Playable + if (attacker.isPlayable()) { + if (isInsideZone(ZoneId.PEACE)) { + return false; + } + + // Get L2PcInstance + L2PcInstance attackerPlayer = attacker.getActingPlayer(); + + if (getClan() != null) { + Siege siege = SiegeManager.getInstance().getSiege(getX(), getY(), getZ()); + if (siege != null) { + // Check if a siege is in progress and if attacker and the L2PcInstance aren't in the Defender clan + if (siege.checkIsDefender(attackerPlayer.getClan()) && siege.checkIsDefender(getClan())) { + return false; + } + + // Check if a siege is in progress and if attacker and the L2PcInstance aren't in the Attacker clan + if (siege.checkIsAttacker(attackerPlayer.getClan()) && siege.checkIsAttacker(getClan())) { + return false; + } + } + + // Check if clan is at war + if ((getClan() != null) && (attackerPlayer.getClan() != null) && getClan().isAtWarWith(attackerPlayer.getClanId()) && attackerPlayer.getClan().isAtWarWith(getClanId()) && (getWantsPeace() == 0) && (attackerPlayer.getWantsPeace() == 0) && !isAcademyMember()) { + return true; + } + } + + // Check if the L2PcInstance is in an arena, but NOT siege zone. NOTE: This check comes before clan/ally checks, but after party checks. + // This is done because in arenas, clan/ally members can autoattack if they arent in party. + if ((isInsideZone(ZoneId.PVP) && attackerPlayer.isInsideZone(ZoneId.PVP)) && !(isInsideZone(ZoneId.SIEGE) && attackerPlayer.isInsideZone(ZoneId.SIEGE))) { + return true; + } + + // Check if the attacker is not in the same clan + if ((getClan() != null) && getClan().isMember(attacker.getObjectId())) { + return false; + } + + // Check if the attacker is not in the same ally + if (attacker.isPlayer() && (getAllyId() != 0) && (getAllyId() == attackerPlayer.getAllyId())) { + return false; + } + + // Now check again if the L2PcInstance is in pvp zone, but this time at siege PvP zone, applying clan/ally checks + if ((isInsideZone(ZoneId.PVP) && attackerPlayer.isInsideZone(ZoneId.PVP)) && (isInsideZone(ZoneId.SIEGE) && attackerPlayer.isInsideZone(ZoneId.SIEGE))) { + return true; + } + } else if (attacker instanceof L2DefenderInstance) { + if (getClan() != null) { + Siege siege = SiegeManager.getInstance().getSiege(this); + return ((siege != null) && siege.checkIsAttacker(getClan())); + } + } + + // Check if the L2PcInstance has Karma + if ((getKarma() > 0) || (getPvpFlag() > 0)) { + return true; + } + + return false; + } + + /** + * Check if the active L2Skill can be casted.
      + * Actions: + *
        + *
      • Check if the skill isn't toggle and is offensive
      • + *
      • Check if the target is in the skill cast range
      • + *
      • Check if the skill is Spoil type and if the target isn't already spoiled
      • + *
      • Check if the caster owns enough consumed Item, enough HP and MP to cast the skill
      • + *
      • Check if the caster isn't sitting
      • + *
      • Check if all skills are enabled and this skill is enabled
      • + *
      • Check if the caster own the weapon needed
      • + *
      • Check if the skill is active
      • + *
      • Check if all casting conditions are completed
      • + *
      • Notify the AI with AI_INTENTION_CAST and target
      • + *
      + * @param skill The L2Skill to use + * @param forceUse used to force ATTACK on players + * @param dontMove used to prevent movement, if not in range + */ + @Override + public boolean useMagic(Skill skill, boolean forceUse, boolean dontMove) { + // Check if the skill is active + if (skill.isPassive()) { + // just ignore the passive skill request. why does the client send it anyway ?? + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // ************************************* Check Casting in Progress ******************************************* + + // If a skill is currently being used, queue this one if this is not the same + if (isCastingNow()) { + SkillUseHolder currentSkill = getCurrentSkill(); + // Check if new skill different from current skill in progress + if ((currentSkill != null) && (skill.getId() == currentSkill.getSkillId())) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } else if (isSkillDisabled(skill)) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Create a new SkillDat object and queue it in the player _queuedSkill + setQueuedSkill(skill, forceUse, dontMove); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + setIsCastingNow(true); + // Create a new SkillDat object and set the player _currentSkill + // This is used mainly to save & queue the button presses, since L2Character has + // _lastSkillCast which could otherwise replace it + setCurrentSkill(skill, forceUse, dontMove); + + if (getQueuedSkill() != null) { + setQueuedSkill(null, false, false); + } + + // Check if the target is correct and Notify the AI with AI_INTENTION_CAST and target + L2Object target = switch (skill.getTargetType()) { + // SELF should be cast even if no target has been found + case GROUND, SELF -> this; + // Get the first target of the list + default -> { + final var targets = skill.getTargets(this); + yield targets.isEmpty() ? null : targets.get(0); + } + }; + + // Notify the AI with AI_INTENTION_CAST and target + getAI().setIntention(CtrlIntention.AI_INTENTION_CAST, skill, target); + return true; + } + + private boolean checkUseMagicConditions(Skill skill, boolean forceUse, boolean dontMove) { + // ************************************* Check Player State ******************************************* + + // Abnormal effects(ex : Stun, Sleep...) are checked in L2Character useMagic() + if (isOutOfControl() || isStunned() || isSleeping()) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the player is dead + if (isDead()) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (isFishing() && !skill.hasEffectType(L2EffectType.FISHING, L2EffectType.FISHING_START)) { + // Only fishing skills are available + sendPacket(SystemMessageId.ONLY_FISHING_SKILLS_NOW); + return false; + } + + if (inObserverMode()) { + sendPacket(SystemMessageId.OBSERVERS_CANNOT_PARTICIPATE); + abortCast(); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the caster is sitting + if (isSitting()) { + // Send a System Message to the caster + sendPacket(SystemMessageId.CANT_MOVE_SITTING); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the skill type is toggle. + if (skill.isToggle() && isAffectedBySkill(skill.getId())) { + stopSkillEffects(true, skill.getId()); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if the player uses "Fake Death" skill + // Note: do not check this before TOGGLE reset + if (isFakeDeath()) { + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // ************************************* Check Target ******************************************* + // Create and set a L2Object containing the target of the skill + L2Object target = null; + TargetType sklTargetType = skill.getTargetType(); + Location worldPosition = getCurrentSkillWorldPosition(); + + if ((sklTargetType == TargetType.GROUND) && (worldPosition == null)) { + LOG.warn("WorldPosition is null for {}, {}", skill.getName(), this); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Target the player if skill type is AURA, PARTY, CLAN or SELF + target = switch (sklTargetType) { + case GROUND, SELF -> this; + case SUMMON -> getSummon(); + default -> getTarget(); + }; + + // Check the validity of the target + if (target == null) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // skills can be used on Walls and Doors only during siege + if (target.isDoor()) { + final L2DoorInstance door = (L2DoorInstance) target; + + if ((door.getCastle() != null) && (door.getCastle().getResidenceId() > 0)) { + if (!door.getCastle().getSiege().isInProgress()) { + sendPacket(SystemMessageId.INCORRECT_TARGET); + return false; + } + } else if ((door.getFort() != null) && (door.getFort().getResidenceId() > 0)) { + if (!door.getFort().getSiege().isInProgress() || !door.getIsShowHp()) { + sendPacket(SystemMessageId.INCORRECT_TARGET); + return false; + } + } + } + + // Are the target and the player in the same duel? + if (isInDuel()) { + final L2PcInstance cha = target.getActingPlayer(); + if ((cha != null) && (cha.getDuelId() != getDuelId())) { + sendMessage("You cannot do this while duelling."); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + + // ************************************* Check skill availability ******************************************* + + // Check if this skill is enabled (ex : reuse time) + if (isSkillDisabled(skill)) { + final SystemMessage sm; + if (hasSkillReuse(skill.getReuseHashCode())) { + int remainingTime = (int) (getSkillRemainingReuseTime(skill.getReuseHashCode()) / 1000); + int hours = remainingTime / 3600; + int minutes = (remainingTime % 3600) / 60; + int seconds = (remainingTime % 60); + if (hours > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_HOURS_S3_MINUTES_S4_SECONDS_REMAINING_FOR_REUSE_S1); + sm.addSkillName(skill); + sm.addInt(hours); + sm.addInt(minutes); + } else if (minutes > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_MINUTES_S3_SECONDS_REMAINING_FOR_REUSE_S1); + sm.addSkillName(skill); + sm.addInt(minutes); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S2_SECONDS_REMAINING_FOR_REUSE_S1); + sm.addSkillName(skill); + } + + sm.addInt(seconds); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_PREPARED_FOR_REUSE); + sm.addSkillName(skill); + } + + sendPacket(sm); + return false; + } + + // ************************************* Check casting conditions ******************************************* + + // Check if all casting conditions are completed + if (!skill.checkCondition(this, target, false)) { + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // ************************************* Check Skill Type ******************************************* + + // Check if this is bad magic skill + if (skill.isBad()) { + if ((isInsidePeaceZone(this, target)) && !getAccessLevel().allowPeaceAttack()) { + // If L2Character or target is in a peace zone, send a system message TARGET_IN_PEACEZONE a Server->Client packet ActionFailed + sendPacket(SystemMessageId.TARGET_IN_PEACEZONE); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (isInOlympiadMode() && !isOlympiadStart()) { + // if L2PcInstance is in Olympia and the match isn't already start, send a Server->Client packet ActionFailed + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if ((target.getActingPlayer() != null) && (getSiegeState() > 0) && isInsideZone(ZoneId.SIEGE) && (target.getActingPlayer().getSiegeState() == getSiegeState()) && (target.getActingPlayer() != this) && (target.getActingPlayer().getSiegeSide() == getSiegeSide())) { + if (TerritoryWarManager.getInstance().isTWInProgress()) { + sendPacket(SystemMessageId.YOU_CANNOT_ATTACK_A_MEMBER_OF_THE_SAME_TERRITORY); + } else { + sendPacket(SystemMessageId.FORCED_ATTACK_IS_IMPOSSIBLE_AGAINST_SIEGE_SIDE_TEMPORARY_ALLIED_MEMBERS); + } + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + if (!target.canBeAttacked() && !getAccessLevel().allowPeaceAttack() && !target.isDoor()) { + // If target is not attackable, send a Server->Client packet ActionFailed + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check for Event Mob's + if ((target instanceof L2EventMonsterInstance) && ((L2EventMonsterInstance) target).eventSkillAttackBlocked()) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if a Forced ATTACK is in progress on non-attackable target + if (!target.isAutoAttackable(this) && !forceUse) { + switch (sklTargetType) { + case SELF: + case GROUND: + case DOOR_TREASURE: + break; + default: // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + + // Check if the target is in the skill cast range + if (dontMove) { + // Calculate the distance between the L2PcInstance and the target + if (sklTargetType == TargetType.GROUND) { + if (!isInsideRadius(worldPosition.getX(), worldPosition.getY(), worldPosition.getZ(), skill.getCastRange() + getTemplate().getCollisionRadius(), false, false)) { + // Send a System Message to the caster + sendPacket(SystemMessageId.TARGET_TOO_FAR); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } else if ((skill.getCastRange() > 0) && !isInsideRadius(target, skill.getCastRange() + getTemplate().getCollisionRadius(), false, false)) { + // Send a System Message to the caster + sendPacket(SystemMessageId.TARGET_TOO_FAR); + + // Send a Server->Client packet ActionFailed to the L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + } + + // Check if the skill is a good magic, target is a monster and if force attack is set, if not then we don't want to cast. + if ((skill.getEffectPoint() > 0) && target.isMonster() && !forceUse) { + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + + // Check if this is a Pvp skill and target isn't a non-flagged/non-karma player + switch (sklTargetType) { + case GROUND: + case SELF: + case ENEMY: + break; + default: + // Verify that player can attack a player or summon + if (target.isPlayable() && !getAccessLevel().allowPeaceAttack() && !checkPvpSkill(target, skill)) { + // Send a System Message to the player + sendPacket(SystemMessageId.INCORRECT_TARGET); + + // Send a Server->Client packet ActionFailed to the player + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + + // GeoData Los Check here + if (skill.getCastRange() > 0) { + if (sklTargetType == TargetType.GROUND) { + if (!GeoData.getInstance().canSeeTarget(this, worldPosition)) { + sendPacket(SystemMessageId.CANT_SEE_TARGET); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } else if (!GeoData.getInstance().canSeeTarget(this, target)) { + sendPacket(SystemMessageId.CANT_SEE_TARGET); + sendPacket(ActionFailed.STATIC_PACKET); + return false; + } + } + + if ((skill.isFlyType()) && !GeoData.getInstance().canMove(this, target)) { + sendPacket(SystemMessageId.THE_TARGET_IS_LOCATED_WHERE_YOU_CANNOT_CHARGE); + return false; + } + + // finally, after passing all conditions + return true; + } + + public boolean isInLooterParty(int LooterId) { + L2PcInstance looter = L2World.getInstance().getPlayer(LooterId); + + // if L2PcInstance is in a CommandChannel + if (isInParty() && getParty().isInCommandChannel() && (looter != null)) { + return getParty().getCommandChannel().getMembers().contains(looter); + } + + if (isInParty() && (looter != null)) { + return getParty().getMembers().contains(looter); + } + + return false; + } + + /** + * Check if the requested casting is a Pc->Pc skill cast and if it's a valid pvp condition + * @param target L2Object instance containing the target + * @param skill L2Skill instance with the skill being casted + * @return {@code false} if the skill is a pvpSkill and target is not a valid pvp target, {@code true} otherwise. + */ + public boolean checkPvpSkill(L2Object target, Skill skill) { + if ((skill == null) || (target == null)) { + return false; + } + + if (!target.isPlayable()) { + return true; + } + + if (skill.isDebuff() || skill.hasEffectType(L2EffectType.STEAL_ABNORMAL) || skill.isBad()) { + final L2PcInstance targetPlayer = target.getActingPlayer(); + + if ((targetPlayer == null) || (this == target)) { + return false; + } + + final boolean isCtrlPressed = (getCurrentSkill() != null) && getCurrentSkill().isCtrlPressed(); + + // Peace Zone + if (target.isInsideZone(ZoneId.PEACE)) { + return false; + } + + // Siege + if ((getSiegeState() != 0) && (targetPlayer.getSiegeState() != 0)) { + // Register for same siege + if (getSiegeSide() == targetPlayer.getSiegeSide()) { + // Same side + if (getSiegeState() == targetPlayer.getSiegeState()) { + sendPacket(SystemMessageId.FORCED_ATTACK_IS_IMPOSSIBLE_AGAINST_SIEGE_SIDE_TEMPORARY_ALLIED_MEMBERS); + return false; + } + } + } + + // Duel + if (isInDuel() && targetPlayer.isInDuel()) { + if (getDuelId() == targetPlayer.getDuelId()) { + return true; + } + } + + // Party + if (isInParty() && targetPlayer.isInParty()) { + // Same Party + if (getParty().getLeader() == targetPlayer.getParty().getLeader()) { + if ((skill.getEffectRange() > 0) && isCtrlPressed && (getTarget() == target)) { + if (skill.isDamage()) { + return true; + } + } + return false; + } else if ((getParty().getCommandChannel() != null) && getParty().getCommandChannel().containsPlayer(targetPlayer)) { + if ((skill.getEffectRange() > 0) && isCtrlPressed && (getTarget() == target)) { + if (skill.isDamage()) { + return true; + } + } + return false; + } + } + + // You can debuff anyone except party members while in an arena... + if (isInsideZone(ZoneId.PVP) && targetPlayer.isInsideZone(ZoneId.PVP)) { + return true; + } + + // Olympiad + if (isInOlympiadMode() && targetPlayer.isInOlympiadMode()) { + if (getOlympiadGameId() == targetPlayer.getOlympiadGameId()) { + return true; + } + } + + final L2Clan aClan = getClan(); + final L2Clan tClan = targetPlayer.getClan(); + + if ((aClan != null) && (tClan != null)) { + if (aClan.isAtWarWith(tClan.getId()) && tClan.isAtWarWith(aClan.getId())) { + // Check if skill can do damage + if ((skill.isAOE() && (skill.getEffectRange() > 0)) && isCtrlPressed && (getTarget() == target)) { + return true; + } + return isCtrlPressed; + } else if ((getClanId() == targetPlayer.getClanId()) || ((getAllyId() > 0) && (getAllyId() == targetPlayer.getAllyId()))) { + // Check if skill can do damage + if ((skill.getEffectRange() > 0) && isCtrlPressed && (getTarget() == target) && skill.isDamage()) { + return true; + } + return false; + } + } + + // On retail, it is impossible to debuff a "peaceful" player. + if ((targetPlayer.getPvpFlag() == 0) && (targetPlayer.getKarma() == 0)) { + // Check if skill can do damage + if ((skill.getEffectRange() > 0) && isCtrlPressed && (getTarget() == target) && skill.isDamage()) { + return true; + } + return false; + } + + if ((targetPlayer.getPvpFlag() > 0) || (targetPlayer.getKarma() > 0)) { + return true; + } + return false; + } + return true; + } + + /** + * @return True if the L2PcInstance is a Mage. + */ + public boolean isMageClass() { + return getClassId().isMage(); + } + + public boolean isMounted() { + return _mountType != MountType.NONE; + } + + public boolean checkLandingState() { + // Check if char is in a no landing zone + if (isInsideZone(ZoneId.NO_LANDING)) { + return true; + } else + // if this is a castle that is currently being sieged, and the rider is NOT a castle owner + // he cannot land. + // castle owner is the leader of the clan that owns the castle where the pc is + if (isInsideZone(ZoneId.SIEGE) && !((getClan() != null) && (CastleManager.getInstance().getCastle(this) == CastleManager.getInstance().getCastleByOwner(getClan())) && (this == getClan().getLeader().getPlayerInstance()))) { + return true; + } + + return false; + } + + // returns false if the change of mount type fails. + public void setMount(int npcId, int npcLevel) { + final MountType type = MountType.findByNpcId(npcId); + switch (type) { + case NONE -> setIsFlying(false); + case STRIDER -> { + if (isNoble()) { + addSkill(CommonSkill.STRIDER_SIEGE_ASSAULT.getSkill(), false); + } + } + case WYVERN -> setIsFlying(true); + } + + _mountType = type; + _mountNpcId = npcId; + _mountLevel = npcLevel; + } + + /** + * @return the type of Pet mounted (0 : none, 1 : Strider, 2 : Wyvern, 3: Wolf). + */ + public MountType getMountType() { + return _mountType; + } + + @Override + public void stopAllEffects() { + super.stopAllEffects(); + updateAndBroadcastStatus(2); + } + + @Override + public void stopAllEffectsExceptThoseThatLastThroughDeath() { + super.stopAllEffectsExceptThoseThatLastThroughDeath(); + updateAndBroadcastStatus(2); + } + + public void stopAllEffectsNotStayOnSubclassChange() { + getEffectList().stopAllEffectsNotStayOnSubclassChange(); + updateAndBroadcastStatus(2); + } + + public void stopCubics() { + if (!_cubics.isEmpty()) { + for (L2CubicInstance cubic : _cubics.values()) { + cubic.stopAction(); + cubic.cancelDisappear(); + } + _cubics.clear(); + broadcastUserInfo(); + } + } + + public void stopCubicsByOthers() { + if (!_cubics.isEmpty()) { + boolean broadcast = false; + for (L2CubicInstance cubic : _cubics.values()) { + if (cubic.givenByOther()) { + cubic.stopAction(); + cubic.cancelDisappear(); + _cubics.remove(cubic.getId()); + broadcast = true; + } + } + if (broadcast) { + broadcastUserInfo(); + } + } + } + + /** + * Send a Server->Client packet UserInfo to this L2PcInstance and CharInfo to all L2PcInstance in its _KnownPlayers.
      + * Concept:
      + * Others L2PcInstance in the detection area of the L2PcInstance are identified in _knownPlayers.
      + * In order to inform other players of this L2PcInstance state modifications, server just need to go through _knownPlayers to send Server->Client Packet
      + * Actions: + *
        + *
      • Send a Server->Client packet UserInfo to this L2PcInstance (Public and Private Data)
      • + *
      • Send a Server->Client packet CharInfo to all L2PcInstance in _KnownPlayers of the L2PcInstance (Public data only)
      • + *
      + * Caution : DON'T SEND UserInfo packet to other players instead of CharInfo packet. Indeed, UserInfo packet contains PRIVATE DATA as MaxHP, STR, DEX... + */ + @Override + public void updateAbnormalEffect() { + broadcastUserInfo(); + } + + /** + * Disable the Inventory and create a new task to enable it after 1.5s. + * @param val + */ + public void setInventoryBlockingStatus(boolean val) { + _inventoryDisable = val; + if (val) { + ThreadPoolManager.getInstance().scheduleGeneral(new InventoryEnableTask(this), 1500); + } + } + + /** + * @return True if the Inventory is disabled. + */ + public boolean isInventoryDisabled() { + return _inventoryDisable; + } + + /** + * Add a cubic to this player. + * @param cubicId the cubic ID + * @param level + * @param cubicPower + * @param cubicDelay + * @param cubicSkillChance + * @param cubicMaxCount + * @param cubicDuration + * @param givenByOther + * @return the old cubic for this cubic ID if any, otherwise {@code null} + */ + public L2CubicInstance addCubic(int cubicId, int level, double cubicPower, int cubicDelay, int cubicSkillChance, int cubicMaxCount, int cubicDuration, boolean givenByOther) { + return _cubics.put(cubicId, new L2CubicInstance(this, cubicId, level, (int) cubicPower, cubicDelay, cubicSkillChance, cubicMaxCount, cubicDuration, givenByOther)); + } + + /** + * Get the player's cubics. + * @return the cubics + */ + public Map getCubics() { + return _cubics; + } + + /** + * Get the player cubic by cubic ID, if any. + * @param cubicId the cubic ID + * @return the cubic with the given cubic ID, {@code null} otherwise + */ + public L2CubicInstance getCubicById(int cubicId) { + return _cubics.get(cubicId); + } + + /** + * @return the modifier corresponding to the Enchant Effect of the Active Weapon (Min : 127). + */ + public int getEnchantEffect() { + L2ItemInstance wpn = getActiveWeaponInstance(); + + if (wpn == null) { + return 0; + } + + return Math.min(127, wpn.getEnchantLevel()); + } + + /** + * @return the _lastFolkNpc of the L2PcInstance corresponding to the last Folk wich one the player talked. + */ + public L2Npc getLastFolkNPC() { + return _lastFolkNpc; + } + + /** + * Set the _lastFolkNpc of the L2PcInstance corresponding to the last Folk wich one the player talked. + * @param folkNpc + */ + public void setLastFolkNPC(L2Npc folkNpc) { + _lastFolkNpc = folkNpc; + } + + /** + * @return True if L2PcInstance is a participant in the Festival of Darkness. + */ + public boolean isFestivalParticipant() { + return SevenSignsFestival.getInstance().isParticipant(this); + } + + public void addAutoSoulShot(int itemId) { + _activeSoulShots.add(itemId); + } + + public boolean removeAutoSoulShot(int itemId) { + return _activeSoulShots.remove(itemId); + } + + public Set getAutoSoulShot() { + return _activeSoulShots; + } + + @Override + public void rechargeShots(boolean physical, boolean magic) { + L2ItemInstance item; + IItemHandler handler; + + if ((_activeSoulShots == null) || _activeSoulShots.isEmpty()) { + return; + } + + for (int itemId : _activeSoulShots) { + item = getInventory().getItemByItemId(itemId); + + if (item != null) { + if (magic) { + if (item.getItem().getDefaultAction() == ActionType.SPIRITSHOT) { + handler = ItemHandler.getInstance().getHandler(item.getEtcItem()); + if (handler != null) { + handler.useItem(this, item, false); + } + } + } + + if (physical) { + if (item.getItem().getDefaultAction() == ActionType.SOULSHOT) { + handler = ItemHandler.getInstance().getHandler(item.getEtcItem()); + if (handler != null) { + handler.useItem(this, item, false); + } + } + } + } else { + removeAutoSoulShot(itemId); + } + } + } + + /** + * Cancel autoshot for all shots matching crystaltype
      + * {@link L2Item#getCrystalType()} + * @param crystalType int type to disable + */ + public void disableAutoShotByCrystalType(int crystalType) { + for (int itemId : _activeSoulShots) { + if (ItemTable.getInstance().getTemplate(itemId).getCrystalType().getId() == crystalType) { + disableAutoShot(itemId); + } + } + } + + /** + * Cancel autoshot use for shot itemId + * @param itemId int id to disable + * @return true if canceled. + */ + public boolean disableAutoShot(int itemId) { + if (_activeSoulShots.contains(itemId)) { + removeAutoSoulShot(itemId); + sendPacket(new ExAutoSoulShot(itemId, 0)); + + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.AUTO_USE_OF_S1_CANCELLED); + sm.addItemName(itemId); + sendPacket(sm); + return true; + } + return false; + } + + /** + * Cancel all autoshots for player + */ + public void disableAutoShotsAll() { + for (int itemId : _activeSoulShots) { + sendPacket(new ExAutoSoulShot(itemId, 0)); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.AUTO_USE_OF_S1_CANCELLED); + sm.addItemName(itemId); + sendPacket(sm); + } + _activeSoulShots.clear(); + } + + public EnumIntBitmask getClanPrivileges() { + return _clanPrivileges; + } + + public void setClanPrivileges(EnumIntBitmask clanPrivileges) { + _clanPrivileges = clanPrivileges.clone(); + } + + public boolean hasClanPrivilege(ClanPrivilege privilege) { + return _clanPrivileges.has(privilege); + } + + public int getPledgeClass() { + return _pledgeClass; + } + + // baron etc + public void setPledgeClass(int classId) { + _pledgeClass = classId; + checkItemRestriction(); + } + + @Override + public int getPledgeType() { + return _pledgeType; + } + + public void setPledgeType(int typeId) { + _pledgeType = typeId; + } + + public int getApprentice() { + return _apprentice; + } + + public void setApprentice(int apprentice_id) { + _apprentice = apprentice_id; + } + + public int getSponsor() { + return _sponsor; + } + + public void setSponsor(int sponsor_id) { + _sponsor = sponsor_id; + } + + public int getBookMarkSlot() { + return _bookmarkslot; + } + + public void setBookMarkSlot(int slot) { + _bookmarkslot = slot; + sendPacket(new ExGetBookMarkInfoPacket(this)); + } + + @Override + public void sendMessage(String message) { + sendPacket(SystemMessage.sendString(message)); + } + + public void enterObserverMode(Location loc) { + setLastLocation(); + + // Remove Hide. + getEffectList().stopSkillEffects(true, AbnormalType.HIDE); + + _observerMode = true; + setTarget(null); + startStunning(); + setIsInvul(true); + setInvisible(true); + sendPacket(new ObservationMode(loc)); + + teleToLocation(loc, false); + + broadcastUserInfo(); + } + + public void setLastLocation() { + _lastLoc.setXYZ(getX(), getY(), getZ()); + } + + public void unsetLastLocation() { + _lastLoc.setXYZ(0, 0, 0); + } + + public void enterOlympiadObserverMode(Location loc, int id) { + if (hasSummon()) { + getSummon().unSummon(this); + } + + // Remove Hide. + getEffectList().stopSkillEffects(true, AbnormalType.HIDE); + + if (!_cubics.isEmpty()) { + for (L2CubicInstance cubic : _cubics.values()) { + cubic.stopAction(); + cubic.cancelDisappear(); + } + _cubics.clear(); + } + + if (getParty() != null) { + getParty().removePartyMember(this, messageType.Expelled); + } + + _olympiadGameId = id; + if (isSitting()) { + standUp(); + } + if (!_observerMode) { + setLastLocation(); + } + + _observerMode = true; + setTarget(null); + setIsInvul(true); + setInvisible(true); + teleToLocation(loc, false); + sendPacket(new ExOlympiadMode(3)); + + broadcastUserInfo(); + } + + public void leaveObserverMode() { + setTarget(null); + + teleToLocation(_lastLoc, false); + unsetLastLocation(); + sendPacket(new ObservationReturn(getLocation())); + + stopStunning(false); + if (!isGM()) { + setInvisible(false); + setIsInvul(false); + } + if (hasAI()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + } + + setFalling(); // prevent receive falling damage + _observerMode = false; + + broadcastUserInfo(); + } + + public void leaveOlympiadObserverMode() { + if (_olympiadGameId == -1) { + return; + } + _olympiadGameId = -1; + _observerMode = false; + setTarget(null); + sendPacket(new ExOlympiadMode(0)); + setInstanceId(0); + teleToLocation(_lastLoc, true); + if (!isGM()) { + setInvisible(false); + setIsInvul(false); + } + if (hasAI()) { + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + } + unsetLastLocation(); + broadcastUserInfo(); + } + + public int getOlympiadSide() { + return _olympiadSide; + } + + public void setOlympiadSide(int i) { + _olympiadSide = i; + } + + public int getOlympiadGameId() { + return _olympiadGameId; + } + + public void setOlympiadGameId(int id) { + _olympiadGameId = id; + } + + /** + * Gets the player's olympiad buff count. + * @return the olympiad's buff count + */ + public int getOlympiadBuffCount() { + return _olyBuffsCount; + } + + /** + * Sets the player's olympiad buff count. + * @param buffs the olympiad's buff count + */ + public void setOlympiadBuffCount(int buffs) { + _olyBuffsCount = buffs; + } + + public Location getLastLocation() { + return _lastLoc; + } + + public boolean inObserverMode() { + return _observerMode; + } + + public int getTeleMode() { + return _telemode; + } + + public void setTeleMode(int mode) { + _telemode = mode; + } + + public void setLoto(int i, int val) { + _loto[i] = val; + } + + public int getLoto(int i) { + return _loto[i]; + } + + public void setRace(int i, int val) { + _race[i] = val; + } + + public int getRace(int i) { + return _race[i]; + } + + public boolean getMessageRefusal() { + return _messageRefusal; + } + + public void setMessageRefusal(boolean mode) { + _messageRefusal = mode; + sendPacket(new EtcStatusUpdate(this)); + } + + public boolean getDietMode() { + return _dietMode; + } + + public void setDietMode(boolean mode) { + _dietMode = mode; + } + + public boolean getTradeRefusal() { + return _tradeRefusal; + } + + public void setTradeRefusal(boolean mode) { + _tradeRefusal = mode; + } + + public boolean getExchangeRefusal() { + return _exchangeRefusal; + } + + public void setExchangeRefusal(boolean mode) { + _exchangeRefusal = mode; + } + + public BlockList getBlockList() { + return _blockList; + } + + public void setIsInOlympiadMode(boolean b) { + _inOlympiadMode = b; + } + + public void setIsOlympiadStart(boolean b) { + _OlympiadStart = b; + } + + public boolean isOlympiadStart() { + return _OlympiadStart; + } + + public boolean isHero() { + return _hero; + } + + public void setHero(boolean hero) { + if (hero && (_baseClass == _activeClass)) { + for (Skill skill : SkillTreesData.getInstance().getHeroSkillTree().values()) { + addSkill(skill, false); // Don't persist hero skills into database + } + } else { + for (Skill skill : SkillTreesData.getInstance().getHeroSkillTree().values()) { + removeSkill(skill, false, true); // Just remove skills from non-hero players + } + } + _hero = hero; + + sendSkillList(); + } + + public boolean isInOlympiadMode() { + return _inOlympiadMode; + } + + @Override + public boolean isInDuel() { + return _duelState != DuelState.NO_DUEL; + } + + @Override + public int getDuelId() { + return _duelId; + } + + public DuelState getDuelState() { + return _duelState; + } + + public void setDuelState(DuelState mode) { + _duelState = mode; + } + + /** + * Sets up the duel state using a non 0 duelId. + * @param duelId 0=not in a duel + */ + public void setIsInDuel(int duelId) { + if (duelId > 0) { + _duelState = DuelState.DUELLING; + _duelId = duelId; + } else { + if (_duelState == DuelState.DEAD) { + enableAllSkills(); + getStatus().startHpMpRegeneration(); + } + _duelState = DuelState.NO_DUEL; + _duelId = 0; + } + } + + public boolean isNoble() { + return _noble; + } + + public void setNoble(boolean val) { + final Collection nobleSkillTree = SkillTreesData.getInstance().getNobleSkillTree().values(); + if (val) { + for (Skill skill : nobleSkillTree) { + addSkill(skill, false); + } + } else { + for (Skill skill : nobleSkillTree) { + removeSkill(skill, false, true); + } + } + + _noble = val; + + sendSkillList(); + } + + public int getLvlJoinedAcademy() { + return _lvlJoinedAcademy; + } + + public void setLvlJoinedAcademy(int lvl) { + _lvlJoinedAcademy = lvl; + } + + @Override + public boolean isAcademyMember() { + return _lvlJoinedAcademy > 0; + } + + @Override + public void setTeam(Team team) { + super.setTeam(team); + broadcastUserInfo(); + if (hasSummon()) { + getSummon().broadcastStatusUpdate(); + } + } + + public int getWantsPeace() { + return _wantsPeace; + } + + public void setWantsPeace(int wantsPeace) { + _wantsPeace = wantsPeace; + } + + public boolean isFishing() { + return _fishing; + } + + public void setFishing(boolean fishing) { + _fishing = fishing; + } + + public void sendSkillList() { + boolean isDisabled = false; + SkillList sl = new SkillList(); + + for (Skill s : getAllSkills()) { + if (s == null) { + continue; + } + + if (((_transformation != null) && !s.isPassive()) || (hasTransformSkill(s.getId()) && s.isPassive())) { + continue; + } + if (getClan() != null) { + isDisabled = s.isClanSkill() && (getClan().getReputationScore() < 0); + } + + boolean isEnchantable = SkillData.getInstance().isEnchantable(s.getId()); + if (isEnchantable) { + L2EnchantSkillLearn esl = EnchantSkillGroupsData.getInstance().getSkillEnchantmentBySkillId(s.getId()); + if ((esl == null) || (s.getLevel() < esl.getBaseLevel())) { + isEnchantable = false; + } + } + + sl.addSkill(s.getDisplayId(), s.getDisplayLevel(), s.isPassive(), isDisabled, isEnchantable); + } + if (_transformation != null) { + Map ts = new TreeMap<>(); + + for (SkillHolder holder : _transformation.getTemplate(this).getSkills()) { + ts.putIfAbsent(holder.getSkillId(), holder.getSkillLvl()); + + if (ts.get(holder.getSkillId()) < holder.getSkillLvl()) { + ts.put(holder.getSkillId(), holder.getSkillLvl()); + } + } + + for (AdditionalSkillHolder holder : _transformation.getTemplate(this).getAdditionalSkills()) { + if (getLevel() >= holder.getMinLevel()) { + ts.putIfAbsent(holder.getSkillId(), holder.getSkillLvl()); + if (ts.get(holder.getSkillId()) < holder.getSkillLvl()) { + ts.put(holder.getSkillId(), holder.getSkillLvl()); + } + } + } + + // Add collection skills. + for (L2SkillLearn skill : SkillTreesData.getInstance().getCollectSkillTree().values()) { + if (getKnownSkill(skill.getSkillId()) != null) { + addTransformSkill(SkillData.getInstance().getSkill(skill.getSkillId(), skill.getSkillLevel())); + } + } + + for (Entry transformSkill : ts.entrySet()) { + Skill sk = SkillData.getInstance().getSkill(transformSkill.getKey(), transformSkill.getValue()); + addTransformSkill(sk); + sl.addSkill(transformSkill.getKey(), transformSkill.getValue(), false, false, false); + } + } + sendPacket(sl); + } + + /** + * 1. Add the specified class ID as a subclass (up to the maximum number of three) for this character.
      + * 2. This method no longer changes the active _classIndex of the player. This is only done by the calling of setActiveClass() method as that should be the only way to do so. + * @param classId + * @param classIndex + * @return boolean subclassAdded + */ + public boolean addSubClass(int classId, int classIndex) { + if (!_subclassLock.tryLock()) { + return false; + } + + try { + if ((getTotalSubClasses() == character().getMaxSubclass()) || (classIndex == 0)) { + return false; + } + + if (getSubClasses().containsKey(classIndex)) { + return false; + } + + // Note: Never change _classIndex in any method other than setActiveClass(). + + SubClass newClass = new SubClass(this); + newClass.setClassId(classId); + newClass.setClassIndex(classIndex); + + if (!DAOFactory.getInstance().getSubclassDAO().insert(this, newClass)) { + return false; + } + + // Commit after database INSERT in case exception is thrown. + getSubClasses().put(newClass.getClassIndex(), newClass); + + final ClassId subTemplate = ClassId.getClassId(classId); + final Map skillTree = SkillTreesData.getInstance().getCompleteClassSkillTree(subTemplate); + final Map prevSkillList = new HashMap<>(); + for (L2SkillLearn skillInfo : skillTree.values()) { + if (skillInfo.getGetLevel() <= 40) { + Skill prevSkill = prevSkillList.get(skillInfo.getSkillId()); + Skill newSkill = SkillData.getInstance().getSkill(skillInfo.getSkillId(), skillInfo.getSkillLevel()); + + if ((prevSkill != null) && (prevSkill.getLevel() > newSkill.getLevel())) { + continue; + } + + prevSkillList.put(newSkill.getId(), newSkill); + storeSkill(newSkill, prevSkill, classIndex); + } + } + return true; + } finally { + _subclassLock.unlock(); + } + } + + /** + * 1. Completely erase all existence of the subClass linked to the classIndex.
      + * 2. Send over the newClassId to addSubClass() to create a new instance on this classIndex.
      + * 3. Upon Exception, revert the player to their BaseClass to avoid further problems. + * @param classIndex the class index to delete + * @param newClassId the new class Id + * @return {@code true} if the sub-class was modified, {@code false} otherwise + */ + public boolean modifySubClass(int classIndex, int newClassId) { + if (!_subclassLock.tryLock()) { + return false; + } + + try { + DAOFactory.getInstance().getHennaDAO().deleteAll(this, classIndex); + + DAOFactory.getInstance().getSkillDAO().deleteAll(this, classIndex); + + DAOFactory.getInstance().getShortcutDAO().delete(this, classIndex); + + DAOFactory.getInstance().getPlayerSkillSaveDAO().delete(this, classIndex); + + DAOFactory.getInstance().getSubclassDAO().delete(this, classIndex); + + // Notify to scripts + int classId = getSubClasses().get(classIndex).getClassId(); + EventDispatcher.getInstance().notifyEventAsync(new PlayerProfessionCancel(this, classId), this); + + getSubClasses().remove(classIndex); + } finally { + _subclassLock.unlock(); + } + + return addSubClass(newClassId, classIndex); + } + + public boolean isSubClassActive() { + return _classIndex > 0; + } + + public Map getSubClasses() { + if (_subClasses == null) { + _subClasses = new ConcurrentSkipListMap<>(); + } + + return _subClasses; + } + + public int getTotalSubClasses() { + return getSubClasses().size(); + } + + public int getBaseClass() { + return _baseClass; + } + + public void setBaseClass(ClassId classId) { + _baseClass = classId.ordinal(); + } + + public int getActiveClass() { + return _activeClass; + } + + public void setActiveClass(int classId) { + _activeClass = classId; + } + + public int getClassIndex() { + return _classIndex; + } + + public void setClassIndex(int classIndex) { + _classIndex = classIndex; + } + + private void setClassTemplate(int classId) { + _activeClass = classId; + + final L2PcTemplate pcTemplate = PlayerTemplateData.getInstance().getTemplate(classId); + if (pcTemplate == null) { + LOG.error("Missing template for classId: {}", classId); + throw new Error(); + } + // Set the template of the L2PcInstance + setTemplate(pcTemplate); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new PlayerProfessionChange(this, pcTemplate, isSubClassActive()), this); + } + + /** + * Changes the character's class based on the given class index.
      + * An index of zero specifies the character's original (base) class, while indexes 1-3 specifies the character's sub-classes respectively.
      + * WARNING: Use only on subclass change + * @param classIndex + * @return + */ + public boolean changeActiveClass(int classIndex) { + if (!_subclassLock.tryLock()) { + return false; + } + + try { + // Cannot switch or change subclasses while transformed + if (_transformation != null) { + return false; + } + + // Remove active item skills before saving char to database + // because next time when choosing this class, weared items can + // be different + for (L2ItemInstance item : getInventory().getAugmentedItems()) { + if ((item != null) && item.isEquipped()) { + item.getAugmentation().removeBonus(this); + } + } + + // abort any kind of cast. + abortCast(); + + if (isChannelized()) { + getSkillChannelized().abortChannelization(); + } + + // 1. Call store() before modifying _classIndex to avoid skill effects rollover. + // 2. Register the correct _classId against applied 'classIndex'. + store(character().subclassStoreSkillCooltime()); + + resetTimeStamps(); + + // clear charges + _charges.set(0); + stopChargeTask(); + + if (hasServitor()) { + getSummon().unSummon(this); + } + + if (classIndex == 0) { + setClassTemplate(getBaseClass()); + } else { + try { + setClassTemplate(getSubClasses().get(classIndex).getClassId()); + } catch (Exception e) { + LOG.warn("Could not switch {} sub class to class index {}, {}", this, classIndex, e); + return false; + } + } + + _classIndex = classIndex; + + setLearningClass(getClassId()); + + if (isInParty()) { + getParty().recalculatePartyLevel(); + } + + // Update the character's change in class status. + // 1. Remove any active cubics from the player. + // 2. Renovate the characters table in the database with the new class info, storing also buff/effect data. + // 3. Remove all existing skills. + // 4. Restore all the learned skills for the current class from the database. + // 5. Restore effect/buff data for the new class. + // 6. Restore henna data for the class, applying the new stat modifiers while removing existing ones. + // 7. Reset HP/MP/CP stats and send Server->Client character status packet to reflect changes. + // 8. Restore shortcut data related to this class. + // 9. Resend a class change animation effect to broadcast to all nearby players. + for (Skill oldSkill : getAllSkills()) { + removeSkill(oldSkill, false, true); + } + + stopAllEffectsExceptThoseThatLastThroughDeath(); + stopAllEffectsNotStayOnSubclassChange(); + stopCubics(); + + DAOFactory.getInstance().getRecipeBookDAO().load(this, false); + + // Restore any Death Penalty Buff + restoreDeathPenaltyBuffLevel(); + + DAOFactory.getInstance().getSkillDAO().load(this); + + rewardSkills(); + regiveTemporarySkills(); + + // Prevents some issues when changing between subclasses that shares skills + resetDisabledSkills(); + + restoreEffects(); + + sendPacket(new EtcStatusUpdate(this)); + + // if player has quest 422: Repent Your Sins, remove it + QuestState st = getQuestState("Q00422_RepentYourSins"); + if (st != null) { + st.exitQuest(true); + } + + for (int i = 0; i < 3; i++) { + _henna[i] = null; + } + + DAOFactory.getInstance().getHennaDAO().load(this); + + // Calculate henna modifiers of this player. + recalcHennaStats(); + + sendPacket(new HennaInfo(this)); + + if (getCurrentHp() > getMaxHp()) { + setCurrentHp(getMaxHp()); + } + if (getCurrentMp() > getMaxMp()) { + setCurrentMp(getMaxMp()); + } + if (getCurrentCp() > getMaxCp()) { + setCurrentCp(getMaxCp()); + } + + refreshOverloaded(); + refreshExpertisePenalty(); + broadcastUserInfo(); + + // Clear resurrect xp calculation + setExpBeforeDeath(0); + + _shortCuts.restoreMe(); + sendPacket(new ShortCutInit(this)); + + broadcastPacket(new SocialAction(getObjectId(), SocialAction.LEVEL_UP)); + sendPacket(new SkillCoolTime(this)); + sendPacket(new ExStorageMaxCount(this)); + return true; + } finally { + _subclassLock.unlock(); + } + } + + public boolean isLocked() { + return _subclassLock.isLocked(); + } + + public void stopWarnUserTakeBreak() { + if (_taskWarnUserTakeBreak != null) { + _taskWarnUserTakeBreak.cancel(true); + _taskWarnUserTakeBreak = null; + } + } + + public void startWarnUserTakeBreak() { + if (_taskWarnUserTakeBreak == null) { + _taskWarnUserTakeBreak = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new WarnUserTakeBreakTask(this), 7200000, 7200000); + } + } + + public void stopRentPet() { + if (_taskRentPet != null) { + // if the rent of a wyvern expires while over a flying zone, tp to down before unmounting + if (checkLandingState() && (getMountType() == MountType.WYVERN)) { + teleToLocation(TeleportWhereType.TOWN); + } + + if (dismount()) // this should always be true now, since we teleported already + { + _taskRentPet.cancel(true); + _taskRentPet = null; + } + } + } + + public void startRentPet(int seconds) { + if (_taskRentPet == null) { + _taskRentPet = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new RentPetTask(this), seconds * 1000L, seconds * 1000L); + } + } + + public boolean isRentedPet() { + if (_taskRentPet != null) { + return true; + } + + return false; + } + + public void stopWaterTask() { + if (_taskWater != null) { + _taskWater.cancel(false); + _taskWater = null; + sendPacket(new SetupGauge(2, 0)); + } + } + + public void startWaterTask() { + if (!isDead() && (_taskWater == null)) { + int timeinwater = (int) calcStat(Stats.BREATH, 60000, this, null); + + sendPacket(new SetupGauge(2, timeinwater)); + _taskWater = ThreadPoolManager.getInstance().scheduleEffectAtFixedRate(new WaterTask(this), timeinwater, 1000); + } + } + + public boolean isInWater() { + if (_taskWater != null) { + return true; + } + + return false; + } + + public void checkWaterState() { + if (isInsideZone(ZoneId.WATER)) { + startWaterTask(); + } else { + stopWaterTask(); + } + } + + public void onPlayerEnter() { + startWarnUserTakeBreak(); + + getHuntingSystem().onPlayerLogin(); + + if (isGM()) { + if (isInvul()) { + sendMessage("Entering world in Invulnerable mode."); + } + if (isInvisible()) { + sendMessage("Entering world in Invisible mode."); + } + if (isSilenceMode()) { + sendMessage("Entering world in Silence mode."); + } + } + + revalidateZone(true); + + notifyFriends(); + if (!canOverrideCond(PcCondOverride.SKILL_CONDITIONS) && character().decreaseSkillOnDelevel()) { + checkPlayerSkills(); + } + + try { + for (L2ZoneType zone : ZoneManager.getInstance().getZones(this)) { + zone.onPlayerLoginInside(this); + } + } catch (Exception e) { + LOG.error("{}", e); + } + + EventDispatcher.getInstance().notifyEventAsync(new PlayerLogin(this), this); + } + + public long getLastAccess() { + return _lastAccess; + } + + public void setLastAccess(long lastAccess) { + _lastAccess = lastAccess; + } + + @Override + public void doRevive() { + super.doRevive(); + updateEffectIcons(); + sendPacket(new EtcStatusUpdate(this)); + _revivePet = false; + _reviveRequested = 0; + _revivePower = 0; + + if (isMounted()) { + startFeed(_mountNpcId); + } + if (isInParty() && getParty().isInDimensionalRift()) { + if (!DimensionalRiftManager.getInstance().checkIfInPeaceZone(getX(), getY(), getZ())) { + getParty().getDimensionalRift().memberResurrected(this); + } + } + if (getInstanceId() > 0) { + final Instance instance = InstanceManager.getInstance().getInstance(getInstanceId()); + if (instance != null) { + instance.cancelEjectDeadPlayer(this); + } + } + } + + @Override + public void setName(String value) { + super.setName(value); + if (general().cacheCharNames()) { + CharNameTable.getInstance().addName(this); + } + } + + @Override + public void doRevive(double revivePower) { + doRevive(); + restoreExp(revivePower); + } + + public void reviveRequest(L2PcInstance reviver, Skill skill, boolean Pet, int resPower, int resRecovery) { + if (isResurrectionBlocked()) { + return; + } + + if (_reviveRequested == 1) { + if (_revivePet == Pet) { + reviver.sendPacket(SystemMessageId.RES_HAS_ALREADY_BEEN_PROPOSED); // Resurrection is already been proposed. + } else { + if (Pet) { + reviver.sendPacket(SystemMessageId.CANNOT_RES_PET2); // A pet cannot be resurrected while it's owner is in the process of resurrecting. + } else { + reviver.sendPacket(SystemMessageId.MASTER_CANNOT_RES); // While a pet is attempting to resurrect, it cannot help in resurrecting its master. + } + } + return; + } + + if ((Pet && hasPet() && getSummon().isDead()) || (!Pet && isDead())) { + _reviveRequested = 1; + _reviveRecovery = resRecovery; + int restoreExp = 0; + + _revivePower = Formulas.calculateSkillResurrectRestorePercent(resPower, reviver); + restoreExp = (int) Math.round(((getExpBeforeDeath() - getExp()) * _revivePower) / 100); + _revivePet = Pet; + + if (hasCharmOfCourage()) { + ConfirmDlg dlg = new ConfirmDlg(SystemMessageId.RESURRECT_USING_CHARM_OF_COURAGE.getId()); + dlg.addTime(60000); + sendPacket(dlg); + return; + } + ConfirmDlg dlg = new ConfirmDlg(SystemMessageId.RESURRECTION_REQUEST_BY_C1_FOR_S2_XP.getId()); + dlg.addPcName(reviver); + dlg.addString(Integer.toString(Math.abs(restoreExp))); + sendPacket(dlg); + } + } + + public void reviveAnswer(int answer) { + if ((_reviveRequested != 1) || (!isDead() && !_revivePet) || (_revivePet && hasPet() && !getSummon().isDead())) { + return; + } + + if (answer == 1) { + if (!_revivePet) { + if (_revivePower != 0) { + doRevive(_revivePower); + } else { + doRevive(); + } + + if (_reviveRecovery != 0) { + setCurrentHpMp(getMaxHp() * (_reviveRecovery / 100.0), getMaxMp() * (_reviveRecovery / 100.0)); + setCurrentCp(0); + } + } else if (hasPet()) { + if (_revivePower != 0) { + getSummon().doRevive(_revivePower); + } else { + getSummon().doRevive(); + } + + if (_reviveRecovery != 0) { + getSummon().setCurrentHpMp(getSummon().getMaxHp() * (_reviveRecovery / 100.0), getSummon().getMaxMp() * (_reviveRecovery / 100.0)); + } + } + } + _revivePet = false; + _reviveRequested = 0; + _revivePower = 0; + _reviveRecovery = 0; + } + + public boolean isReviveRequested() { + return (_reviveRequested == 1); + } + + public boolean isRevivingPet() { + return _revivePet; + } + + public void removeReviving() { + _reviveRequested = 0; + _revivePower = 0; + } + + public void onActionRequest() { + if (isSpawnProtected()) { + sendPacket(SystemMessageId.YOU_ARE_NO_LONGER_PROTECTED_FROM_AGGRESSIVE_MONSTERS); + + if (character().restoreServitorOnReconnect() && !hasSummon() && CharSummonTable.getInstance().getServitors().containsKey(getObjectId())) { + CharSummonTable.getInstance().restoreServitor(this); + } + if (character().restorePetOnReconnect() && !hasSummon() && CharSummonTable.getInstance().getPets().containsKey(getObjectId())) { + CharSummonTable.getInstance().restorePet(this); + } + } + if (isTeleportProtected()) { + sendMessage("Teleport spawn protection ended."); + } + setProtection(false); + setTeleportProtection(false); + } + + /** + * Expertise of the L2PcInstance (None=0, D=1, C=2, B=3, A=4, S=5, S80=6, S84=7) + * @return int Expertise skill level. + */ + public int getExpertiseLevel() { + int level = getSkillLevel(239); + if (level < 0) { + level = 0; + } + return level; + } + + @Override + public void teleToLocation(ILocational loc, boolean allowRandomOffset) { + if ((getVehicle() != null) && !getVehicle().isTeleporting()) { + setVehicle(null); + } + + if (isFlyingMounted() && (loc.getZ() < -1005)) { + super.teleToLocation(loc.getX(), loc.getY(), -1005, loc.getHeading(), loc.getInstanceId()); + } + + super.teleToLocation(loc, allowRandomOffset); + } + + @Override + public void onTeleported() { + super.onTeleported(); + + if (isInAirShip()) { + getAirShip().sendInfo(this); + } + + // Force a revalidation + revalidateZone(true); + + checkItemRestriction(); + + if ((character().getPlayerTeleportProtection() > 0) && !isInOlympiadMode()) { + setTeleportProtection(true); + } + + if (hasTamedBeasts()) { + for (L2TamedBeastInstance tamedBeast : _tamedBeasts) { + tamedBeast.deleteMe(); + } + _tamedBeasts.clear(); + } + + // Modify the position of the pet if necessary + final L2Summon summon = getSummon(); + if (summon != null) { + summon.setFollowStatus(false); + summon.teleToLocation(getLocation(), false); + ((L2SummonAI) summon.getAI()).setStartFollowController(true); + summon.setFollowStatus(true); + summon.updateAndBroadcastStatus(0); + } + + TvTEvent.onTeleported(this); + } + + @Override + public void setIsTeleporting(boolean teleport) { + setIsTeleporting(teleport, true); + } + + public void setIsTeleporting(boolean teleport, boolean useWatchDog) { + super.setIsTeleporting(teleport); + if (!useWatchDog) { + return; + } + if (teleport) { + if ((_teleportWatchdog == null) && (character().getTeleportWatchdogTimeout() > 0)) { + synchronized (this) { + if (_teleportWatchdog == null) { + _teleportWatchdog = ThreadPoolManager.getInstance().scheduleGeneral(new TeleportWatchdogTask(this), character().getTeleportWatchdogTimeout()); + } + } + } + } else { + if (_teleportWatchdog != null) { + _teleportWatchdog.cancel(false); + _teleportWatchdog = null; + } + } + } + + public void setLastServerPosition(int x, int y, int z) { + _lastServerPosition.setXYZ(x, y, z); + } + + public Location getLastServerPosition() { + return _lastServerPosition; + } + + public int getLastServerDistance(int x, int y, int z) { + return (int) Util.calculateDistance(x, y, z, _lastServerPosition.getX(), _lastServerPosition.getY(), _lastServerPosition.getZ(), true, false); + } + + @Override + public void reduceCurrentHp(double value, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + getStatus().reduceHp(value, attacker, awake, isDOT, false, false); + + // notify the tamed beast of attacks + if (hasTamedBeasts()) { + for (L2TamedBeastInstance tamedBeast : _tamedBeasts) { + tamedBeast.onOwnerGotAttacked(attacker); + } + } + } + + public void broadcastSnoop(int type, String name, String _text) { + if (!_snoopListener.isEmpty()) { + Snoop sn = new Snoop(getObjectId(), getName(), type, name, _text); + + for (L2PcInstance pci : _snoopListener) { + if (pci != null) { + pci.sendPacket(sn); + } + } + } + } + + public void addSnooper(L2PcInstance pci) { + _snoopListener.add(pci); + } + + public void removeSnooper(L2PcInstance pci) { + _snoopListener.remove(pci); + } + + public void addSnooped(L2PcInstance pci) { + _snoopedPlayer.add(pci); + } + + public void removeSnooped(L2PcInstance pci) { + _snoopedPlayer.remove(pci); + } + + public void addHtmlAction(HtmlActionScope scope, String action) { + _htmlActionCaches[scope.ordinal()].add(action); + } + + public void clearHtmlActions(HtmlActionScope scope) { + _htmlActionCaches[scope.ordinal()].clear(); + } + + public void setHtmlActionOriginObjectId(HtmlActionScope scope, int npcObjId) { + if (npcObjId < 0) { + throw new IllegalArgumentException(); + } + + _htmlActionOriginObjectIds[scope.ordinal()] = npcObjId; + } + + public int getLastHtmlActionOriginId() { + return _lastHtmlActionOriginObjId; + } + + private boolean validateHtmlAction(Iterable actionIter, String action) { + for (String cachedAction : actionIter) { + if (cachedAction.charAt(cachedAction.length() - 1) == AbstractHtmlPacket.VAR_PARAM_START_CHAR) { + if (action.startsWith(cachedAction.substring(0, cachedAction.length() - 1).trim())) { + return true; + } + } else if (cachedAction.equals(action)) { + return true; + } + } + + return false; + } + + /** + * Check if the HTML action was sent in a HTML packet.
      + * If the HTML action was not sent for whatever reason, -1 is returned.
      + * Otherwise, the NPC object ID or 0 is returned.
      + * 0 means the HTML action was not bound to an NPC
      + * and no range checks need to be made. + * @param action the HTML action to check + * @return NPC object ID, 0 or -1 + */ + public int validateHtmlAction(String action) { + for (int i = 0; i < _htmlActionCaches.length; ++i) { + if ((_htmlActionCaches[i] != null) && validateHtmlAction(_htmlActionCaches[i], action)) { + _lastHtmlActionOriginObjId = _htmlActionOriginObjectIds[i]; + return _lastHtmlActionOriginObjId; + } + } + + return -1; + } + + /** + * Performs following tests: + *
        + *
      • Inventory contains item
      • + *
      • Item owner id == owner id
      • + *
      • It isn't pet control item while mounting pet or pet summoned
      • + *
      • It isn't active enchant item
      • + *
      • It isn't cursed weapon/item
      • + *
      • It isn't wear item
      • + *
      + * @param objectId item object id + * @param action just for login purpose + * @return + */ + public boolean validateItemManipulation(int objectId, String action) { + L2ItemInstance item = getInventory().getItemByObjectId(objectId); + + if ((item == null) || (item.getOwnerId() != getObjectId())) { + LOG.info("{} tried to {} item he is not owner of", this, action); + return false; + } + + // Pet is summoned and not the item that summoned the pet AND not the buggle from strider you're mounting + if ((hasSummon() && (getSummon().getControlObjectId() == objectId)) || (getMountObjectID() == objectId)) { + if (general().debug()) { + LOG.debug("{} tried to {} item controling pet", this, action); + } + + return false; + } + + if (getActiveEnchantItemId() == objectId) { + if (general().debug()) { + LOG.debug("{} tried to {} an enchant scroll he was using", this, action); + } + return false; + } + + if (CursedWeaponsManager.getInstance().isCursed(item.getId())) { + // can not trade a cursed weapon + return false; + } + + return true; + } + + /** + * @return Returns the inBoat. + */ + public boolean isInBoat() { + return (_vehicle != null) && _vehicle.isBoat(); + } + + /** + * @return + */ + public L2BoatInstance getBoat() { + return (L2BoatInstance) _vehicle; + } + + /** + * @return Returns the inAirShip. + */ + public boolean isInAirShip() { + return (_vehicle != null) && _vehicle.isAirShip(); + } + + /** + * @return + */ + public L2AirShipInstance getAirShip() { + return (L2AirShipInstance) _vehicle; + } + + public L2Vehicle getVehicle() { + return _vehicle; + } + + public void setVehicle(L2Vehicle v) { + if ((v == null) && (_vehicle != null)) { + _vehicle.removePassenger(this); + } + + _vehicle = v; + } + + public boolean isInVehicle() { + return _vehicle != null; + } + + public boolean isInCrystallize() { + return _inCrystallize; + } + + public void setInCrystallize(boolean inCrystallize) { + _inCrystallize = inCrystallize; + } + + /** + * @return + */ + public Location getInVehiclePosition() { + return _inVehiclePosition; + } + + public void setInVehiclePosition(Location pt) { + _inVehiclePosition = pt; + } + + /** + * Manage the delete task of a L2PcInstance (Leave Party, Unsummon pet, Save its inventory in the database, Remove it from the world...).
      + * Actions: + *
        + *
      • If the L2PcInstance is in observer mode, set its position to its position before entering in observer mode
      • + *
      • Set the online Flag to True or False and update the characters table of the database with online status and lastAccess
      • + *
      • Stop the HP/MP/CP Regeneration task
      • + *
      • Cancel Crafting, Attak or Cast
      • + *
      • Remove the L2PcInstance from the world
      • + *
      • Stop Party and Unsummon Pet
      • + *
      • Update database with items in its inventory and remove them from the world
      • + *
      • Remove all L2Object from _knownObjects and _knownPlayer of the L2Character then cancel Attak or Cast and notify AI
      • + *
      • Close the connection with the client
      • + *
      + */ + @Override + public boolean deleteMe() { + cleanup(); + storeMe(); + return super.deleteMe(); + } + + private synchronized void cleanup() { + EventDispatcher.getInstance().notifyEventAsync(new PlayerLogout(this), this); + + try { + for (L2ZoneType zone : ZoneManager.getInstance().getZones(this)) { + zone.onPlayerLogoutInside(this); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Set the online Flag to True or False and update the characters table of the database with online status and lastAccess (called when login and logout) + try { + if (!isOnline()) { + LOG.error("deleteMe() called on offline {}", this); + } + setOnlineStatus(false, true); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + if (general().enableBlockCheckerEvent() && (getBlockCheckerArena() != -1)) { + HandysBlockCheckerManager.getInstance().onDisconnect(this); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + _isOnline = false; + abortAttack(); + abortCast(); + stopMove(null); + setDebug(null); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // remove combat flag + try { + if (getInventory().getItemByItemId(9819) != null) { + Fort fort = FortManager.getInstance().getFort(this); + if (fort != null) { + FortSiegeManager.getInstance().dropCombatFlag(this, fort.getResidenceId()); + } else { + int slot = getInventory().getSlotFromItem(getInventory().getItemByItemId(9819)); + getInventory().unEquipItemInBodySlot(slot); + destroyItem("CombatFlag", getInventory().getItemByItemId(9819), null, true); + } + } else if (isCombatFlagEquipped()) { + TerritoryWarManager.getInstance().dropCombatFlag(this, false, false); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + PartyMatchWaitingList.getInstance().removePlayer(this); + if (_partyroom != 0) { + PartyMatchRoom room = PartyMatchRoomList.getInstance().getRoom(_partyroom); + if (room != null) { + room.deleteMember(this); + } + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + if (isFlying()) { + removeSkill(SkillData.getInstance().getSkill(4289, 1)); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Recommendations must be saved before task (timer) is canceled + try { + getRecSystem().store(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + // Stop the HP/MP/CP Regeneration task (scheduled tasks) + try { + stopAllTimers(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + setIsTeleporting(false); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Stop crafting, if in progress + try { + RecipeController.getInstance().requestMakeItemAbort(this); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Cancel Attak or Cast + try { + setTarget(null); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + if (isChannelized()) { + getSkillChannelized().abortChannelization(); + } + + // Hunting System + getHuntingSystem().onPlayerLogout(); + + // Stop all toggles. + getEffectList().stopAllToggles(); + + // Remove from world regions zones + final L2WorldRegion oldRegion = getWorldRegion(); + if (oldRegion != null) { + oldRegion.removeFromZones(this); + } + + // Remove the L2PcInstance from the world + try { + decayMe(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // If a Party is in progress, leave it (and festival party) + if (isInParty()) { + try { + leaveParty(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + } + + if (OlympiadManager.getInstance().isRegistered(this) || (getOlympiadGameId() != -1)) { + OlympiadManager.getInstance().removeDisconnectedCompetitor(this); + } + + // If the L2PcInstance has Pet, unsummon it + if (hasSummon()) { + try { + getSummon().setRestoreSummon(true); + + getSummon().unSummon(this); + // Dead pet wasn't unsummoned, broadcast npcinfo changes (pet will be without owner name - means owner offline) + if (hasSummon()) { + getSummon().broadcastNpcInfo(0); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } // returns pet to control item + } + + if (getClan() != null) { + // set the status for pledge member list to OFFLINE + try { + L2ClanMember clanMember = getClan().getClanMember(getObjectId()); + if (clanMember != null) { + clanMember.setPlayerInstance(null); + } + + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + } + + if (getActiveRequester() != null) { + // deals with sudden exit in the middle of transaction + setActiveRequester(null); + cancelActiveTrade(); + } + + // If the L2PcInstance is a GM, remove it from the GM List + if (isGM()) { + try { + AdminData.getInstance().deleteGm(this); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + } + + try { + // Check if the L2PcInstance is in observer mode to set its position to its position + // before entering in observer mode + if (inObserverMode()) { + setLocationInvisible(_lastLoc); + } + + if (getVehicle() != null) { + getVehicle().oustPlayer(this); + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // remove player from instance and set spawn location if any + try { + final int instanceId = getInstanceId(); + if ((instanceId != 0) && !general().restorePlayerInstance()) { + final Instance inst = InstanceManager.getInstance().getInstance(instanceId); + if (inst != null) { + inst.removePlayer(getObjectId()); + final Location loc = inst.getExitLoc(); + if (loc != null) { + final int x = loc.getX() + Rnd.get(-30, 30); + final int y = loc.getY() + Rnd.get(-30, 30); + setXYZInvisible(x, y, loc.getZ()); + if (hasSummon()) // dead pet + { + getSummon().teleToLocation(loc, true); + getSummon().setInstanceId(0); + } + } + } + } + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // TvT Event removal + try { + TvTEvent.onLogout(this); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Update database with items in its inventory and remove them from the world + try { + getInventory().deleteMe(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + // Update database with items in its warehouse and remove them from the world + try { + clearWarehouse(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + if (general().warehouseCache()) { + WarehouseCacheManager.getInstance().remCacheTask(this); + } + + try { + getFreight().deleteMe(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + try { + clearRefund(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + if (isCursedWeaponEquipped()) { + try { + CursedWeaponsManager.getInstance().getCursedWeapon(_cursedWeaponEquippedId).setPlayer(null); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + } + + // Remove all L2Object from _knownObjects and _knownPlayer of the L2Character then cancel Attak or Cast and notify AI + try { + getKnownList().removeAllKnownObjects(); + } catch (Exception e) { + LOG.error("deleteMe() {}", e); + } + + if (getClanId() > 0) { + getClan().broadcastToOtherOnlineMembers(new PledgeShowMemberListUpdate(this), this); + // ClanTable.getInstance().getClan(getClanId()).broadcastToOnlineMembers(new PledgeShowMemberListAdd(this)); + } + + for (L2PcInstance player : _snoopedPlayer) { + player.removeSnooper(this); + } + + for (L2PcInstance player : _snoopListener) { + player.removeSnooped(this); + } + + // Remove L2Object object from _allObjects of L2World + L2World.getInstance().removeObject(this); + L2World.getInstance().removeFromAllPlayers(this); + + try { + notifyFriends(); + getBlockList().playerLogout(); + } catch (Exception e) { + LOG.error("Exception on deleteMe() notifyFriends: {}", e); + } + } + + // startFishing() was stripped of any pre-fishing related checks, namely the fishing zone check. + // Also worthy of note is the fact the code to find the hook landing position was also striped. + // The stripped code was moved into fishing.java. + // In my opinion it makes more sense for it to be there since all other skill related checks were also there. + // Last but not least, moving the zone check there, fixed a bug where baits would always be consumed no matter if fishing actually took place. + // startFishing() now takes up 3 arguments, which are accurately described as being the hook landing coordinates. + public void startFishing(int _x, int _y, int _z) { + stopMove(null); + setIsImmobilized(true); + _fishing = true; + _fishx = _x; + _fishy = _y; + _fishz = _z; + // broadcastUserInfo(); + // Starts fishing + int lvl = getRandomFishLvl(); + int grade = getRandomFishGrade(); + int group = getRandomFishGroup(grade); + List fish = FishData.getInstance().getFish(lvl, group, grade); + if ((fish == null) || fish.isEmpty()) { + sendMessage("Error - Fish are not defined"); + endFishing(false); + return; + } + // Use a copy constructor else the fish data may be over-written below + _fish = fish.get(Rnd.get(fish.size())).clone(); + fish.clear(); + sendPacket(SystemMessageId.CAST_LINE_AND_START_FISHING); + if (!GameTimeController.getInstance().isNight() && _lure.isNightLure()) { + _fish.setFishGroup(-1); + } + // sendMessage("Hook x,y: " + _x + "," + _y + " - Water Z, Player Z:" + _z + ", " + getZ()); //debug line, uncomment to show coordinates used in fishing. + broadcastPacket(new ExFishingStart(this, _fish.getFishGroup(), _x, _y, _z, _lure.isNightLure())); + sendPacket(Music.SF_P_01.getPacket()); + startLookingForFishTask(); + } + + public void stopLookingForFishTask() { + if (_taskforfish != null) { + _taskforfish.cancel(false); + _taskforfish = null; + } + } + + public void startLookingForFishTask() { + if (!isDead() && (_taskforfish == null)) { + int checkDelay = 0; + boolean isNoob = false; + boolean isUpperGrade = false; + + if (_lure != null) { + int lureid = _lure.getId(); + isNoob = _fish.getFishGrade() == 0; + isUpperGrade = _fish.getFishGrade() == 2; + if ((lureid == 6519) || (lureid == 6522) || (lureid == 6525) || (lureid == 8505) || (lureid == 8508) || (lureid == 8511)) { + checkDelay = _fish.getGutsCheckTime() * 133; + } else if ((lureid == 6520) || (lureid == 6523) || (lureid == 6526) || ((lureid >= 8505) && (lureid <= 8513)) || ((lureid >= 7610) && (lureid <= 7613)) || ((lureid >= 7807) && (lureid <= 7809)) || ((lureid >= 8484) && (lureid <= 8486))) { + checkDelay = _fish.getGutsCheckTime() * 100; + } else if ((lureid == 6521) || (lureid == 6524) || (lureid == 6527) || (lureid == 8507) || (lureid == 8510) || (lureid == 8513)) { + checkDelay = _fish.getGutsCheckTime() * 66; + } + } + _taskforfish = ThreadPoolManager.getInstance().scheduleEffectAtFixedRate(new LookingForFishTask(this, _fish.getStartCombatTime(), _fish.getFishGuts(), _fish.getFishGroup(), isNoob, isUpperGrade), 10000, checkDelay); + } + } + + private int getRandomFishGrade() { + return switch (_lure.getId()) { + case 7807: // green for beginners + case 7808: // purple for beginners + case 7809: // yellow for beginners + case 8486: // prize-winning for beginners + yield 0; + case 8485: // prize-winning luminous + case 8506: // green luminous + case 8509: // purple luminous + case 8512: // yellow luminous + yield 2; + default: + yield 1; + }; + } + + private int getRandomFishGroup(int group) { + int check = Rnd.get(100); + int type = 1; + switch (group) { + case 0: // fish for novices + switch (_lure.getId()) { + case 7807: // green lure, preferred by fast-moving (nimble) fish (type 5) + if (check <= 54) { + type = 5; + } else if (check <= 77) { + type = 4; + } else { + type = 6; + } + break; + case 7808: // purple lure, preferred by fat fish (type 4) + if (check <= 54) { + type = 4; + } else if (check <= 77) { + type = 6; + } else { + type = 5; + } + break; + case 7809: // yellow lure, preferred by ugly fish (type 6) + if (check <= 54) { + type = 6; + } else if (check <= 77) { + type = 5; + } else { + type = 4; + } + break; + case 8486: // prize-winning fishing lure for beginners + if (check <= 33) { + type = 4; + } else if (check <= 66) { + type = 5; + } else { + type = 6; + } + break; + } + break; + case 1: // normal fish + switch (_lure.getId()) { + case 7610: + case 7611: + case 7612: + case 7613: + type = 3; + break; + case 6519: // all theese lures (green) are prefered by fast-moving (nimble) fish (type 1) + case 8505: + case 6520: + case 6521: + case 8507: + if (check <= 54) { + type = 1; + } else if (check <= 74) { + type = 0; + } else if (check <= 94) { + type = 2; + } else { + type = 3; + } + break; + case 6522: // all these lures (purple) are preferred by fat fish (type 0) + case 8508: + case 6523: + case 6524: + case 8510: + if (check <= 54) { + type = 0; + } else if (check <= 74) { + type = 1; + } else if (check <= 94) { + type = 2; + } else { + type = 3; + } + break; + case 6525: // all these lures (yellow) are preferred by ugly fish (type 2) + case 8511: + case 6526: + case 6527: + case 8513: + if (check <= 55) { + type = 2; + } else if (check <= 74) { + type = 1; + } else if (check <= 94) { + type = 0; + } else { + type = 3; + } + break; + case 8484: // prize-winning fishing lure + if (check <= 33) { + type = 0; + } else if (check <= 66) { + type = 1; + } else { + type = 2; + } + break; + } + break; + case 2: // upper grade fish, luminous lure + switch (_lure.getId()) { + case 8506: // green lure, preferred by fast-moving (nimble) fish (type 8) + if (check <= 54) { + type = 8; + } else if (check <= 77) { + type = 7; + } else { + type = 9; + } + break; + case 8509: // purple lure, preferred by fat fish (type 7) + if (check <= 54) { + type = 7; + } else if (check <= 77) { + type = 9; + } else { + type = 8; + } + break; + case 8512: // yellow lure, preferred by ugly fish (type 9) + if (check <= 54) { + type = 9; + } else if (check <= 77) { + type = 8; + } else { + type = 7; + } + break; + case 8485: // prize-winning fishing lure + if (check <= 33) { + type = 7; + } else if (check <= 66) { + type = 8; + } else { + type = 9; + } + break; + } + } + return type; + } + + private int getRandomFishLvl() { + int skilllvl = getSkillLevel(1315); + final BuffInfo info = getEffectList().getBuffInfoBySkillId(2274); + if (info != null) { + // TODO (Adry_85): Unhardcode + skilllvl = 0; + switch (info.getSkill().getLevel()) { + case 1 -> skilllvl = 2; + case 2 -> skilllvl = 5; + case 3 -> skilllvl = 8; + case 4 -> skilllvl = 11; + case 5 -> skilllvl = 14; + case 6 -> skilllvl = 17; + case 7 -> skilllvl = 20; + case 8 -> skilllvl = 23; + } + } + if (skilllvl <= 0) { + return 1; + } + int randomlvl; + int check = Rnd.get(100); + + if (check <= 50) { + randomlvl = skilllvl; + } else if (check <= 85) { + randomlvl = skilllvl - 1; + if (randomlvl <= 0) { + randomlvl = 1; + } + } else { + randomlvl = skilllvl + 1; + if (randomlvl > 27) { + randomlvl = 27; + } + } + + return randomlvl; + } + + public void startFishCombat(boolean isNoob, boolean isUpperGrade) { + _fishCombat = new L2Fishing(this, _fish, isNoob, isUpperGrade); + } + + public void endFishing(boolean win) { + _fishing = false; + _fishx = 0; + _fishy = 0; + _fishz = 0; + // broadcastUserInfo(); + if (_fishCombat == null) { + sendPacket(SystemMessageId.BAIT_LOST_FISH_GOT_AWAY); + } + _fishCombat = null; + _lure = null; + // Ends fishing + broadcastPacket(new ExFishingEnd(win, this)); + sendPacket(SystemMessageId.REEL_LINE_AND_STOP_FISHING); + setIsImmobilized(false); + stopLookingForFishTask(); + } + + public L2Fishing getFishCombat() { + return _fishCombat; + } + + public int getFishx() { + return _fishx; + } + + public int getFishy() { + return _fishy; + } + + public int getFishz() { + return _fishz; + } + + public L2ItemInstance getLure() { + return _lure; + } + + public void setLure(L2ItemInstance lure) { + _lure = lure; + } + + public int getInventoryLimit() { + int ivlim; + if (isGM()) { + ivlim = character().getMaximumSlotsForGMPlayer(); + } else if (getRace() == Race.DWARF) { + ivlim = character().getMaximumSlotsForDwarf(); + } else { + ivlim = character().getMaximumSlotsForNoDwarf(); + } + ivlim += (int) getStat().calcStat(Stats.INV_LIM, 0, null, null); + + return ivlim; + } + + public int getWareHouseLimit() { + int whlim; + if (getRace() == Race.DWARF) { + whlim = character().getMaximumWarehouseSlotsForDwarf(); + } else { + whlim = character().getMaximumWarehouseSlotsForNoDwarf(); + } + + whlim += (int) getStat().calcStat(Stats.WH_LIM, 0, null, null); + + return whlim; + } + + public int getPrivateSellStoreLimit() { + int pslim; + + if (getRace() == Race.DWARF) { + pslim = character().getMaxPvtStoreSellSlotsDwarf(); + } else { + pslim = character().getMaxPvtStoreSellSlotsOther(); + } + + pslim += (int) getStat().calcStat(Stats.P_SELL_LIM, 0, null, null); + + return pslim; + } + + public int getPrivateBuyStoreLimit() { + int pblim; + + if (getRace() == Race.DWARF) { + pblim = character().getMaxPvtStoreBuySlotsDwarf(); + } else { + pblim = character().getMaxPvtStoreBuySlotsOther(); + } + pblim += (int) getStat().calcStat(Stats.P_BUY_LIM, 0, null, null); + + return pblim; + } + + public int getDwarfRecipeLimit() { + int recdlim = character().getDwarfRecipeLimit(); + recdlim += (int) getStat().calcStat(Stats.REC_D_LIM, 0, null, null); + return recdlim; + } + + public int getCommonRecipeLimit() { + int recclim = character().getCommonRecipeLimit(); + recclim += (int) getStat().calcStat(Stats.REC_C_LIM, 0, null, null); + return recclim; + } + + public int getMountNpcId() { + return _mountNpcId; + } + + public int getMountLevel() { + return _mountLevel; + } + + public int getMountObjectID() { + return _mountObjectID; + } + + public void setMountObjectID(int newID) { + _mountObjectID = newID; + } + + /** + * @return the current skill in use or return null. + */ + public SkillUseHolder getCurrentSkill() { + return _currentSkill; + } + + /** + * Create a new SkillDat object and set the player _currentSkill. + * @param currentSkill + * @param ctrlPressed + * @param shiftPressed + */ + public void setCurrentSkill(Skill currentSkill, boolean ctrlPressed, boolean shiftPressed) { + if (currentSkill == null) { + _currentSkill = null; + return; + } + _currentSkill = new SkillUseHolder(currentSkill, ctrlPressed, shiftPressed); + } + + /** + * @return the current pet skill in use or return null. + */ + public SkillUseHolder getCurrentPetSkill() { + return _currentPetSkill; + } + + /** + * Create a new SkillDat object and set the player _currentPetSkill. + * @param currentSkill + * @param ctrlPressed + * @param shiftPressed + */ + public void setCurrentPetSkill(Skill currentSkill, boolean ctrlPressed, boolean shiftPressed) { + if (currentSkill == null) { + _currentPetSkill = null; + return; + } + _currentPetSkill = new SkillUseHolder(currentSkill, ctrlPressed, shiftPressed); + } + + public SkillUseHolder getQueuedSkill() { + return _queuedSkill; + } + + /** + * Create a new SkillDat object and queue it in the player _queuedSkill. + * @param queuedSkill + * @param ctrlPressed + * @param shiftPressed + */ + public void setQueuedSkill(Skill queuedSkill, boolean ctrlPressed, boolean shiftPressed) { + if (queuedSkill == null) { + _queuedSkill = null; + return; + } + _queuedSkill = new SkillUseHolder(queuedSkill, ctrlPressed, shiftPressed); + } + + /** + * @return {@code true} if player is jailed, {@code false} otherwise. + */ + public boolean isJailed() { + return PunishmentManager.getInstance().hasPunishment(getObjectId(), PunishmentAffect.CHARACTER, PunishmentType.JAIL) || PunishmentManager.getInstance().hasPunishment(getAccountName(), PunishmentAffect.ACCOUNT, PunishmentType.JAIL) + || PunishmentManager.getInstance().hasPunishment(getIPAddress(), PunishmentAffect.IP, PunishmentType.JAIL); + } + + /** + * @return {@code true} if player is chat banned, {@code false} otherwise. + */ + public boolean isChatBanned() { + return PunishmentManager.getInstance().hasPunishment(getObjectId(), PunishmentAffect.CHARACTER, PunishmentType.CHAT_BAN) || PunishmentManager.getInstance().hasPunishment(getAccountName(), PunishmentAffect.ACCOUNT, PunishmentType.CHAT_BAN) + || PunishmentManager.getInstance().hasPunishment(getIPAddress(), PunishmentAffect.IP, PunishmentType.CHAT_BAN); + } + + public void startFameTask(long delay, int fameFixRate) { + if ((getLevel() < 40) || (getClassId().level() < 2)) { + return; + } + if (_fameTask == null) { + _fameTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FameTask(this, fameFixRate), delay, delay); + } + } + + public void stopFameTask() { + if (_fameTask != null) { + _fameTask.cancel(false); + _fameTask = null; + } + } + + public void startVitalityTask() { + if (vitality().enabled() && (_vitalityTask == null)) { + _vitalityTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new VitalityTask(this), 1000, 60000); + } + } + + public void stopVitalityTask() { + if (_vitalityTask != null) { + _vitalityTask.cancel(false); + _vitalityTask = null; + } + } + + public int getPowerGrade() { + return _powerGrade; + } + + public void setPowerGrade(int power) { + _powerGrade = power; + } + + public boolean isCursedWeaponEquipped() { + return _cursedWeaponEquippedId != 0; + } + + public int getCursedWeaponEquippedId() { + return _cursedWeaponEquippedId; + } + + public void setCursedWeaponEquippedId(int value) { + _cursedWeaponEquippedId = value; + } + + public boolean isCombatFlagEquipped() { + return _combatFlagEquippedId; + } + + public void setCombatFlagEquipped(boolean value) { + _combatFlagEquippedId = value; + } + + /** + * Returns the Number of Souls this L2PcInstance got. + * @return + */ + public int getChargedSouls() { + return _souls; + } + + /** + * Increase Souls + * @param count + */ + public void increaseSouls(int count) { + _souls += count; + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOUR_SOUL_HAS_INCREASED_BY_S1_SO_IT_IS_NOW_AT_S2); + sm.addInt(count); + sm.addInt(_souls); + sendPacket(sm); + restartSoulTask(); + sendPacket(new EtcStatusUpdate(this)); + } + + /** + * Decreases existing Souls. + * @param count + * @return consumed souls count + */ + public int decreaseSouls(int count) { + if (_souls == 0) { + return 0; + } + + int consumedSouls; + if (_souls <= count) { + consumedSouls = _souls; + _souls = 0; + stopSoulTask(); + } else { + _souls -= count; + consumedSouls = count; + restartSoulTask(); + } + + sendPacket(new EtcStatusUpdate(this)); + return consumedSouls; + } + + /** + * Clear out all Souls from this L2PcInstance + */ + public void clearSouls() { + _souls = 0; + stopSoulTask(); + sendPacket(new EtcStatusUpdate(this)); + } + + /** + * Starts/Restarts the SoulTask to Clear Souls after 10 Mins. + */ + private void restartSoulTask() { + if (_soulTask != null) { + _soulTask.cancel(false); + _soulTask = null; + } + _soulTask = ThreadPoolManager.getInstance().scheduleGeneral(new ResetSoulsTask(this), 600000); + + } + + /** + * Stops the Clearing Task. + */ + public void stopSoulTask() { + if (_soulTask != null) { + _soulTask.cancel(false); + _soulTask = null; + } + } + + public int getDeathPenaltyBuffLevel() { + return _deathPenaltyBuffLevel; + } + + public void setDeathPenaltyBuffLevel(int level) { + _deathPenaltyBuffLevel = level; + } + + public void calculateDeathPenaltyBuffLevel(L2Character killer) { + if (killer == null) { + LOG.warn("{} called calculateDeathPenaltyBuffLevel with killer null!", this); + return; + } + + if (isResurrectSpecialAffected() || isLucky() || isBlockedFromDeathPenalty() || isInsideZone(ZoneId.PVP) || isInsideZone(ZoneId.SIEGE) || canOverrideCond(PcCondOverride.DEATH_PENALTY)) { + return; + } + double percent = 1.0; + + if (killer.isRaid()) { + percent *= calcStat(Stats.REDUCE_DEATH_PENALTY_BY_RAID, 1); + } else if (killer.isMonster()) { + percent *= calcStat(Stats.REDUCE_DEATH_PENALTY_BY_MOB, 1); + } else if (killer.isPlayable()) { + percent *= calcStat(Stats.REDUCE_DEATH_PENALTY_BY_PVP, 1); + } + + if (Rnd.get(1, 100) <= (character().getDeathPenaltyChance() * percent)) { + if (!killer.isPlayable() || (getKarma() > 0)) { + increaseDeathPenaltyBuffLevel(); + } + } + } + + public void increaseDeathPenaltyBuffLevel() { + if (getDeathPenaltyBuffLevel() >= 15) { + return; + } + + if (getDeathPenaltyBuffLevel() != 0) { + Skill skill = SkillData.getInstance().getSkill(5076, getDeathPenaltyBuffLevel()); + + if (skill != null) { + removeSkill(skill, true); + } + } + _deathPenaltyBuffLevel++; + addSkill(SkillData.getInstance().getSkill(5076, getDeathPenaltyBuffLevel()), false); + sendPacket(new EtcStatusUpdate(this)); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.DEATH_PENALTY_LEVEL_S1_ADDED); + sm.addInt(getDeathPenaltyBuffLevel()); + sendPacket(sm); + } + + public void reduceDeathPenaltyBuffLevel() { + if (getDeathPenaltyBuffLevel() <= 0) { + return; + } + + Skill skill = SkillData.getInstance().getSkill(5076, getDeathPenaltyBuffLevel()); + + if (skill != null) { + removeSkill(skill, true); + } + + _deathPenaltyBuffLevel--; + + if (getDeathPenaltyBuffLevel() > 0) { + addSkill(SkillData.getInstance().getSkill(5076, getDeathPenaltyBuffLevel()), false); + sendPacket(new EtcStatusUpdate(this)); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.DEATH_PENALTY_LEVEL_S1_ADDED); + sm.addInt(getDeathPenaltyBuffLevel()); + sendPacket(sm); + } else { + sendPacket(new EtcStatusUpdate(this)); + sendPacket(SystemMessageId.DEATH_PENALTY_LIFTED); + } + } + + public void restoreDeathPenaltyBuffLevel() { + if (getDeathPenaltyBuffLevel() > 0) { + addSkill(SkillData.getInstance().getSkill(5076, getDeathPenaltyBuffLevel()), false); + } + } + + @Override + public L2PcInstance getActingPlayer() { + return this; + } + + @Override + public int getMaxLoad() { + return (int) calcStat(Stats.WEIGHT_LIMIT, Math.floor(BaseStats.CON.calcBonus(this) * 69000 * character().getWeightLimit()), this, null); + } + + @Override + public int getBonusWeightPenalty() { + return (int) calcStat(Stats.WEIGHT_PENALTY, 1, this, null); + } + + @Override + public int getCurrentLoad() { + return getInventory().getTotalWeight(); + } + + @Override + public void sendDamageMessage(L2Character target, int damage, boolean mcrit, boolean pcrit, boolean miss) { + // Check if hit is missed + if (miss) { + if (target.isPlayer()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_EVADED_C2_ATTACK); + sm.addPcName(target.getActingPlayer()); + sm.addCharName(this); + target.sendPacket(sm); + } + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_ATTACK_WENT_ASTRAY); + sm.addPcName(this); + sendPacket(sm); + return; + } + + // Check if hit is critical + if (pcrit) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_HAD_CRITICAL_HIT); + sm.addPcName(this); + sendPacket(sm); + } + if (mcrit) { + sendPacket(SystemMessageId.CRITICAL_HIT_MAGIC); + } + + if (isInOlympiadMode() && target.isPlayer() && target.getActingPlayer().isInOlympiadMode() && (target.getActingPlayer().getOlympiadGameId() == getOlympiadGameId())) { + OlympiadGameManager.getInstance().notifyCompetitorDamage(this, damage); + } + + final SystemMessage sm; + + if ((target.isInvul() || target.isHpBlocked()) && !target.isNpc()) { + sm = SystemMessage.getSystemMessage(SystemMessageId.ATTACK_WAS_BLOCKED); + } else if (target.isDoor() || (target instanceof L2ControlTowerInstance)) { + sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_DID_S1_DMG); + sm.addInt(damage); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DONE_S3_DAMAGE_TO_C2); + sm.addPcName(this); + sm.addCharName(target); + sm.addInt(damage); + } + sendPacket(sm); + } + + public int getAgathionId() { + return _agathionId; + } + + public void setAgathionId(int npcId) { + _agathionId = npcId; + } + + public int getVitalityPoints() { + return getStat().getVitalityPoints(); + } + + public int getVitalityLevel() { + return getStat().getVitalityLevel(); + } + + public void setVitalityPoints(int points, boolean quiet) { + getStat().setVitalityPoints(points, quiet); + } + + public void updateVitalityPoints(float points, boolean useRates, boolean quiet) { + getStat().updateVitalityPoints(points, useRates, quiet); + } + + public void checkItemRestriction() { + for (int i = 0; i < Inventory.PAPERDOLL_TOTALSLOTS; i++) { + L2ItemInstance equippedItem = getInventory().getPaperdollItem(i); + if ((equippedItem != null) && !equippedItem.getItem().checkCondition(this, this, false)) { + getInventory().unEquipItemInSlot(i); + + InventoryUpdate iu = new InventoryUpdate(); + iu.addModifiedItem(equippedItem); + sendPacket(iu); + + SystemMessage sm = null; + if (equippedItem.getItem().getBodyPart() == L2Item.SLOT_BACK) { + sendPacket(SystemMessageId.CLOAK_REMOVED_BECAUSE_ARMOR_SET_REMOVED); + return; + } + + if (equippedItem.getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.EQUIPMENT_S1_S2_REMOVED); + sm.addInt(equippedItem.getEnchantLevel()); + sm.addItemName(equippedItem); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISARMED); + sm.addItemName(equippedItem); + } + sendPacket(sm); + } + } + } + + public void addTransformSkill(Skill sk) { + if (_transformSkills == null) { + synchronized (this) { + if (_transformSkills == null) { + _transformSkills = new ConcurrentHashMap<>(); + } + } + } + _transformSkills.put(sk.getId(), sk); + if (sk.isPassive()) { + addSkill(sk, false); + } + } + + public Skill getTransformSkill(int id) { + if (_transformSkills == null) { + return null; + } + return _transformSkills.get(id); + } + + public boolean hasTransformSkill(int id) { + if (_transformSkills == null) { + return false; + } + return _transformSkills.containsKey(id); + } + + public void removeAllTransformSkills() { + _transformSkills = null; + } + + protected void startFeed(int npcId) { + _canFeed = npcId > 0; + if (!isMounted()) { + return; + } + if (hasSummon()) { + setCurrentFeed(((L2PetInstance) getSummon()).getCurrentFed()); + _controlItemId = getSummon().getControlObjectId(); + sendPacket(new SetupGauge(3, (getCurrentFeed() * 10000) / getFeedConsume(), (getMaxFeed() * 10000) / getFeedConsume())); + if (!isDead()) { + _mountFeedTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new PetFeedTask(this), 10000, 10000); + } + } else if (_canFeed) { + setCurrentFeed(getMaxFeed()); + SetupGauge sg = new SetupGauge(3, (getCurrentFeed() * 10000) / getFeedConsume(), (getMaxFeed() * 10000) / getFeedConsume()); + sendPacket(sg); + if (!isDead()) { + _mountFeedTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new PetFeedTask(this), 10000, 10000); + } + } + } + + public void stopFeed() { + if (_mountFeedTask != null) { + _mountFeedTask.cancel(false); + _mountFeedTask = null; + } + } + + private L2PetLevelData getPetLevelData(int npcId) { + if (_leveldata == null) { + _leveldata = PetDataTable.getInstance().getPetData(npcId).getPetLevelData(getMountLevel()); + } + return _leveldata; + } + + public int getCurrentFeed() { + return _curFeed; + } + + public void setCurrentFeed(int num) { + boolean lastHungryState = isHungry(); + _curFeed = num > getMaxFeed() ? getMaxFeed() : num; + SetupGauge sg = new SetupGauge(3, (getCurrentFeed() * 10000) / getFeedConsume(), (getMaxFeed() * 10000) / getFeedConsume()); + sendPacket(sg); + // broadcast move speed change when strider becomes hungry / full + if (lastHungryState != isHungry()) { + broadcastUserInfo(); + } + } + + public int getFeedConsume() { + // if pet is attacking + if (isAttackingNow()) { + return getPetLevelData(_mountNpcId).getPetFeedBattle(); + } + return getPetLevelData(_mountNpcId).getPetFeedNormal(); + } + + private int getMaxFeed() { + return getPetLevelData(_mountNpcId).getPetMaxFeed(); + } + + public boolean isHungry() { + return _canFeed ? (getCurrentFeed() < ((PetDataTable.getInstance().getPetData(getMountNpcId()).getHungryLimit() / 100f) * getPetLevelData(getMountNpcId()).getPetMaxFeed())) : false; + } + + public void enteredNoLanding(int delay) { + _dismountTask = ThreadPoolManager.getInstance().scheduleGeneral(new DismountTask(this), delay * 1000); + } + + public void exitedNoLanding() { + if (_dismountTask != null) { + _dismountTask.cancel(true); + _dismountTask = null; + } + } + + public void setIsInSiege(boolean b) { + _isInSiege = b; + } + + public boolean isInSiege() { + return _isInSiege; + } + + /** + * @param isInHideoutSiege sets the value of {@link #_isInHideoutSiege}. + */ + public void setIsInHideoutSiege(boolean isInHideoutSiege) { + _isInHideoutSiege = isInHideoutSiege; + } + + /** + * @return the value of {@link #_isInHideoutSiege}, {@code true} if the player is participing on a Hideout Siege, otherwise {@code false}. + */ + public boolean isInHideoutSiege() { + return _isInHideoutSiege; + } + + public FloodProtectors getFloodProtectors() { + return getClient().getFloodProtectors(); + } + + public boolean isFlyingMounted() { + return (isTransformed() && (getTransformation().isFlying())); + } + + /** + * Returns the Number of Charges this L2PcInstance got. + * @return + */ + public int getCharges() { + return _charges.get(); + } + + public void increaseCharges(int count, int max) { + if (_charges.get() >= max) { + sendPacket(SystemMessageId.FORCE_MAXLEVEL_REACHED); + return; + } + + // Charge clear task should be reset every time a charge is increased. + restartChargeTask(); + + if (_charges.addAndGet(count) >= max) { + _charges.set(max); + sendPacket(SystemMessageId.FORCE_MAXLEVEL_REACHED); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.FORCE_INCREASED_TO_S1); + sm.addInt(_charges.get()); + sendPacket(sm); + } + + sendPacket(new EtcStatusUpdate(this)); + } + + public boolean decreaseCharges(int count) { + if (_charges.get() < count) { + return false; + } + + // Charge clear task should be reset every time a charge is decreased and stopped when charges become 0. + if (_charges.addAndGet(-count) == 0) { + stopChargeTask(); + } else { + restartChargeTask(); + } + + sendPacket(new EtcStatusUpdate(this)); + return true; + } + + public void clearCharges() { + _charges.set(0); + sendPacket(new EtcStatusUpdate(this)); + } + + /** + * Starts/Restarts the ChargeTask to Clear Charges after 10 Mins. + */ + private void restartChargeTask() { + if (_chargeTask != null) { + synchronized (this) { + if (_chargeTask != null) { + _chargeTask.cancel(false); + } + } + } + _chargeTask = ThreadPoolManager.getInstance().scheduleGeneral(new ResetChargesTask(this), 600000); + } + + /** + * Stops the Charges Clearing Task. + */ + public void stopChargeTask() { + if (_chargeTask != null) { + _chargeTask.cancel(false); + _chargeTask = null; + } + } + + public void teleportBookmarkModify(int id, int icon, String tag, String name) { + final TeleportBookmark bookmark = _tpbookmarks.get(id); + if (bookmark != null) { + bookmark.setIcon(icon); + bookmark.setTag(tag); + bookmark.setName(name); + + DAOFactory.getInstance().getTeleportBookmarkDAO().update(this, id, icon, tag, name); + } + + sendPacket(new ExGetBookMarkInfoPacket(this)); + } + + public void teleportBookmarkDelete(int id) { + if (_tpbookmarks.remove(id) != null) { + sendPacket(new ExGetBookMarkInfoPacket(this)); + + DAOFactory.getInstance().getTeleportBookmarkDAO().delete(this, id); + } + } + + public void teleportBookmarkGo(int id) { + if (!teleportBookmarkCondition(0)) { + return; + } + if (getInventory().getInventoryItemCount(13016, 0) == 0) { + sendPacket(SystemMessageId.YOU_CANNOT_TELEPORT_BECAUSE_YOU_DO_NOT_HAVE_A_TELEPORT_ITEM); + return; + } + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(13016); + sendPacket(sm); + + final TeleportBookmark bookmark = _tpbookmarks.get(id); + if (bookmark != null) { + destroyItem("Consume", getInventory().getItemByItemId(13016).getObjectId(), 1, null, false); + teleToLocation(bookmark, false); + } + sendPacket(new ExGetBookMarkInfoPacket(this)); + } + + public boolean teleportBookmarkCondition(int type) { + if (isInCombat()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_DURING_A_BATTLE); + return false; + } else if (isInSiege() || (getSiegeState() != 0)) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_WHILE_PARTICIPATING); + return false; + } else if (isInDuel()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_DURING_A_DUEL); + return false; + } else if (isFlying()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_WHILE_FLYING); + return false; + } else if (isInOlympiadMode()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_WHILE_PARTICIPATING_IN_AN_OLYMPIAD_MATCH); + return false; + } else if (isStunned()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_WHILE_YOU_ARE_PARALYZED); + return false; + } else if (isDead()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_WHILE_YOU_ARE_DEAD); + return false; + } else if ((type == 1) && (isIn7sDungeon() || (isInParty() && getParty().isInDimensionalRift()))) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_TO_REACH_THIS_AREA); + return false; + } else if (isInWater()) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_UNDERWATER); + return false; + } else if ((type == 1) && (isInsideZone(ZoneId.SIEGE) || isInsideZone(ZoneId.CLAN_HALL) || isInsideZone(ZoneId.JAIL) || isInsideZone(ZoneId.CASTLE) || isInsideZone(ZoneId.NO_SUMMON_FRIEND) || isInsideZone(ZoneId.FORT))) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_TO_REACH_THIS_AREA); + return false; + } else if (isInsideZone(ZoneId.NO_BOOKMARK) || isInBoat() || isInAirShip()) { + if (type == 0) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_IN_THIS_AREA); + } else if (type == 1) { + sendPacket(SystemMessageId.YOU_CANNOT_USE_MY_TELEPORTS_TO_REACH_THIS_AREA); + } + return false; + } + /* + * TODO: Instant Zone still not implemented else if (isInsideZone(ZoneId.INSTANT)) { sendPacket(SystemMessage.getSystemMessage(2357)); return; } + */ + else { + return true; + } + } + + public void teleportBookmarkAdd(int x, int y, int z, int icon, String tag, String name) { + if (!teleportBookmarkCondition(1)) { + return; + } + + if (_tpbookmarks.size() >= _bookmarkslot) { + sendPacket(SystemMessageId.YOU_HAVE_NO_SPACE_TO_SAVE_THE_TELEPORT_LOCATION); + return; + } + + if (getInventory().getInventoryItemCount(20033, 0) == 0) { + sendPacket(SystemMessageId.YOU_CANNOT_BOOKMARK_THIS_LOCATION_BECAUSE_YOU_DO_NOT_HAVE_A_MY_TELEPORT_FLAG); + return; + } + + int id; + for (id = 1; id <= _bookmarkslot; ++id) { + if (!_tpbookmarks.containsKey(id)) { + break; + } + } + _tpbookmarks.put(id, new TeleportBookmark(id, x, y, z, icon, tag, name)); + + destroyItem("Consume", getInventory().getItemByItemId(20033).getObjectId(), 1, null, false); + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(20033); + sendPacket(sm); + + sendPacket(new ExGetBookMarkInfoPacket(this)); + + DAOFactory.getInstance().getTeleportBookmarkDAO().insert(this, id, x, y, z, icon, tag, name); + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + if (isInBoat()) { + setXYZ(getBoat().getLocation()); + + activeChar.sendPacket(new CharInfo(this)); + activeChar.sendPacket(new ExBrExtraUserInfo(this)); + int relation1 = getRelation(activeChar); + int relation2 = activeChar.getRelation(this); + Integer oldrelation = getKnownList().getKnownRelations().get(activeChar.getObjectId()); + if ((oldrelation != null) && (oldrelation != relation1)) { + activeChar.sendPacket(new RelationChanged(this, relation1, isAutoAttackable(activeChar))); + if (hasSummon()) { + activeChar.sendPacket(new RelationChanged(getSummon(), relation1, isAutoAttackable(activeChar))); + } + } + oldrelation = activeChar.getKnownList().getKnownRelations().get(getObjectId()); + if ((oldrelation != null) && (oldrelation != relation2)) { + sendPacket(new RelationChanged(activeChar, relation2, activeChar.isAutoAttackable(this))); + if (activeChar.hasSummon()) { + sendPacket(new RelationChanged(activeChar.getSummon(), relation2, activeChar.isAutoAttackable(this))); + } + } + activeChar.sendPacket(new GetOnVehicle(getObjectId(), getBoat().getObjectId(), getInVehiclePosition())); + } else if (isInAirShip()) { + setXYZ(getAirShip().getLocation()); + activeChar.sendPacket(new CharInfo(this)); + activeChar.sendPacket(new ExBrExtraUserInfo(this)); + int relation1 = getRelation(activeChar); + int relation2 = activeChar.getRelation(this); + Integer oldrelation = getKnownList().getKnownRelations().get(activeChar.getObjectId()); + if ((oldrelation != null) && (oldrelation != relation1)) { + activeChar.sendPacket(new RelationChanged(this, relation1, isAutoAttackable(activeChar))); + if (hasSummon()) { + activeChar.sendPacket(new RelationChanged(getSummon(), relation1, isAutoAttackable(activeChar))); + } + } + oldrelation = activeChar.getKnownList().getKnownRelations().get(getObjectId()); + if ((oldrelation != null) && (oldrelation != relation2)) { + sendPacket(new RelationChanged(activeChar, relation2, activeChar.isAutoAttackable(this))); + if (activeChar.hasSummon()) { + sendPacket(new RelationChanged(activeChar.getSummon(), relation2, activeChar.isAutoAttackable(this))); + } + } + activeChar.sendPacket(new ExGetOnAirShip(this, getAirShip())); + } else { + activeChar.sendPacket(new CharInfo(this)); + activeChar.sendPacket(new ExBrExtraUserInfo(this)); + int relation1 = getRelation(activeChar); + int relation2 = activeChar.getRelation(this); + Integer oldrelation = getKnownList().getKnownRelations().get(activeChar.getObjectId()); + if ((oldrelation != null) && (oldrelation != relation1)) { + activeChar.sendPacket(new RelationChanged(this, relation1, isAutoAttackable(activeChar))); + if (hasSummon()) { + activeChar.sendPacket(new RelationChanged(getSummon(), relation1, isAutoAttackable(activeChar))); + } + } + oldrelation = activeChar.getKnownList().getKnownRelations().get(getObjectId()); + if ((oldrelation != null) && (oldrelation != relation2)) { + sendPacket(new RelationChanged(activeChar, relation2, activeChar.isAutoAttackable(this))); + if (activeChar.hasSummon()) { + sendPacket(new RelationChanged(activeChar.getSummon(), relation2, activeChar.isAutoAttackable(this))); + } + } + } + + switch (getPrivateStoreType()) { + case SELL -> activeChar.sendPacket(new PrivateStoreMsgSell(this)); + case PACKAGE_SELL -> activeChar.sendPacket(new ExPrivateStoreSetWholeMsg(this)); + case BUY -> activeChar.sendPacket(new PrivateStoreMsgBuy(this)); + case MANUFACTURE -> activeChar.sendPacket(new RecipeShopMsg(this)); + } + if (isTransformed()) { + // Required double send for fix Mounted H5+ + sendPacket(new CharInfo(activeChar)); + } + } + + public void showQuestMovie(int id) { + if (_movieId > 0) { + return; + } + abortAttack(); + abortCast(); + stopMove(null); + _movieId = id; + sendPacket(new ExStartScenePlayer(id)); + } + + public boolean isAllowedToEnchantSkills() { + if (isLocked()) { + return false; + } + if (isTransformed() || isInStance()) { + return false; + } + if (AttackStanceTaskManager.getInstance().hasAttackStanceTask(this)) { + return false; + } + if (isCastingNow() || isCastingSimultaneouslyNow()) { + return false; + } + if (isInBoat() || isInAirShip()) { + return false; + } + return true; + } + + /** + * @return the _createDate of the L2PcInstance. + */ + public Calendar getCreateDate() { + return _createDate; + } + + /** + * Set the _createDate of the L2PcInstance. + * @param createDate + */ + public void setCreateDate(Calendar createDate) { + _createDate = createDate; + } + + /** + * @return number of days to char birthday. + */ + public int checkBirthDay() { + Calendar now = Calendar.getInstance(); + + // "Characters with a February 29 creation date will receive a gift on February 28." + if ((_createDate.get(Calendar.DAY_OF_MONTH) == 29) && (_createDate.get(Calendar.MONTH) == 1)) { + _createDate.add(Calendar.HOUR_OF_DAY, -24); + } + + if ((now.get(Calendar.MONTH) == _createDate.get(Calendar.MONTH)) && (now.get(Calendar.DAY_OF_MONTH) == _createDate.get(Calendar.DAY_OF_MONTH)) && (now.get(Calendar.YEAR) != _createDate.get(Calendar.YEAR))) { + return 0; + } + + int i; + for (i = 1; i < 6; i++) { + now.add(Calendar.HOUR_OF_DAY, 24); + if ((now.get(Calendar.MONTH) == _createDate.get(Calendar.MONTH)) && (now.get(Calendar.DAY_OF_MONTH) == _createDate.get(Calendar.DAY_OF_MONTH)) && (now.get(Calendar.YEAR) != _createDate.get(Calendar.YEAR))) { + return i; + } + } + return -1; + } + + public Set getFriends() { + if (_friends == null) { + synchronized (this) { + if (_friends == null) { + _friends = ConcurrentHashMap.newKeySet(1); + } + } + } + return _friends; + } + + public boolean hasFriends() { + return (_friends != null) && !_friends.isEmpty(); + } + + public boolean isFriend(int objectId) { + return hasFriends() && _friends.contains(objectId); + } + + public void addFriend(int objectId) { + getFriends().add(objectId); + } + + public void removeFriend(int objectId) { + if (hasFriends()) { + _friends.remove(objectId); + } + } + + private void notifyFriends() { + if (hasFriends()) { + final FriendStatusPacket pkt = new FriendStatusPacket(getObjectId()); + for (int id : _friends) { + final L2PcInstance friend = L2World.getInstance().getPlayer(id); + if (friend != null) { + friend.sendPacket(pkt); + } + } + } + } + + /** + * Verify if this player is in silence mode. + * @return the {@code true} if this player is in silence mode, {@code false} otherwise + */ + public boolean isSilenceMode() { + return _silenceMode; + } + + /** + * Set the silence mode. + * @param mode the value + */ + public void setSilenceMode(boolean mode) { + _silenceMode = mode; + if (_silenceModeExcluded != null) { + _silenceModeExcluded.clear(); // Clear the excluded list on each setSilenceMode + } + sendPacket(new EtcStatusUpdate(this)); + } + + /** + * While at silenceMode, checks if this player blocks PMs for this user + * @param playerObjId the player object Id + * @return {@code true} if the given Id is not excluded and this player is in silence mode, {@code false} otherwise + */ + public boolean isSilenceMode(int playerObjId) { + if (character().silenceModeExclude() && _silenceMode && (_silenceModeExcluded != null)) { + return !_silenceModeExcluded.contains(playerObjId); + } + return _silenceMode; + } + + /** + * Add a player to the "excluded silence mode" list. + * @param playerObjId the player's object Id + */ + public void addSilenceModeExcluded(int playerObjId) { + if (_silenceModeExcluded == null) { + _silenceModeExcluded = new ArrayList<>(1); + } + _silenceModeExcluded.add(playerObjId); + } + + public double getCollisionRadius() { + if (isMounted() && (getMountNpcId() > 0)) { + return NpcData.getInstance().getTemplate(getMountNpcId()).getfCollisionRadius(); + } else if (isTransformed()) { + return getTransformation().getCollisionRadius(this); + } + return getAppearance().getSex() ? getBaseTemplate().getFCollisionRadiusFemale() : getBaseTemplate().getfCollisionRadius(); + } + + public double getCollisionHeight() { + if (isMounted() && (getMountNpcId() > 0)) { + return NpcData.getInstance().getTemplate(getMountNpcId()).getfCollisionHeight(); + } else if (isTransformed()) { + return getTransformation().getCollisionHeight(this); + } + return getAppearance().getSex() ? getBaseTemplate().getFCollisionHeightFemale() : getBaseTemplate().getfCollisionHeight(); + } + + public int getClientX() { + return _clientX; + } + + public void setClientX(int val) { + _clientX = val; + } + + public int getClientY() { + return _clientY; + } + + public void setClientY(int val) { + _clientY = val; + } + + public int getClientZ() { + return _clientZ; + } + + public void setClientZ(int val) { + _clientZ = val; + } + + public int getClientHeading() { + return _clientHeading; + } + + public void setClientHeading(int val) { + _clientHeading = val; + } + + /** + * @param z + * @return true if character falling now on the start of fall return false for correct coord sync! + */ + public boolean isFalling(int z) { + if (isDead() || isFlying() || isFlyingMounted() || isInsideZone(ZoneId.WATER)) { + return false; + } + + if (System.currentTimeMillis() < _fallingTimestamp) { + return true; + } + + final int deltaZ = getZ() - z; + if (deltaZ <= getBaseTemplate().getSafeFallHeight()) { + return false; + } + + // If there is no geodata loaded for the place we are client Z correction might cause falling damage. + if (!GeoData.getInstance().hasGeo(getX(), getY())) { + return false; + } + + final int damage = (int) Formulas.calcFallDam(this, deltaZ); + if (damage > 0) { + reduceCurrentHp(Math.min(damage, getCurrentHp() - 1), null, false, true, null); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.FALL_DAMAGE_S1); + sm.addInt(damage); + sendPacket(sm); + } + + setFalling(); + + return false; + } + + /** + * Set falling timestamp + */ + public void setFalling() { + _fallingTimestamp = System.currentTimeMillis() + FALLING_VALIDATION_DELAY; + } + + /** + * @return the _movieId + */ + public int getMovieId() { + return _movieId; + } + + public void setMovieId(int id) { + _movieId = id; + } + + /** + * Update last item auction request timestamp to current + */ + public void updateLastItemAuctionRequest() { + _lastItemAuctionInfoRequest = System.currentTimeMillis(); + } + + /** + * @return true if receiving item auction requests
      + * (last request was in 2 seconds before) + */ + public boolean isItemAuctionPolling() { + return (System.currentTimeMillis() - _lastItemAuctionInfoRequest) < 2000; + } + + @Override + public boolean isMovementDisabled() { + return super.isMovementDisabled() || (_movieId > 0); + } + + private void restoreUISettings() { + _uiKeySettings = new UIKeysSettings(getObjectId()); + } + + private void storeUISettings() { + if (_uiKeySettings == null) { + return; + } + + if (!_uiKeySettings.isSaved()) { + _uiKeySettings.saveInDB(); + } + } + + public UIKeysSettings getUISettings() { + return _uiKeySettings; + } + + public String getHtmlPrefix() { + if (!customs().multiLangEnable()) { + return null; + } + return _htmlPrefix; + } + + public String getLang() { + return _lang; + } + + public boolean setLang(String lang) { + boolean result = false; + if (customs().multiLangEnable()) { + if (customs().getMultiLangAllowed().contains(lang)) { + _lang = lang; + result = true; + } else { + _lang = customs().getMultiLangDefault(); + } + + _htmlPrefix = "data/lang/" + _lang + "/"; + } else { + _lang = null; + _htmlPrefix = null; + } + + return result; + } + + public long getOfflineStartTime() { + return _offlineShopStart; + } + + public void setOfflineStartTime(long time) { + _offlineShopStart = time; + } + + /** + * Remove player from BossZones (used on char logout/exit) + */ + public void removeFromBossZone() { + try { + for (L2BossZone zone : GrandBossManager.getInstance().getZones().values()) { + zone.removePlayer(this); + } + } catch (Exception e) { + LOG.warn("Exception on removeFromBossZone(): {}", e); + } + } + + /** + * Check all player skills for skill level. If player level is lower than skill learn level - 9, skill level is decreased to next possible level. + */ + public void checkPlayerSkills() { + for (Entry e : getSkills().entrySet()) { + final L2SkillLearn learn = SkillTreesData.getInstance().getClassSkill(e.getKey(), e.getValue().getLevel() % 100, getClassId()); + if (learn != null) { + int lvlDiff = e.getKey() == CommonSkill.EXPERTISE.getId() ? 0 : 9; + if (getLevel() < (learn.getGetLevel() - lvlDiff)) { + deacreaseSkillLevel(e.getValue(), lvlDiff); + } + } + } + } + + private void deacreaseSkillLevel(Skill skill, int lvlDiff) { + int nextLevel = -1; + final Map skillTree = SkillTreesData.getInstance().getCompleteClassSkillTree(getClassId()); + for (L2SkillLearn sl : skillTree.values()) { + if ((sl.getSkillId() == skill.getId()) && (nextLevel < sl.getSkillLevel()) && (getLevel() >= (sl.getGetLevel() - lvlDiff))) { + nextLevel = sl.getSkillLevel(); // next possible skill level + } + } + + if (nextLevel == -1) { + LOG.info("Removing {}, from {}", skill, this); + removeSkill(skill, true); // there is no lower skill + } else { + LOG.info("Decreasing {} to {} for {}", skill, nextLevel, this); + addSkill(SkillData.getInstance().getSkill(skill.getId(), nextLevel), true); // replace with lower one + } + } + + public boolean canMakeSocialAction() { + return ((getPrivateStoreType() == PrivateStoreType.NONE) && (getActiveRequester() == null) && !isAlikeDead() && !isAllSkillsDisabled() && !isCastingNow() && !isCastingSimultaneouslyNow() && (getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE)); + } + + public void setMultiSocialAction(int id, int targetId) { + _multiSociaAction = id; + _multiSocialTarget = targetId; + } + + public int getMultiSociaAction() { + return _multiSociaAction; + } + + public int getMultiSocialTarget() { + return _multiSocialTarget; + } + + public Collection getTeleportBookmarks() { + return _tpbookmarks.values(); + } + + public int getBookmarkslot() { + return _bookmarkslot; + } + + public int getQuestInventoryLimit() { + return character().getMaximumSlotsForQuestItems(); + } + + public boolean canAttackCharacter(L2Character cha) { + if (cha instanceof L2Attackable) { + return true; + } else if (cha instanceof L2Playable) { + if (cha.isInsideZone(ZoneId.PVP) && !cha.isInsideZone(ZoneId.SIEGE)) { + return true; + } + + L2PcInstance target; + if (cha instanceof L2Summon) { + target = ((L2Summon) cha).getOwner(); + } else { + target = (L2PcInstance) cha; + } + + if (isInDuel() && target.isInDuel() && (target.getDuelId() == getDuelId())) { + return true; + } else if (isInParty() && target.isInParty()) { + if (getParty() == target.getParty()) { + return false; + } + if (((getParty().getCommandChannel() != null) || (target.getParty().getCommandChannel() != null)) && (getParty().getCommandChannel() == target.getParty().getCommandChannel())) { + return false; + } + } else if ((getClan() != null) && (target.getClan() != null)) { + if (getClanId() == target.getClanId()) { + return false; + } + if (((getAllyId() > 0) || (target.getAllyId() > 0)) && (getAllyId() == target.getAllyId())) { + return false; + } + if (getClan().isAtWarWith(target.getClan().getId()) && target.getClan().isAtWarWith(getClan().getId())) { + return true; + } + } else if ((getClan() == null) || (target.getClan() == null)) { + if ((target.getPvpFlag() == 0) && (target.getKarma() == 0)) { + return false; + } + } + } + return true; + } + + /** + * Test if player inventory is under 90% capaity + * @param includeQuestInv check also quest inventory + * @return + */ + public boolean isInventoryUnder90(boolean includeQuestInv) { + return (getInventory().getSize(includeQuestInv) <= (getInventoryLimit() * 0.9)); + } + + public boolean havePetInvItems() { + return _petItems; + } + + public void setPetInvItems(boolean haveit) { + _petItems = haveit; + } + + public String getAdminConfirmCmd() { + return _adminConfirmCmd; + } + + public void setAdminConfirmCmd(String adminConfirmCmd) { + _adminConfirmCmd = adminConfirmCmd; + } + + public int getBlockCheckerArena() { + return _handysBlockCheckerEventArena; + } + + public void setBlockCheckerArena(byte arena) { + _handysBlockCheckerEventArena = arena; + } + + /** + * @return the hunting system controller for this player + */ + public HuntingSystem getHuntingSystem() { + return _huntingSystem; + } + + /** + * @return the recommendation system controller for this player + */ + public RecommendationSystem getRecSystem() { + return _recSystem; + } + + public boolean hasAbnormalType(AbnormalType at) { + return getEffectList().getBuffInfoByAbnormalType(at) != null; + } + + public boolean hasAbnormalTypeVote() { + return hasAbnormalType(AbnormalType.VOTE); + } + + public String getLastPetitionGmName() { + return _lastPetitionGmName; + } + + public void setLastPetitionGmName(String gmName) { + _lastPetitionGmName = gmName; + } + + public L2ContactList getContactList() { + return _contactList; + } + + public void setEventStatus() { + eventStatus = new PlayerEventHolder(this); + } + + public PlayerEventHolder getEventStatus() { + return eventStatus; + } + + public void setEventStatus(PlayerEventHolder pes) { + eventStatus = pes; + } + + public long getNotMoveUntil() { + return _notMoveUntil; + } + + public void updateNotMoveUntil() { + _notMoveUntil = System.currentTimeMillis() + SECONDS.toMillis(character().getNpcTalkBlockingTime()); + } + + @Override + public boolean isPlayer() { + return true; + } + + @Override + public boolean isChargedShot(ShotType type) { + final L2ItemInstance weapon = getActiveWeaponInstance(); + return (weapon != null) && weapon.isChargedShot(type); + } + + @Override + public void setChargedShot(ShotType type, boolean charged) { + final L2ItemInstance weapon = getActiveWeaponInstance(); + if (weapon != null) { + weapon.setChargedShot(type, charged); + } + } + + /** + * @param skillId the display skill Id + * @return the custom skill + */ + public Skill getCustomSkill(int skillId) { + return (_customSkills != null) ? _customSkills.get(skillId) : null; + } + + /** + * Add a skill level to the custom skills map. + * @param skill the skill to add + */ + private void addCustomSkill(Skill skill) { + if ((skill != null) && (skill.getDisplayId() != skill.getId())) { + if (_customSkills == null) { + _customSkills = new ConcurrentHashMap<>(); + } + _customSkills.put(skill.getDisplayId(), skill); + } + } + + /** + * Remove a skill level from the custom skill map. + * @param skill the skill to remove + */ + private void removeCustomSkill(Skill skill) { + if ((skill != null) && (_customSkills != null) && (skill.getDisplayId() != skill.getId())) { + _customSkills.remove(skill.getDisplayId()); + } + } + + /** + * @return {@code true} if current player can revive and shows 'To Village' button upon death, {@code false} otherwise. + */ + @Override + public boolean canRevive() { + for (IEventListener listener : _eventListeners) { + if (listener.isOnEvent() && !listener.canRevive()) { + return false; + } + } + return _canRevive; + } + + /** + * This method can prevent from displaying 'To Village' button upon death. + * @param val + */ + @Override + public void setCanRevive(boolean val) { + _canRevive = val; + } + + /** + * @return {@code true} if player is on event, {@code false} otherwise. + */ + @Override + public boolean isOnEvent() { + for (IEventListener listener : _eventListeners) { + if (listener.isOnEvent()) { + return true; + } + } + return super.isOnEvent(); + } + + public boolean isBlockedFromExit() { + for (IEventListener listener : _eventListeners) { + if (listener.isOnEvent() && listener.isBlockingExit()) { + return true; + } + } + return false; + } + + public boolean isBlockedFromDeathPenalty() { + for (IEventListener listener : _eventListeners) { + if (listener.isOnEvent() && listener.isBlockingDeathPenalty()) { + return true; + } + } + return false; + } + + @Override + public void addOverrideCond(PcCondOverride... excs) { + super.addOverrideCond(excs); + getVariables().set(COND_OVERRIDE_KEY, Long.toString(_exceptions)); + } + + @Override + public void removeOverridedCond(PcCondOverride... excs) { + super.removeOverridedCond(excs); + getVariables().set(COND_OVERRIDE_KEY, Long.toString(_exceptions)); + } + + /** + * @return {@code true} if {@link PlayerVariables} instance is attached to current player's scripts, {@code false} otherwise. + */ + public boolean hasVariables() { + return getScript(PlayerVariables.class) != null; + } + + /** + * @return {@link PlayerVariables} instance containing parameters regarding player. + */ + public PlayerVariables getVariables() { + final PlayerVariables vars = getScript(PlayerVariables.class); + return vars != null ? vars : addScript(new PlayerVariables(getObjectId())); + } + + /** + * @return {@code true} if {@link AccountVariables} instance is attached to current player's scripts, {@code false} otherwise. + */ + public boolean hasAccountVariables() { + return getScript(AccountVariables.class) != null; + } + + /** + * @return {@link AccountVariables} instance containing parameters regarding player. + */ + public AccountVariables getAccountVariables() { + final AccountVariables vars = getScript(AccountVariables.class); + return vars != null ? vars : addScript(new AccountVariables(getAccountName())); + } + + /** + * Adds a event listener. + * @param listener + */ + public void addEventListener(IEventListener listener) { + _eventListeners.add(listener); + } + + /** + * Removes event listener + * @param listener + */ + public void removeEventListener(IEventListener listener) { + _eventListeners.remove(listener); + } + + public void removeEventListener(Class clazz) { + _eventListeners.removeIf(e -> e.getClass() == clazz); + } + + public Collection getEventListeners() { + return _eventListeners; + } + + @Override + public int getId() { + return getClassId().getId(); + } + + public boolean isPartyBanned() { + return PunishmentManager.getInstance().hasPunishment(getObjectId(), PunishmentAffect.CHARACTER, PunishmentType.PARTY_BAN); + } + + /** + * @param act + * @return {@code true} if action was added successfully, {@code false} otherwise. + */ + public boolean addAction(PlayerAction act) { + if (!hasAction(act)) { + _actionMask |= act.getMask(); + return true; + } + return false; + } + + /** + * @param act + * @return {@code true} if action was removed successfully, {@code false} otherwise. + */ + public boolean removeAction(PlayerAction act) { + if (hasAction(act)) { + _actionMask &= ~act.getMask(); + return true; + } + return false; + } + + /** + * @param act + * @return {@code true} if action is present, {@code false} otherwise. + */ + public boolean hasAction(PlayerAction act) { + return (_actionMask & act.getMask()) == act.getMask(); + } + + /** + * Set true/false if character got Charm of Courage + * @param val true/false + */ + public void setCharmOfCourage(boolean val) { + _hasCharmOfCourage = val; + + } + + /** + * @return {@code true} if effect is present, {@code false} otherwise. + */ + public boolean hasCharmOfCourage() { + return _hasCharmOfCourage; + } + + /** + * @param target the target + * @return {@code true} if this player got war with the target, {@code false} otherwise. + */ + public boolean isAtWarWith(L2Character target) { + if (target == null) { + return false; + } + if ((_clan != null) && !isAcademyMember()) { + if ((target.getClan() != null) && !target.isAcademyMember()) { + return _clan.isAtWarWith(target.getClan()); + } + } + return false; + } + + /** + * @param target the target + * @return {@code true} if this player in same party with the target, {@code false} otherwise. + */ + public boolean isInPartyWith(L2Character target) { + if (!isInParty() || !target.isInParty()) { + return false; + } + return getParty().getLeaderObjectId() == target.getParty().getLeaderObjectId(); + } + + /** + * @param target the target + * @return {@code true} if this player in same command channel with the target, {@code false} otherwise. + */ + public boolean isInCommandChannelWith(L2Character target) { + if (!isInParty() || !target.isInParty()) { + return false; + } + + if (!getParty().isInCommandChannel() || !target.getParty().isInCommandChannel()) { + return false; + } + return getParty().getCommandChannel().getLeaderObjectId() == target.getParty().getCommandChannel().getLeaderObjectId(); + } + + /** + * @param target the target + * @return {@code true} if this player in same clan with the target, {@code false} otherwise. + */ + public boolean isInClanWith(L2Character target) { + if ((getClanId() == 0) || (target.getClanId() == 0)) { + return false; + } + return getClanId() == target.getClanId(); + } + + /** + * @param target the target + * @return {@code true} if this player in same ally with the target, {@code false} otherwise. + */ + public boolean isInAllyWith(L2Character target) { + if ((getAllyId() == 0) || (target.getAllyId() == 0)) { + return false; + } + return getAllyId() == target.getAllyId(); + } + + /** + * @param target the target + * @return {@code true} if this player at duel with the target, {@code false} otherwise. + */ + public boolean isInDuelWith(L2Character target) { + if (!isInDuel() || !target.isInDuel()) { + return false; + } + return getDuelId() == target.getDuelId(); + } + + /** + * @param target the target + * @return {@code true} if this player is on same siege side with the target, {@code false} otherwise. + */ + public boolean isOnSameSiegeSideWith(L2Character target) { + return (getSiegeState() > 0) && isInsideZone(ZoneId.SIEGE) && (getSiegeState() == target.getSiegeState()) && (getSiegeSide() == target.getSiegeSide()); + } + + public void setServitorShare(Map map) { + _servitorShare = map; + } + + public double getServitorShareBonus(Stats stat) { + if (_servitorShare == null) { + return 1.0d; + } + return _servitorShare.get(stat); + } + + public boolean canSummonTarget(L2PcInstance target) { + if (this == target) { + return false; + } + + if (target.isAlikeDead()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_IS_DEAD_AT_THE_MOMENT_AND_CANNOT_BE_SUMMONED); + sm.addPcName(target); + sendPacket(sm); + return false; + } + + if (target.isInStoreMode()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_CURRENTLY_TRADING_OR_OPERATING_PRIVATE_STORE_AND_CANNOT_BE_SUMMONED); + sm.addPcName(target); + sendPacket(sm); + return false; + } + + if (target.isRooted() || target.isInCombat()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_IS_ENGAGED_IN_COMBAT_AND_CANNOT_BE_SUMMONED); + sm.addPcName(target); + sendPacket(sm); + return false; + } + + if (target.isInOlympiadMode() || OlympiadManager.getInstance().isRegisteredInComp(target)) { + sendPacket(SystemMessageId.YOU_CANNOT_SUMMON_PLAYERS_WHO_ARE_IN_OLYMPIAD); + return false; + } + + if (target.isFestivalParticipant() || target.isFlyingMounted() || target.isCombatFlagEquipped() || !TvTEvent.onEscapeUse(target.getObjectId())) { + sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + return false; + } + + if (target.inObserverMode()) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_STATE_FORBIDS_SUMMONING); + sm.addCharName(target); + sendPacket(sm); + return false; + } + + if (target.isInsideZone(ZoneId.NO_SUMMON_FRIEND) || target.isInsideZone(ZoneId.JAIL)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_IN_SUMMON_BLOCKING_AREA); + sm.addString(target.getName()); + sendPacket(sm); + return false; + } + + if (isInsideZone(ZoneId.NO_SUMMON_FRIEND) || isInsideZone(ZoneId.JAIL) || isFlyingMounted()) { + sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + return false; + } + + if (getInstanceId() > 0) { + if (!general().allowSummonInInstance() || !InstanceManager.getInstance().getInstance(getInstanceId()).isSummonAllowed()) { + sendPacket(SystemMessageId.YOU_MAY_NOT_SUMMON_FROM_YOUR_CURRENT_LOCATION); + return false; + } + } + + // TODO: on retail character can enter 7s dungeon with summon friend, but should be teleported away by mobs, because currently this is not working in L2J we do not allowing summoning. + if (isIn7sDungeon()) { + int targetCabal = SevenSigns.getInstance().getPlayerCabal(target.getObjectId()); + if (SevenSigns.getInstance().isSealValidationPeriod()) { + if (targetCabal != SevenSigns.getInstance().getCabalHighestScore()) { + sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + return false; + } + } else if (targetCabal == SevenSigns.CABAL_NULL) { + sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + return false; + } + } + return true; + } + + public Map getTpbookmarks() { + return _tpbookmarks; + } + + public long getOnlineTime() { + return _onlineTime; + } + + public void setOnlineTime(long time) { + _onlineTime = time; + _onlineBeginTime = System.currentTimeMillis(); + } + + public void setHenna(L2Henna[] henna) { + _henna = henna; + } + + public long getOnlineBeginTime() { + return _onlineBeginTime; + } + + public int getControlItemId() { + return _controlItemId; + } + + public void setControlItemId(int controlItemId) { + _controlItemId = controlItemId; + } + + // TODO(Zoey76): Improve this. + public void getDwarvenRecipeBookClear() { + _dwarvenRecipeBook.clear(); + } + + public void debugFeature(String feature, String msg) { + msg = feature + " (" + getName() + ") " + msg; + LOG.debug(msg); + sendDebugMessage(msg); + } + + public void debugFeature(String feature, String msg, Object arg) { + msg = feature + " (" + getName() + ") " + msg; + var formatted = MessageFormatter.format(msg, arg); + LOG.debug(formatted.getMessage()); + sendDebugMessage(formatted.getMessage()); + } + + public void debugFeature(String feature, String msg, Object... args) { + msg = feature + " (" + getName() + ") " + msg; + var formatted = MessageFormatter.format(msg, args); + LOG.debug(formatted.getMessage()); + sendDebugMessage(formatted.getMessage()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetInstance.java new file mode 100644 index 00000000..24155972 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetInstance.java @@ -0,0 +1,1086 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.config.Configuration.rates; + +import java.util.List; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.CharSummonTable; +import com.l2jserver.gameserver.data.sql.impl.SummonEffectsTable; +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.ItemLocation; +import com.l2jserver.gameserver.enums.PartyDistributionType; +import com.l2jserver.gameserver.handler.IItemHandler; +import com.l2jserver.gameserver.handler.ItemHandler; +import com.l2jserver.gameserver.instancemanager.CursedWeaponsManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.ItemsOnGroundManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2PetData; +import com.l2jserver.gameserver.model.L2PetLevelData; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.stat.PetStat; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.itemcontainer.PetInventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.BaseStats; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExChangeNpcState; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.PetInventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.StatusUpdate; +import com.l2jserver.gameserver.network.serverpackets.StopMove; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; + +/** + * Pet. + */ +public class L2PetInstance extends L2Summon { + private static final Logger LOG = LoggerFactory.getLogger(L2PetInstance.class); + + private int _curFed; + private final PetInventory _inventory; + private final int _controlObjectId; + private boolean _respawned; + private final boolean _mountable; + private Future _feedTask; + private L2PetData _data; + private L2PetLevelData _leveldata; + + /** The Experience before the last Death Penalty */ + private long _expBeforeDeath = 0; + private int _curWeightPenalty = 0; + + protected boolean _bufferMode = true; + + /** + * Creates a pet. + * @param template the pet NPC template + * @param owner the owner + * @param control the summoning item + */ + public L2PetInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, L2ItemInstance control) { + this(objectId, template, owner, control, (byte) (template.getDisplayId() == 12564 ? owner.getLevel() : template.getLevel())); + } + + /** + * Creates a pet. + * @param template the pet NPC template + * @param owner the pet NPC template + * @param control the summoning item + * @param level the level + */ + public L2PetInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, L2ItemInstance control, byte level) { + super(objectId, template, owner); + setInstanceType(InstanceType.L2PetInstance); + + _controlObjectId = control.getObjectId(); + + getStat().setLevel(Math.max(level, getMinLevel())); + + _inventory = new PetInventory(this); + _inventory.restore(); + + int npcId = template.getId(); + _mountable = PetDataTable.isMountable(npcId); + getPetData(); + getPetLevelData(); + } + + public final L2PetLevelData getPetLevelData() { + if (_leveldata == null) { + _leveldata = PetDataTable.getInstance().getPetLevelData(getTemplate().getId(), getStat().getLevel()); + } + + return _leveldata; + } + + public final L2PetData getPetData() { + if (_data == null) { + _data = PetDataTable.getInstance().getPetData(getTemplate().getId()); + } + + return _data; + } + + public final void setPetData(L2PetLevelData value) { + _leveldata = value; + } + + /** + * Manage Feeding Task.
      + * Feed or kill the pet depending on hunger level.
      + * If pet has food in inventory and feed level drops below 55% then consume food from inventory.
      + * Send a broadcastStatusUpdate packet for this L2PetInstance + */ + class FeedTask implements Runnable { + private final Logger LOG = LoggerFactory.getLogger(FeedTask.class); + + @Override + public void run() { + try { + if ((getOwner() == null) || !getOwner().hasSummon() || (getOwner().getSummon().getObjectId() != getObjectId())) { + stopFeed(); + return; + } else if (getCurrentFed() > getFeedConsume()) { + setCurrentFed(getCurrentFed() - getFeedConsume()); + } else { + setCurrentFed(0); + } + + broadcastStatusUpdate(); + + List foodIds = getPetData().getFood(); + if (foodIds.isEmpty()) { + if (isUncontrollable()) { + // Owl Monk remove PK + if ((getTemplate().getId() == 16050) && (getOwner() != null)) { + getOwner().setPkKills(Math.max(0, getOwner().getPkKills() - Rnd.get(1, 6))); + } + sendPacket(SystemMessageId.THE_HELPER_PET_LEAVING); + deleteMe(getOwner()); + } else if (isHungry()) { + sendPacket(SystemMessageId.THERE_NOT_MUCH_TIME_REMAINING_UNTIL_HELPER_LEAVES); + } + return; + } + + L2ItemInstance food = null; + for (int id : foodIds) { + food = getInventory().getItemByItemId(id); + if (food != null) { + break; + } + } + + if ((food != null) && isHungry()) { + final IItemHandler handler = ItemHandler.getInstance().getHandler(food.getEtcItem()); + if (handler != null) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PET_TOOK_S1_BECAUSE_HE_WAS_HUNGRY); + sm.addItemName(food.getId()); + sendPacket(sm); + handler.useItem(L2PetInstance.this, food, false); + } + } + + if (isUncontrollable()) { + sendPacket(SystemMessageId.YOUR_PET_IS_STARVING_AND_WILL_NOT_OBEY_UNTIL_IT_GETS_ITS_FOOD_FEED_YOUR_PET); + } + } catch (Exception e) { + LOG.error("Pet [ObjectId: {}] a feed task error has occurred", getObjectId(), e); + } + } + + private int getFeedConsume() { + // if pet is attacking + if (isAttackingNow()) { + return getPetLevelData().getPetFeedBattle(); + } + return getPetLevelData().getPetFeedNormal(); + } + } + + public synchronized static L2PetInstance spawnPet(L2NpcTemplate template, L2PcInstance owner, L2ItemInstance control) { + if (L2World.getInstance().getPet(owner.getObjectId()) != null) { + return null; // owner has a pet listed in world + } + + final var data = PetDataTable.getInstance().getPetData(template.getId()); + final var pet = DAOFactory.getInstance().getPetDAO().load(control, template, owner); + if (pet != null) { + pet.setTitle(owner.getName()); + if (data.isSyncLevel() && (pet.getLevel() != owner.getLevel())) { + pet.getStat().setLevel(owner.getLevel()); + pet.getStat().setExp(pet.getStat().getExpForLevel(owner.getLevel())); + } + L2World.getInstance().addPet(owner.getObjectId(), pet); + } + return pet; + } + + @Override + public PetStat getStat() { + return (PetStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new PetStat(this)); + } + + public boolean isRespawned() { + return _respawned; + } + + public void setRespawned(boolean respawned) { + _respawned = respawned; + } + + @Override + public int getSummonType() { + return 2; + } + + @Override + public int getControlObjectId() { + return _controlObjectId; + } + + public L2ItemInstance getControlItem() { + return getOwner().getInventory().getItemByObjectId(_controlObjectId); + } + + public int getCurrentFed() { + return _curFed; + } + + public void setCurrentFed(int num) { + if (num <= 0) { + sendPacket(new ExChangeNpcState(getObjectId(), 0x64)); + } else if (_curFed <= 0) { + sendPacket(new ExChangeNpcState(getObjectId(), 0x65)); + } + _curFed = Math.min(num, getMaxFed()); + } + + /** + * Returns the pet's currently equipped weapon instance (if any). + */ + @Override + public L2ItemInstance getActiveWeaponInstance() { + for (L2ItemInstance item : getInventory().getItems()) { + if ((item.getItemLocation() == ItemLocation.PET_EQUIP) && (item.getItem().getBodyPart() == L2Item.SLOT_R_HAND)) { + return item; + } + } + + return null; + } + + /** + * Returns the pet's currently equipped weapon (if any). + */ + @Override + public L2Weapon getActiveWeaponItem() { + L2ItemInstance weapon = getActiveWeaponInstance(); + + if (weapon == null) { + return null; + } + + return (L2Weapon) weapon.getItem(); + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + // temporary? unavailable + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + // temporary? unavailable + return null; + } + + @Override + public PetInventory getInventory() { + return _inventory; + } + + /** + * Destroys item from inventory and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param objectId : int Item Instance identifier of the item to be destroyed + * @param count : int Quantity of items to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + @Override + public boolean destroyItem(String process, int objectId, long count, L2Object reference, boolean sendMessage) { + L2ItemInstance item = _inventory.destroyItem(process, objectId, count, getOwner(), reference); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + + return false; + } + + // Send Pet inventory update packet + PetInventoryUpdate petIU = new PetInventoryUpdate(); + petIU.addItem(item); + sendPacket(petIU); + + if (sendMessage) { + if (count > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(item.getId()); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(item.getId()); + sendPacket(sm); + } + } + return true; + } + + /** + * Destroy item from inventory by using its itemId and send a Server->Client InventoryUpdate packet to the L2PcInstance. + * @param process : String Identifier of process triggering this action + * @param itemId : int Item identifier of the item to be destroyed + * @param count : int Quantity of items to be destroyed + * @param reference : L2Object Object referencing current action like NPC selling item or previous item in transformation + * @param sendMessage : boolean Specifies whether to send message to Client about this action + * @return boolean informing if the action was successful + */ + @Override + public boolean destroyItemByItemId(String process, int itemId, long count, L2Object reference, boolean sendMessage) { + L2ItemInstance item = _inventory.destroyItemByItemId(process, itemId, count, getOwner(), reference); + + if (item == null) { + if (sendMessage) { + sendPacket(SystemMessageId.NOT_ENOUGH_ITEMS); + } + return false; + } + + // Send Pet inventory update packet + PetInventoryUpdate petIU = new PetInventoryUpdate(); + petIU.addItem(item); + sendPacket(petIU); + + if (sendMessage) { + if (count > 1) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S2_S1_DISAPPEARED); + sm.addItemName(item.getId()); + sm.addLong(count); + sendPacket(sm); + } else { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(item.getId()); + sendPacket(sm); + } + } + + return true; + } + + @Override + public void doPickupItem(L2Object object) { + if (isDead()) { + return; + } + + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + broadcastPacket(new StopMove(this)); + + if (!(object instanceof L2ItemInstance)) { + // dont try to pickup anything that is not an item :) + LOG.warn("{} trying to pickup wrong target. {}", this, object); + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + boolean follow = getFollowStatus(); + final L2ItemInstance target = (L2ItemInstance) object; + + // Cursed weapons + if (CursedWeaponsManager.getInstance().isCursed(target.getId())) { + SystemMessage smsg = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1); + smsg.addItemName(target.getId()); + sendPacket(smsg); + return; + } else if (FortSiegeManager.getInstance().isCombat(target.getId())) { + return; + } + + SystemMessage sm; + synchronized (target) { + // Check if the target to pick up is visible + if (!target.isVisible()) { + // Send a Server->Client packet ActionFailed to this L2PcInstance + sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + if (!target.getDropProtection().tryPickUp(this)) { + sendPacket(ActionFailed.STATIC_PACKET); + sm = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1); + sm.addItemName(target); + sendPacket(sm); + return; + } + + if (((isInParty() && (getParty().getDistributionType() == PartyDistributionType.FINDERS_KEEPERS)) || !isInParty()) && !_inventory.validateCapacity(target)) { + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(SystemMessageId.YOUR_PET_CANNOT_CARRY_ANY_MORE_ITEMS); + return; + } + + if ((target.getOwnerId() != 0) && (target.getOwnerId() != getOwner().getObjectId()) && !getOwner().isInLooterParty(target.getOwnerId())) { + if (target.getId() == Inventory.ADENA_ID) { + sm = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1_ADENA); + sm.addLong(target.getCount()); + } else if (target.getCount() > 1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S2_S1_S); + sm.addItemName(target); + sm.addLong(target.getCount()); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.FAILED_TO_PICKUP_S1); + sm.addItemName(target); + } + sendPacket(ActionFailed.STATIC_PACKET); + sendPacket(sm); + return; + } + + if ((target.getItemLootSchedule() != null) && ((target.getOwnerId() == getOwner().getObjectId()) || getOwner().isInLooterParty(target.getOwnerId()))) { + target.resetOwnerTimer(); + } + + // Remove from the ground! + target.pickupMe(this); + + if (general().saveDroppedItem()) { + ItemsOnGroundManager.getInstance().removeObject(target); + } + } + + // Herbs + if (target.getItem().hasExImmediateEffect()) { + IItemHandler handler = ItemHandler.getInstance().getHandler(target.getEtcItem()); + if (handler == null) { + LOG.warn("No item handler registered for item ID: {}.", target.getId()); + } else { + handler.useItem(this, target, false); + } + + ItemTable.getInstance().destroyItem("Consume", target, getOwner(), null); + broadcastStatusUpdate(); + } else { + if (target.getId() == Inventory.ADENA_ID) { + sm = SystemMessage.getSystemMessage(SystemMessageId.PET_PICKED_S1_ADENA); + sm.addLong(target.getCount()); + sendPacket(sm); + } else if (target.getEnchantLevel() > 0) { + sm = SystemMessage.getSystemMessage(SystemMessageId.PET_PICKED_S1_S2); + sm.addInt(target.getEnchantLevel()); + sm.addItemName(target); + sendPacket(sm); + } else if (target.getCount() > 1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.PET_PICKED_S2_S1_S); + sm.addLong(target.getCount()); + sm.addItemName(target); + sendPacket(sm); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.PET_PICKED_S1); + sm.addItemName(target); + sendPacket(sm); + } + + // If owner is in party and it isn't finders keepers, distribute the item instead of stealing it -.- + if (getOwner().isInParty() && (getOwner().getParty().getDistributionType() != PartyDistributionType.FINDERS_KEEPERS)) { + getOwner().getParty().distributeItem(getOwner(), target); + } else { + final L2ItemInstance item = getInventory().addItem("Pickup", target, getOwner(), this); + // sendPacket(new PetItemList(getInventory().getItems())); + sendPacket(new PetInventoryUpdate(item)); + } + } + + getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + + if (follow) { + followOwner(); + } + } + + @Override + public void deleteMe(L2PcInstance owner) { + getInventory().transferItemsToOwner(); + super.deleteMe(owner); + destroyControlItem(owner, false); // this should also delete the pet from the db + CharSummonTable.getInstance().getPets().remove(getOwner().getObjectId()); + } + + @Override + public boolean doDie(L2Character killer) { + L2PcInstance owner = getOwner(); + if ((owner != null) && !owner.isInDuel() && (!isInsideZone(ZoneId.PVP) || isInsideZone(ZoneId.SIEGE))) { + deathPenalty(); + } + if (!super.doDie(killer, true)) { + return false; + } + stopFeed(); + sendPacket(SystemMessageId.MAKE_SURE_YOU_RESSURECT_YOUR_PET_WITHIN_24_HOURS); + DecayTaskManager.getInstance().add(this); + // do not decrease exp if is in duel, arena + return true; + } + + @Override + public void doRevive() { + getOwner().removeReviving(); + + super.doRevive(); + + // stopDecay + DecayTaskManager.getInstance().cancel(this); + startFeed(); + if (!isHungry()) { + setRunning(); + } + getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + + @Override + public void doRevive(double revivePower) { + // Restore the pet's lost experience, + // depending on the % return of the skill used (based on its power). + restoreExp(revivePower); + doRevive(); + } + + /** + * Transfers item to another inventory + * @param process string identifier of process triggering this action + * @param objectId Item Identifier of the item to be transferred + * @param count Quantity of items to be transferred + * @param target + * @param actor the player requesting the item transfer + * @param reference Object referencing current action like NPC selling item or previous item in transformation + * @return L2ItemInstance corresponding to the new item or the updated item in inventory + */ + public L2ItemInstance transferItem(String process, int objectId, long count, Inventory target, L2PcInstance actor, L2Object reference) { + L2ItemInstance oldItem = getInventory().getItemByObjectId(objectId); + L2ItemInstance playerOldItem = target.getItemByItemId(oldItem.getId()); + L2ItemInstance newItem = getInventory().transferItem(process, objectId, count, target, actor, reference); + + if (newItem == null) { + return null; + } + + // Send inventory update packet + PetInventoryUpdate petIU = new PetInventoryUpdate(); + if ((oldItem.getCount() > 0) && (oldItem != newItem)) { + petIU.addModifiedItem(oldItem); + } else { + petIU.addRemovedItem(oldItem); + } + sendPacket(petIU); + + // Send target update packet + if (!newItem.isStackable()) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addNewItem(newItem); + sendPacket(iu); + } else if (playerOldItem != null) { + InventoryUpdate iu = new InventoryUpdate(); + iu.addModifiedItem(newItem); + sendPacket(iu); + } + return newItem; + } + + /** + * Remove the Pet from DB and its associated item from the player inventory + * @param owner The owner from whose inventory we should delete the item + * @param evolve + */ + public void destroyControlItem(L2PcInstance owner, boolean evolve) { + // remove the pet instance from world + L2World.getInstance().removePet(owner.getObjectId()); + + // delete from inventory + try { + L2ItemInstance removedItem; + if (evolve) { + removedItem = owner.getInventory().destroyItem("Evolve", getControlObjectId(), 1, getOwner(), this); + } else { + removedItem = owner.getInventory().destroyItem("PetDestroy", getControlObjectId(), 1, getOwner(), this); + if (removedItem != null) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DISAPPEARED); + sm.addItemName(removedItem); + owner.sendPacket(sm); + } + } + + if (removedItem == null) { + LOG.warn("Couldn't destroy pet control item for {} pet: {} evolve: {}", owner, this, evolve); + } else { + InventoryUpdate iu = new InventoryUpdate(); + iu.addRemovedItem(removedItem); + + owner.sendPacket(iu); + + StatusUpdate su = new StatusUpdate(owner); + su.addAttribute(StatusUpdate.CUR_LOAD, owner.getCurrentLoad()); + owner.sendPacket(su); + + owner.broadcastUserInfo(); + + L2World.getInstance().removeObject(removedItem); + } + } catch (Exception ex) { + LOG.error("Error destroying control item for pet {} and player {}!", this, owner, ex); + } + + // Pet control item no longer exists, delete the pet from the database. + DAOFactory.getInstance().getPetDAO().delete(this); + } + + public void dropAllItems() { + try { + for (L2ItemInstance item : getInventory().getItems()) { + dropItemHere(item); + } + } catch (Exception ex) { + LOG.warn("Error dropping all items for pet {}!", this, ex); + } + } + + public void dropItemHere(L2ItemInstance dropit, boolean protect) { + dropit = getInventory().dropItem("Drop", dropit.getObjectId(), dropit.getCount(), getOwner(), this); + + if (dropit != null) { + if (protect) { + dropit.getDropProtection().protect(getOwner()); + } + LOG.info("Item id to drop: {} amount: {}", dropit.getId(), dropit.getCount()); + dropit.dropMe(this, getX(), getY(), getZ() + 100); + } + } + + public void dropItemHere(L2ItemInstance dropit) { + dropItemHere(dropit, false); + } + + /** + * @return Returns the mount able. + */ + @Override + public boolean isMountable() { + return _mountable; + } + + @Override + public final void stopSkillEffects(boolean removed, int skillId) { + super.stopSkillEffects(removed, skillId); + SummonEffectsTable.getInstance().removePetEffects(getControlObjectId(), skillId); + } + + @Override + public void storeMe() { + if (getControlObjectId() == 0) { + // this is a summon, not a pet, don't store anything + return; + } + + if (!character().restorePetOnReconnect()) { + setRestoreSummon(false); + } + + if (!isRespawned()) { + DAOFactory.getInstance().getPetDAO().insert(this); + } else { + DAOFactory.getInstance().getPetDAO().update(this); + } + + setRespawned(true); + + if (isRestoreSummon()) { + CharSummonTable.getInstance().getPets().put(getOwner().getObjectId(), getControlObjectId()); + } else { + CharSummonTable.getInstance().getPets().remove(getOwner().getObjectId()); + } + + final L2ItemInstance itemInst = getControlItem(); + if ((itemInst != null) && (itemInst.getEnchantLevel() != getStat().getLevel())) { + itemInst.setEnchantLevel(getStat().getLevel()); + itemInst.updateDatabase(); + } + } + + @Override + public void storeEffect(boolean storeEffects) { + if (!character().summonStoreSkillCooltime()) { + return; + } + + // Clear list for overwrite + SummonEffectsTable.getInstance().clearPetEffects(getControlObjectId()); + + DAOFactory.getInstance().getPetSkillSaveDAO().insert(this, storeEffects); + } + + @Override + public void restoreEffects() { + DAOFactory.getInstance().getPetSkillSaveDAO().load(this); + SummonEffectsTable.getInstance().applyPetEffects(this, getControlObjectId()); + } + + public synchronized void stopFeed() { + if (_feedTask != null) { + _feedTask.cancel(false); + _feedTask = null; + } + } + + public synchronized void startFeed() { + stopFeed(); + + if (!isDead() && (getOwner().getSummon() == this)) { + _feedTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FeedTask(), 10000, 10000); + } + } + + @Override + public int getMaxLoad() { + return (int) calcStat(Stats.WEIGHT_LIMIT, Math.floor(BaseStats.CON.calcBonus(this) * 34500 * character().getWeightLimit()), this, null); + } + + @Override + public int getBonusWeightPenalty() { + return (int) calcStat(Stats.WEIGHT_PENALTY, 1, this, null); + } + + @Override + public int getCurrentLoad() { + return getInventory().getTotalWeight(); + } + + @Override + public synchronized void unSummon(L2PcInstance owner) { + stopFeed(); + stopHpMpRegeneration(); + super.unSummon(owner); + + if (!isDead()) { + if (getInventory() != null) { + getInventory().deleteMe(); + } + L2World.getInstance().removePet(owner.getObjectId()); + } + } + + /** + * Restore the specified % of experience this L2PetInstance has lost.
      + *
      + * @param restorePercent + */ + public void restoreExp(double restorePercent) { + if (_expBeforeDeath > 0) { + // Restore the specified % of lost experience. + getStat().addExp(Math.round(((_expBeforeDeath - getStat().getExp()) * restorePercent) / 100)); + _expBeforeDeath = 0; + } + } + + private void deathPenalty() { + // TODO: Need Correct Penalty + + int lvl = getStat().getLevel(); + double percentLost = (-0.07 * lvl) + 6.5; + + // Calculate the Experience loss + long lostExp = Math.round(((getStat().getExpForLevel(lvl + 1) - getStat().getExpForLevel(lvl)) * percentLost) / 100); + + // Get the Experience before applying penalty + _expBeforeDeath = getStat().getExp(); + + // Set the new Experience value of the L2PetInstance + getStat().removeExp(lostExp); + } + + public void addExp(long exp) { + getStat().addExp(exp); + } + + public long getExpForLevel(int level) { + return getStat().getExpForLevel(level); + } + + @Override + public void addExpAndSp(long addToExp, int addToSp) { + getStat().addExpAndSp(Math.round(addToExp * (isSinEater() ? rates().getSinEaterXpRate() : rates().getPetXpRate())), addToSp); + } + + public boolean isSinEater() { + return getId() == 12564; + } + + @Override + public long getExpForThisLevel() { + return getStat().getExpForLevel(getLevel()); + } + + @Override + public long getExpForNextLevel() { + return getStat().getExpForLevel(getLevel() + 1); + } + + @Override + public final boolean addLevel(int value) { + if ((getLevel() + value) > getStat().getMaxLevel()) { + return false; + } + + boolean levelIncreased = getStat().addLevel(value); + onLevelChange(levelIncreased); + return levelIncreased; + } + + @Override + public long getExp() { + return getStat().getExp(); + } + + public void setExp(long exp) { + getStat().setExp(exp); + } + + public void setSp(int sp) { + getStat().setSp(sp); + } + + @Override + public void onLevelChange(boolean levelIncreased) { + StatusUpdate su = new StatusUpdate(getStat().getActiveChar()); + su.addAttribute(StatusUpdate.LEVEL, getLevel()); + su.addAttribute(StatusUpdate.MAX_HP, getMaxHp()); + su.addAttribute(StatusUpdate.MAX_MP, getMaxMp()); + getStat().getActiveChar().broadcastPacket(su); + if (levelIncreased) { + getStat().getActiveChar().broadcastPacket(new SocialAction(getObjectId(), SocialAction.LEVEL_UP)); + } + // Send a Server->Client packet PetInfo to the L2PcInstance + getStat().getActiveChar().updateAndBroadcastStatus(1); + + if (getStat().getActiveChar().getControlItem() != null) { + getStat().getActiveChar().getControlItem().setEnchantLevel(getLevel()); + } + } + + @Override + public final int getLevel() { + return getStat().getLevel(); + } + + @Override + public int getMinLevel() { + return PetDataTable.getInstance().getPetMinLevel((this).getTemplate().getId()); + } + + public int getMaxFed() { + return getStat().getMaxFeed(); + } + + @Override + public int getCriticalHit(L2Character target, Skill skill) { + return getStat().getCriticalHit(target, skill); + } + + public void updateRefOwner(L2PcInstance owner) { + int oldOwnerId = getOwner().getObjectId(); + + setOwner(owner); + L2World.getInstance().removePet(oldOwnerId); + L2World.getInstance().addPet(oldOwnerId, this); + } + + public int getInventoryLimit() { + return npc().getMaximumSlotsForPet(); + } + + public void refreshOverloaded() { + int maxLoad = getMaxLoad(); + if (maxLoad > 0) { + long weightproc = (((getCurrentLoad() - getBonusWeightPenalty()) * 1000L) / maxLoad); + int newWeightPenalty; + if ((weightproc < 500) || getOwner().getDietMode()) { + newWeightPenalty = 0; + } else if (weightproc < 666) { + newWeightPenalty = 1; + } else if (weightproc < 800) { + newWeightPenalty = 2; + } else if (weightproc < 1000) { + newWeightPenalty = 3; + } else { + newWeightPenalty = 4; + } + + if (_curWeightPenalty != newWeightPenalty) { + _curWeightPenalty = newWeightPenalty; + if (newWeightPenalty > 0) { + addSkill(SkillData.getInstance().getSkill(4270, newWeightPenalty)); + setIsOverloaded(getCurrentLoad() >= maxLoad); + } else { + removeSkill(getKnownSkill(4270), true); + setIsOverloaded(false); + } + } + } + } + + @Override + public void updateAndBroadcastStatus(int val) { + refreshOverloaded(); + super.updateAndBroadcastStatus(val); + } + + @Override + public final boolean isHungry() { + return getCurrentFed() < ((getPetData().getHungryLimit() / 100f) * getPetLevelData().getPetMaxFeed()); + } + + /** + * Verifies if a pet can be controlled by it's owner.
      + * Starving pets cannot be controlled. + * @return {@code true} if the per cannot be controlled + */ + public boolean isUncontrollable() { + return getCurrentFed() <= 0; + } + + @Override + public final int getWeapon() { + L2ItemInstance weapon = getInventory().getPaperdollItem(Inventory.PAPERDOLL_RHAND); + if (weapon != null) { + return weapon.getId(); + } + return 0; + } + + @Override + public final int getArmor() { + L2ItemInstance weapon = getInventory().getPaperdollItem(Inventory.PAPERDOLL_CHEST); + if (weapon != null) { + return weapon.getId(); + } + return 0; + } + + public final int getJewel() { + L2ItemInstance weapon = getInventory().getPaperdollItem(Inventory.PAPERDOLL_NECK); + if (weapon != null) { + return weapon.getId(); + } + return 0; + } + + @Override + public short getSoulShotsPerHit() { + return getPetLevelData().getPetSoulShot(); + } + + @Override + public short getSpiritShotsPerHit() { + return getPetLevelData().getPetSpiritShot(); + } + + @Override + public void setName(String name) { + final L2ItemInstance controlItem = getControlItem(); + if (controlItem != null) { + if (controlItem.getCustomType2() == (name == null ? 1 : 0)) { + // name not set yet + controlItem.setCustomType2(name != null ? 1 : 0); + controlItem.updateDatabase(); + InventoryUpdate iu = new InventoryUpdate(); + iu.addModifiedItem(controlItem); + sendPacket(iu); + } + } else { + LOG.warn("Pet control item null, for pet: {}", toString()); + } + super.setName(name); + } + + public boolean canEatFoodId(int itemId) { + return _data.getFood().contains(itemId); + } + + @Override + public boolean isPet() { + return true; + } + + @Override + public final double getRunSpeed() { + return super.getRunSpeed() * (isUncontrollable() ? 0.5d : 1.0d); + } + + @Override + public final double getWalkSpeed() { + return super.getWalkSpeed() * (isUncontrollable() ? 0.5d : 1.0d); + } + + @Override + public final double getMovementSpeedMultiplier() { + return super.getMovementSpeedMultiplier() * (isUncontrollable() ? 0.5d : 1.0d); + } + + @Override + public final double getMoveSpeed() { + if (isInsideZone(ZoneId.WATER)) { + return isRunning() ? getSwimRunSpeed() : getSwimWalkSpeed(); + } + return isRunning() ? getRunSpeed() : getWalkSpeed(); + } + + /** + * Verify if this pet is in support mode. + * @return {@code true} if this baby pet is in support mode, {@code false} otherwise + */ + public boolean isInSupportMode() { + return _bufferMode; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetManagerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetManagerInstance.java new file mode 100644 index 00000000..861c8760 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2PetManagerInstance.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2PetManagerInstance extends L2MerchantInstance { + + public L2PetManagerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2PetManagerInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2QuestGuardInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2QuestGuardInstance.java new file mode 100644 index 00000000..d52b8fc3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2QuestGuardInstance.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableAttack; +import com.l2jserver.gameserver.model.events.impl.character.npc.attackable.AttackableKill; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * This class extends Guard class for quests, that require tracking of onAttack and onKill events from monsters' attacks. + * @author GKR + */ +public final class L2QuestGuardInstance extends L2GuardInstance { + private boolean _isAutoAttackable = true; + private boolean _isPassive = false; + + /** + * Creates a quest guard. + * @param template the quest guard NPC template + */ + public L2QuestGuardInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2QuestGuardInstance); + } + + @Override + public void addDamage(L2Character attacker, int damage, Skill skill) { + super.addDamage(attacker, damage, skill); + + if (attacker instanceof L2Attackable) { + EventDispatcher.getInstance().notifyEventAsync(new AttackableAttack(null, this, damage, skill, false), this); + } + } + + @Override + public boolean doDie(L2Character killer) { + // Kill the L2NpcInstance (the corpse disappeared after 7 seconds) + if (!super.doDie(killer)) { + return false; + } + + if (killer instanceof L2Attackable) { + // Delayed notification + EventDispatcher.getInstance().notifyEventAsyncDelayed(new AttackableKill(null, this, false), this, _onKillDelay); + } + return true; + } + + @Override + public void addDamageHate(L2Character attacker, int damage, long aggro) { + if (!_isPassive && !(attacker instanceof L2PcInstance)) { + super.addDamageHate(attacker, damage, aggro); + } + } + + public void setPassive(boolean state) { + _isPassive = state; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return _isAutoAttackable && !(attacker instanceof L2PcInstance); + } + + @Override + public void setAutoAttackable(boolean state) { + _isAutoAttackable = state; + } + + public boolean isPassive() { + return _isPassive; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaceManagerInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaceManagerInstance.java new file mode 100644 index 00000000..613b2625 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaceManagerInstance.java @@ -0,0 +1,457 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.MonsterRace; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.audio.Music; +import com.l2jserver.gameserver.enums.audio.Sound; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.knownlist.RaceManagerKnownList; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.DeleteObject; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.MonRaceInfo; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.PlaySound; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Broadcast; + +public class L2RaceManagerInstance extends L2Npc { + public static final int LANES = 8; + public static final int WINDOW_START = 0; + + private static List _managers; + protected static int _raceNumber = 4; + + // Time Constants + private static final long SECOND = 1000; + private static final long MINUTE = 60 * SECOND; + + private static int _minutes = 5; + + // States + private static final int ACCEPTING_BETS = 0; + private static final int WAITING = 1; + private static final int STARTING_RACE = 2; + private static final int RACE_END = 3; + private static int _state = RACE_END; + + protected static final int[][] _codes = { + { + -1, + 0 + }, + { + 0, + 15322 + }, + { + 13765, + -1 + } + }; + private static boolean _notInitialized = true; + protected static MonRaceInfo _packet; + protected static final int[] _cost = { + 100, + 500, + 1000, + 5000, + 10000, + 20000, + 50000, + 100000 + }; + + public L2RaceManagerInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2RaceManagerInstance); + if (_notInitialized) { + _notInitialized = false; + + _managers = new ArrayList<>(); + + ThreadPoolManager s = ThreadPoolManager.getInstance(); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_AVAILABLE_FOR_S1_RACE), 0, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_NOW_AVAILABLE_FOR_S1_RACE), 30 * SECOND, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_AVAILABLE_FOR_S1_RACE), MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_NOW_AVAILABLE_FOR_S1_RACE), MINUTE + (30 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES), 2 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES), 3 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES), 4 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES), 5 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES), 6 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_S1_TICKET_SALES_CLOSED), 7 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_S2_BEGINS_IN_S1_MINUTES), 7 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_S2_BEGINS_IN_S1_MINUTES), 8 * MINUTE, 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_S1_BEGINS_IN_30_SECONDS), (8 * MINUTE) + (30 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_S1_COUNTDOWN_IN_FIVE_SECONDS), (8 * MINUTE) + (50 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS), (8 * MINUTE) + (55 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS), (8 * MINUTE) + (56 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS), (8 * MINUTE) + (57 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS), (8 * MINUTE) + (58 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS), (8 * MINUTE) + (59 * SECOND), 10 * MINUTE); + s.scheduleGeneralAtFixedRate(new Announcement(SystemMessageId.MONSRACE_RACE_START), 9 * MINUTE, 10 * MINUTE); + } + _managers.add(this); + } + + @Override + public final RaceManagerKnownList getKnownList() { + return (RaceManagerKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new RaceManagerKnownList(this)); + } + + class Announcement implements Runnable { + private final SystemMessageId _type; + + public Announcement(SystemMessageId pType) { + _type = pType; + } + + @Override + public void run() { + makeAnnouncement(_type); + } + } + + public void makeAnnouncement(SystemMessageId type) { + SystemMessage sm = SystemMessage.getSystemMessage(type); + switch (type.getId()) { + case 816: // SystemMessageId.MONSRACE_TICKETS_AVAILABLE_FOR_S1_RACE + case 817: // SystemMessageId.MONSRACE_TICKETS_NOW_AVAILABLE_FOR_S1_RACE + if (_state != ACCEPTING_BETS) {// _log.info("Race Initializing"); + _state = ACCEPTING_BETS; + startRace(); + } // else{_log.info("Race open");} + sm.addInt(_raceNumber); + break; + case 818: // SystemMessageId.MONSRACE_TICKETS_STOP_IN_S1_MINUTES + case 820: // SystemMessageId.MONSRACE_S2_BEGINS_IN_S1_MINUTES + case 823: // SystemMessageId.MONSRACE_BEGINS_IN_S1_SECONDS + sm.addInt(_minutes); + if (type.getId() == 820) { + sm.addInt(_raceNumber); + } + _minutes--; + break; + case 819: // SystemMessageId.MONSRACE_S1_TICKET_SALES_CLOSED + sm.addInt(_raceNumber); + _state = WAITING; + _minutes = 2; + break; + case 821: // SystemMessageId.MONSRACE_S1_BEGINS_IN_30_SECONDS + case 822: // SystemMessageId.MONSRACE_S1_COUNTDOWN_IN_FIVE_SECONDS + case 825: // SystemMessageId.MONSRACE_S1_RACE_END + sm.addInt(_raceNumber); + _minutes = 5; + break; + case 826: // SystemMessageId.MONSRACE_FIRST_PLACE_S1_SECOND_S2 + _state = RACE_END; + sm.addInt(MonsterRace.getInstance().getFirstPlace()); + sm.addInt(MonsterRace.getInstance().getSecondPlace()); + break; + } + + broadcast(sm); + + if (type == SystemMessageId.MONSRACE_RACE_START) { + _state = STARTING_RACE; + startRace(); + _minutes = 5; + } + } + + protected void broadcast(L2GameServerPacket pkt) { + for (L2RaceManagerInstance manager : _managers) { + if (!manager.isDead()) { + Broadcast.toKnownPlayers(manager, pkt); + } + } + } + + public void sendMonsterInfo() { + broadcast(_packet); + } + + private void startRace() { + MonsterRace race = MonsterRace.getInstance(); + if (_state == STARTING_RACE) { + PlaySound SRace = Music.S_RACE.getPacket(); + broadcast(SRace); + // TODO find correct sender 121209259, 12125, 182487, -3559 + PlaySound SRace2 = Sound.ITEMSOUND2_RACE_START.getPacket(); + broadcast(SRace2); + _packet = new MonRaceInfo(_codes[1][0], _codes[1][1], race.getMonsters(), race.getSpeeds()); + sendMonsterInfo(); + + ThreadPoolManager.getInstance().scheduleGeneral(new RunRace(), 5000); + } else { + race.newRace(); + race.newSpeeds(); + _packet = new MonRaceInfo(_codes[0][0], _codes[0][1], race.getMonsters(), race.getSpeeds()); + sendMonsterInfo(); + } + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + if (command.startsWith("BuyTicket") && (_state != ACCEPTING_BETS)) { + player.sendPacket(SystemMessageId.MONSRACE_TICKETS_NOT_AVAILABLE); + command = "Chat 0"; + } + if (command.startsWith("ShowOdds") && (_state == ACCEPTING_BETS)) { + player.sendPacket(SystemMessageId.MONSRACE_NO_PAYOUT_INFO); + command = "Chat 0"; + } + + if (command.startsWith("BuyTicket")) { + int val = Integer.parseInt(command.substring(10)); + if (val == 0) { + player.setRace(0, 0); + player.setRace(1, 0); + } + if (((val == 10) && (player.getRace(0) == 0)) || ((val == 20) && (player.getRace(0) == 0) && (player.getRace(1) == 0))) { + val = 0; + } + showBuyTicket(player, val); + } else if (command.equals("ShowOdds")) { + showOdds(player); + } else if (command.equals("ShowInfo")) { + showMonsterInfo(player); + } else if (command.equals("calculateWin")) { + // displayCalculateWinnings(player); + } else if (command.equals("viewHistory")) { + // displayHistory(player); + } else { + // getKnownList().removeKnownObject(player); + super.onBypassFeedback(player, command); + } + } + + public void showOdds(L2PcInstance player) { + if (_state == ACCEPTING_BETS) { + return; + } + int npcId = getTemplate().getId(); + String filename, search; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + filename = getHtmlPath(npcId, 5); + html.setFile(player.getHtmlPrefix(), filename); + for (int i = 0; i < 8; i++) { + int n = i + 1; + search = "Mob" + n; + html.replace(search, MonsterRace.getInstance().getMonsters()[i].getTemplate().getName()); + } + html.replace("1race", String.valueOf(_raceNumber)); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public void showMonsterInfo(L2PcInstance player) { + int npcId = getTemplate().getId(); + String filename, search; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + filename = getHtmlPath(npcId, 6); + html.setFile(player.getHtmlPrefix(), filename); + for (int i = 0; i < 8; i++) { + int n = i + 1; + search = "Mob" + n; + html.replace(search, MonsterRace.getInstance().getMonsters()[i].getTemplate().getName()); + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public void showBuyTicket(L2PcInstance player, int val) { + if (_state != ACCEPTING_BETS) { + return; + } + int npcId = getTemplate().getId(); + SystemMessage sm; + String filename, search, replace; + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + if (val < 10) { + filename = getHtmlPath(npcId, 2); + html.setFile(player.getHtmlPrefix(), filename); + for (int i = 0; i < 8; i++) { + int n = i + 1; + search = "Mob" + n; + html.replace(search, MonsterRace.getInstance().getMonsters()[i].getTemplate().getName()); + } + search = "No1"; + if (val == 0) { + html.replace(search, ""); + } else { + html.replace(search, "" + val); + player.setRace(0, val); + } + } else if (val < 20) { + if (player.getRace(0) == 0) { + return; + } + filename = getHtmlPath(npcId, 3); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("0place", "" + player.getRace(0)); + search = "Mob1"; + replace = MonsterRace.getInstance().getMonsters()[player.getRace(0) - 1].getTemplate().getName(); + html.replace(search, replace); + search = "0adena"; + if (val == 10) { + html.replace(search, ""); + } else { + html.replace(search, "" + _cost[val - 11]); + player.setRace(1, val - 10); + } + } else if (val == 20) { + if ((player.getRace(0) == 0) || (player.getRace(1) == 0)) { + return; + } + filename = getHtmlPath(npcId, 4); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("0place", "" + player.getRace(0)); + search = "Mob1"; + replace = MonsterRace.getInstance().getMonsters()[player.getRace(0) - 1].getTemplate().getName(); + html.replace(search, replace); + search = "0adena"; + int price = _cost[player.getRace(1) - 1]; + html.replace(search, "" + price); + search = "0tax"; + int tax = 0; + html.replace(search, "" + tax); + search = "0total"; + int total = price + tax; + html.replace(search, "" + total); + } else { + if ((player.getRace(0) == 0) || (player.getRace(1) == 0)) { + return; + } + int ticket = player.getRace(0); + int priceId = player.getRace(1); + if (!player.reduceAdena("Race", _cost[priceId - 1], this, true)) { + return; + } + player.setRace(0, 0); + player.setRace(1, 0); + sm = SystemMessage.getSystemMessage(SystemMessageId.ACQUIRED_S1_S2); + sm.addInt(_raceNumber); + sm.addItemName(4443); + player.sendPacket(sm); + final var objectId = IdFactory.getInstance().getNextId(); + final var item = new L2ItemInstance(objectId, 4443); + item.setCount(1); + item.setEnchantLevel(_raceNumber); + item.setCustomType1(ticket); + item.setCustomType2(_cost[priceId - 1] / 100); + player.getInventory().addItem("Race", item, player, this); + InventoryUpdate iu = new InventoryUpdate(); + iu.addItem(item); + final var adenaupdate = player.getInventory().getItemByItemId(Inventory.ADENA_ID); + iu.addModifiedItem(adenaupdate); + player.sendPacket(iu); + return; + } + html.replace("1race", String.valueOf(_raceNumber)); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + public static class Race { + private final Info[] _info; + + public Race(Info[] pInfo) { + _info = pInfo; + } + + public Info getLaneInfo(int lane) { + return _info[lane]; + } + + public static class Info { + private final int _id; + private final int _place; + private final int _odds; + private final int _payout; + + public Info(int pId, int pPlace, int pOdds, int pPayout) { + _id = pId; + _place = pPlace; + _odds = pOdds; + _payout = pPayout; + } + + public int getId() { + return _id; + } + + public int getOdds() { + return _odds; + } + + public int getPayout() { + return _payout; + } + + public int getPlace() { + return _place; + } + } + + } + + class RunRace implements Runnable { + @Override + public void run() { + _packet = new MonRaceInfo(_codes[2][0], _codes[2][1], MonsterRace.getInstance().getMonsters(), MonsterRace.getInstance().getSpeeds()); + sendMonsterInfo(); + ThreadPoolManager.getInstance().scheduleGeneral(new RunEnd(), 30000); + } + } + + class RunEnd implements Runnable { + @Override + public void run() { + makeAnnouncement(SystemMessageId.MONSRACE_FIRST_PLACE_S1_SECOND_S2); + makeAnnouncement(SystemMessageId.MONSRACE_S1_RACE_END); + _raceNumber++; + + for (int i = 0; i < 8; i++) { + broadcast(new DeleteObject(MonsterRace.getInstance().getMonsters()[i])); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaidBossInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaidBossInstance.java new file mode 100644 index 00000000..2e0040be --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RaidBossInstance.java @@ -0,0 +1,146 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.npc; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.RaidBossPointsManager; +import com.l2jserver.gameserver.instancemanager.RaidBossSpawnManager; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Hero; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * This class manages all RaidBoss.
      + * In a group mob, there are one master called RaidBoss and several slaves called Minions. + */ +public class L2RaidBossInstance extends L2MonsterInstance { + private static final int RAID_BOSS_MAINTENANCE_INTERVAL = 30000; // 30 sec + + private RaidBossSpawnManager.StatusEnum _raidStatus; + private boolean _useRaidCurse = true; + + public L2RaidBossInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2RaidBossInstance); + setIsRaid(true); + setLethalable(false); + } + + @Override + public void onSpawn() { + setIsNoRndWalk(true); + super.onSpawn(); + } + + @Override + protected int getMaintenanceInterval() { + return RAID_BOSS_MAINTENANCE_INTERVAL; + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + final L2PcInstance player = killer.getActingPlayer(); + if (player != null) { + broadcastPacket(SystemMessage.getSystemMessage(SystemMessageId.RAID_WAS_SUCCESSFUL)); + if (player.getParty() != null) { + for (L2PcInstance member : player.getParty().getMembers()) { + RaidBossPointsManager.getInstance().addPoints(member, getId(), (getLevel() / 2) + Rnd.get(-5, 5)); + if (member.isNoble()) { + Hero.getInstance().setRBkilled(member.getObjectId(), getId()); + } + } + } else { + RaidBossPointsManager.getInstance().addPoints(player, getId(), (getLevel() / 2) + Rnd.get(-5, 5)); + if (player.isNoble()) { + Hero.getInstance().setRBkilled(player.getObjectId(), getId()); + } + } + } + + RaidBossSpawnManager.getInstance().updateStatus(this, true); + return true; + } + + /** + * Spawn all minions at a regular interval Also if boss is too far from home location at the time of this check, teleport it home. + */ + @Override + protected void startMaintenanceTask() { + _maintenanceTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(this::checkAndReturnToSpawn, 60000, getMaintenanceInterval() + Rnd.get(5000)); + } + + protected void checkAndReturnToSpawn() { + if (isDead() || isMovementDisabled() || !canReturnToSpawnPoint()) { + return; + } + + final L2Spawn spawn = getSpawn(); + if (spawn == null) { + return; + } + + final int spawnX = spawn.getX(); + final int spawnY = spawn.getY(); + final int spawnZ = spawn.getZ(); + + if (!isInCombat() && !isMovementDisabled()) { + if (!isInsideRadius(spawnX, spawnY, spawnZ, Math.max(npc().getMaxDriftRange(), 200), true, false)) { + teleToLocation(spawnX, spawnY, spawnZ, false); + } + } + } + + public void setRaidStatus(RaidBossSpawnManager.StatusEnum status) { + _raidStatus = status; + } + + public RaidBossSpawnManager.StatusEnum getRaidStatus() { + return _raidStatus; + } + + @Override + public float getVitalityPoints(int damage) { + return -super.getVitalityPoints(damage) / 100; + } + + @Override + public boolean useVitalityRate() { + return false; + } + + public void setUseRaidCurse(boolean val) { + _useRaidCurse = val; + } + + @Override + public boolean giveRaidCurse() { + return _useRaidCurse; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RiftInvaderInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RiftInvaderInstance.java new file mode 100644 index 00000000..b8a18ac5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2RiftInvaderInstance.java @@ -0,0 +1,30 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public class L2RiftInvaderInstance extends L2MonsterInstance { + // Not longer needed since rift monster targeting control now is handled by the room zones for any mob + public L2RiftInvaderInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2RiftInvaderInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherMonsterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherMonsterInstance.java new file mode 100644 index 00000000..ca67b3c3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherMonsterInstance.java @@ -0,0 +1,343 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.concurrent.Future; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.network.serverpackets.NpcSay; + +/** + * Sepulcher Monster. + * @author sandman + */ +public class L2SepulcherMonsterInstance extends L2MonsterInstance { + protected static final SkillHolder FAKE_PETRIFICATION = new SkillHolder(4616, 1); + + public int mysteriousBoxId = 0; + + protected Future _victimSpawnKeyBoxTask = null; + protected Future _victimShout = null; + protected Future _changeImmortalTask = null; + protected Future _onDeadEventTask = null; + + public L2SepulcherMonsterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2SepulcherMonsterInstance); + setShowSummonAnimation(true); + switch (template.getId()) { + case 25339, 25342, 25346, 25349 -> setIsRaid(true); + } + } + + @Override + public void onSpawn() { + setShowSummonAnimation(false); + switch (getId()) { + case 18150: + case 18151: + case 18152: + case 18153: + case 18154: + case 18155: + case 18156: + case 18157: + if (_victimSpawnKeyBoxTask != null) { + _victimSpawnKeyBoxTask.cancel(true); + } + _victimSpawnKeyBoxTask = ThreadPoolManager.getInstance().scheduleEffect(new VictimSpawnKeyBox(this), 300000); + if (_victimShout != null) { + _victimShout.cancel(true); + } + _victimShout = ThreadPoolManager.getInstance().scheduleEffect(new VictimShout(this), 5000); + break; + case 18196: + case 18197: + case 18198: + case 18199: + case 18200: + case 18201: + case 18202: + case 18203: + case 18204: + case 18205: + case 18206: + case 18207: + case 18208: + case 18209: + case 18210: + case 18211: + case 18256: + break; + case 18231: + case 18232: + case 18233: + case 18234: + case 18235: + case 18236: + case 18237: + case 18238: + case 18239: + case 18240: + case 18241: + case 18242: + case 18243: + if (_changeImmortalTask != null) { + _changeImmortalTask.cancel(true); + } + _changeImmortalTask = ThreadPoolManager.getInstance().scheduleEffect(new ChangeImmortal(this), 1600); + break; + case 25339: + case 25342: + case 25346: + case 25349: + setIsRaid(true); + break; + } + super.onSpawn(); + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + switch (getId()) { + case 18120: + case 18121: + case 18122: + case 18123: + case 18124: + case 18125: + case 18126: + case 18127: + case 18128: + case 18129: + case 18130: + case 18131: + case 18149: + case 18158: + case 18159: + case 18160: + case 18161: + case 18162: + case 18163: + case 18164: + case 18165: + case 18183: + case 18184: + case 18212: + case 18213: + case 18214: + case 18215: + case 18216: + case 18217: + case 18218: + case 18219: + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + } + _onDeadEventTask = ThreadPoolManager.getInstance().scheduleEffect(new OnDeadEvent(this), 3500); + break; + + case 18150: + case 18151: + case 18152: + case 18153: + case 18154: + case 18155: + case 18156: + case 18157: + if (_victimSpawnKeyBoxTask != null) { + _victimSpawnKeyBoxTask.cancel(true); + _victimSpawnKeyBoxTask = null; + } + if (_victimShout != null) { + _victimShout.cancel(true); + _victimShout = null; + } + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + } + _onDeadEventTask = ThreadPoolManager.getInstance().scheduleEffect(new OnDeadEvent(this), 3500); + break; + + case 18141: + case 18142: + case 18143: + case 18144: + case 18145: + case 18146: + case 18147: + case 18148: + if (FourSepulchersManager.getInstance().isViscountMobsAnnihilated(mysteriousBoxId)) { + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + } + _onDeadEventTask = ThreadPoolManager.getInstance().scheduleEffect(new OnDeadEvent(this), 3500); + } + break; + + case 18220: + case 18221: + case 18222: + case 18223: + case 18224: + case 18225: + case 18226: + case 18227: + case 18228: + case 18229: + case 18230: + case 18231: + case 18232: + case 18233: + case 18234: + case 18235: + case 18236: + case 18237: + case 18238: + case 18239: + case 18240: + if (FourSepulchersManager.getInstance().isDukeMobsAnnihilated(mysteriousBoxId)) { + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + } + _onDeadEventTask = ThreadPoolManager.getInstance().scheduleEffect(new OnDeadEvent(this), 3500); + } + break; + + case 25339: + case 25342: + case 25346: + case 25349: + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + } + _onDeadEventTask = ThreadPoolManager.getInstance().scheduleEffect(new OnDeadEvent(this), 8500); + break; + } + return true; + } + + @Override + public boolean deleteMe() { + if (_victimSpawnKeyBoxTask != null) { + _victimSpawnKeyBoxTask.cancel(true); + _victimSpawnKeyBoxTask = null; + } + if (_onDeadEventTask != null) { + _onDeadEventTask.cancel(true); + _onDeadEventTask = null; + } + + return super.deleteMe(); + } + + private class VictimShout implements Runnable { + private final L2SepulcherMonsterInstance _activeChar; + + public VictimShout(L2SepulcherMonsterInstance activeChar) { + _activeChar = activeChar; + } + + @Override + public void run() { + if (_activeChar.isDead()) { + return; + } + + if (!_activeChar.isVisible()) { + return; + } + + broadcastPacket(new NpcSay(getObjectId(), 0, getId(), "forgive me!!")); + } + } + + private class VictimSpawnKeyBox implements Runnable { + private final L2SepulcherMonsterInstance _activeChar; + + public VictimSpawnKeyBox(L2SepulcherMonsterInstance activeChar) { + _activeChar = activeChar; + } + + @Override + public void run() { + if (_activeChar.isDead()) { + return; + } + + if (!_activeChar.isVisible()) { + return; + } + + FourSepulchersManager.getInstance().spawnKeyBox(_activeChar); + broadcastPacket(new NpcSay(getObjectId(), 0, getId(), "Many thanks for rescue me.")); + if (_victimShout != null) { + _victimShout.cancel(true); + } + + } + } + + private static class OnDeadEvent implements Runnable { + L2SepulcherMonsterInstance _activeChar; + + public OnDeadEvent(L2SepulcherMonsterInstance activeChar) { + _activeChar = activeChar; + } + + @Override + public void run() { + switch (_activeChar.getId()) { + case 18120, 18121, 18122, 18123, 18124, 18125, 18126, 18127, 18128, 18129, 18130, 18131, 18149, 18158, 18159, 18160, 18161, 18162, 18163, 18164, 18165, 18183, 18184, 18212, 18213, 18214, 18215, 18216, 18217, 18218, 18219 -> FourSepulchersManager.getInstance().spawnKeyBox(_activeChar); + case 18150, 18151, 18152, 18153, 18154, 18155, 18156, 18157 -> FourSepulchersManager.getInstance().spawnExecutionerOfHalisha(_activeChar); + case 18141, 18142, 18143, 18144, 18145, 18146, 18147, 18148 -> FourSepulchersManager.getInstance().spawnMonster(_activeChar.mysteriousBoxId); + case 18220, 18221, 18222, 18223, 18224, 18225, 18226, 18227, 18228, 18229, 18230, 18231, 18232, 18233, 18234, 18235, 18236, 18237, 18238, 18239, 18240 -> FourSepulchersManager.getInstance().spawnArchonOfHalisha(_activeChar.mysteriousBoxId); + case 25339, 25342, 25346, 25349 -> FourSepulchersManager.getInstance().spawnEmperorsGraveNpc(_activeChar.mysteriousBoxId); + } + } + } + + private static class ChangeImmortal implements Runnable { + L2SepulcherMonsterInstance activeChar; + + public ChangeImmortal(L2SepulcherMonsterInstance mob) { + activeChar = mob; + } + + @Override + public void run() { + // Invulnerable by petrification + FAKE_PETRIFICATION.getSkill().applyEffects(activeChar, activeChar); + } + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherNpcInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherNpcInstance.java new file mode 100644 index 00000000..5e5ab5a6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SepulcherNpcInstance.java @@ -0,0 +1,345 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.FourSepulchersManager; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.EventType; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcFirstTalk; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.CreatureSay; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.util.Util; + +/** + * Sepulcher. + * @author sandman + */ +public class L2SepulcherNpcInstance extends L2Npc { + private static final Logger LOG = LoggerFactory.getLogger(L2SepulcherNpcInstance.class); + protected Future _closeTask = null; + protected Future _spawnNextMysteriousBoxTask = null; + protected Future _spawnMonsterTask = null; + + private static final String HTML_FILE_PATH = "data/html/SepulcherNpc/"; + private static final int HALLS_KEY = 7260; + + public L2SepulcherNpcInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2SepulcherNpcInstance); + setShowSummonAnimation(true); + + if (_closeTask != null) { + _closeTask.cancel(true); + } + if (_spawnNextMysteriousBoxTask != null) { + _spawnNextMysteriousBoxTask.cancel(true); + } + if (_spawnMonsterTask != null) { + _spawnMonsterTask.cancel(true); + } + _closeTask = null; + _spawnNextMysteriousBoxTask = null; + _spawnMonsterTask = null; + } + + @Override + public void onSpawn() { + super.onSpawn(); + setShowSummonAnimation(false); + } + + @Override + public boolean deleteMe() { + if (_closeTask != null) { + _closeTask.cancel(true); + _closeTask = null; + } + if (_spawnNextMysteriousBoxTask != null) { + _spawnNextMysteriousBoxTask.cancel(true); + _spawnNextMysteriousBoxTask = null; + } + if (_spawnMonsterTask != null) { + _spawnMonsterTask.cancel(true); + _spawnMonsterTask = null; + } + return super.deleteMe(); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if (!canTarget(player)) { + return; + } + + // Check if the L2PcInstance already target the L2NpcInstance + if (this != player.getTarget()) { + if (general().debug()) { + LOG.debug("new target selected: {}", getObjectId()); + } + + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + // Check if the player is attackable (without a forced attack) and + // isn't dead + if (isAutoAttackable(player) && !isAlikeDead()) { + // Check the height difference + if (Math.abs(player.getZ() - getZ()) < 400) // this max height + // difference might + // need some tweaking + { + // Set the L2PcInstance Intention to AI_INTENTION_ATTACK + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } else { + // Send a Server->Client packet ActionFailed (target is out + // of attack range) to the L2PcInstance player + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + + if (!isAutoAttackable(player)) { + // Calculate the distance between the L2PcInstance and the + // L2NpcInstance + if (!canInteract(player)) { + // Notify the L2PcInstance AI with AI_INTENTION_INTERACT + player.getAI().setIntention(CtrlIntention.AI_INTENTION_INTERACT, this); + } else { + // Send a Server->Client packet SocialAction to the all + // L2PcInstance on the _knownPlayer of the L2NpcInstance + // to display a social action of the L2NpcInstance on their + // client + SocialAction sa = new SocialAction(getObjectId(), Rnd.get(8)); + broadcastPacket(sa); + + doAction(player); + } + } + // Send a Server->Client ActionFailed to the L2PcInstance in order + // to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + + private void doAction(L2PcInstance player) { + if (isDead()) { + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + switch (getId()) { + case 31468, 31469, 31470, 31471, 31472, 31473, 31474, 31475, 31476, 31477, 31478, 31479, 31480, 31481, 31482, 31483, 31484, 31485, 31486, 31487 -> { + setIsInvul(false); + reduceCurrentHp(getMaxHp() + 1, player, null); + if (_spawnMonsterTask != null) { + _spawnMonsterTask.cancel(true); + } + _spawnMonsterTask = ThreadPoolManager.getInstance().scheduleEffect(new SpawnMonster(getId()), 3500); + } + case 31455, 31456, 31457, 31458, 31459, 31460, 31461, 31462, 31463, 31464, 31465, 31466, 31467 -> { + setIsInvul(false); + reduceCurrentHp(getMaxHp() + 1, player, null); + if ((player.getParty() != null) && !player.getParty().isLeader(player)) { + player = player.getParty().getLeader(); + } + player.addItem("Quest", HALLS_KEY, 1, player, true); + } + default -> { + if (hasListener(EventType.NPC_QUEST_START)) { + player.setLastQuestNpcObject(getObjectId()); + } + + if (hasListener(EventType.NPC_FIRST_TALK)) { + EventDispatcher.getInstance().notifyEventAsync(new NpcFirstTalk(this, player), this); + } else { + showChatWindow(player, 0); + } + } + } + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public String getHtmlPath(int npcId, int val) { + String pom; + if (val == 0) { + pom = "" + npcId; + } else { + pom = npcId + "-" + val; + } + return HTML_FILE_PATH + pom + ".htm"; + } + + @Override + public void showChatWindow(L2PcInstance player, int val) { + String filename = getHtmlPath(getId(), val); + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), filename); + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + if (isBusy()) { + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), "data/html/npcbusy.htm"); + html.replace("%busymessage%", getBusyMessage()); + html.replace("%npcname%", getName()); + html.replace("%playername%", player.getName()); + player.sendPacket(html); + } else if (command.startsWith("Chat")) { + int val = 0; + try { + val = Integer.parseInt(command.substring(5)); + } catch (Exception ex) { + + } + showChatWindow(player, val); + } else if (command.startsWith("open_gate")) { + L2ItemInstance hallsKey = player.getInventory().getItemByItemId(HALLS_KEY); + if (hallsKey == null) { + showHtmlFile(player, "Gatekeeper-no.htm"); + } else if (FourSepulchersManager.getInstance().isAttackTime()) { + switch (getId()) { + case 31929: + case 31934: + case 31939: + case 31944: + FourSepulchersManager.getInstance().spawnShadow(getId()); + default: { + openNextDoor(getId()); + if (player.getParty() != null) { + for (L2PcInstance mem : player.getParty().getMembers()) { + if ((mem != null) && (mem.getInventory().getItemByItemId(HALLS_KEY) != null)) { + mem.destroyItemByItemId("Quest", HALLS_KEY, mem.getInventory().getItemByItemId(HALLS_KEY).getCount(), mem, true); + } + } + } else { + player.destroyItemByItemId("Quest", HALLS_KEY, hallsKey.getCount(), player, true); + } + } + } + } + } else { + super.onBypassFeedback(player, command); + } + } + + public void openNextDoor(int npcId) { + int doorId = FourSepulchersManager.getInstance().getHallGateKeepers().get(npcId); + DoorData _doorTable = DoorData.getInstance(); + _doorTable.getDoor(doorId).openMe(); + + if (_closeTask != null) { + _closeTask.cancel(true); + } + _closeTask = ThreadPoolManager.getInstance().scheduleEffect(new CloseNextDoor(doorId), 10000); + if (_spawnNextMysteriousBoxTask != null) { + _spawnNextMysteriousBoxTask.cancel(true); + } + _spawnNextMysteriousBoxTask = ThreadPoolManager.getInstance().scheduleEffect(new SpawnNextMysteriousBox(npcId), 0); + } + + private static class CloseNextDoor implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(CloseNextDoor.class); + final DoorData _DoorTable = DoorData.getInstance(); + + private final int _DoorId; + + public CloseNextDoor(int doorId) { + _DoorId = doorId; + } + + @Override + public void run() { + try { + _DoorTable.getDoor(_DoorId).closeMe(); + } catch (Exception ex) { + LOG.warn("Error running task close next door!", ex); + } + } + } + + private static class SpawnNextMysteriousBox implements Runnable { + private final int _NpcId; + + public SpawnNextMysteriousBox(int npcId) { + _NpcId = npcId; + } + + @Override + public void run() { + FourSepulchersManager.getInstance().spawnMysteriousBox(_NpcId); + } + } + + private static class SpawnMonster implements Runnable { + private final int _NpcId; + + public SpawnMonster(int npcId) { + _NpcId = npcId; + } + + @Override + public void run() { + FourSepulchersManager.getInstance().spawnMonster(_NpcId); + } + } + + public void sayInShout(NpcStringId msg) { + if (msg == null) { + return;// wrong usage + } + + final CreatureSay creatureSay = new CreatureSay(0, Say2.NPC_SHOUT, getName(), msg); + for (L2PcInstance player : L2World.getInstance().getPlayers()) { + if (Util.checkIfInRange(15000, player, this, true)) { + player.sendPacket(creatureSay); + } + } + } + + public void showHtmlFile(L2PcInstance player, String file) { + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + html.setFile(player.getHtmlPrefix(), "data/html/SepulcherNpc/" + file); + html.replace("%npcname%", getName()); + player.sendPacket(html); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ServitorInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ServitorInstance.java new file mode 100644 index 00000000..d9198915 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2ServitorInstance.java @@ -0,0 +1,342 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.concurrent.Future; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.dao.factory.impl.DAOFactory; +import com.l2jserver.gameserver.data.sql.impl.CharSummonTable; +import com.l2jserver.gameserver.data.sql.impl.SummonEffectsTable; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SetSummonRemainTime; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Servitor. + * @author UnAfraid + */ +public class L2ServitorInstance extends L2Summon implements Runnable { + private float _expMultiplier = 0; + private ItemHolder _itemConsume; + private int _lifeTime; + private int _lifeTimeRemaining; + private int _consumeItemInterval; + private int _consumeItemIntervalRemaining; + protected Future _summonLifeTask; + + private int _referenceSkill; + + public L2ServitorInstance(int objectId, L2NpcTemplate template, L2PcInstance owner) { + super(objectId, template, owner); + setInstanceType(InstanceType.L2ServitorInstance); + setShowSummonAnimation(true); + } + + @Override + public void onSpawn() { + super.onSpawn(); + if (_summonLifeTask == null) { + _summonLifeTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(this, 0, 5000); + } + } + + @Override + public final int getLevel() { + return (getTemplate() != null ? getTemplate().getLevel() : 0); + } + + @Override + public int getSummonType() { + return 1; + } + + public void setExpMultiplier(float expMultiplier) { + _expMultiplier = expMultiplier; + } + + public float getExpMultiplier() { + return _expMultiplier; + } + + public void setItemConsume(ItemHolder item) { + _itemConsume = item; + } + + public ItemHolder getItemConsume() { + return _itemConsume; + } + + public void setItemConsumeInterval(int interval) { + _consumeItemInterval = interval; + _consumeItemIntervalRemaining = interval; + } + + public int getItemConsumeInterval() { + return _consumeItemInterval; + } + + public void setLifeTime(int lifeTime) { + _lifeTime = lifeTime; + _lifeTimeRemaining = lifeTime; + } + + public int getLifeTime() { + return _lifeTime; + } + + public void setLifeTimeRemaining(int time) { + _lifeTimeRemaining = time; + } + + public int getLifeTimeRemaining() { + return _lifeTimeRemaining; + } + + public void setReferenceSkill(int skillId) { + _referenceSkill = skillId; + } + + public int getReferenceSkill() { + return _referenceSkill; + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + if (_summonLifeTask != null) { + _summonLifeTask.cancel(false); + } + + CharSummonTable.getInstance().removeServitor(getOwner()); + return true; + + } + + @Override + public void doPickupItem(L2Object object) { + + } + + @Override + public final void stopSkillEffects(boolean removed, int skillId) { + super.stopSkillEffects(removed, skillId); + SummonEffectsTable.getInstance().removeServitorEffects(getOwner(), getReferenceSkill(), skillId); + } + + @Override + public void storeMe() { + if ((_referenceSkill == 0) || isDead()) { + return; + } + + if (character().restoreServitorOnReconnect()) { + CharSummonTable.getInstance().saveSummon(this); + } + } + + @Override + public void storeEffect(boolean storeEffects) { + if (!character().summonStoreSkillCooltime()) { + return; + } + + if ((getOwner() == null) || getOwner().isInOlympiadMode()) { + return; + } + + // Clear list for overwrite + SummonEffectsTable.getInstance().clearServitorEffects(getOwner(), getReferenceSkill()); + + DAOFactory.getInstance().getServitorSkillSaveDAO().insert(this, storeEffects); + } + + @Override + public void restoreEffects() { + if (getOwner().isInOlympiadMode()) { + return; + } + + DAOFactory.getInstance().getServitorSkillSaveDAO().load(this); + + SummonEffectsTable.getInstance().applyServitorEffects(this, getOwner(), getReferenceSkill()); + } + + @Override + public void unSummon(L2PcInstance owner) { + if (_summonLifeTask != null) { + _summonLifeTask.cancel(false); + } + + super.unSummon(owner); + + if (!_restoreSummon) { + CharSummonTable.getInstance().removeServitor(owner); + } + } + + @Override + public boolean destroyItem(String process, int objectId, long count, L2Object reference, boolean sendMessage) { + return getOwner().destroyItem(process, objectId, count, reference, sendMessage); + } + + @Override + public boolean destroyItemByItemId(String process, int itemId, long count, L2Object reference, boolean sendMessage) { + return getOwner().destroyItemByItemId(process, itemId, count, reference, sendMessage); + } + + @Override + public byte getAttackElement() { + if (getOwner() != null) { + return getOwner().getAttackElement(); + } + return super.getAttackElement(); + } + + @Override + public int getAttackElementValue(byte attackAttribute) { + if (getOwner() != null) { + return (getOwner().getAttackElementValue(attackAttribute)); + } + return super.getAttackElementValue(attackAttribute); + } + + @Override + public int getDefenseElementValue(byte defenseAttribute) { + if (getOwner() != null) { + return (getOwner().getDefenseElementValue(defenseAttribute)); + } + return super.getDefenseElementValue(defenseAttribute); + } + + @Override + public boolean isServitor() { + return true; + } + + @Override + public void run() { + int usedTime = 5000; + _lifeTimeRemaining -= usedTime; + + if (isDead() || !isVisible()) { + if (_summonLifeTask != null) { + _summonLifeTask.cancel(false); + } + return; + } + + // check if the summon's lifetime has ran out + if (_lifeTimeRemaining < 0) { + sendPacket(SystemMessageId.SERVITOR_PASSED_AWAY); + unSummon(getOwner()); + return; + } + + if (_consumeItemInterval > 0) { + _consumeItemIntervalRemaining -= usedTime; + + // check if it is time to consume another item + if ((_consumeItemIntervalRemaining <= 0) && (getItemConsume().getCount() > 0) && (getItemConsume().getId() > 0) && !isDead()) { + if (destroyItemByItemId("Consume", getItemConsume().getId(), getItemConsume().getCount(), this, false)) { + final SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.SUMMONED_MOB_USES_S1); + msg.addItemName(getItemConsume().getId()); + sendPacket(msg); + + // Reset + _consumeItemIntervalRemaining = _consumeItemInterval; + } else { + sendPacket(SystemMessageId.SERVITOR_DISAPPEARED_NOT_ENOUGH_ITEMS); + unSummon(getOwner()); + } + } + } + + sendPacket(new SetSummonRemainTime(getLifeTime(), _lifeTimeRemaining)); + updateEffectIcons(); + } + + @Override + public double getMAtk(L2Character target, Skill skill) { + return super.getMAtk(target, skill) + (getActingPlayer().getMAtk(target, skill) * (getActingPlayer().getServitorShareBonus(Stats.MAGIC_ATTACK) - 1.0)); + } + + @Override + public double getMDef(L2Character target, Skill skill) { + return super.getMDef(target, skill) + (getActingPlayer().getMDef(target, skill) * (getActingPlayer().getServitorShareBonus(Stats.MAGIC_DEFENCE) - 1.0)); + } + + @Override + public double getPAtk(L2Character target) { + return super.getPAtk(target) + (getActingPlayer().getPAtk(target) * (getActingPlayer().getServitorShareBonus(Stats.POWER_ATTACK) - 1.0)); + } + + @Override + public double getPDef(L2Character target) { + return super.getPDef(target) + (getActingPlayer().getPDef(target) * (getActingPlayer().getServitorShareBonus(Stats.POWER_DEFENCE) - 1.0)); + } + + @Override + public int getMAtkSpd() { + return (int) (super.getMAtkSpd() + (getActingPlayer().getMAtkSpd() * (getActingPlayer().getServitorShareBonus(Stats.MAGIC_ATTACK_SPEED) - 1.0))); + } + + @Override + public int getMaxHp() { + return (int) (super.getMaxHp() + (getActingPlayer().getMaxHp() * (getActingPlayer().getServitorShareBonus(Stats.MAX_HP) - 1.0))); + } + + @Override + public int getMaxMp() { + return (int) (super.getMaxMp() + (getActingPlayer().getMaxMp() * (getActingPlayer().getServitorShareBonus(Stats.MAX_MP) - 1.0))); + } + + @Override + public int getCriticalHit(L2Character target, Skill skill) { + return (int) (super.getCriticalHit(target, skill) + ((getActingPlayer().getCriticalHit(target, skill)) * (getActingPlayer().getServitorShareBonus(Stats.CRITICAL_RATE) - 1.0))); + } + + @Override + public double getPAtkSpd() { + return super.getPAtkSpd() + (getActingPlayer().getPAtkSpd() * (getActingPlayer().getServitorShareBonus(Stats.POWER_ATTACK_SPEED) - 1.0)); + } + + @Override + public int getMaxRecoverableHp() { + return (int) calcStat(Stats.MAX_RECOVERABLE_HP, getMaxHp()); + } + + @Override + public int getMaxRecoverableMp() { + return (int) calcStat(Stats.MAX_RECOVERABLE_MP, getMaxMp()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SiegeFlagInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SiegeFlagInstance.java new file mode 100644 index 00000000..91cc1c60 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2SiegeFlagInstance.java @@ -0,0 +1,188 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.ClanHallSiegeManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2SiegeClan; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.status.SiegeFlagStatus; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.entity.Siegable; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class L2SiegeFlagInstance extends L2Npc { + private final L2Clan _clan; + private Siegable _siege; + private final boolean _isAdvanced; + private boolean _canTalk; + + public L2SiegeFlagInstance(int objectId, L2PcInstance player, L2NpcTemplate template, boolean advanced, boolean outPost) { + super(objectId, template); + setInstanceType(InstanceType.L2SiegeFlagInstance); + + if (TerritoryWarManager.getInstance().isTWInProgress()) { + _clan = player.getClan(); + _canTalk = false; + if (_clan == null) { + deleteMe(); + } + if (outPost) { + _isAdvanced = false; + setIsInvul(true); + } else { + _isAdvanced = advanced; + setIsInvul(false); + } + getStatus(); + return; + } + _clan = player.getClan(); + _canTalk = true; + _siege = SiegeManager.getInstance().getSiege(player.getX(), player.getY(), player.getZ()); + if (_siege == null) { + _siege = FortSiegeManager.getInstance().getSiege(player.getX(), player.getY(), player.getZ()); + } + if (_siege == null) { + _siege = ClanHallSiegeManager.getInstance().getSiege(player); + } + if ((_clan == null) || (_siege == null)) { + throw new NullPointerException(getClass().getSimpleName() + ": Initialization failed."); + } + + L2SiegeClan sc = _siege.getAttackerClan(_clan); + if (sc == null) { + throw new NullPointerException(getClass().getSimpleName() + ": Cannot find siege clan."); + } + + sc.addFlag(this); + _isAdvanced = advanced; + getStatus(); + setIsInvul(false); + } + + @Override + public boolean canBeAttacked() { + return !(isInvul() || isHpBlocked()); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return !(isInvul() || isHpBlocked()); + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + if ((_siege != null) && (_clan != null)) { + L2SiegeClan sc = _siege.getAttackerClan(_clan); + if (sc != null) { + sc.removeFlag(this); + } + } else if (_clan != null) { + TerritoryWarManager.getInstance().removeClanFlag(_clan); + } + return true; + } + + @Override + public void onForcedAttack(L2PcInstance player) { + onAction(player); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if ((player == null) || !canTarget(player)) { + return; + } + + // Check if the L2PcInstance already target the L2NpcInstance + if (this != player.getTarget()) { + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + if (isAutoAttackable(player) && (Math.abs(player.getZ() - getZ()) < 100)) { + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } else { + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + } + + public boolean isAdvancedHeadquarter() { + return _isAdvanced; + } + + @Override + public SiegeFlagStatus getStatus() { + return (SiegeFlagStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new SiegeFlagStatus(this)); + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, Skill skill) { + super.reduceCurrentHp(damage, attacker, skill); + if (canTalk()) { + if (((getCastle() != null) && getCastle().getSiege().isInProgress()) || ((getFort() != null) && getFort().getSiege().isInProgress()) || ((getConquerableHall() != null) && getConquerableHall().isInSiege())) { + if (_clan != null) { + // send warning to owners of headquarters that theirs base is under attack + _clan.broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.BASE_UNDER_ATTACK)); + setCanTalk(false); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleTalkTask(), 20000); + } + } + } + } + + private class ScheduleTalkTask implements Runnable { + + public ScheduleTalkTask() { + } + + @Override + public void run() { + setCanTalk(true); + } + } + + void setCanTalk(boolean val) { + _canTalk = val; + } + + private boolean canTalk() { + return _canTalk; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2StaticObjectInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2StaticObjectInstance.java new file mode 100644 index 00000000..67061b81 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2StaticObjectInstance.java @@ -0,0 +1,194 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.knownlist.StaticObjectKnownList; +import com.l2jserver.gameserver.model.actor.stat.StaticObjStat; +import com.l2jserver.gameserver.model.actor.status.StaticObjStatus; +import com.l2jserver.gameserver.model.actor.templates.L2CharTemplate; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.ShowTownMap; +import com.l2jserver.gameserver.network.serverpackets.StaticObject; + +/** + * Static Object instance. + * @author godson + */ +public final class L2StaticObjectInstance extends L2Character { + /** The interaction distance of the L2StaticObjectInstance */ + public static final int INTERACTION_DISTANCE = 150; + public static final int MAP = 0; + public static final int CHAIR_THRONE = 1; + public static final int SIGNBOARD = 2; + public static final int FLAGPOLE = 3; + + private final int _staticObjectId; + private int _meshIndex = 0; // 0 - static objects, alternate static objects + private int _type = -1; // 0 - map signs, 1 - throne , 2 - arena signs, 3 - flagpole + private ShowTownMap _map; + + public L2StaticObjectInstance(int objectId, L2CharTemplate template, int staticId) { + super(objectId, template); + setInstanceType(InstanceType.L2StaticObjectInstance); + _staticObjectId = staticId; + } + + @Override + protected L2CharacterAI initAI() { + return null; + } + + @Override + public int getId() { + return _staticObjectId; + } + + @Override + public StaticObjectKnownList getKnownList() { + return (StaticObjectKnownList) super.getKnownList(); + } + + @Override + public void initKnownList() { + setKnownList(new StaticObjectKnownList(this)); + } + + @Override + public StaticObjStat getStat() { + return (StaticObjStat) super.getStat(); + } + + @Override + public void initCharStat() { + setStat(new StaticObjStat(this)); + } + + @Override + public StaticObjStatus getStatus() { + return (StaticObjStatus) super.getStatus(); + } + + @Override + public void initCharStatus() { + setStatus(new StaticObjStatus(this)); + } + + public int getType() { + return _type; + } + + public void setType(int type) { + _type = type; + } + + public void setMap(String texture, int x, int y) { + _map = new ShowTownMap("town_map." + texture, x, y); + } + + public ShowTownMap getMap() { + return _map; + } + + @Override + public int getLevel() { + return 1; + } + + @Override + public L2ItemInstance getActiveWeaponInstance() { + return null; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return false; + } + + /** + * Set the meshIndex of the object.
      + * Values : + *
        + *
      • default textures : 0
      • + *
      • alternate textures : 1
      • + *
      + * @param meshIndex + */ + public void setMeshIndex(int meshIndex) { + _meshIndex = meshIndex; + this.broadcastPacket(new StaticObject(this)); + } + + /** + * Values : + *
        + *
      • default textures : 0
      • + *
      • alternate textures : 1
      • + *
      + * @return the meshIndex of the object + */ + public int getMeshIndex() { + return _meshIndex; + } + + @Override + public void updateAbnormalEffect() { + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + activeChar.sendPacket(new StaticObject(this)); + } + + @Override + public void moveToLocation(int x, int y, int z, int offset) { + } + + @Override + public void stopMove(Location loc) { + } + + @Override + public void doAttack(L2Character target) { + } + + @Override + public void doCast(Skill skill) { + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TamedBeastInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TamedBeastInstance.java new file mode 100644 index 00000000..9138ae80 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TamedBeastInstance.java @@ -0,0 +1,485 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.ai.CtrlIntention.AI_INTENTION_IDLE; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.StopMove; + +// While a tamed beast behaves a lot like a pet (ingame) and does have +// an owner, in all other aspects, it acts like a mob. +// In addition, it can be fed in order to increase its duration. +// This class handles the running tasks, AI, and feed of the mob. +// The (mostly optional) AI on feeding the spawn is handled by the datapack ai script +public final class L2TamedBeastInstance extends L2FeedableBeastInstance { + private int _foodSkillId; + private static final int MAX_DISTANCE_FROM_HOME = 30000; + private static final int MAX_DISTANCE_FROM_OWNER = 2000; + private static final int MAX_DURATION = 1200000; // 20 minutes + private static final int DURATION_CHECK_INTERVAL = 60000; // 1 minute + private static final int DURATION_INCREASE_INTERVAL = 20000; // 20 secs (gained upon feeding) + private static final int BUFF_INTERVAL = 5000; // 5 seconds + private int _remainingTime = MAX_DURATION; + private int _homeX, _homeY, _homeZ; + protected L2PcInstance _owner; + private Future _buffTask = null; + private Future _durationCheckTask = null; + protected boolean _isFreyaBeast; + private List _beastSkills = null; + + public L2TamedBeastInstance(int objectId, int npcTemplateId) { + super(objectId, NpcData.getInstance().getTemplate(npcTemplateId)); + setInstanceType(InstanceType.L2TamedBeastInstance); + setHome(this); + } + + public L2TamedBeastInstance(int objectId, int npcTemplateId, L2PcInstance owner, int foodSkillId, int x, int y, int z) { + super(objectId, NpcData.getInstance().getTemplate(npcTemplateId)); + _isFreyaBeast = false; + setInstanceType(InstanceType.L2TamedBeastInstance); + setCurrentHp(getMaxHp()); + setCurrentMp(getMaxMp()); + setOwner(owner); + setFoodType(foodSkillId); + setHome(x, y, z); + spawnMe(x, y, z); + } + + public L2TamedBeastInstance(int objectId, int npcTemplateId, L2PcInstance owner, int food, int x, int y, int z, boolean isFreyaBeast) { + super(objectId, NpcData.getInstance().getTemplate(npcTemplateId)); + _isFreyaBeast = isFreyaBeast; + setInstanceType(InstanceType.L2TamedBeastInstance); + setCurrentHp(getMaxHp()); + setCurrentMp(getMaxMp()); + setFoodType(food); + setHome(x, y, z); + spawnMe(x, y, z); + setOwner(owner); + if (isFreyaBeast) { + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner); + } + } + + public void onReceiveFood() { + // Eating food extends the duration by 20secs, to a max of 20minutes + _remainingTime = _remainingTime + DURATION_INCREASE_INTERVAL; + if (_remainingTime > MAX_DURATION) { + _remainingTime = MAX_DURATION; + } + } + + public Location getHome() { + return new Location(_homeX, _homeY, _homeZ); + } + + public void setHome(int x, int y, int z) { + _homeX = x; + _homeY = y; + _homeZ = z; + } + + public void setHome(L2Character c) { + setHome(c.getX(), c.getY(), c.getZ()); + } + + public int getRemainingTime() { + return _remainingTime; + } + + public void setRemainingTime(int duration) { + _remainingTime = duration; + } + + public int getFoodType() { + return _foodSkillId; + } + + public void setFoodType(int foodItemId) { + if (foodItemId > 0) { + _foodSkillId = foodItemId; + + // start the duration checks + // start the buff tasks + if (_durationCheckTask != null) { + _durationCheckTask.cancel(true); + } + _durationCheckTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new CheckDuration(this), DURATION_CHECK_INTERVAL, DURATION_CHECK_INTERVAL); + } + } + + @Override + public boolean doDie(L2Character killer) { + if (!super.doDie(killer)) { + return false; + } + + getAI().stopFollow(); + if (_buffTask != null) { + _buffTask.cancel(true); + } + if (_durationCheckTask != null) { + _durationCheckTask.cancel(true); + } + + if (_owner != null) { + _owner.removeTamedBeast(this); + } + _buffTask = null; + _durationCheckTask = null; + _owner = null; + _foodSkillId = 0; + _remainingTime = 0; + return true; + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + return !_isFreyaBeast; + } + + public boolean isFreyaBeast() { + return _isFreyaBeast; + } + + public void addBeastSkill(Skill skill) { + if (_beastSkills == null) { + _beastSkills = new CopyOnWriteArrayList<>(); + } + _beastSkills.add(skill); + } + + public void castBeastSkills() { + if ((_owner == null) || (_beastSkills == null)) { + return; + } + int delay = 100; + for (Skill skill : _beastSkills) { + ThreadPoolManager.getInstance().scheduleGeneral(new buffCast(skill), delay); + delay += (100 + skill.getHitTime()); + } + ThreadPoolManager.getInstance().scheduleGeneral(new buffCast(null), delay); + } + + private class buffCast implements Runnable { + private final Skill _skill; + + public buffCast(Skill skill) { + _skill = skill; + } + + @Override + public void run() { + if (_skill == null) { + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner); + } else { + sitCastAndFollow(_skill, _owner); + } + } + } + + public L2PcInstance getOwner() { + return _owner; + } + + public void setOwner(L2PcInstance owner) { + if (owner != null) { + _owner = owner; + setTitle(owner.getName()); + // broadcast the new title + setShowSummonAnimation(true); + broadcastPacket(new AbstractNpcInfo.NpcInfo(this, owner)); + + owner.addTamedBeast(this); + + // always and automatically follow the owner. + getAI().startFollow(_owner, 100); + + if (!_isFreyaBeast) { + // instead of calculating this value each time, let's get this now and pass it on + int totalBuffsAvailable = 0; + for (Skill skill : getTemplate().getSkills().values()) { + // if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ] + if (skill.isContinuous() && !skill.isDebuff()) { + totalBuffsAvailable++; + } + } + + // start the buff tasks + if (_buffTask != null) { + _buffTask.cancel(true); + } + _buffTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new CheckOwnerBuffs(this, totalBuffsAvailable), BUFF_INTERVAL, BUFF_INTERVAL); + } + } else { + deleteMe(); // despawn if no owner + } + } + + public boolean isTooFarFromHome() { + return !isInsideRadius(_homeX, _homeY, _homeZ, MAX_DISTANCE_FROM_HOME, true, true); + } + + @Override + public boolean deleteMe() { + if (_buffTask != null) { + _buffTask.cancel(true); + } + _durationCheckTask.cancel(true); + stopHpMpRegeneration(); + + if (_owner != null) { + _owner.removeTamedBeast(this); + } + setTarget(null); + _buffTask = null; + _durationCheckTask = null; + _owner = null; + _foodSkillId = 0; + _remainingTime = 0; + + // remove the spawn + return super.deleteMe(); + } + + // notification triggered by the owner when the owner is attacked. + // tamed mobs will heal/recharge or debuff the enemy according to their skills + public void onOwnerGotAttacked(L2Character attacker) { + // check if the owner is no longer around...if so, despawn + if ((_owner == null) || !_owner.isOnline()) { + deleteMe(); + return; + } + // if the owner is too far away, stop anything else and immediately run towards the owner. + if (!_owner.isInsideRadius(this, MAX_DISTANCE_FROM_OWNER, true, true)) { + getAI().startFollow(_owner); + return; + } + // if the owner is dead, do nothing... + if (_owner.isDead() || _isFreyaBeast) { + return; + } + + // if the tamed beast is currently in the middle of casting, let it complete its skill... + if (isCastingNow()) { + return; + } + + float HPRatio = ((float) _owner.getCurrentHp()) / _owner.getMaxHp(); + + // if the owner has a lot of HP, then debuff the enemy with a random debuff among the available skills + // use of more than one debuff at this moment is acceptable + if (HPRatio >= 0.8) { + for (Skill skill : getTemplate().getSkills().values()) { + // if the skill is a debuff, check if the attacker has it already [ attacker.getEffect(L2Skill skill) ] + if (skill.isDebuff() && (Rnd.get(3) < 1) && ((attacker != null) && attacker.isAffectedBySkill(skill.getId()))) { + sitCastAndFollow(skill, attacker); + } + } + } + // for HP levels between 80% and 50%, do not react to attack events (so that MP can regenerate a bit) + // for lower HP ranges, heal or recharge the owner with 1 skill use per attack. + else if (HPRatio < 0.5) { + int chance = 1; + if (HPRatio < 0.25) { + chance = 2; + } + + // if the owner has a lot of HP, then debuff the enemy with a random debuff among the available skills + for (Skill skill : getTemplate().getSkills().values()) { + // if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ] + if ((Rnd.get(5) < chance) && skill.hasEffectType(L2EffectType.CP, L2EffectType.HP, L2EffectType.MANAHEAL_BY_LEVEL, L2EffectType.MANAHEAL_PERCENT)) { + sitCastAndFollow(skill, _owner); + } + } + } + } + + /** + * Prepare and cast a skill:
      + * First smoothly prepare the beast for casting, by abandoning other actions.
      + * Next, call super.doCast(skill) in order to actually cast the spell.
      + * Finally, return to auto-following the owner. + * @param skill + * @param target + */ + protected void sitCastAndFollow(Skill skill, L2Character target) { + stopMove(null); + broadcastPacket(new StopMove(this)); + getAI().setIntention(AI_INTENTION_IDLE); + + setTarget(target); + doCast(skill); + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _owner); + } + + private static class CheckDuration implements Runnable { + private final L2TamedBeastInstance _tamedBeast; + + CheckDuration(L2TamedBeastInstance tamedBeast) { + _tamedBeast = tamedBeast; + } + + @Override + public void run() { + int foodTypeSkillId = _tamedBeast.getFoodType(); + L2PcInstance owner = _tamedBeast.getOwner(); + + L2ItemInstance item = null; + if (_tamedBeast._isFreyaBeast) { + item = owner.getInventory().getItemByItemId(foodTypeSkillId); + if ((item != null) && (item.getCount() >= 1)) { + owner.destroyItem("BeastMob", item, 1, _tamedBeast, true); + _tamedBeast.broadcastPacket(new SocialAction(_tamedBeast.getObjectId(), 3)); + } else { + _tamedBeast.deleteMe(); + } + } else { + _tamedBeast.setRemainingTime(_tamedBeast.getRemainingTime() - DURATION_CHECK_INTERVAL); + // I tried to avoid this as much as possible...but it seems I can't avoid hardcoding + // ids further, except by carrying an additional variable just for these two lines... + // Find which food item needs to be consumed. + if (foodTypeSkillId == 2188) { + item = owner.getInventory().getItemByItemId(6643); + } else if (foodTypeSkillId == 2189) { + item = owner.getInventory().getItemByItemId(6644); + } + + // if the owner has enough food, call the item handler (use the food and triffer all necessary actions) + if ((item != null) && (item.getCount() >= 1)) { + L2Object oldTarget = owner.getTarget(); + owner.setTarget(_tamedBeast); + + // emulate a call to the owner using food, but bypass all checks for range, etc + // this also causes a call to the AI tasks handling feeding, which may call onReceiveFood as required. + owner.callSkill(SkillData.getInstance().getSkill(foodTypeSkillId, 1), List.of(_tamedBeast)); + owner.setTarget(oldTarget); + } else { + // if the owner has no food, the beast immediately despawns, except when it was only + // newly spawned. Newly spawned beasts can last up to 5 minutes + if (_tamedBeast.getRemainingTime() < (MAX_DURATION - 300000)) { + _tamedBeast.setRemainingTime(-1); + } + } + // There are too many conflicting reports about whether distance from home should be taken into consideration. Disabled for now. + // if (_tamedBeast.isTooFarFromHome()) + // _tamedBeast.setRemainingTime(-1); + + if (_tamedBeast.getRemainingTime() <= 0) { + _tamedBeast.deleteMe(); + } + } + } + } + + private class CheckOwnerBuffs implements Runnable { + private final L2TamedBeastInstance _tamedBeast; + private final int _numBuffs; + + CheckOwnerBuffs(L2TamedBeastInstance tamedBeast, int numBuffs) { + _tamedBeast = tamedBeast; + _numBuffs = numBuffs; + } + + @Override + public void run() { + L2PcInstance owner = _tamedBeast.getOwner(); + + // check if the owner is no longer around...if so, despawn + if ((owner == null) || !owner.isOnline()) { + deleteMe(); + return; + } + // if the owner is too far away, stop anything else and immediately run towards the owner. + if (!isInsideRadius(owner, MAX_DISTANCE_FROM_OWNER, true, true)) { + getAI().startFollow(owner); + return; + } + // if the owner is dead, do nothing... + if (owner.isDead()) { + return; + } + // if the tamed beast is currently casting a spell, do not interfere (do not attempt to cast anything new yet). + if (isCastingNow()) { + return; + } + + int totalBuffsOnOwner = 0; + int i = 0; + int rand = Rnd.get(_numBuffs); + Skill buffToGive = null; + + // get this npc's skills: getSkills() + for (Skill skill : _tamedBeast.getTemplate().getSkills().values()) { + // if the skill is a buff, check if the owner has it already [ owner.getEffect(L2Skill skill) ] + if (skill.isContinuous() && !skill.isDebuff()) { + if (i++ == rand) { + buffToGive = skill; + } + if (owner.isAffectedBySkill(skill.getId())) { + totalBuffsOnOwner++; + } + } + } + // if the owner has less than 60% of this beast's available buff, cast a random buff + if (((_numBuffs * 2) / 3) > totalBuffsOnOwner) { + _tamedBeast.sitCastAndFollow(buffToGive, owner); + } + getAI().setIntention(CtrlIntention.AI_INTENTION_FOLLOW, _tamedBeast.getOwner()); + } + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if ((player == null) || !canTarget(player)) { + return; + } + + // Check if the L2PcInstance already target the L2NpcInstance + if (this != player.getTarget()) { + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + if (isAutoAttackable(player) && (Math.abs(player.getZ() - getZ()) < 100)) { + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } else { + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TeleporterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TeleporterInstance.java new file mode 100644 index 00000000..8a78f717 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TeleporterInstance.java @@ -0,0 +1,34 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +/** + * @author NightMarez + */ +public final class L2TeleporterInstance extends L2Npc { + + public L2TeleporterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2TeleporterInstance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerrainObjectInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerrainObjectInstance.java new file mode 100644 index 00000000..955709ad --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerrainObjectInstance.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; + +public final class L2TerrainObjectInstance extends L2Npc { + + public L2TerrainObjectInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2TerrainObjectInstance); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + + @Override + public void onActionShift(L2PcInstance player) { + if (player.isGM()) { + super.onActionShift(player); + } else { + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerritoryWardInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerritoryWardInstance.java new file mode 100644 index 00000000..b4bfee62 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TerritoryWardInstance.java @@ -0,0 +1,155 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public final class L2TerritoryWardInstance extends L2Attackable { + private static final Logger LOG = LoggerFactory.getLogger(L2TerritoryWardInstance.class); + + public L2TerritoryWardInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + + disableCoreAI(true); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + if (isInvul()) { + return false; + } + if ((getCastle() == null) || !getCastle().getZone().isActive()) { + return false; + } + + final L2PcInstance actingPlayer = attacker.getActingPlayer(); + if (actingPlayer == null) { + return false; + } + if (actingPlayer.getSiegeSide() == 0) { + return false; + } + return !TerritoryWarManager.getInstance().isAllyField(actingPlayer, getCastle().getResidenceId()); + } + + @Override + public boolean hasRandomAnimation() { + return false; + } + + @Override + public void onSpawn() { + super.onSpawn(); + + if (getCastle() == null) { + LOG.warn("L2TerritoryWardInstance({}) spawned outside Castle Zone!", getName()); + } + } + + @Override + public void reduceCurrentHp(double damage, L2Character attacker, boolean awake, boolean isDOT, Skill skill) { + if ((skill != null) || !TerritoryWarManager.getInstance().isTWInProgress()) { + return; + } + + final L2PcInstance actingPlayer = attacker.getActingPlayer(); + if (actingPlayer == null) { + return; + } + if (actingPlayer.isCombatFlagEquipped()) { + return; + } + if (actingPlayer.getSiegeSide() == 0) { + return; + } + if (getCastle() == null) { + return; + } + if (TerritoryWarManager.getInstance().isAllyField(actingPlayer, getCastle().getResidenceId())) { + return; + } + + super.reduceCurrentHp(damage, attacker, awake, isDOT, skill); + } + + @Override + public void reduceCurrentHpByDOT(double i, L2Character attacker, Skill skill) { + // wards can't be damaged by DOTs + } + + @Override + public boolean doDie(L2Character killer) { + // Kill the L2NpcInstance (the corpse disappeared after 7 seconds) + if (!super.doDie(killer) || (getCastle() == null) || !TerritoryWarManager.getInstance().isTWInProgress()) { + return false; + } + + if (killer instanceof L2PcInstance) { + if ((killer.getSiegeSide() > 0) && !((L2PcInstance) killer).isCombatFlagEquipped()) { + ((L2PcInstance) killer).addItem("Pickup", getId() - 23012, 1, null, false); + } else { + TerritoryWarManager.getInstance().getTerritoryWard(getId() - 36491).spawnMe(); + } + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_S1_WARD_HAS_BEEN_DESTROYED_C2_HAS_THE_WARD); + sm.addString(getName().replace(" Ward", "")); + sm.addPcName((L2PcInstance) killer); + TerritoryWarManager.getInstance().announceToParticipants(sm, 0, 0); + } else { + TerritoryWarManager.getInstance().getTerritoryWard(getId() - 36491).spawnMe(); + } + decayMe(); + return true; + } + + @Override + public void onForcedAttack(L2PcInstance player) { + onAction(player); + } + + @Override + public void onAction(L2PcInstance player, boolean interact) { + if ((player == null) || !canTarget(player)) { + return; + } + + // Check if the L2PcInstance already target the L2NpcInstance + if (this != player.getTarget()) { + // Set the target of the L2PcInstance player + player.setTarget(this); + } else if (interact) { + if (isAutoAttackable(player) && (Math.abs(player.getZ() - getZ()) < 100)) { + player.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, this); + } else { + // Send a Server->Client ActionFailed to the L2PcInstance in order to avoid that the client wait another packet + player.sendPacket(ActionFailed.STATIC_PACKET); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TrapInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TrapInstance.java new file mode 100644 index 00000000..e183da96 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2TrapInstance.java @@ -0,0 +1,405 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.TrapAction; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.knownlist.TrapKnownList; +import com.l2jserver.gameserver.model.actor.tasks.npc.trap.TrapTask; +import com.l2jserver.gameserver.model.actor.tasks.npc.trap.TrapTriggerTask; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.trap.OnTrapAction; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.olympiad.OlympiadGameManager; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.AbstractNpcInfo.TrapInfo; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.taskmanager.DecayTaskManager; + +/** + * Trap instance. + * @author Zoey76 + */ +public final class L2TrapInstance extends L2Npc { + private static final int TICK = 1000; // 1s + private boolean _hasLifeTime; + private boolean _isInArena = false; + private boolean _isTriggered; + private final int _lifeTime; + private L2PcInstance _owner; + private final List _playersWhoDetectedMe = new ArrayList<>(); + private final SkillHolder _skill; + private int _remainingTime; + // Tasks + private ScheduledFuture _trapTask = null; + + /** + * Creates a trap. + * @param template the trap NPC template + * @param lifeTime the life time + */ + public L2TrapInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, int instanceId, int lifeTime) { + super(objectId, template); + setInstanceType(InstanceType.L2TrapInstance); + setInstanceId(instanceId); + setName(template.getName()); + setIsInvul(false); + + _owner = owner; + _isTriggered = false; + _skill = getTemplate().getParameters().getObject("trap_skill", SkillHolder.class); + _hasLifeTime = lifeTime >= 0; + _lifeTime = lifeTime != 0 ? lifeTime : 30000; + _remainingTime = _lifeTime; + if (_skill != null) { + _trapTask = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new TrapTask(this), TICK, TICK); + } + } + + public L2TrapInstance(int objectId, L2NpcTemplate template, L2PcInstance owner, int lifeTime) { + this(objectId, template, owner, owner.getInstanceId(), lifeTime); + } + + public L2TrapInstance(int objectId, L2NpcTemplate template, int instanceId, int lifeTime) { + this(objectId, template, null, instanceId, lifeTime); + } + + @Override + public void broadcastPacket(L2GameServerPacket mov) { + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + if ((player != null) && (_isTriggered || isVisibleFor(player))) { + player.sendPacket(mov); + } + } + } + + @Override + public void broadcastPacket(L2GameServerPacket mov, int radiusInKnownlist) { + for (L2PcInstance player : getKnownList().getKnownPlayers().values()) { + if ((player != null) && isInsideRadius(player, radiusInKnownlist, false, false) && (_isTriggered || isVisibleFor(player))) { + player.sendPacket(mov); + } + } + } + + /** + * Verify if the character can see the trap. + * @param cha the character to verify + * @return {@code true} if the character can see the trap, {@code false} otherwise + */ + @Override + public boolean isVisibleFor(final L2Character cha) { + if ((cha != null) && _playersWhoDetectedMe.contains(cha.getObjectId())) { + return true; + } + + if ((_owner == null) || (cha == null)) { + return false; + } + if (cha == _owner) { + return true; + } + + if (cha instanceof L2PcInstance player) { + // observers can't see trap + if (player.inObserverMode()) { + return false; + } + + // olympiad competitors can't see trap + if (_owner.isInOlympiadMode() && player.isInOlympiadMode() && (player.getOlympiadSide() != _owner.getOlympiadSide())) { + return false; + } + } + + if (_isInArena) { + return true; + } + + return isOwnerInSameParty(cha); + } + + /** + * Checks whether a character is a potential target for the trap. + * @param target The character we're checking against + * @return {@code true} if the character is a valid target, {@code false} otherwise + */ + public boolean checkTarget(L2Character target) { + // Range seems to be reduced from Freya(300) to H5(150) + if (!target.isInsideRadius(this, 150, false, false)) { + return false; + } + + if (!Skill.checkForAreaOffensiveSkills(this, target, _skill.getSkill(), _isInArena)) { + return false; + } + + // observers + if (target.isPlayer() && target.getActingPlayer().inObserverMode()) { + return false; + } + + // olympiad own team and their summons not attacked + if ((_owner != null) && _owner.isInOlympiadMode()) { + final L2PcInstance player = target.getActingPlayer(); + if ((player != null) && player.isInOlympiadMode() && (player.getOlympiadSide() == _owner.getOlympiadSide())) { + return false; + } + } + + if (_isInArena) { + return true; + } + + // trap owned by players not attack non-flagged players + if (_owner != null) { + if (target instanceof L2Attackable) { + return true; + } + + final L2PcInstance player = target.getActingPlayer(); + return (player != null) && ((player.getPvpFlag() != 0) || (player.getKarma() != 0)); + } + return true; + } + + @Override + public boolean deleteMe() { + if (_owner != null) { + _owner.setTrap(null); + _owner = null; + } + return super.deleteMe(); + } + + @Override + public L2PcInstance getActingPlayer() { + return _owner; + } + + @Override + public L2Weapon getActiveWeaponItem() { + return null; + } + + public int getKarma() { + return _owner != null ? _owner.getKarma() : 0; + } + + @Override + public TrapKnownList getKnownList() { + return (TrapKnownList) super.getKnownList(); + } + + /** + * Get the owner of this trap. + * @return the owner + */ + public L2PcInstance getOwner() { + return _owner; + } + + public byte getPvpFlag() { + return _owner != null ? _owner.getPvpFlag() : 0; + } + + @Override + public L2ItemInstance getSecondaryWeaponInstance() { + return null; + } + + @Override + public L2Weapon getSecondaryWeaponItem() { + return null; + } + + public Skill getSkill() { + return _skill.getSkill(); + } + + @Override + public void initKnownList() { + setKnownList(new TrapKnownList(this)); + } + + @Override + public boolean isAutoAttackable(L2Character attacker) { + if ((attacker == null) || (attacker == _owner) || isOwnerInSameParty(attacker)) { + return false; + } + + return isVisibleFor(attacker); + } + + @Override + public boolean isTrap() { + return true; + } + + /** + * Checks is triggered + * @return True if trap is triggered. + */ + public boolean isTriggered() { + return _isTriggered; + } + + @Override + public void onSpawn() { + super.onSpawn(); + _isInArena = isInsideZone(ZoneId.PVP) && !isInsideZone(ZoneId.SIEGE); + _playersWhoDetectedMe.clear(); + } + + @Override + public void sendDamageMessage(L2Character target, int damage, boolean mcrit, boolean pcrit, boolean miss) { + if (miss || (_owner == null)) { + return; + } + + if (_owner.isInOlympiadMode() && (target instanceof L2PcInstance) && ((L2PcInstance) target).isInOlympiadMode() && (((L2PcInstance) target).getOlympiadGameId() == _owner.getOlympiadGameId())) { + OlympiadGameManager.getInstance().notifyCompetitorDamage(getOwner(), damage); + } + + if ((target.isInvul() || target.isHpBlocked()) && !target.isNpc()) { + _owner.sendPacket(SystemMessageId.ATTACK_WAS_BLOCKED); + } else { + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_DONE_S3_DAMAGE_TO_C2); + sm.addCharName(this); + sm.addCharName(target); + sm.addInt(damage); + _owner.sendPacket(sm); + } + } + + @Override + public void sendInfo(L2PcInstance activeChar) { + if (_isTriggered || isVisibleFor(activeChar)) { + activeChar.sendPacket(new TrapInfo(this, activeChar)); + } + } + + public void setDetected(L2Character detector) { + if (_isInArena) { + if (detector.isPlayable()) { + sendInfo(detector.getActingPlayer()); + } + return; + } + + if ((_owner != null) && (_owner.getPvpFlag() == 0) && (_owner.getKarma() == 0)) { + return; + } + + _playersWhoDetectedMe.add(detector.getObjectId()); + + // Notify to scripts + EventDispatcher.getInstance().notifyEventAsync(new OnTrapAction(this, detector, TrapAction.TRAP_DETECTED), this); + + if (detector.isPlayable()) { + sendInfo(detector.getActingPlayer()); + } + } + + public void stopDecay() { + DecayTaskManager.getInstance().cancel(this); + } + + /** + * Trigger the trap. + * @param target the target + */ + public void triggerTrap(L2Character target) { + if (_trapTask != null) { + _trapTask.cancel(true); + _trapTask = null; + } + + _isTriggered = true; + broadcastPacket(new TrapInfo(this, null)); + setTarget(target); + + EventDispatcher.getInstance().notifyEventAsync(new OnTrapAction(this, target, TrapAction.TRAP_TRIGGERED), this); + + ThreadPoolManager.getInstance().scheduleGeneral(new TrapTriggerTask(this), 500); + } + + public void unSummon() { + if (_trapTask != null) { + _trapTask.cancel(true); + _trapTask = null; + } + + if (_owner != null) { + _owner.setTrap(null); + _owner = null; + } + + if (isVisible() && !isDead()) { + if (getWorldRegion() != null) { + getWorldRegion().removeFromZones(this); + } + + deleteMe(); + } + } + + @Override + public void updateAbnormalEffect() { + + } + + public boolean hasLifeTime() { + return _hasLifeTime; + } + + public void setHasLifeTime(boolean val) { + _hasLifeTime = val; + } + + public int getRemainingTime() { + return _remainingTime; + } + + public void setRemainingTime(int time) { + _remainingTime = time; + } + + public int getLifeTime() { + return _lifeTime; + } + + private boolean isOwnerInSameParty(final L2Character character) { + return character.isInParty() && _owner.isInParty() && (character.getParty().getLeaderObjectId() == _owner.getParty().getLeaderObjectId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDElfInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDElfInstance.java new file mode 100644 index 00000000..8258be23 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDElfInstance.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterDElfInstance extends L2VillageMasterInstance { + + public L2VillageMasterDElfInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfRace(Race.DARK_ELF); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDwarfInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDwarfInstance.java new file mode 100644 index 00000000..47cb49f6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterDwarfInstance.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterDwarfInstance extends L2VillageMasterInstance { + + public L2VillageMasterDwarfInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfRace(Race.DWARF); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterFighterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterFighterInstance.java new file mode 100644 index 00000000..0a83f2ca --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterFighterInstance.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.ClassType; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterFighterInstance extends L2VillageMasterInstance { + + public L2VillageMasterFighterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfRace(Race.HUMAN) || pclass.isOfRace(Race.ELF); + } + + @Override + protected boolean checkVillageMasterTeachType(PlayerClass pclass) { + if (pclass == null) { + return false; + } + + return pclass.isOfType(ClassType.Fighter); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterInstance.java new file mode 100644 index 00000000..f6fa9b51 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterInstance.java @@ -0,0 +1,913 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.clan; +import static com.l2jserver.gameserver.model.base.AcquireSkillType.PLEDGE; +import static com.l2jserver.gameserver.network.SystemMessageId.DO_NOT_HAVE_FURTHER_SKILLS_TO_LEARN_S1; + +import java.util.Iterator; +import java.util.Set; +import java.util.StringTokenizer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.xml.impl.ClassListData; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Clan.SubPledge; +import com.l2jserver.gameserver.model.L2ClanMember; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.base.PlayerClass; +import com.l2jserver.gameserver.model.base.SubClass; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.AcquireSkillList; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillLaunched; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Village Master instance. + * @since 2005/03/29 23:15:15 + */ +public class L2VillageMasterInstance extends L2NpcInstance { + private static final Logger LOG = LoggerFactory.getLogger(L2VillageMasterInstance.class); + + public L2VillageMasterInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2VillageMasterInstance); + } + + @Override + public String getHtmlPath(int npcId, int val) { + String pom; + if (val == 0) { + pom = "" + npcId; + } else { + pom = npcId + "-" + val; + } + return "data/html/villagemaster/" + pom + ".htm"; + } + + @Override + public void onBypassFeedback(L2PcInstance player, String command) { + final var st = new StringTokenizer(command, " "); + switch (st.nextToken()) { + case "create_clan" -> { + if (st.hasMoreTokens()) { + final String clanName = st.nextToken(); + if (st.hasMoreTokens() || !isValidName(clanName)) { + player.sendPacket(SystemMessageId.CLAN_NAME_INCORRECT); + return; + } + ClanTable.getInstance().createClan(player, clanName); + } + } + case "create_academy" -> { + if (st.hasMoreTokens()) { + createSubPledge(player, st.nextToken(), null, L2Clan.SUBUNIT_ACADEMY, 5); + } + } + case "rename_pledge" -> { + if (st.countTokens() > 1) { + renameSubPledge(player, Integer.parseInt(st.nextToken()), st.nextToken()); + } + } + case "create_royal" -> { + if (st.countTokens() > 1) { + createSubPledge(player, st.nextToken(), st.nextToken(), L2Clan.SUBUNIT_ROYAL1, 6); + } + } + case "create_knight" -> { + if (st.countTokens() > 1) { + createSubPledge(player, st.nextToken(), st.nextToken(), L2Clan.SUBUNIT_KNIGHT1, 7); + } + } + case "assign_subpl_leader" -> { + if (st.countTokens() > 1) { + assignSubPledgeLeader(player, st.nextToken(), st.nextToken()); + } + } + case "create_ally" -> { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.ONLY_CLAN_LEADER_CREATE_ALLIANCE); + } else { + if (st.hasMoreTokens()) { + player.getClan().createAlly(player, st.nextToken()); + } + } + } + case "dissolve_ally" -> player.getClan().dissolveAlly(player); + case "dissolve_clan" -> dissolveClan(player, player.getClanId()); + case "change_clan_leader" -> { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + if (!st.hasMoreTokens()) { + break; + } + + final String newLeaderName = st.nextToken(); + + if (player.getName().equalsIgnoreCase(newLeaderName)) { + return; + } + + final L2Clan clan = player.getClan(); + final L2ClanMember member = clan.getClanMember(newLeaderName); + if (member == null) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_DOES_NOT_EXIST); + sm.addString(newLeaderName); + player.sendPacket(sm); + return; + } + + if (!member.isOnline()) { + player.sendPacket(SystemMessageId.INVITED_USER_NOT_ONLINE); + return; + } + + // To avoid clans with null clan leader, academy members shouldn't be eligible for clan leader. + if (member.getPlayerInstance().isAcademyMember()) { + player.sendPacket(SystemMessageId.RIGHT_CANT_TRANSFERRED_TO_ACADEMY_MEMBER); + return; + } + + if (character().clanLeaderInstantActivation()) { + clan.setNewLeader(member); + } else { + final NpcHtmlMessage msg = new NpcHtmlMessage(getObjectId()); + if (clan.getNewLeaderId() == 0) { + clan.setNewLeaderId(member.getObjectId(), true); + msg.setFile(player.getHtmlPrefix(), "com/l2jserver/datapack/village_master/Clan/9000-07-success.htm"); + } else { + msg.setFile(player.getHtmlPrefix(), "com/l2jserver/datapack/village_master/Clan/9000-07-in-progress.htm"); + } + player.sendPacket(msg); + } + } + case "cancel_clan_leader_change" -> { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + final L2Clan clan = player.getClan(); + final NpcHtmlMessage msg = new NpcHtmlMessage(getObjectId()); + if (clan.getNewLeaderId() != 0) { + clan.setNewLeaderId(0, true); + msg.setFile(player.getHtmlPrefix(), "com/l2jserver/datapack/village_master/Clan/9000-07-canceled.htm"); + } else { + msg.setHtml("You don't have clan leader delegation applications submitted yet!"); + } + + player.sendPacket(msg); + } + case "recover_clan" -> recoverClan(player, player.getClanId()); + case "increase_clan_level" -> { + if (player.getClan().levelUpClan(player)) { + player.broadcastPacket(new MagicSkillUse(player, 5103, 1, 0, 0)); + player.broadcastPacket(new MagicSkillLaunched(player, 5103, 1)); + } + } + case "learn_clan_skills" -> showPledgeSkillList(player); + case "Subclass" -> { + // Subclasses may not be changed while a skill is in use. + if (player.isCastingNow() || player.isAllSkillsDisabled()) { + player.sendPacket(SystemMessageId.SUBCLASS_NO_CHANGE_OR_CREATE_WHILE_SKILL_IN_USE); + return; + } + final NpcHtmlMessage html = new NpcHtmlMessage(getObjectId()); + // Subclasses may not be changed while a transformated state. + if (player.getTransformation() != null) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_NoTransformed.htm"); + player.sendPacket(html); + return; + } + // Subclasses may not be changed while a summon is active. + if (player.hasSummon()) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_NoSummon.htm"); + player.sendPacket(html); + return; + } + // Subclasses may not be changed while you have exceeded your inventory limit. + if (!player.isInventoryUnder90(true)) { + player.sendPacket(SystemMessageId.NOT_SUBCLASS_WHILE_INVENTORY_FULL); + return; + } + // Subclasses may not be changed while a you are over your weight limit. + if (player.getWeightPenalty() >= 2) { + player.sendPacket(SystemMessageId.NOT_SUBCLASS_WHILE_OVERWEIGHT); + return; + } + + int cmdChoice = 0; + int paramOne = 0; + int paramTwo = 0; + try { + cmdChoice = Integer.parseInt(command.substring(9, 10).trim()); + + int endIndex = command.indexOf(' ', 11); + if (endIndex == -1) { + endIndex = command.length(); + } + + if (command.length() > 11) { + paramOne = Integer.parseInt(command.substring(11, endIndex).trim()); + if (command.length() > endIndex) { + paramTwo = Integer.parseInt(command.substring(endIndex).trim()); + } + } + } catch (Exception NumberFormatException) { + LOG.warn("Wrong numeric values for command {}", command); + } + + Set subsAvailable; + switch (cmdChoice) { + case 0: // Subclass change menu + html.setFile(player.getHtmlPrefix(), getSubClassMenu(player.getRace())); + break; + case 1: // Add Subclass - Initial + // Avoid giving player an option to add a new sub class, if they have max sub-classes already. + if (player.getTotalSubClasses() >= character().getMaxSubclass()) { + html.setFile(player.getHtmlPrefix(), getSubClassFail()); + break; + } + + subsAvailable = getAvailableSubClasses(player); + if ((subsAvailable != null) && !subsAvailable.isEmpty()) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Add.htm"); + final var list = new StringBuilder(200); + for (var subClass : subsAvailable) { + list.append("
      ") + .append(ClassListData.getInstance().getClass(subClass.ordinal()).getClientCode()) + .append("
      "); + } + html.replace("%list%", list.toString()); + } else { + if ((player.getRace() == Race.ELF) || (player.getRace() == Race.DARK_ELF)) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Fail_Elves.htm"); + player.sendPacket(html); + } else if (player.getRace() == Race.KAMAEL) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Fail_Kamael.htm"); + player.sendPacket(html); + } else { + // TODO: Retail message + player.sendMessage("There are no sub classes available at this time."); + } + return; + } + break; + case 2: // Change Class - Initial + if (player.getSubClasses().isEmpty()) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ChangeNo.htm"); + } else { + final StringBuilder content2 = StringUtil.startAppend(200); + if (checkVillageMaster(player.getBaseClass())) { + StringUtil.append(content2, "", ClassListData.getInstance().getClass(player.getBaseClass()).getClientCode(), "
      "); + } + + for (Iterator subList = iterSubClasses(player); subList.hasNext();) { + SubClass subClass = subList.next(); + if (checkVillageMaster(subClass.getClassDefinition())) { + StringUtil.append(content2, "", ClassListData.getInstance().getClass(subClass.getClassId()).getClientCode(), "
      "); + } + } + + if (!content2.isEmpty()) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Change.htm"); + html.replace("%list%", content2.toString()); + } else { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ChangeNotFound.htm"); + } + } + break; + case 3: // Change/Cancel Subclass - Initial + if ((player.getSubClasses() == null) || player.getSubClasses().isEmpty()) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyEmpty.htm"); + break; + } + + // custom value + if (player.getTotalSubClasses() > 3) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyCustom.htm"); + final StringBuilder content3 = StringUtil.startAppend(200); + int classIndex = 1; + + for (Iterator subList = iterSubClasses(player); subList.hasNext();) { + SubClass subClass = subList.next(); + + StringUtil.append(content3, "Sub-class ", String.valueOf(classIndex++), "
      ", "", ClassListData.getInstance().getClass(subClass.getClassId()).getClientCode(), "
      "); + } + html.replace("%list%", content3.toString()); + } else { + // retail html contain only 3 subclasses + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Modify.htm"); + if (player.getSubClasses().containsKey(1)) { + html.replace("%sub1%", ClassListData.getInstance().getClass(player.getSubClasses().get(1).getClassId()).getClientCode()); + } else { + html.replace("%sub1%
      ", ""); + } + + if (player.getSubClasses().containsKey(2)) { + html.replace("%sub2%", ClassListData.getInstance().getClass(player.getSubClasses().get(2).getClassId()).getClientCode()); + } else { + html.replace("%sub2%
      ", ""); + } + + if (player.getSubClasses().containsKey(3)) { + html.replace("%sub3%", ClassListData.getInstance().getClass(player.getSubClasses().get(3).getClassId()).getClientCode()); + } else { + html.replace("%sub3%
      ", ""); + } + } + break; + case 4: // Add Subclass - Action (Subclass 4 x[x]) + // If the character is less than level 75 on any of their previously chosen classes then disallow them to change to their most recently added sub-class choice. + if (!player.getFloodProtectors().getSubclass().tryPerformAction("add subclass")) { + LOG.warn("Player {} has performed a subclass change too fast", player.getName()); + return; + } + + boolean allowAddition = player.getTotalSubClasses() < character().getMaxSubclass(); + + if (player.getLevel() < 75) { + allowAddition = false; + } + + if (allowAddition) { + if (!player.getSubClasses().isEmpty()) { + for (Iterator subList = iterSubClasses(player); subList.hasNext();) { + SubClass subClass = subList.next(); + + if (subClass.getLevel() < 75) { + allowAddition = false; + break; + } + } + } + } + + // If quest checking is enabled, verify if the character has completed the Mimir's Elixir (Path to Subclass) + // and Fate's Whisper (A Grade Weapon) quests by checking for instances of their unique reward items. + // If they both exist, remove both unique items and continue with adding the sub-class. + if (allowAddition && !character().subclassWithoutQuests()) { + allowAddition = checkQuests(player); + } + + if (allowAddition && isValidNewSubClass(player, paramOne)) { + if (!player.addSubClass(paramOne, player.getTotalSubClasses() + 1)) { + return; + } + + player.changeActiveClass(player.getTotalSubClasses()); + + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_AddOk.htm"); + + player.sendPacket(SystemMessageId.ADD_NEW_SUBCLASS); // Subclass added. + } else { + html.setFile(player.getHtmlPrefix(), getSubClassFail()); + } + break; + case 5: // Change Class - Action + // If the character is less than level 75 on any of their previously chosen classes then disallow them to change to their most recently added sub-class choice. Note: paramOne = classIndex + if (!player.getFloodProtectors().getSubclass().tryPerformAction("change class")) { + LOG.warn("Player {} has performed a subclass change too fast", player.getName()); + return; + } + + if (player.getClassIndex() == paramOne) { + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_Current.htm"); + break; + } + + if (paramOne == 0) { + if (!checkVillageMaster(player.getBaseClass())) { + return; + } + } else { + try { + if (!checkVillageMaster(player.getSubClasses().get(paramOne).getClassDefinition())) { + return; + } + } catch (NullPointerException e) { + return; + } + } + + player.changeActiveClass(paramOne); + player.sendPacket(SystemMessageId.SUBCLASS_TRANSFER_COMPLETED); // Transfer completed. + return; + case 6: // Change/Cancel Subclass - Choice + // validity check + if ((paramOne < 1) || (paramOne > character().getMaxSubclass())) { + return; + } + + subsAvailable = getAvailableSubClasses(player); + // another validity check + if ((subsAvailable == null) || subsAvailable.isEmpty()) { + // TODO: Retail message + player.sendMessage("There are no sub classes available at this time."); + return; + } + + final StringBuilder content6 = StringUtil.startAppend(200); + for (PlayerClass subClass : subsAvailable) { + StringUtil.append(content6, "", ClassListData.getInstance().getClass(subClass.ordinal()).getClientCode(), "
      "); + } + + switch (paramOne) { + case 1 -> html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyChoice1.htm"); + case 2 -> html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyChoice2.htm"); + case 3 -> html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyChoice3.htm"); + default -> html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyChoice.htm"); + } + html.replace("%list%", content6.toString()); + break; + case 7: // Change Subclass - Action + // Warning: the information about this subclass will be removed from the subclass list even if false! + if (!player.getFloodProtectors().getSubclass().tryPerformAction("change class")) { + LOG.warn("Player {} has performed a subclass change too fast", player.getName()); + return; + } + + if (!isValidNewSubClass(player, paramTwo)) { + return; + } + + if (player.modifySubClass(paramOne, paramTwo)) { + player.abortCast(); + player.stopAllEffectsExceptThoseThatLastThroughDeath(); // all effects from old subclass stopped! + player.stopAllEffectsNotStayOnSubclassChange(); + player.stopCubics(); + player.changeActiveClass(paramOne); + + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/SubClass_ModifyOk.htm"); + html.replace("%name%", ClassListData.getInstance().getClass(paramTwo).getClientCode()); + + player.sendPacket(SystemMessageId.ADD_NEW_SUBCLASS); // Subclass added. + } else { + // This isn't good! modifySubClass() removed subclass from memory we must update _classIndex! + // Else IndexOutOfBoundsException can turn up some place down the line along with other seemingly unrelated problems. + player.changeActiveClass(0); // Also updates _classIndex plus switching _classid to baseclass. + + player.sendMessage("The sub class could not be added, you have been reverted to your base class."); + return; + } + break; + } + html.replace("%objectId%", String.valueOf(getObjectId())); + player.sendPacket(html); + } + default -> super.onBypassFeedback(player, command); + } + } + + protected String getSubClassMenu(Race race) { + if (character().subclassEverywhere() || (race != Race.KAMAEL)) { + return "data/html/villagemaster/SubClass.htm"; + } + + return "data/html/villagemaster/SubClass_NoOther.htm"; + } + + protected String getSubClassFail() { + return "data/html/villagemaster/SubClass_Fail.htm"; + } + + protected boolean checkQuests(L2PcInstance player) { + return player.isNoble() || player.hasQuestCompleted("Q00234_FatesWhisper") || player.hasQuestCompleted("Q00235_MimirsElixir"); + } + + /** + * Returns list of available subclasses Base class and already used subclasses removed + */ + private Set getAvailableSubClasses(L2PcInstance player) { + // get player base class + final int currentBaseId = player.getBaseClass(); + final ClassId baseCID = ClassId.getClassId(currentBaseId); + + // we need 2nd occupation ID + final int baseClassId; + if (baseCID.level() > 2) { + baseClassId = baseCID.getParent().ordinal(); + } else { + baseClassId = currentBaseId; + } + + // If the race of your main class is Elf or Dark Elf, you may not select each class as a subclass to the other class. + // If the race of your main class is Kamael, you may not subclass any other race If the race of your main class is NOT Kamael, + // you may not subclass any Kamael class You may not select Overlord and Warsmith class as a subclass. + // You may not select a similar class as the subclass. + // The occupations classified as similar classes are as follows: Treasure Hunter, Plainswalker and Abyss Walker Hawkeye, + // Silver Ranger and Phantom Ranger Paladin, Dark Avenger, Temple Knight + // and Shillien Knight Warlocks, Elemental Summoner and Phantom Summoner Elder and Shillien Elder Swordsinger and Bladedancer Sorcerer, + // Spellsinger and Spellhowler Also, Kamael have a special, hidden 4 subclass, the inspector, which can only be taken if you have already completed the other two Kamael subclasses + Set availSubs = PlayerClass.values()[baseClassId].getAvailableSubclasses(player); + + if ((availSubs != null) && !availSubs.isEmpty()) { + for (Iterator availSub = availSubs.iterator(); availSub.hasNext();) { + PlayerClass pclass = availSub.next(); + + // check for the village master + if (!checkVillageMaster(pclass)) { + availSub.remove(); + continue; + } + + // scan for already used subclasses + int availClassId = pclass.ordinal(); + ClassId cid = ClassId.getClassId(availClassId); + SubClass prevSubClass; + ClassId subClassId; + for (Iterator subList = iterSubClasses(player); subList.hasNext();) { + prevSubClass = subList.next(); + subClassId = ClassId.getClassId(prevSubClass.getClassId()); + + if (subClassId.equalsOrChildOf(cid)) { + availSub.remove(); + break; + } + } + } + } + + return availSubs; + } + + /** + * Check new subclass classId for validity (villagemaster race/type, is not contains in previous subclasses, is contains in allowed subclasses) Base class not added into allowed subclasses. + */ + private boolean isValidNewSubClass(L2PcInstance player, int classId) { + if (!checkVillageMaster(classId)) { + return false; + } + + final ClassId cid = ClassId.values()[classId]; + SubClass sub; + ClassId subClassId; + for (Iterator subList = iterSubClasses(player); subList.hasNext();) { + sub = subList.next(); + subClassId = ClassId.values()[sub.getClassId()]; + + if (subClassId.equalsOrChildOf(cid)) { + return false; + } + } + + // get player base class + final int currentBaseId = player.getBaseClass(); + final ClassId baseCID = ClassId.getClassId(currentBaseId); + + // we need 2nd occupation ID + final int baseClassId; + if (baseCID.level() > 2) { + baseClassId = baseCID.getParent().ordinal(); + } else { + baseClassId = currentBaseId; + } + + Set availSubs = PlayerClass.values()[baseClassId].getAvailableSubclasses(player); + if ((availSubs == null) || availSubs.isEmpty()) { + return false; + } + + boolean found = false; + for (PlayerClass pclass : availSubs) { + if (pclass.ordinal() == classId) { + found = true; + break; + } + } + return found; + } + + protected boolean checkVillageMasterRace(PlayerClass pclass) { + return true; + } + + protected boolean checkVillageMasterTeachType(PlayerClass pclass) { + return true; + } + + /** + * Returns true if this classId allowed for master + */ + public final boolean checkVillageMaster(int classId) { + return checkVillageMaster(PlayerClass.values()[classId]); + } + + /** + * Returns true if this PlayerClass is allowed for master + */ + private boolean checkVillageMaster(PlayerClass pclass) { + if (character().subclassEverywhere()) { + return true; + } + + return checkVillageMasterRace(pclass) && checkVillageMasterTeachType(pclass); + } + + private static Iterator iterSubClasses(L2PcInstance player) { + return player.getSubClasses().values().iterator(); + } + + private static void dissolveClan(L2PcInstance player, int clanId) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + final L2Clan clan = player.getClan(); + if (clan.getAllyId() != 0) { + player.sendPacket(SystemMessageId.CANNOT_DISPERSE_THE_CLANS_IN_ALLY); + return; + } + if (clan.isAtWar()) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_WHILE_IN_WAR); + return; + } + if ((clan.getCastleId() != 0) || (clan.getHideoutId() != 0) || (clan.getFortId() != 0)) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_WHILE_OWNING_CLAN_HALL_OR_CASTLE); + return; + } + + for (Castle castle : CastleManager.getInstance().getCastles()) { + if (SiegeManager.getInstance().checkIsRegistered(clan, castle.getResidenceId())) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_WHILE_IN_SIEGE); + return; + } + } + for (Fort fort : FortManager.getInstance().getForts()) { + if (FortSiegeManager.getInstance().checkIsRegistered(clan, fort.getResidenceId())) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_WHILE_IN_SIEGE); + return; + } + } + + if (player.isInsideZone(ZoneId.SIEGE)) { + player.sendPacket(SystemMessageId.CANNOT_DISSOLVE_WHILE_IN_SIEGE); + return; + } + if (clan.getDissolvingExpiryTime() > System.currentTimeMillis()) { + player.sendPacket(SystemMessageId.DISSOLUTION_IN_PROGRESS); + return; + } + + clan.setDissolvingExpiryTime(System.currentTimeMillis() + character().getDaysToPassToDissolveAClan()); + clan.updateClanInDB(); + + // The clan leader should take the XP penalty of a full death. + player.calculateDeathExpPenalty(null, false); + ClanTable.getInstance().scheduleRemoveClan(clan.getId()); + } + + private static void recoverClan(L2PcInstance player, int clanId) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + final L2Clan clan = player.getClan(); + clan.setDissolvingExpiryTime(0); + clan.updateClanInDB(); + } + + private static void createSubPledge(L2PcInstance player, String clanName, String leaderName, int pledgeType, int minClanLvl) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + final L2Clan clan = player.getClan(); + if (clan.getLevel() < minClanLvl) { + if (pledgeType == L2Clan.SUBUNIT_ACADEMY) { + player.sendPacket(SystemMessageId.YOU_DO_NOT_MEET_CRITERIA_IN_ORDER_TO_CREATE_A_CLAN_ACADEMY); + } else { + player.sendPacket(SystemMessageId.YOU_DO_NOT_MEET_CRITERIA_IN_ORDER_TO_CREATE_A_MILITARY_UNIT); + } + + return; + } + if (!isValidName(clanName) || (clanName.length() < 2)) { + player.sendPacket(SystemMessageId.CLAN_NAME_INCORRECT); + return; + } + if (clanName.length() > ClanTable.CLAN_NAME_MAX_LENGTH) { + player.sendPacket(SystemMessageId.CLAN_NAME_TOO_LONG); + return; + } + + for (L2Clan tempClan : ClanTable.getInstance().getClans()) { + if (tempClan.getSubPledge(clanName) != null) { + if (pledgeType == L2Clan.SUBUNIT_ACADEMY) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.S1_ALREADY_EXISTS); + sm.addString(clanName); + player.sendPacket(sm); + } else { + player.sendPacket(SystemMessageId.ANOTHER_MILITARY_UNIT_IS_ALREADY_USING_THAT_NAME); + } + + return; + } + } + + if (pledgeType != L2Clan.SUBUNIT_ACADEMY) { + if ((clan.getClanMember(leaderName) == null) || (clan.getClanMember(leaderName).getPledgeType() != 0)) { + if (pledgeType >= L2Clan.SUBUNIT_KNIGHT1) { + player.sendPacket(SystemMessageId.CAPTAIN_OF_ORDER_OF_KNIGHTS_CANNOT_BE_APPOINTED); + } else if (pledgeType >= L2Clan.SUBUNIT_ROYAL1) { + player.sendPacket(SystemMessageId.CAPTAIN_OF_ROYAL_GUARD_CANNOT_BE_APPOINTED); + } + + return; + } + } + + final int leaderId = pledgeType != L2Clan.SUBUNIT_ACADEMY ? clan.getClanMember(leaderName).getObjectId() : 0; + + if (clan.createSubPledge(player, pledgeType, leaderId, clanName) == null) { + return; + } + + SystemMessage sm; + if (pledgeType == L2Clan.SUBUNIT_ACADEMY) { + sm = SystemMessage.getSystemMessage(SystemMessageId.THE_S1S_CLAN_ACADEMY_HAS_BEEN_CREATED); + sm.addString(player.getClan().getName()); + } else if (pledgeType >= L2Clan.SUBUNIT_KNIGHT1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.THE_KNIGHTS_OF_S1_HAVE_BEEN_CREATED); + sm.addString(player.getClan().getName()); + } else if (pledgeType >= L2Clan.SUBUNIT_ROYAL1) { + sm = SystemMessage.getSystemMessage(SystemMessageId.THE_ROYAL_GUARD_OF_S1_HAVE_BEEN_CREATED); + sm.addString(player.getClan().getName()); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.CLAN_CREATED); + } + player.sendPacket(sm); + + if (pledgeType != L2Clan.SUBUNIT_ACADEMY) { + final L2ClanMember leaderSubPledge = clan.getClanMember(leaderName); + final L2PcInstance leaderPlayer = leaderSubPledge.getPlayerInstance(); + if (leaderPlayer != null) { + leaderPlayer.setPledgeClass(L2ClanMember.calculatePledgeClass(leaderPlayer)); + leaderPlayer.sendPacket(new UserInfo(leaderPlayer)); + leaderPlayer.sendPacket(new ExBrExtraUserInfo(leaderPlayer)); + } + } + } + + private static void renameSubPledge(L2PcInstance player, int pledgeType, String pledgeName) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + + final L2Clan clan = player.getClan(); + final SubPledge subPledge = player.getClan().getSubPledge(pledgeType); + + if (subPledge == null) { + player.sendMessage("Pledge don't exists."); + return; + } + if (!isValidName(pledgeName) || (pledgeName.length() < 2)) { + player.sendPacket(SystemMessageId.CLAN_NAME_INCORRECT); + return; + } + if (pledgeName.length() > ClanTable.CLAN_NAME_MAX_LENGTH) { + player.sendPacket(SystemMessageId.CLAN_NAME_TOO_LONG); + return; + } + + subPledge.setName(pledgeName); + clan.updateSubPledgeInDB(subPledge.getId()); + clan.broadcastClanStatus(); + player.sendMessage("Pledge name changed."); + } + + private static void assignSubPledgeLeader(L2PcInstance player, String clanName, String leaderName) { + if (!player.isClanLeader()) { + player.sendPacket(SystemMessageId.YOU_ARE_NOT_AUTHORIZED_TO_DO_THAT); + return; + } + if (leaderName.length() > 16) { + player.sendPacket(SystemMessageId.NAMING_CHARNAME_UP_TO_16CHARS); + return; + } + if (player.getName().equals(leaderName)) { + player.sendPacket(SystemMessageId.CAPTAIN_OF_ROYAL_GUARD_CANNOT_BE_APPOINTED); + return; + } + + final L2Clan clan = player.getClan(); + final SubPledge subPledge = player.getClan().getSubPledge(clanName); + + if ((null == subPledge) || (subPledge.getId() == L2Clan.SUBUNIT_ACADEMY)) { + player.sendPacket(SystemMessageId.CLAN_NAME_INCORRECT); + return; + } + if ((clan.getClanMember(leaderName) == null) || (clan.getClanMember(leaderName).getPledgeType() != 0)) { + if (subPledge.getId() >= L2Clan.SUBUNIT_KNIGHT1) { + player.sendPacket(SystemMessageId.CAPTAIN_OF_ORDER_OF_KNIGHTS_CANNOT_BE_APPOINTED); + } else if (subPledge.getId() >= L2Clan.SUBUNIT_ROYAL1) { + player.sendPacket(SystemMessageId.CAPTAIN_OF_ROYAL_GUARD_CANNOT_BE_APPOINTED); + } + + return; + } + + subPledge.setLeaderId(clan.getClanMember(leaderName).getObjectId()); + clan.updateSubPledgeInDB(subPledge.getId()); + + final L2ClanMember leaderSubPledge = clan.getClanMember(leaderName); + final L2PcInstance leaderPlayer = leaderSubPledge.getPlayerInstance(); + if (leaderPlayer != null) { + leaderPlayer.setPledgeClass(L2ClanMember.calculatePledgeClass(leaderPlayer)); + leaderPlayer.sendPacket(new UserInfo(leaderPlayer)); + leaderPlayer.sendPacket(new ExBrExtraUserInfo(leaderPlayer)); + } + + clan.broadcastClanStatus(); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.C1_HAS_BEEN_SELECTED_AS_CAPTAIN_OF_S2); + sm.addString(leaderName); + sm.addString(clanName); + clan.broadcastToOnlineMembers(sm); + } + + /** + * this displays PledgeSkillList to the player. + */ + public static void showPledgeSkillList(L2PcInstance player) { + // TODO(Zoey76): Unhardcode in Clan script. + if (!player.isClanLeader()) { + final NpcHtmlMessage html = new NpcHtmlMessage(); + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/NotClanLeader.htm"); + player.sendPacket(html); + player.sendPacket(ActionFailed.STATIC_PACKET); + return; + } + + final var skills = SkillTreesData.getInstance().getAvailablePledgeSkills(player.getClan()); + if (skills.isEmpty()) { + if (player.getClan().getLevel() < 8) { + final var sm = SystemMessage.getSystemMessage(DO_NOT_HAVE_FURTHER_SKILLS_TO_LEARN_S1); + if (player.getClan().getLevel() < 5) { + sm.addInt(5); + } else { + sm.addInt(player.getClan().getLevel() + 1); + } + player.sendPacket(sm); + } else { + final var html = new NpcHtmlMessage(); + html.setFile(player.getHtmlPrefix(), "data/html/villagemaster/NoMoreSkills.htm"); + player.sendPacket(html); + } + } else { + player.sendPacket(new AcquireSkillList(PLEDGE, skills)); + } + } + + private static boolean isValidName(String name) { + return clan().getClanNameTemplate().matcher(name).matches(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterKamaelInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterKamaelInstance.java new file mode 100644 index 00000000..a8668a56 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterKamaelInstance.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterKamaelInstance extends L2VillageMasterInstance { + + public L2VillageMasterKamaelInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected String getSubClassMenu(Race race) { + if (character().subclassEverywhere() || (race == Race.KAMAEL)) { + return "data/html/villagemaster/SubClass.htm"; + } + + return "data/html/villagemaster/SubClass_NoKamael.htm"; + } + + @Override + protected String getSubClassFail() { + return "data/html/villagemaster/SubClass_Fail_Kamael.htm"; + } + + @Override + protected boolean checkQuests(L2PcInstance player) { + return player.isNoble() || player.hasQuestCompleted("Q00234_FatesWhisper") || player.hasQuestCompleted("Q00236_SeedsOfChaos"); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfRace(Race.KAMAEL); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterMysticInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterMysticInstance.java new file mode 100644 index 00000000..33d3e531 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterMysticInstance.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.ClassType; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterMysticInstance extends L2VillageMasterInstance { + + public L2VillageMasterMysticInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfRace(Race.HUMAN) || pclass.isOfRace(Race.ELF); + } + + @Override + protected boolean checkVillageMasterTeachType(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfType(ClassType.Mystic); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterOrcInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterOrcInstance.java new file mode 100644 index 00000000..57b0eca1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterOrcInstance.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterOrcInstance extends L2VillageMasterInstance { + + public L2VillageMasterOrcInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + + return pclass.isOfRace(Race.ORC); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterPriestInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterPriestInstance.java new file mode 100644 index 00000000..9c7a4664 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2VillageMasterPriestInstance.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.base.ClassType; +import com.l2jserver.gameserver.model.base.PlayerClass; + +public final class L2VillageMasterPriestInstance extends L2VillageMasterInstance { + + public L2VillageMasterPriestInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + } + + @Override + protected boolean checkVillageMasterRace(PlayerClass pclass) { + if (pclass == null) { + return false; + } + + return pclass.isOfRace(Race.HUMAN) || pclass.isOfRace(Race.ELF); + } + + @Override + protected boolean checkVillageMasterTeachType(PlayerClass pclass) { + if (pclass == null) { + return false; + } + return pclass.isOfType(ClassType.Priest); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2WarehouseInstance.java b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2WarehouseInstance.java new file mode 100644 index 00000000..e8af3cc0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/instance/L2WarehouseInstance.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.instance; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; + +public final class L2WarehouseInstance extends L2NpcInstance { + + public L2WarehouseInstance(int objectId, L2NpcTemplate template) { + super(objectId, template); + setInstanceType(InstanceType.L2WarehouseInstance); + } + + @Override + public boolean isWarehouse() { + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/AttackableKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/AttackableKnownList.java new file mode 100644 index 00000000..2b3998de --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/AttackableKnownList.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import java.util.Collection; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public class AttackableKnownList extends NpcKnownList { + public AttackableKnownList(L2Attackable activeChar) { + super(activeChar); + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + // Remove the L2Object from the _aggrolist of the L2Attackable + if (object instanceof L2Character) { + getActiveChar().getAggroList().remove(object); + } + // Set the L2Attackable Intention to AI_INTENTION_IDLE + final Collection known = getKnownPlayers().values(); + + // FIXME: This is a temporary solution && support for Walking Manager + if (getActiveChar().hasAI() && known.isEmpty() && !getActiveChar().isWalker()) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE, null); + } + + return true; + } + + @Override + public L2Attackable getActiveChar() { + return (L2Attackable) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + return getDistanceToWatchObject(object) + 500; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (!(object instanceof L2Character)) { + return 0; + } + + if (object.isPlayable()) { + return object.getKnownList().getDistanceToWatchObject(getActiveObject()); + } + return Math.max(300, Math.max(getActiveChar().getAggroRange(), getActiveChar().getTemplate().getClanHelpRange())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/CharKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/CharKnownList.java new file mode 100644 index 00000000..c1efbba9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/CharKnownList.java @@ -0,0 +1,232 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.util.Util; + +public class CharKnownList extends ObjectKnownList { + private volatile Map _knownPlayers; + private volatile Map _knownSummons; + private volatile Map _knownRelations; + + public CharKnownList(L2Character activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + if (object.isPlayer()) { + getKnownPlayers().put(object.getObjectId(), object.getActingPlayer()); + getKnownRelations().put(object.getObjectId(), -1); + } else if (object.isSummon()) { + getKnownSummons().put(object.getObjectId(), (L2Summon) object); + } + return true; + } + + /** + * @param player The L2PcInstance to search in _knownPlayer + * @return {@code true} if the player is in _knownPlayer of the character, {@code false} otherwise + */ + public final boolean knowsThePlayer(L2PcInstance player) { + return (getActiveChar() == player) || getKnownPlayers().containsKey(player.getObjectId()); + } + + /** Remove all L2Object from _knownObjects and _knownPlayer of the L2Character then cancel Attack or Cast and notify AI. */ + @Override + public final void removeAllKnownObjects() { + super.removeAllKnownObjects(); + getKnownPlayers().clear(); + getKnownRelations().clear(); + getKnownSummons().clear(); + + // Set _target of the L2Character to null + // Cancel Attack or Cast + getActiveChar().setTarget(null); + + // Cancel AI Task + if (getActiveChar().hasAI()) { + getActiveChar().setAI(null); + } + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + if (!forget) // on forget objects removed by iterator + { + if (object.isPlayer()) { + getKnownPlayers().remove(object.getObjectId()); + getKnownRelations().remove(object.getObjectId()); + } else if (object.isSummon()) { + getKnownSummons().remove(object.getObjectId()); + } + } + + // If object is targeted by the L2Character, cancel Attack or Cast + if (object == getActiveChar().getTarget()) { + getActiveChar().setTarget(null); + } + + return true; + } + + @Override + public void forgetObjects(boolean fullCheck) { + if (!fullCheck) { + final Iterator pIter = getKnownPlayers().values().iterator(); + while (pIter.hasNext()) { + L2PcInstance player = pIter.next(); + if (player == null) { + pIter.remove(); + } else if (!player.isVisible() || !Util.checkIfInShortRadius(getDistanceToForgetObject(player), getActiveObject(), player, true)) { + pIter.remove(); + removeKnownObject(player, true); + getKnownRelations().remove(player.getObjectId()); + getKnownObjects().remove(player.getObjectId()); + } + } + + final Iterator sIter = getKnownSummons().values().iterator(); + while (sIter.hasNext()) { + L2Summon summon = sIter.next(); + if (summon == null) { + sIter.remove(); + } else if (getActiveChar().isPlayer() && (summon.getOwner() == getActiveChar())) { + continue; + } else if (!summon.isVisible() || !Util.checkIfInShortRadius(getDistanceToForgetObject(summon), getActiveObject(), summon, true)) { + sIter.remove(); + removeKnownObject(summon, true); + getKnownObjects().remove(summon.getObjectId()); + } + } + + return; + } + // Go through knownObjects + final Iterator oIter = getKnownObjects().values().iterator(); + while (oIter.hasNext()) { + L2Object object = oIter.next(); + if (object == null) { + oIter.remove(); + } else if (!object.isVisible() || !Util.checkIfInShortRadius(getDistanceToForgetObject(object), getActiveObject(), object, true)) { + oIter.remove(); + removeKnownObject(object, true); + + if (object.isPlayer()) { + getKnownPlayers().remove(object.getObjectId()); + getKnownRelations().remove(object.getObjectId()); + } else if (object.isSummon()) { + getKnownSummons().remove(object.getObjectId()); + } + } + } + } + + public L2Character getActiveChar() { + return (L2Character) super.getActiveObject(); + } + + public List getKnownCharacters() { + List result = new LinkedList<>(); + for (L2Object obj : getKnownObjects().values()) { + if (obj instanceof L2Character) { + result.add((L2Character) obj); + } + } + return result; + } + + public List getKnownCharactersInRadius(long radius) { + List result = new LinkedList<>(); + for (L2Object obj : getKnownObjects().values()) { + if (obj instanceof L2Character) { + if (Util.checkIfInRange((int) radius, getActiveChar(), obj, true)) { + result.add((L2Character) obj); + } + } + } + return result; + } + + public final List getKnownPlayersInRadius(long radius) { + List result = new LinkedList<>(); + for (L2PcInstance player : getKnownPlayers().values()) { + if (Util.checkIfInRange((int) radius, getActiveChar(), player, true)) { + result.add(player); + } + } + return result; + } + + public final Map getKnownPlayers() { + if (_knownPlayers == null) { + synchronized (this) { + if (_knownPlayers == null) { + _knownPlayers = new ConcurrentHashMap<>(); + } + } + } + return _knownPlayers; + } + + public final Map getKnownRelations() { + if (_knownRelations == null) { + synchronized (this) { + if (_knownRelations == null) { + _knownRelations = new ConcurrentHashMap<>(); + } + } + } + return _knownRelations; + } + + public final Map getKnownSummons() { + if (_knownSummons == null) { + synchronized (this) { + if (_knownSummons == null) { + _knownSummons = new ConcurrentHashMap<>(); + } + } + } + return _knownSummons; + } + + @Override + public final String toString() { + return getActiveChar() + " Known Objects " + getKnownObjects(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DecoyKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DecoyKnownList.java new file mode 100644 index 00000000..49cdd869 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DecoyKnownList.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Decoy; + +public class DecoyKnownList extends CharKnownList { + public DecoyKnownList(L2Decoy activeChar) { + super(activeChar); + } + + @Override + public final L2Decoy getActiveChar() { + return (L2Decoy) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if ((object == getActiveChar().getOwner()) || (object == getActiveChar().getTarget())) { + return 6000; + } + return 3000; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + return 1500; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DefenderKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DefenderKnownList.java new file mode 100644 index 00000000..0a402104 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DefenderKnownList.java @@ -0,0 +1,66 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.entity.clanhall.SiegableHall; + +public class DefenderKnownList extends AttackableKnownList { + public DefenderKnownList(L2DefenderInstance activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + Castle castle = getActiveChar().getCastle(); + Fort fortress = getActiveChar().getFort(); + SiegableHall hall = getActiveChar().getConquerableHall(); + // Check if siege is in progress + if (((fortress != null) && fortress.getZone().isActive()) || ((castle != null) && castle.getZone().isActive()) || ((hall != null) && hall.getSiegeZone().isActive())) { + L2PcInstance player = null; + if (object.isPlayable()) { + player = object.getActingPlayer(); + } + int activeSiegeId = (fortress != null ? fortress.getResidenceId() : (castle != null ? castle.getResidenceId() : hall != null ? hall.getId() : 0)); + + // Check if player is an enemy of this defender npc + if ((player != null) && (((player.getSiegeState() == 2) && !player.isRegisteredOnThisSiegeField(activeSiegeId)) || ((player.getSiegeState() == 1) && !TerritoryWarManager.getInstance().isAllyField(player, activeSiegeId)) || (player.getSiegeState() == 0))) { + if (getActiveChar().getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + } + } + return true; + } + + @Override + public final L2DefenderInstance getActiveChar() { + return (L2DefenderInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DoorKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DoorKnownList.java new file mode 100644 index 00000000..0a2d16eb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/DoorKnownList.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; + +public class DoorKnownList extends CharKnownList { + public DoorKnownList(L2DoorInstance activeChar) { + super(activeChar); + } + + @Override + public final L2DoorInstance getActiveChar() { + return (L2DoorInstance) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if (object instanceof L2DefenderInstance) { + return 800; + } else if (!object.isPlayer()) { + return 0; + } + + return 4000; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (object instanceof L2DefenderInstance) { + return 600; + } else if (!object.isPlayer()) { + return 0; + } + return 3500; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/FriendlyMobKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/FriendlyMobKnownList.java new file mode 100644 index 00000000..f23602d6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/FriendlyMobKnownList.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2FriendlyMobInstance; + +public class FriendlyMobKnownList extends AttackableKnownList { + public FriendlyMobKnownList(L2FriendlyMobInstance activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + if (object.isPlayer() && (getActiveChar().getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE)) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + + return true; + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + if (!(object instanceof L2Character)) { + return true; + } + + if (getActiveChar().hasAI()) { + getActiveChar().getAI().notifyEvent(CtrlEvent.EVT_FORGET_OBJECT, object); + if (getActiveChar().getTarget() == object) { + getActiveChar().setTarget(null); + } + } + + if (getActiveChar().isVisible() && getKnownPlayers().isEmpty() && getKnownSummons().isEmpty()) { + getActiveChar().clearAggroList(); + if (getActiveChar().hasAI()) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE, null); + } + } + + return true; + } + + @Override + public final L2FriendlyMobInstance getActiveChar() { + return (L2FriendlyMobInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/GuardKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/GuardKnownList.java new file mode 100644 index 00000000..09d3afe6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/GuardKnownList.java @@ -0,0 +1,95 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.npc; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2GuardInstance; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; + +public class GuardKnownList extends AttackableKnownList { + private static final Logger LOG = LoggerFactory.getLogger(GuardKnownList.class); + + public GuardKnownList(L2GuardInstance activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + if (object.isPlayer()) { + // Check if the object added is a L2PcInstance that owns Karma + if (object.getActingPlayer().getKarma() > 0) { + if (general().debug()) { + LOG.debug("{}: PK {} entered scan range", getActiveChar().getObjectId(), object.getObjectId()); + } + + // Set the L2GuardInstance Intention to AI_INTENTION_ACTIVE + if (getActiveChar().getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + } + } else if ((npc().guardAttackAggroMob() && getActiveChar().isInActiveRegion()) && object.isMonster()) { + // Check if the object added is an aggressive L2MonsterInstance + if (((L2MonsterInstance) object).isAggressive()) { + if (general().debug()) { + LOG.debug("{}: Aggressive mob {} entered scan range", getActiveChar().getObjectId(), object.getObjectId()); + } + + // Set the L2GuardInstance Intention to AI_INTENTION_ACTIVE + if (getActiveChar().getAI().getIntention() == CtrlIntention.AI_INTENTION_IDLE) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + } + } + + return true; + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + // Check if the aggression list of this guard is empty. + if (getActiveChar().getAggroList().isEmpty()) { + // Set the L2GuardInstance to AI_INTENTION_IDLE + if (getActiveChar().hasAI() && !getActiveChar().isWalker()) { + getActiveChar().getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE, null); + } + } + + return true; + } + + @Override + public final L2GuardInstance getActiveChar() { + return (L2GuardInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/MonsterKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/MonsterKnownList.java new file mode 100644 index 00000000..df6b7883 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/MonsterKnownList.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.ai.L2CharacterAI; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public class MonsterKnownList extends AttackableKnownList { + public MonsterKnownList(L2MonsterInstance activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + final L2CharacterAI ai = getActiveChar().getAI(); // force AI creation + + // Set the L2MonsterInstance Intention to AI_INTENTION_ACTIVE if the state was AI_INTENTION_IDLE + if ((object instanceof L2PcInstance) && (ai != null) && (ai.getIntention() == CtrlIntention.AI_INTENTION_IDLE)) { + ai.setIntention(CtrlIntention.AI_INTENTION_ACTIVE, null); + } + + return true; + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + if (!(object instanceof L2Character)) { + return true; + } + + if (getActiveChar().hasAI()) { + // Notify the L2MonsterInstance AI with EVT_FORGET_OBJECT + getActiveChar().getAI().notifyEvent(CtrlEvent.EVT_FORGET_OBJECT, object); + } + + if (getActiveChar().isVisible() && getKnownPlayers().isEmpty() && getKnownSummons().isEmpty()) { + // Clear the _aggroList of the L2MonsterInstance + getActiveChar().clearAggroList(); + } + + return true; + } + + @Override + public final L2MonsterInstance getActiveChar() { + return (L2MonsterInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NpcKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NpcKnownList.java new file mode 100644 index 00000000..0a6ab62c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NpcKnownList.java @@ -0,0 +1,127 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import java.util.concurrent.ScheduledFuture; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2FestivalGuideInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.npc.NpcCreatureSee; + +public class NpcKnownList extends CharKnownList { + private ScheduledFuture _trackingTask = null; + + public NpcKnownList(L2Npc activeChar) { + super(activeChar); + } + + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + if (getActiveObject().isNpc() && (object instanceof L2Character)) { + EventDispatcher.getInstance().notifyEventAsync(new NpcCreatureSee(getActiveChar(), (L2Character) object), getActiveChar()); + } + return true; + } + + @Override + public L2Npc getActiveChar() { + return (L2Npc) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + return getDistanceToWatchObject(object) + 500; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (!(object instanceof L2Character)) { + return 0; + } + + if (object instanceof L2FestivalGuideInstance) { + return 4000; + } + + if (object.isPlayable()) { + return 1500; + } + + return 500; + } + + // Support for Walking monsters aggro + public void startTrackingTask() { + if ((_trackingTask == null) && (getActiveChar().getAggroRange() > 0)) { + _trackingTask = ThreadPoolManager.getInstance().scheduleAiAtFixedRate(new TrackingTask(), 2000, 2000); + } + } + + // Support for Walking monsters aggro + public void stopTrackingTask() { + if (_trackingTask != null) { + _trackingTask.cancel(true); + _trackingTask = null; + } + } + + // Support for Walking monsters aggro + protected class TrackingTask implements Runnable { + @Override + public void run() { + if (!getActiveChar().isAttackable()) { + return; + } + + final L2Attackable monster = (L2Attackable) getActiveChar(); + if (monster.getAI().getIntention() != CtrlIntention.AI_INTENTION_MOVE_TO) { + return; + } + + for (L2PcInstance pl : getKnownPlayers().values()) { + if (!pl.isDead() && !pl.isInvul() && pl.isInsideRadius(monster, monster.getAggroRange(), true, false) && (monster.isMonster() || (monster.isInstanceTypes(InstanceType.L2GuardInstance) && (pl.getKarma() > 0)))) { + // Send aggroRangeEnter + if (monster.getHating(pl) == 0) { + monster.addDamageHate(pl, 0, 0); + } + + // Skip attack for other targets, if one is already chosen for attack + if ((monster.getAI().getIntention() != CtrlIntention.AI_INTENTION_ATTACK) && !monster.isCoreAIDisabled()) { + WalkingManager.getInstance().stopMoving(getActiveChar(), false, true); + monster.addDamageHate(pl, 0, 100); + monster.getAI().setIntention(CtrlIntention.AI_INTENTION_ATTACK, pl, null); + } + } + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NullKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NullKnownList.java new file mode 100644 index 00000000..003f8623 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/NullKnownList.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; + +public class NullKnownList extends ObjectKnownList { + public NullKnownList(L2Object activeObject) { + super(activeObject); + } + + @Override + public boolean addKnownObject(L2Object object) { + return false; + } + + @Override + public L2Object getActiveObject() { + return super.getActiveObject(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + return 0; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + return 0; + } + + @Override + public void removeAllKnownObjects() { + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/ObjectKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/ObjectKnownList.java new file mode 100644 index 00000000..a8a900ad --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/ObjectKnownList.java @@ -0,0 +1,177 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2WorldRegion; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.util.Util; + +public class ObjectKnownList { + private final L2Object _activeObject; + private volatile Map _knownObjects; + + public ObjectKnownList(L2Object activeObject) { + _activeObject = activeObject; + } + + public boolean addKnownObject(L2Object object) { + if (object == null) { + return false; + } + + // Instance -1 is for GMs that can see everything on all instances + if ((getActiveObject().getInstanceId() != -1) && (object.getInstanceId() != getActiveObject().getInstanceId())) { + return false; + } + + // Check if the object is an L2PcInstance in ghost mode + if (object.isPlayer() && object.getActingPlayer().getAppearance().isGhost()) { + return false; + } + + // Check if already know object + if (knowsObject(object)) { + return false; + } + + // Check if object is not inside distance to watch object + if (!Util.checkIfInShortRadius(getDistanceToWatchObject(object), getActiveObject(), object, true)) { + return false; + } + + return (getKnownObjects().put(object.getObjectId(), object) == null); + } + + public final boolean knowsObject(L2Object object) { + if (object == null) { + return false; + } + + return (getActiveObject() == object) || getKnownObjects().containsKey(object.getObjectId()); + } + + /** + * Remove all L2Object from _knownObjects + */ + public void removeAllKnownObjects() { + getKnownObjects().clear(); + } + + public final boolean removeKnownObject(L2Object object) { + return removeKnownObject(object, false); + } + + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (object == null) { + return false; + } + + if (forget) { + return true; + } + + return getKnownObjects().remove(object.getObjectId()) != null; + } + + /** + * Used only in Config.MOVE_BASED_KNOWNLIST and does not support guards seeing moving monsters + */ + public final void findObjects() { + final L2WorldRegion worldRegion = getActiveObject().getWorldRegion(); + if (worldRegion == null) { + return; + } + + if (getActiveObject().isPlayable()) { + for (L2WorldRegion surroundingRegion : worldRegion.getSurroundingRegions()) // offer members of this and surrounding regions + { + for (L2Object object : surroundingRegion.getVisibleObjects().values()) { + if (object != getActiveObject()) { + addKnownObject(object); + if (object instanceof L2Character) { + object.getKnownList().addKnownObject(getActiveObject()); + } + } + } + } + } else if (getActiveObject() instanceof L2Character) { + for (L2WorldRegion surroundingRegion : worldRegion.getSurroundingRegions()) // offer members of this and surrounding regions + { + if (surroundingRegion.isActive()) { + for (L2Object object : surroundingRegion.getVisiblePlayable().values()) { + if (object != getActiveObject()) { + addKnownObject(object); + } + } + } + } + } + } + + /** + * Remove invisible and too far L2Object from _knowObject and if necessary from _knownPlayers of the L2Character + * @param fullCheck + */ + public void forgetObjects(boolean fullCheck) { + final Iterator oIter = getKnownObjects().values().iterator(); + while (oIter.hasNext()) { + final L2Object object = oIter.next(); + if (!fullCheck && !object.isPlayable()) { + continue; + } + + // Remove all objects invisible or too far + if (!object.isVisible() || !Util.checkIfInShortRadius(getDistanceToForgetObject(object), getActiveObject(), object, true)) { + oIter.remove(); + removeKnownObject(object, true); + } + } + } + + public L2Object getActiveObject() { + return _activeObject; + } + + public int getDistanceToForgetObject(L2Object object) { + return 0; + } + + public int getDistanceToWatchObject(L2Object object) { + return 0; + } + + /** + * @return the _knownObjects containing all L2Object known by the L2Character. + */ + public final Map getKnownObjects() { + if (_knownObjects == null) { + synchronized (this) { + if (_knownObjects == null) { + _knownObjects = new ConcurrentHashMap<>(); + } + } + } + return _knownObjects; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PcKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PcKnownList.java new file mode 100644 index 00000000..0b38b5f8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PcKnownList.java @@ -0,0 +1,160 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2AirShipInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.serverpackets.DeleteObject; +import com.l2jserver.gameserver.network.serverpackets.SpawnItem; + +public class PcKnownList extends PlayableKnownList { + public PcKnownList(L2PcInstance activeChar) { + super(activeChar); + } + + /** + * Add a visible L2Object to L2PcInstance _knownObjects and _knownPlayer (if necessary) and send Server-Client Packets needed to inform the L2PcInstance of its state and actions in progress.
      + *
      + * object is a L2ItemInstance :
      + *
      + *
    • Send Server-Client Packet DropItem/SpawnItem to the L2PcInstance

    • + *
      + * object is a L2DoorInstance :
      + *
      + *
    • Send Server-Client Packets DoorInfo and DoorStatusUpdate to the L2PcInstance
    • + *
    • Send Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance

    • + *
      + * object is a L2NpcInstance :
      + *
      + *
    • Send Server-Client Packet NpcInfo to the L2PcInstance
    • + *
    • Send Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance

    • + *
      + * object is a L2Summon :
      + *
      + *
    • Send Server-Client Packet NpcInfo/PetItemList (if the L2PcInstance is the owner) to the L2PcInstance
    • + *
    • Send Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance

    • + *
      + * object is a L2PcInstance :
      + *
      + *
    • Send Server-Client Packet CharInfo to the L2PcInstance
    • + *
    • If the object has a private store, Send Server-Client Packet PrivateStoreMsgSell to the L2PcInstance
    • + *
    • Send Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance

    • + *
      + * @param object The L2Object to add to _knownObjects and _knownPlayer + */ + @Override + public boolean addKnownObject(L2Object object) { + if (!super.addKnownObject(object)) { + return false; + } + + if (object.getPoly().isMorphed() && object.getPoly().getPolyType().equals("item")) { + // if (object.getPolytype().equals("item")) + getActiveChar().sendPacket(new SpawnItem(object)); + // else if (object.getPolytype().equals("npc")) + // sendPacket(new NpcInfoPoly(object, this)); + } else { + if (object.isVisibleFor(getActiveChar())) { + object.sendInfo(getActiveChar()); + + if (object instanceof L2Character obj) { + // Update the state of the L2Character object client side by sending Server->Client packet MoveToPawn/CharMoveToLocation and AutoAttackStart to the L2PcInstance + if (obj.hasAI()) { + obj.getAI().describeStateToPlayer(getActiveChar()); + } + } + } + } + + return true; + } + + /** + * Remove a L2Object from L2PcInstance _knownObjects and _knownPlayer (if necessary) and send Server-Client Packet DeleteObject to the L2PcInstance.
      + *
      + * @param object The L2Object to remove from _knownObjects and _knownPlayer + */ + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + if (object instanceof L2AirShipInstance airShip) { + if ((airShip.getCaptainId() != 0) && (airShip.getCaptainId() != getActiveChar().getObjectId())) { + getActiveChar().sendPacket(new DeleteObject(airShip.getCaptainId())); + } + if (airShip.getHelmObjectId() != 0) { + getActiveChar().sendPacket(new DeleteObject(airShip.getHelmObjectId())); + } + } + + // Send Server-Client Packet DeleteObject to the L2PcInstance + getActiveChar().sendPacket(new DeleteObject(object)); + + if (general().checkKnownList() && (object instanceof L2Npc) && getActiveChar().isGM()) { + getActiveChar().sendMessage("Removed NPC: " + object.getName()); + } + + return true; + } + + @Override + public final L2PcInstance getActiveChar() { + return (L2PcInstance) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if (object.isVehicle()) { + return 10000; + } + return getDistanceToWatchObject(object) + 500; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (object.isVehicle()) { + return 9000; + } + + final int knownlistSize = getKnownObjects().size(); + if ((knownlistSize <= 3) && !object.isAttackable()) { + return 4500; // Empty fields + } + if ((knownlistSize <= 10) && !object.isAttackable()) { + return 4000; // Empty fields + } + if ((knownlistSize <= 20) && !object.isAttackable()) { + return 3400; + } + if (knownlistSize <= 30) { + return 2900; + } + if (knownlistSize <= 40) { + return 2300; + } + return 3000; // Siege, TOI, City + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PlayableKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PlayableKnownList.java new file mode 100644 index 00000000..a5fd8978 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/PlayableKnownList.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.actor.L2Playable; + +public class PlayableKnownList extends CharKnownList { + public PlayableKnownList(L2Playable activeChar) { + super(activeChar); + } + + @Override + public L2Playable getActiveChar() { + return (L2Playable) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/RaceManagerKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/RaceManagerKnownList.java new file mode 100644 index 00000000..decd1369 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/RaceManagerKnownList.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.MonsterRace; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2RaceManagerInstance; +import com.l2jserver.gameserver.network.serverpackets.DeleteObject; + +public class RaceManagerKnownList extends NpcKnownList { + public RaceManagerKnownList(L2RaceManagerInstance activeChar) { + super(activeChar); + } + + @Override + protected boolean removeKnownObject(L2Object object, boolean forget) { + if (!super.removeKnownObject(object, forget)) { + return false; + } + + if (object.isPlayer()) { + for (int i = 0; i < 8; i++) { + object.sendPacket(new DeleteObject(MonsterRace.getInstance().getMonsters()[i])); + } + } + + return true; + } + + @Override + public L2RaceManagerInstance getActiveChar() { + return (L2RaceManagerInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/StaticObjectKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/StaticObjectKnownList.java new file mode 100644 index 00000000..2ea67846 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/StaticObjectKnownList.java @@ -0,0 +1,57 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2DefenderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; + +public class StaticObjectKnownList extends CharKnownList { + public StaticObjectKnownList(L2StaticObjectInstance activeChar) { + super(activeChar); + } + + @Override + public final L2StaticObjectInstance getActiveChar() { + return (L2StaticObjectInstance) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if (object instanceof L2DefenderInstance) { + return 800; + } + if (!object.isPlayer()) { + return 0; + } + + return 4000; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (object instanceof L2DefenderInstance) { + return 600; + } + if (!object.isPlayer()) { + return 0; + } + return 2000; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/SummonKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/SummonKnownList.java new file mode 100644 index 00000000..db8a9a0f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/SummonKnownList.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Summon; + +public class SummonKnownList extends PlayableKnownList { + public SummonKnownList(L2Summon activeChar) { + super(activeChar); + } + + @Override + public final L2Summon getActiveChar() { + return (L2Summon) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if ((object == getActiveChar().getOwner()) || (object == getActiveChar().getTarget())) { + return 6000; + } + return 3000; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + return 1500; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/TrapKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/TrapKnownList.java new file mode 100644 index 00000000..d9eb5f56 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/TrapKnownList.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.instance.L2TrapInstance; + +public class TrapKnownList extends NpcKnownList { + public TrapKnownList(L2TrapInstance activeChar) { + super(activeChar); + } + + @Override + public final L2TrapInstance getActiveChar() { + return (L2TrapInstance) super.getActiveChar(); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if ((object == getActiveChar().getActingPlayer()) || (object == getActiveChar().getTarget())) { + return 6000; + } + + return 3000; + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + return 1500; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/VehicleKnownList.java b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/VehicleKnownList.java new file mode 100644 index 00000000..e1c10c90 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/knownlist/VehicleKnownList.java @@ -0,0 +1,46 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.knownlist; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; + +public class VehicleKnownList extends CharKnownList { + public VehicleKnownList(L2Character activeChar) { + super(activeChar); + } + + @Override + public int getDistanceToForgetObject(L2Object object) { + if (!object.isPlayer()) { + return 0; + } + + return object.getKnownList().getDistanceToForgetObject(getActiveObject()); + } + + @Override + public int getDistanceToWatchObject(L2Object object) { + if (!object.isPlayer()) { + return 0; + } + + return object.getKnownList().getDistanceToWatchObject(getActiveObject()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/poly/ObjectPoly.java b/src/main/java/com/l2jserver/gameserver/model/actor/poly/ObjectPoly.java new file mode 100644 index 00000000..7b0c95ac --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/poly/ObjectPoly.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.poly; + +import com.l2jserver.gameserver.model.L2Object; + +public class ObjectPoly { + private final L2Object _activeObject; + private int _polyId; + private String _polyType; + + public ObjectPoly(L2Object activeObject) { + _activeObject = activeObject; + } + + public void setPolyInfo(String polyType, String polyId) { + setPolyId(Integer.parseInt(polyId)); + setPolyType(polyType); + } + + public final L2Object getActiveObject() { + return _activeObject; + } + + public final boolean isMorphed() { + return getPolyType() != null; + } + + public final int getPolyId() { + return _polyId; + } + + public final void setPolyId(int value) { + _polyId = value; + } + + public final String getPolyType() { + return _polyType; + } + + public final void setPolyType(String value) { + _polyType = value; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/CharStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/CharStat.java new file mode 100644 index 00000000..63ba1840 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/CharStat.java @@ -0,0 +1,629 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.npc; + +import java.util.Arrays; + +import com.l2jserver.gameserver.model.Elementals; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Calculator; +import com.l2jserver.gameserver.model.stats.MoveType; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.model.stats.TraitType; +import com.l2jserver.gameserver.model.zone.ZoneId; + +public class CharStat { + private final L2Character _activeChar; + private int _level = 1; + private final float[] _attackTraits = new float[TraitType.values().length]; + private final int[] _attackTraitsCount = new int[TraitType.values().length]; + private final float[] _defenceTraits = new float[TraitType.values().length]; + private final int[] _defenceTraitsCount = new int[TraitType.values().length]; + private final int[] _traitsInvul = new int[TraitType.values().length]; + + public CharStat(L2Character activeChar) { + _activeChar = activeChar; + Arrays.fill(_attackTraits, 1.0f); + Arrays.fill(_defenceTraits, 1.0f); + } + + public final double calcStat(Stats stat, double init) { + return calcStat(stat, init, null, null); + } + + /** + * Calculate the new value of the state with modifiers that will be applied on the targeted L2Character.
      + * Concept :
      _calculators. Each Calculator (a calculator per state) own a table of Func object. A Func object is a mathematical function that permit to calculate the modifier of a state (ex : REGENERATE_HP_RATE...) :
      + * FuncAtkAccuracy -> Math.sqrt(_player.getDEX())*6+_player.getLevel()
      + * When the calc method of a calculator is launched, each mathematical function is called according to its priority _order.
      + * Indeed, Func with lowest priority order is executed first and Funcs with the same order are executed in unspecified order.
      + * The result of the calculation is stored in the value property of an Env class instance.
      + * @param stat The stat to calculate the new value with modifiers + * @param initVal The initial value of the stat before applying modifiers + * @param target The L2Character whose properties will be used in the calculation (ex : CON, INT...) + * @param skill The L2Skill whose properties will be used in the calculation (ex : Level...) + * @return + */ + public final double calcStat(Stats stat, double initVal, L2Character target, Skill skill) { + double value = initVal; + if (stat == null) { + return value; + } + + final int id = stat.ordinal(); + final Calculator c = _activeChar.getCalculators()[id]; + + // If no Func object found, no modifier is applied + if ((c == null) || (c.size() == 0)) { + return value; + } + + // Apply transformation stats. + if (getActiveChar().isPlayer() && getActiveChar().isTransformed()) { + double val = getActiveChar().getTransformation().getStat(getActiveChar().getActingPlayer(), stat); + if (val > 0) { + value = val; + } + } + + // Launch the calculation + value = c.calc(_activeChar, target, skill, value); + + // avoid some troubles with negative stats (some stats should never be negative) + if (value <= 0) { + switch (stat) { + case MAX_HP, MAX_MP, MAX_CP, MAGIC_DEFENCE, POWER_DEFENCE, POWER_ATTACK, MAGIC_ATTACK, POWER_ATTACK_SPEED, MAGIC_ATTACK_SPEED, SHIELD_DEFENCE, STAT_CON, STAT_DEX, STAT_INT, STAT_MEN, STAT_STR, STAT_WIT -> value = 1.0; + } + } + return value; + } + + /** + * @return the Accuracy (base+modifier) of the L2Character in function of the Weapon Expertise Penalty. + */ + public int getAccuracy() { + return (int) Math.round(calcStat(Stats.ACCURACY_COMBAT, 0, null, null)); + } + + public L2Character getActiveChar() { + return _activeChar; + } + + /** + * @return the Attack Speed multiplier (base+modifier) of the L2Character to get proper animations. + */ + public final float getAttackSpeedMultiplier() { + return (float) (((1.1) * getPAtkSpd()) / _activeChar.getTemplate().getBasePAtkSpd()); + } + + /** + * @return the CON of the L2Character (base+modifier). + */ + public final int getCON() { + return (int) calcStat(Stats.STAT_CON, _activeChar.getTemplate().getBaseCON()); + } + + /** + * @param target + * @param init + * @return the Critical Damage rate (base+modifier) of the L2Character. + */ + public final double getCriticalDmg(L2Character target, double init) { + return calcStat(Stats.CRITICAL_DAMAGE, init, target, null); + } + + /** + * @param target + * @param skill + * @return the Critical Hit rate (base+modifier) of the L2Character. + */ + public int getCriticalHit(L2Character target, Skill skill) { + double val = (int) calcStat(Stats.CRITICAL_RATE, _activeChar.getTemplate().getBaseCritRate(), target, skill); + if (!_activeChar.canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + val = Math.min(val, character().getMaxPCritRate()); + } + return (int) (val + .5); + } + + /** + * @param base + * @return the Critical Hit Pos rate of the L2Character + */ + public int getCriticalHitPos(int base) { + return (int) calcStat(Stats.CRITICAL_RATE_POS, base); + } + + /** + * @return the DEX of the L2Character (base+modifier). + */ + public final int getDEX() { + return (int) calcStat(Stats.STAT_DEX, _activeChar.getTemplate().getBaseDEX()); + } + + /** + * @param target + * @return the Attack Evasion rate (base+modifier) of the L2Character. + */ + public int getEvasionRate(L2Character target) { + int val = (int) Math.round(calcStat(Stats.EVASION_RATE, 0, target, null)); + if (!_activeChar.canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + val = Math.min(val, character().getMaxEvasion()); + } + return val; + } + + /** + * @return the INT of the L2Character (base+modifier). + */ + public int getINT() { + return (int) calcStat(Stats.STAT_INT, _activeChar.getTemplate().getBaseINT()); + } + + public int getLevel() { + return _level; + } + + public void setLevel(int value) { + _level = value; + } + + /** + * @param skill + * @return the Magical Attack range (base+modifier) of the L2Character. + */ + public final int getMagicalAttackRange(Skill skill) { + if (skill != null) { + return (int) calcStat(Stats.MAGIC_ATTACK_RANGE, skill.getCastRange(), null, skill); + } + + return _activeChar.getTemplate().getBaseAttackRange(); + } + + public int getMaxCp() { + return (int) calcStat(Stats.MAX_CP, _activeChar.getTemplate().getBaseCpMax()); + } + + public int getMaxRecoverableCp() { + return (int) calcStat(Stats.MAX_RECOVERABLE_CP, getMaxCp()); + } + + public int getMaxHp() { + return (int) calcStat(Stats.MAX_HP, _activeChar.getTemplate().getBaseHpMax()); + } + + public int getMaxRecoverableHp() { + return (int) calcStat(Stats.MAX_RECOVERABLE_HP, getMaxHp()); + } + + public int getMaxMp() { + return (int) calcStat(Stats.MAX_MP, _activeChar.getTemplate().getBaseMpMax()); + } + + public int getMaxRecoverableMp() { + return (int) calcStat(Stats.MAX_RECOVERABLE_MP, getMaxMp()); + } + + /** + * Return the MAtk (base+modifier) of the L2Character.
      + * Example of use: Calculate Magic damage + * @param target The L2Character targeted by the skill + * @param skill The L2Skill used against the target + * @return + */ + public double getMAtk(L2Character target, Skill skill) { + float bonusAtk = 1; + if (customs().championEnable() && _activeChar.isChampion()) { + bonusAtk = customs().getChampionAtk(); + } + if (_activeChar.isRaid()) { + bonusAtk *= npc().getRaidMAttackMultiplier(); + } + // Calculate modifiers Magic Attack + return calcStat(Stats.MAGIC_ATTACK, _activeChar.getTemplate().getBaseMAtk() * bonusAtk, target, skill); + } + + /** + * @return the MAtk Speed (base+modifier) of the L2Character in function of the Armour Expertise Penalty. + */ + public int getMAtkSpd() { + float bonusSpdAtk = 1; + if (customs().championEnable() && _activeChar.isChampion()) { + bonusSpdAtk = customs().getChampionSpdAtk(); + } + + double val = calcStat(Stats.MAGIC_ATTACK_SPEED, _activeChar.getTemplate().getBaseMAtkSpd() * bonusSpdAtk); + if (!_activeChar.canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + val = Math.min(val, character().getMaxMAtkSpeed()); + } + return (int) val; + } + + /** + * @param target + * @param skill + * @return the Magic Critical Hit rate (base+modifier) of the L2Character. + */ + public final int getMCriticalHit(L2Character target, Skill skill) { + int val = (int) calcStat(Stats.MCRITICAL_RATE, 1, target, skill) * 10; + if (!_activeChar.canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + val = Math.min(val, character().getMaxMCritRate()); + } + return val; + } + + /** + * Example of use : Calculate Magic damage. + * @param target The L2Character targeted by the skill + * @param skill The L2Skill used against the target + * @return the MDef (base+modifier) of the L2Character against a skill in function of abnormal effects in progress. + */ + public double getMDef(L2Character target, Skill skill) { + // Get the base MDef of the L2Character + double defence = _activeChar.getTemplate().getBaseMDef(); + + // Calculate modifier for Raid Bosses + if (_activeChar.isRaid()) { + defence *= npc().getRaidMDefenceMultiplier(); + } + // Calculate modifiers Magic Attack + return calcStat(Stats.MAGIC_DEFENCE, defence, target, skill); + } + + /** + * @return the MEN of the L2Character (base+modifier). + */ + public final int getMEN() { + return (int) calcStat(Stats.STAT_MEN, _activeChar.getTemplate().getBaseMEN()); + } + + public double getMovementSpeedMultiplier() { + double baseSpeed; + if (_activeChar.isInsideZone(ZoneId.WATER)) { + baseSpeed = getBaseMoveSpeed(_activeChar.isRunning() ? MoveType.FAST_SWIM : MoveType.SLOW_SWIM); + } else { + baseSpeed = getBaseMoveSpeed(_activeChar.isRunning() ? MoveType.RUN : MoveType.WALK); + } + return getMoveSpeed() * (1. / baseSpeed); + } + + /** + * @return the RunSpeed (base+modifier) of the L2Character in function of the Armour Expertise Penalty. + */ + public double getRunSpeed() { + final double baseRunSpd = _activeChar.isInsideZone(ZoneId.WATER) ? getSwimRunSpeed() : getBaseMoveSpeed(MoveType.RUN); + if (baseRunSpd <= 0) { + return 0; + } + + return calcStat(Stats.MOVE_SPEED, baseRunSpd, null, null); + } + + /** + * @return the WalkSpeed (base+modifier) of the L2Character. + */ + public double getWalkSpeed() { + final double baseWalkSpd = _activeChar.isInsideZone(ZoneId.WATER) ? getSwimWalkSpeed() : getBaseMoveSpeed(MoveType.WALK); + if (baseWalkSpd <= 0) { + return 0; + } + + return calcStat(Stats.MOVE_SPEED, baseWalkSpd); + } + + /** + * @return the SwimRunSpeed (base+modifier) of the L2Character. + */ + public double getSwimRunSpeed() { + final double baseRunSpd = getBaseMoveSpeed(MoveType.FAST_SWIM); + if (baseRunSpd <= 0) { + return 0; + } + + return calcStat(Stats.MOVE_SPEED, baseRunSpd, null, null); + } + + /** + * @return the SwimWalkSpeed (base+modifier) of the L2Character. + */ + public double getSwimWalkSpeed() { + final double baseWalkSpd = getBaseMoveSpeed(MoveType.SLOW_SWIM); + if (baseWalkSpd <= 0) { + return 0; + } + + return calcStat(Stats.MOVE_SPEED, baseWalkSpd); + } + + /** + * @param type movement type + * @return the base move speed of given movement type. + */ + public double getBaseMoveSpeed(MoveType type) { + return _activeChar.getTemplate().getBaseMoveSpeed(type); + } + + /** + * @return the RunSpeed (base+modifier) or WalkSpeed (base+modifier) of the L2Character in function of the movement type. + */ + public double getMoveSpeed() { + if (_activeChar.isInsideZone(ZoneId.WATER)) { + return _activeChar.isRunning() ? getSwimRunSpeed() : getSwimWalkSpeed(); + } + return _activeChar.isRunning() ? getRunSpeed() : getWalkSpeed(); + } + + /** + * @param skill + * @return the MReuse rate (base+modifier) of the L2Character. + */ + public final double getMReuseRate(Skill skill) { + return calcStat(Stats.MAGIC_REUSE_RATE, 1, null, skill); + } + + /** + * @param target + * @return the PAtk (base+modifier) of the L2Character. + */ + public double getPAtk(L2Character target) { + float bonusAtk = 1; + if (customs().championEnable() && _activeChar.isChampion()) { + bonusAtk = customs().getChampionAtk(); + } + if (_activeChar.isRaid()) { + bonusAtk *= npc().getRaidPAttackMultiplier(); + } + return calcStat(Stats.POWER_ATTACK, _activeChar.getTemplate().getBasePAtk() * bonusAtk, target, null); + } + + /** + * @return the PAtk Speed (base+modifier) of the L2Character in function of the Armour Expertise Penalty. + */ + public double getPAtkSpd() { + float bonusAtk = 1; + if (customs().championEnable() && _activeChar.isChampion()) { + bonusAtk = customs().getChampionSpdAtk(); + } + return Math.round(calcStat(Stats.POWER_ATTACK_SPEED, _activeChar.getTemplate().getBasePAtkSpd() * bonusAtk, null, null)); + } + + /** + * @param target + * @return the PDef (base+modifier) of the L2Character. + */ + public double getPDef(L2Character target) { + return calcStat(Stats.POWER_DEFENCE, (_activeChar.isRaid()) ? _activeChar.getTemplate().getBasePDef() * npc().getRaidPDefenceMultiplier() : _activeChar.getTemplate().getBasePDef(), target, null); + } + + /** + * @return the Physical Attack range (base+modifier) of the L2Character. + */ + public final int getPhysicalAttackRange() { + final L2Weapon weapon = _activeChar.getActiveWeaponItem(); + int baseAttackRange; + if (_activeChar.isTransformed() && _activeChar.isPlayer()) { + baseAttackRange = _activeChar.getTransformation().getBaseAttackRange(_activeChar.getActingPlayer()); + } else if (weapon != null) { + baseAttackRange = weapon.getBaseAttackRange(); + } else { + baseAttackRange = _activeChar.getTemplate().getBaseAttackRange(); + } + + return (int) calcStat(Stats.POWER_ATTACK_RANGE, baseAttackRange, null, null); + } + + public int getPhysicalAttackAngle() { + final L2Weapon weapon = _activeChar.getActiveWeaponItem(); + final int baseAttackAngle; + if (weapon != null) { + baseAttackAngle = weapon.getBaseAttackAngle(); + } else { + baseAttackAngle = 120; + } + return baseAttackAngle; + } + + /** + * @param target + * @return the weapon reuse modifier. + */ + public final double getWeaponReuseModifier(L2Character target) { + return calcStat(Stats.ATK_REUSE, 1, target, null); + } + + /** + * @return the ShieldDef rate (base+modifier) of the L2Character. + */ + public final int getShldDef() { + return (int) calcStat(Stats.SHIELD_DEFENCE, 0); + } + + /** + * @return the STR of the L2Character (base+modifier). + */ + public final int getSTR() { + return (int) calcStat(Stats.STAT_STR, _activeChar.getTemplate().getBaseSTR()); + } + + /** + * @return the WIT of the L2Character (base+modifier). + */ + public final int getWIT() { + return (int) calcStat(Stats.STAT_WIT, _activeChar.getTemplate().getBaseWIT()); + } + + /** + * @param skill + * @return the mpConsume. + */ + public final int getMpConsume2(Skill skill) { + if (skill == null) { + return 1; + } + double mpConsume2 = skill.getMpConsume2(); + double nextDanceMpCost = Math.ceil(skill.getMpConsume2() / 2.); + if (skill.isDance()) { + if (character().danceConsumeAdditionalMP() && (_activeChar != null) && (_activeChar.getDanceCount() > 0)) { + mpConsume2 += _activeChar.getDanceCount() * nextDanceMpCost; + } + } + + mpConsume2 = calcStat(Stats.MP_CONSUME, mpConsume2, null, skill); + + if (skill.isDance()) { + return (int) calcStat(Stats.DANCE_MP_CONSUME_RATE, mpConsume2); + } else if (skill.isMagic()) { + return (int) calcStat(Stats.MAGICAL_MP_CONSUME_RATE, mpConsume2); + } else { + return (int) calcStat(Stats.PHYSICAL_MP_CONSUME_RATE, mpConsume2); + } + } + + /** + * @param skill + * @return the mpConsume1. + */ + public final int getMpConsume1(Skill skill) { + if (skill == null) { + return 1; + } + + return (int) calcStat(Stats.MP_CONSUME, skill.getMpConsume1(), null, skill); + } + + public byte getAttackElement() { + L2ItemInstance weaponInstance = _activeChar.getActiveWeaponInstance(); + // 1st order - weapon element + if ((weaponInstance != null) && (weaponInstance.getAttackElementType() >= 0)) { + return weaponInstance.getAttackElementType(); + } + + // temp fix starts + int tempVal = 0; + int[] stats = { + 0, + 0, + 0, + 0, + 0, + 0 + }; + + byte returnVal = -2; + stats[0] = (int) calcStat(Stats.FIRE_POWER, _activeChar.getTemplate().getBaseFire()); + stats[1] = (int) calcStat(Stats.WATER_POWER, _activeChar.getTemplate().getBaseWater()); + stats[2] = (int) calcStat(Stats.WIND_POWER, _activeChar.getTemplate().getBaseWind()); + stats[3] = (int) calcStat(Stats.EARTH_POWER, _activeChar.getTemplate().getBaseEarth()); + stats[4] = (int) calcStat(Stats.HOLY_POWER, _activeChar.getTemplate().getBaseHoly()); + stats[5] = (int) calcStat(Stats.DARK_POWER, _activeChar.getTemplate().getBaseDark()); + + for (byte x = 0; x < 6; x++) { + if (stats[x] > tempVal) { + returnVal = x; + tempVal = stats[x]; + } + } + + return returnVal; + // temp fix ends + + /* + * uncomment me once deadlocks in getAllEffects() fixed return _activeChar.getElementIdFromEffects(); + */ + } + + public int getAttackElementValue(byte attackAttribute) { + return switch (attackAttribute) { + case Elementals.FIRE -> (int) calcStat(Stats.FIRE_POWER, _activeChar.getTemplate().getBaseFire()); + case Elementals.WATER -> (int) calcStat(Stats.WATER_POWER, _activeChar.getTemplate().getBaseWater()); + case Elementals.WIND -> (int) calcStat(Stats.WIND_POWER, _activeChar.getTemplate().getBaseWind()); + case Elementals.EARTH -> (int) calcStat(Stats.EARTH_POWER, _activeChar.getTemplate().getBaseEarth()); + case Elementals.HOLY -> (int) calcStat(Stats.HOLY_POWER, _activeChar.getTemplate().getBaseHoly()); + case Elementals.DARK -> (int) calcStat(Stats.DARK_POWER, _activeChar.getTemplate().getBaseDark()); + default -> 0; + }; + } + + public int getDefenseElementValue(byte defenseAttribute) { + return switch (defenseAttribute) { + case Elementals.FIRE -> (int) calcStat(Stats.FIRE_RES, _activeChar.getTemplate().getBaseFireRes()); + case Elementals.WATER -> (int) calcStat(Stats.WATER_RES, _activeChar.getTemplate().getBaseWaterRes()); + case Elementals.WIND -> (int) calcStat(Stats.WIND_RES, _activeChar.getTemplate().getBaseWindRes()); + case Elementals.EARTH -> (int) calcStat(Stats.EARTH_RES, _activeChar.getTemplate().getBaseEarthRes()); + case Elementals.HOLY -> (int) calcStat(Stats.HOLY_RES, _activeChar.getTemplate().getBaseHolyRes()); + case Elementals.DARK -> (int) calcStat(Stats.DARK_RES, _activeChar.getTemplate().getBaseDarkRes()); + default -> (int) _activeChar.getTemplate().getBaseElementRes(); + }; + } + + public float getAttackTrait(TraitType traitType) { + return _attackTraits[traitType.getId()]; + } + + public float[] getAttackTraits() { + return _attackTraits; + } + + public boolean hasAttackTrait(TraitType traitType) { + return _attackTraitsCount[traitType.getId()] > 0; + } + + public int[] getAttackTraitsCount() { + return _attackTraitsCount; + } + + public float getDefenceTrait(TraitType traitType) { + return _defenceTraits[traitType.getId()]; + } + + public float[] getDefenceTraits() { + return _defenceTraits; + } + + public boolean hasDefenceTrait(TraitType traitType) { + return _defenceTraitsCount[traitType.getId()] > 0; + } + + public int[] getDefenceTraitsCount() { + return _defenceTraitsCount; + } + + public boolean isTraitInvul(TraitType traitType) { + return _traitsInvul[traitType.getId()] > 0; + } + + public int[] getTraitsInvul() { + return _traitsInvul; + } + + /** + * Gets the maximum buff count. + * @return the maximum buff count + */ + public int getMaxBuffCount() { + return (int) calcStat(Stats.ENLARGE_ABNORMAL_SLOT, character().getMaxBuffAmount()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/ControllableAirShipStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/ControllableAirShipStat.java new file mode 100644 index 00000000..8eab8e08 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/ControllableAirShipStat.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.instance.L2ControllableAirShipInstance; + +public class ControllableAirShipStat extends VehicleStat { + public ControllableAirShipStat(L2ControllableAirShipInstance activeChar) { + super(activeChar); + } + + @Override + public L2ControllableAirShipInstance getActiveChar() { + return (L2ControllableAirShipInstance) super.getActiveChar(); + } + + @Override + public double getMoveSpeed() { + if (getActiveChar().isInDock() || (getActiveChar().getFuel() > 0)) { + return super.getMoveSpeed(); + } + return super.getMoveSpeed() * 0.05f; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/DoorStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/DoorStat.java new file mode 100644 index 00000000..e681e71e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/DoorStat.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; + +/** + * @author malyelfik + */ +public class DoorStat extends CharStat { + private int _upgradeHpRatio = 1; + + public DoorStat(L2DoorInstance activeChar) { + super(activeChar); + } + + @Override + public L2DoorInstance getActiveChar() { + return (L2DoorInstance) super.getActiveChar(); + } + + @Override + public int getMaxHp() { + return super.getMaxHp() * _upgradeHpRatio; + } + + public void setUpgradeHpRatio(int ratio) { + _upgradeHpRatio = ratio; + } + + public int getUpgradeHpRatio() { + return _upgradeHpRatio; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/NpcStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/NpcStat.java new file mode 100644 index 00000000..0864ddb0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/NpcStat.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.L2Npc; + +public class NpcStat extends CharStat { + public NpcStat(L2Npc activeChar) { + super(activeChar); + } + + @Override + public int getLevel() { + return getActiveChar().getTemplate().getLevel(); + } + + @Override + public L2Npc getActiveChar() { + return (L2Npc) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/PcStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PcStat.java new file mode 100644 index 00000000..b4b8668e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PcStat.java @@ -0,0 +1,477 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.config.Configuration.hunting; +import static com.l2jserver.gameserver.config.Configuration.vitality; + +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.model.L2PetLevelData; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.transform.TransformTemplate; +import com.l2jserver.gameserver.model.stats.MoveType; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExNevitAdventPointInfoPacket; +import com.l2jserver.gameserver.network.serverpackets.ExVitalityPointInfo; + +public class PcStat extends PlayableStat { + private int _oldMaxHp; // stats watch + private int _oldMaxMp; // stats watch + private int _oldMaxCp; // stats watch + private float _vitalityPoints = 1; + private int _nevitPoints = 0; + private int _huntingBonusTime = 0; + private int _nevitBlessingTime = 0; + private byte _vitalityLevel = 0; + private long _startingXp; + /** Player's maximum cubic count. */ + private int _maxCubicCount = 1; + /** Player's maximum talisman count. */ + private final AtomicInteger _talismanSlots = new AtomicInteger(); + private boolean _cloakSlot = false; + + public static final int[] VITALITY_LEVELS = { + 240, + 2000, + 13000, + 17000, + 20000 + }; + + public static final int MAX_VITALITY_POINTS = VITALITY_LEVELS[4]; + public static final int MIN_VITALITY_POINTS = 1; + + public PcStat(L2PcInstance activeChar) { + super(activeChar); + } + + public void setStartingExp(long value) { + if (general().enableBotReportButton()) { + _startingXp = value; + } + } + + public long getStartingExp() { + return _startingXp; + } + + /** + * Gets the maximum cubic count. + * @return the maximum cubic count + */ + public int getMaxCubicCount() { + return _maxCubicCount; + } + + /** + * Sets the maximum cubic count. + * @param cubicCount the maximum cubic count + */ + public void setMaxCubicCount(int cubicCount) { + _maxCubicCount = cubicCount; + } + + /** + * Gets the maximum talisman count. + * @return the maximum talisman count + */ + public int getTalismanSlots() { + return _talismanSlots.get(); + } + + public void addTalismanSlots(int count) { + _talismanSlots.addAndGet(count); + } + + public boolean canEquipCloak() { + return _cloakSlot; + } + + public void setCloakSlotStatus(boolean cloakSlot) { + _cloakSlot = cloakSlot; + } + + @Override + public final int getMaxCp() { + // Get the Max CP (base+modifier) of the L2PcInstance + int val = (getActiveChar() == null) ? 1 : (int) calcStat(Stats.MAX_CP, getActiveChar().getTemplate().getBaseCpMax(getActiveChar().getLevel())); + if (val != _oldMaxCp) { + _oldMaxCp = val; + + // Launch a regen task if the new Max CP is higher than the old one + if (getActiveChar().getStatus().getCurrentCp() != val) { + getActiveChar().getStatus().setCurrentCp(getActiveChar().getStatus().getCurrentCp()); // trigger start of regeneration + } + } + return val; + } + + @Override + public final int getMaxHp() { + // Get the Max HP (base+modifier) of the L2PcInstance + int val = (getActiveChar() == null) ? 1 : (int) calcStat(Stats.MAX_HP, getActiveChar().getTemplate().getBaseHpMax(getActiveChar().getLevel())); + if (val != _oldMaxHp) { + _oldMaxHp = val; + + // Launch a regen task if the new Max HP is higher than the old one + if (getActiveChar().getStatus().getCurrentHp() != val) { + getActiveChar().getStatus().setCurrentHp(getActiveChar().getStatus().getCurrentHp()); // trigger start of regeneration + } + } + + return val; + } + + @Override + public final int getMaxMp() { + // Get the Max MP (base+modifier) of the L2PcInstance + int val = (getActiveChar() == null) ? 1 : (int) calcStat(Stats.MAX_MP, getActiveChar().getTemplate().getBaseMpMax(getActiveChar().getLevel())); + + if (val != _oldMaxMp) { + _oldMaxMp = val; + + // Launch a regen task if the new Max MP is higher than the old one + if (getActiveChar().getStatus().getCurrentMp() != val) { + getActiveChar().getStatus().setCurrentMp(getActiveChar().getStatus().getCurrentMp()); // trigger start of regeneration + } + } + + return val; + } + + /** + * @param type movement type + * @return the base move speed of given movement type. + */ + @Override + public double getBaseMoveSpeed(MoveType type) { + final L2PcInstance player = getActiveChar(); + if (player.isTransformed()) { + final TransformTemplate template = player.getTransformation().getTemplate(player); + if (template != null) { + return template.getBaseMoveSpeed(type); + } + } else if (player.isMounted()) { + final L2PetLevelData data = PetDataTable.getInstance().getPetLevelData(player.getMountNpcId(), player.getMountLevel()); + if (data != null) { + return data.getSpeedOnRide(type); + } + } + return super.getBaseMoveSpeed(type); + } + + @Override + public double getRunSpeed() { + double val = super.getRunSpeed() + character().getRunSpeedBoost(); + + // Apply max run speed cap. + if ((val > character().getMaxRunSpeed()) && !getActiveChar().canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + return character().getMaxRunSpeed(); + } + + // Check for mount penalties + if (getActiveChar().isMounted()) { + // if level diff with mount >= 10, it decreases move speed by 50% + if ((getActiveChar().getMountLevel() - getActiveChar().getLevel()) >= 10) { + val /= 2; + } + // if mount is hungry, it decreases move speed by 50% + if (getActiveChar().isHungry()) { + val /= 2; + } + } + + return val; + } + + @Override + public double getWalkSpeed() { + double val = super.getWalkSpeed() + character().getRunSpeedBoost(); + + // Apply max run speed cap. + if ((val > character().getMaxRunSpeed()) && !getActiveChar().canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + return character().getMaxRunSpeed(); + } + + if (getActiveChar().isMounted()) { + // if level diff with mount >= 10, it decreases move speed by 50% + if ((getActiveChar().getMountLevel() - getActiveChar().getLevel()) >= 10) { + val /= 2; + } + // if mount is hungry, it decreases move speed by 50% + if (getActiveChar().isHungry()) { + val /= 2; + } + } + + return val; + } + + @Override + public double getPAtkSpd() { + double val = super.getPAtkSpd(); + + if ((val > character().getMaxPAtkSpeed()) && !getActiveChar().canOverrideCond(PcCondOverride.MAX_STATS_VALUE)) { + return character().getMaxPAtkSpeed(); + } + + return val; + } + + private void updateVitalityLevel(boolean quiet) { + final byte level; + + if (_vitalityPoints <= VITALITY_LEVELS[0]) { + level = 0; + } else if (_vitalityPoints <= VITALITY_LEVELS[1]) { + level = 1; + } else if (_vitalityPoints <= VITALITY_LEVELS[2]) { + level = 2; + } else if (_vitalityPoints <= VITALITY_LEVELS[3]) { + level = 3; + } else { + level = 4; + } + + if (!quiet && (level != _vitalityLevel)) { + if (level < _vitalityLevel) { + // Gain Nevit Points on Decrease Vitality Level + getActiveChar().getHuntingSystem().addPoints(hunting().getNevitDecreaseVitalityAcquirePoints()); + getActiveChar().sendPacket(SystemMessageId.VITALITY_HAS_DECREASED); + } else { + getActiveChar().sendPacket(SystemMessageId.VITALITY_HAS_INCREASED); + } + if (level == 0) { + getActiveChar().sendPacket(SystemMessageId.VITALITY_IS_EXHAUSTED); + } else if (level == 4) { + getActiveChar().sendPacket(SystemMessageId.VITALITY_IS_AT_MAXIMUM); + } + } + + _vitalityLevel = level; + } + + /* + * Return current vitality points in integer format + */ + public int getVitalityPoints() { + return (int) _vitalityPoints; + } + + /* + * Set current vitality points to this value if quiet = true - does not send system messages + */ + public void setVitalityPoints(int points, boolean quiet) { + points = Math.min(Math.max(points, MIN_VITALITY_POINTS), MAX_VITALITY_POINTS); + if (points == _vitalityPoints) { + return; + } + + _vitalityPoints = points; + updateVitalityLevel(quiet); + getActiveChar().sendPacket(new ExVitalityPointInfo(getVitalityPoints())); + } + + public synchronized void updateVitalityPoints(float points, boolean useRates, boolean quiet) { + if ((points == 0) || !vitality().enabled()) { + return; + } + + if (useRates) { + if (getActiveChar().isLucky()) { + return; + } + + // vitality consumed + if (points < 0) { + int stat = (int) calcStat(Stats.VITALITY_CONSUME_RATE, 1, getActiveChar(), null); + + if (getActiveChar().getHuntingSystem().isNevitBlessingActive()) { + stat = -10; // Increase Vitality During Blessing + } + + if (stat == 0) { + return; + } + if (stat < 0) { + points = -points; + } + } + + // vitality increased + if (points > 0) { + points *= vitality().getRateVitalityGain(); + } else { + // vitality decreased + points *= vitality().getRateVitalityLost(); + } + } + + if (points > 0) { + points = Math.min(_vitalityPoints + points, MAX_VITALITY_POINTS); + } else { + points = Math.max(_vitalityPoints + points, MIN_VITALITY_POINTS); + } + + if (Math.abs(points - _vitalityPoints) <= 1e-6) { + return; + } + + _vitalityPoints = points; + updateVitalityLevel(quiet); + } + + public double getVitalityMultiplier() { + double vitality = 1.0; + if (vitality().enabled()) { + switch (getVitalityLevel()) { + case 1 -> vitality = vitality().getRateVitalityLevel1(); + case 2 -> vitality = vitality().getRateVitalityLevel2(); + case 3 -> vitality = vitality().getRateVitalityLevel3(); + case 4 -> vitality = vitality().getRateVitalityLevel4(); + } + } + return vitality; + } + + public byte getVitalityLevel() { + if (getActiveChar().getHuntingSystem().isNevitBlessingActive()) { + return 4; + } + return _vitalityLevel; + } + + /* + * Return current nevit points in integer format + */ + public int getNevitBlessingPoints() { + return _nevitPoints; + } + + /* + * Set current nevit blessing points + */ + public void setNevitBlessingPoints(int points) { + _nevitPoints = points; + getActiveChar().sendPacket(new ExNevitAdventPointInfoPacket(getNevitBlessingPoints())); + } + + public int getHuntingBonusTime() { + return _huntingBonusTime; + } + + public void setHuntingBonusTime(int time) { + _huntingBonusTime = time; + } + + public int getNevitBlessingTime() { + return _nevitBlessingTime; + } + + public void setNevitBlessingTime(int time) { + _nevitBlessingTime = time; + } + + public double getExpBonusMultiplier() { + double bonus = 1.0; + double hunting = 1.0; + + // Bonus from Vitality System + double vitality = getVitalityMultiplier(); + + // Bonus from Nevit's Blessing / Nevit's Hunting + double nevits = getActiveChar().getHuntingSystem().getNevitHourglassMultiplier(); + + // Bonus exp from skills + double bonusExp = 1 + (calcStat(Stats.BONUS_EXP, 0, null, null) / 100); + + if (vitality > 1.0) { + bonus += (vitality - 1); + } + if (nevits > 1.0) { + bonus += (nevits - 1); + } + if (hunting > 1.0) { + bonus += (hunting - 1); + } + if (bonusExp > 1) { + bonus += (bonusExp - 1); + } + + // Check for abnormal bonuses + bonus = Math.max(bonus, 1); + bonus = Math.min(bonus, character().getMaxExpBonus()); + + return bonus; + } + + public double getSpBonusMultiplier() { + double bonus = 1.0; + double hunting = 1.0; + + // Bonus from Vitality System + double vitality = getVitalityMultiplier(); + + // Bonus from Nevit's Blessing / Nevit's Hunting + double nevits = getActiveChar().getHuntingSystem().getNevitHourglassMultiplier(); + + // Bonus sp from skills + double bonusSp = 1 + (calcStat(Stats.BONUS_SP, 0, null, null) / 100); + + if (vitality > 1.0) { + bonus += (vitality - 1); + } + if (nevits > 1.0) { + bonus += (nevits - 1); + } + if (hunting > 1.0) { + bonus += (hunting - 1); + } + if (bonusSp > 1) { + bonus += (bonusSp - 1); + } + + // Check for abnormal bonuses + bonus = Math.max(bonus, 1); + bonus = Math.min(bonus, character().getMaxSpBonus()); + + return bonus; + } + + @Override + public int getMaxLevel() { + return getActiveChar().isSubClassActive() ? character().getMaxSubclassLevel() : character().getMaxPlayerLevel(); + } + + @Override + public int getMaxExpLevel() { + return getActiveChar().isSubClassActive() ? character().getMaxSubclassLevel() + 1 : character().getMaxPlayerLevel() + 1; + } + + @Override + public final L2PcInstance getActiveChar() { + return (L2PcInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/PetStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PetStat.java new file mode 100644 index 00000000..59882ae7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PetStat.java @@ -0,0 +1,159 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Stats; + +public class PetStat extends SummonStat { + public PetStat(L2PetInstance activeChar) { + super(activeChar); + } + + public boolean addExp(int value) { + if (getActiveChar().isUncontrollable() || !super.addExp(value)) { + return false; + } + + getActiveChar().updateAndBroadcastStatus(1); + // The PetInfo packet wipes the PartySpelled (list of active spells' icons). Re-add them + getActiveChar().updateEffectIcons(true); + + return true; + } + + public boolean addExpAndSp(long addToExp, int addToSp) { + if (getActiveChar().isUncontrollable() || !addExp(addToExp)) { + return false; + } + + // Not used in H5 + // SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PET_EARNED_S1_EXP).addLong(addToExp); + getActiveChar().updateAndBroadcastStatus(1); + return true; + } + + @Override + public final long getExpForLevel(int level) { + try { + return PetDataTable.getInstance().getPetLevelData(getActiveChar().getId(), Math.min(level, getMaxExpLevel())).getPetMaxExp(); + } catch (NullPointerException e) { + if (getActiveChar() != null) { + LOG.warn("Pet objectId:{}, NpcId:{}, level:{} is missing data from pets_stats table!", getActiveChar().getObjectId(), getActiveChar().getId(), level); + } + throw e; + } + } + + @Override + public void setLevel(int value) { + getActiveChar().setPetData(PetDataTable.getInstance().getPetLevelData(getActiveChar().getTemplate().getId(), value)); + if (getActiveChar().getPetLevelData() == null) { + throw new IllegalArgumentException("No pet data for npc: " + getActiveChar().getTemplate().getId() + " level: " + value); + } + getActiveChar().stopFeed(); + super.setLevel(value); + + getActiveChar().startFeed(); + + if (getActiveChar().getControlItem() != null) { + getActiveChar().getControlItem().setEnchantLevel(getLevel()); + } + } + + @Override + public int getMaxLevel() { + return character().getMaxPetLevel(); + } + + @Override + public int getMaxExpLevel() { + return character().getMaxPetLevel() + 1; + } + + @Override + public L2PetInstance getActiveChar() { + return (L2PetInstance) super.getActiveChar(); + } + + public final int getFeedBattle() { + return getActiveChar().getPetLevelData().getPetFeedBattle(); + } + + public final int getFeedNormal() { + return getActiveChar().getPetLevelData().getPetFeedNormal(); + } + + public final int getMaxFeed() { + return getActiveChar().getPetLevelData().getPetMaxFeed(); + } + + @Override + public int getMaxHp() { + return (int) calcStat(Stats.MAX_HP, getActiveChar().getPetLevelData().getPetMaxHP(), null, null); + } + + @Override + public int getMaxMp() { + return (int) calcStat(Stats.MAX_MP, getActiveChar().getPetLevelData().getPetMaxMP(), null, null); + } + + @Override + public double getMAtk(L2Character target, Skill skill) { + return calcStat(Stats.MAGIC_ATTACK, getActiveChar().getPetLevelData().getPetMAtk(), target, skill); + } + + @Override + public double getMDef(L2Character target, Skill skill) { + return calcStat(Stats.MAGIC_DEFENCE, getActiveChar().getPetLevelData().getPetMDef(), target, skill); + } + + @Override + public double getPAtk(L2Character target) { + return calcStat(Stats.POWER_ATTACK, getActiveChar().getPetLevelData().getPetPAtk(), target, null); + } + + @Override + public double getPDef(L2Character target) { + return calcStat(Stats.POWER_DEFENCE, getActiveChar().getPetLevelData().getPetPDef(), target, null); + } + + @Override + public double getPAtkSpd() { + double val = super.getPAtkSpd(); + if (getActiveChar().isHungry()) { + val = val / 2; + } + return val; + } + + @Override + public int getMAtkSpd() { + int val = super.getMAtkSpd(); + if (getActiveChar().isHungry()) { + val = val / 2; + } + return val; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/PlayableStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PlayableStat.java new file mode 100644 index 00000000..6cedb3d6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/PlayableStat.java @@ -0,0 +1,263 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.json.ExperienceData; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.character.playable.PlayableExpChanged; +import com.l2jserver.gameserver.model.events.impl.character.player.PlayerLevelChanged; +import com.l2jserver.gameserver.model.events.returns.TerminateReturn; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.model.zone.type.L2SwampZone; + +public class PlayableStat extends CharStat { + protected static final Logger LOG = LoggerFactory.getLogger(PlayableStat.class); + + private final AtomicLong _exp = new AtomicLong(); + private final AtomicInteger _sp = new AtomicInteger(); + + public PlayableStat(L2Playable activeChar) { + super(activeChar); + } + + public long getExp() { + return _exp.get(); + } + + public int getSp() { + return _sp.get(); + } + + public void setExp(long exp) { + _exp.set(exp); + } + + public void setSp(int sp) { + _sp.set(sp); + } + + /** + * Contains only under zero check + */ + public boolean removeExp(long exp) { + final long currentExp = getExp(); + if (currentExp < exp) { + _exp.addAndGet(-currentExp); + } else { + _exp.addAndGet(-exp); + } + syncExpLevel(false); + return true; + } + + /** + * Contains only under zero check + */ + public boolean removeSp(int sp) { + final int currentSp = getSp(); + if (currentSp < sp) { + _sp.addAndGet(-currentSp); + } else { + _sp.addAndGet(-sp); + } + return true; + } + + public boolean addExp(long value) { + final long currentExp = getExp(); + final long totalExp = currentExp + value; + final var term = EventDispatcher.getInstance().notifyEvent(new PlayableExpChanged(getActiveChar(), currentExp, totalExp), getActiveChar(), TerminateReturn.class); + if ((term != null) && term.terminate()) { + return false; + } + + if ((totalExp < 0) || ((value > 0) && (currentExp == (getExpForLevel(getMaxExpLevel()) - 1)))) { + return true; + } + + if (totalExp >= getExpForLevel(getMaxExpLevel())) { + value = (getExpForLevel(getMaxExpLevel()) - 1 - currentExp); + } + + if (_exp.addAndGet(value) >= getExpForLevel(getLevel() + 1)) { + syncExpLevel(true); + } + + return true; + } + + /** + * Check if level need to be increased / decreased + * @param isExpIncreased + */ + private void syncExpLevel(boolean isExpIncreased) { + int minimumLevel = getActiveChar().getMinLevel(); + long currentExp = getExp(); + int maxLevel = getMaxLevel(); + int currentLevel = getLevel(); + + if (isExpIncreased) { + for (int tmp = currentLevel; tmp <= maxLevel; tmp++) { + if (currentExp >= getExpForLevel(tmp)) { + if (currentExp >= getExpForLevel(tmp + 1)) { + continue; + } + if (tmp < minimumLevel) { + tmp = minimumLevel; + } + + if (tmp != currentLevel) { + int newLevel = tmp - currentLevel; + EventDispatcher.getInstance().notifyEventAsync(new PlayerLevelChanged(getActiveChar().getActingPlayer(), currentLevel, newLevel), getActiveChar()); + getActiveChar().addLevel(newLevel); + } + break; + } + } + } else { + for (int tmp = currentLevel; tmp >= minimumLevel; tmp--) { + if (currentExp < getExpForLevel(tmp)) { + if (currentExp < getExpForLevel(tmp - 1)) { + continue; + } + --tmp; + if (tmp < minimumLevel) { + tmp = minimumLevel; + } + + if (tmp != currentLevel) { + int newLevel = tmp - currentLevel; + EventDispatcher.getInstance().notifyEventAsync(new PlayerLevelChanged(getActiveChar().getActingPlayer(), currentLevel, newLevel), getActiveChar()); + getActiveChar().addLevel(newLevel); + } + break; + } + } + } + } + + public boolean addSp(int sp) { + if (sp < 0) { + LOG.warn("addSp accepts only positive numbers!"); + return false; + } + int currentSp = getSp(); + if (currentSp == Integer.MAX_VALUE) { + return false; + } + + if (sp > (Integer.MAX_VALUE - currentSp)) { + _sp.set(Integer.MAX_VALUE); + } else { + _sp.addAndGet(sp); + } + return true; + } + + public boolean addLevel(int value) { + final int currentLevel = getLevel(); + if ((currentLevel + value) > getMaxLevel()) { + if (currentLevel < getMaxLevel()) { + value = getMaxLevel() - currentLevel; + } else { + return false; + } + } + + boolean levelIncreased = ((currentLevel + value) > currentLevel); + value += currentLevel; + setLevel(value); + + // Sync up exp with current level + if ((getExp() >= getExpForLevel(getLevel() + 1)) || (getExpForLevel(getLevel()) > getExp())) { + setExp(getExpForLevel(getLevel())); + } + + if (!levelIncreased) { + return false; + } + + getActiveChar().getStatus().setCurrentHp(getActiveChar().getStat().getMaxHp()); + getActiveChar().getStatus().setCurrentMp(getActiveChar().getStat().getMaxMp()); + + return true; + } + + /** + * Get required exp for specific level + */ + public long getExpForLevel(int level) { + return ExperienceData.getInstance().getExpForLevel(level); + } + + /** + * Get maximum level that playable can reach.
      + * Overridden in : + *
    • PcStat
    • + *
    • PetStat
    • + */ + public int getMaxLevel() { + // Dummy method + return character().getMaxPlayerLevel(); + } + + /** + * Get maximum level of experience is max level +1 for get (100%). + */ + public int getMaxExpLevel() { + return character().getMaxPlayerLevel() + 1; + } + + @Override + public L2Playable getActiveChar() { + return (L2Playable) super.getActiveChar(); + } + + @Override + public double getRunSpeed() { + if (getActiveChar().isInsideZone(ZoneId.SWAMP)) { + final L2SwampZone zone = ZoneManager.getInstance().getZone(getActiveChar(), L2SwampZone.class); + if (zone != null) { + return super.getRunSpeed() * zone.getMoveBonus(); + } + } + return super.getRunSpeed(); + } + + @Override + public double getWalkSpeed() { + if (getActiveChar().isInsideZone(ZoneId.SWAMP)) { + final L2SwampZone zone = ZoneManager.getInstance().getZone(getActiveChar(), L2SwampZone.class); + if (zone != null) { + return super.getWalkSpeed() * zone.getMoveBonus(); + } + } + return super.getWalkSpeed(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/StaticObjStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/StaticObjStat.java new file mode 100644 index 00000000..38e10303 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/StaticObjStat.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; + +public class StaticObjStat extends CharStat { + public StaticObjStat(L2StaticObjectInstance activeChar) { + super(activeChar); + } + + @Override + public L2StaticObjectInstance getActiveChar() { + return (L2StaticObjectInstance) super.getActiveChar(); + } + + @Override + public final int getLevel() { + return getActiveChar().getLevel(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/SummonStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/SummonStat.java new file mode 100644 index 00000000..f8aa281e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/SummonStat.java @@ -0,0 +1,37 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.L2Summon; + +public class SummonStat extends PlayableStat { + public SummonStat(L2Summon activeChar) { + super(activeChar); + } + + @Override + public L2Summon getActiveChar() { + return (L2Summon) super.getActiveChar(); + } + + @Override + public int getMaxBuffCount() { + return getActiveChar().getOwner().getStat().getMaxBuffCount(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/stat/VehicleStat.java b/src/main/java/com/l2jserver/gameserver/model/actor/stat/VehicleStat.java new file mode 100644 index 00000000..22172561 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/stat/VehicleStat.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.stat; + +import com.l2jserver.gameserver.model.actor.L2Vehicle; + +public class VehicleStat extends CharStat { + private float _moveSpeed = 0; + private int _rotationSpeed = 0; + + public VehicleStat(L2Vehicle activeChar) { + super(activeChar); + } + + @Override + public double getMoveSpeed() { + return _moveSpeed; + } + + public final void setMoveSpeed(float speed) { + _moveSpeed = speed; + } + + public final double getRotationSpeed() { + return _rotationSpeed; + } + + public final void setRotationSpeed(int speed) { + _rotationSpeed = speed; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/AttackableStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/AttackableStatus.java new file mode 100644 index 00000000..f9e9f223 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/AttackableStatus.java @@ -0,0 +1,62 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; + +public class AttackableStatus extends NpcStatus { + public AttackableStatus(L2Attackable activeChar) { + super(activeChar); + } + + @Override + public final void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public final void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHpConsumption) { + if (getActiveChar().isDead()) { + return; + } + + if (value > 0) { + if (getActiveChar().isOverhit()) { + getActiveChar().setOverhitValues(attacker, value); + } else { + getActiveChar().overhitEnabled(false); + } + } else { + getActiveChar().overhitEnabled(false); + } + + super.reduceHp(value, attacker, awake, isDOT, isHpConsumption); + + if (!getActiveChar().isDead()) { + // And the attacker's hit didn't kill the mob, clear the over-hit flag + getActiveChar().overhitEnabled(false); + } + } + + @Override + public L2Attackable getActiveChar() { + return (L2Attackable) super.getActiveChar(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/CharStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/CharStatus.java new file mode 100644 index 00000000..1145f727 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/CharStatus.java @@ -0,0 +1,373 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.stats.Formulas; + +public class CharStatus { + protected static final Logger LOG = LoggerFactory.getLogger(CharStatus.class); + + private final L2Character _activeChar; + + private double _currentHp = 0; // Current HP of the L2Character + private double _currentMp = 0; // Current MP of the L2Character + + /** Array containing all clients that need to be notified about hp/mp updates of the L2Character */ + private Set _statusListener; + + private Future _regTask; + + protected byte _flagsRegenActive = 0; + + protected static final byte REGEN_FLAG_CP = 4; + private static final byte REGEN_FLAG_HP = 1; + private static final byte REGEN_FLAG_MP = 2; + + public CharStatus(L2Character activeChar) { + _activeChar = activeChar; + } + + /** + * Add the object to the list of L2Character that must be informed of HP/MP updates of this L2Character.
      + * Concept:
      + * Each L2Character owns a list called _statusListener that contains all L2PcInstance to inform of HP/MP updates.
      + * Players who must be informed are players that target this L2Character.
      + * When a RegenTask is in progress sever just need to go through this list to send Server->Client packet StatusUpdate.
      + * Example of use: + *
        + *
      • Target a PC or NPC
      • + *
          + * @param object L2Character to add to the listener + */ + public final void addStatusListener(L2Character object) { + if (object == getActiveChar()) { + return; + } + + getStatusListener().add(object); + } + + /** + * Remove the object from the list of L2Character that must be informed of HP/MP updates of this L2Character.
          + * Concept:
          + * Each L2Character owns a list called _statusListener that contains all L2PcInstance to inform of HP/MP updates.
          + * Players who must be informed are players that target this L2Character.
          + * When a RegenTask is in progress sever just need to go through this list to send Server->Client packet StatusUpdate.
          + * Example of use : + *
            + *
          • Untarget a PC or NPC
          • + *
          + * @param object L2Character to add to the listener + */ + public final void removeStatusListener(L2Character object) { + getStatusListener().remove(object); + } + + /** + * Return the list of L2Character that must be informed of HP/MP updates of this L2Character.
          + * Concept:
          + * Each L2Character owns a list called _statusListener that contains all L2PcInstance to inform of HP/MP updates.
          + * Players who must be informed are players that target this L2Character.
          + * When a RegenTask is in progress sever just need to go through this list to send Server->Client packet StatusUpdate. + * @return The list of L2Character to inform or null if empty + */ + public final Set getStatusListener() { + if (_statusListener == null) { + _statusListener = ConcurrentHashMap.newKeySet(); + } + return _statusListener; + } + + // place holder, only PcStatus has CP + public void reduceCp(int value) { + } + + /** + * Reduce the current HP of the L2Character and launch the doDie Task if necessary. + */ + public void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + public void reduceHp(double value, L2Character attacker, boolean isHpConsumption) { + reduceHp(value, attacker, true, false, isHpConsumption); + } + + public void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHPConsumption) { + if (getActiveChar().isDead()) { + return; + } + + // invul handling + if ((getActiveChar().isInvul() || getActiveChar().isHpBlocked()) && !(isDOT || isHPConsumption)) { + return; + } + + if (attacker != null) { + final L2PcInstance attackerPlayer = attacker.getActingPlayer(); + if ((attackerPlayer != null) && attackerPlayer.isGM() && !attackerPlayer.getAccessLevel().canGiveDamage()) { + return; + } + } + + if (!isDOT && !isHPConsumption) { + getActiveChar().stopEffectsOnDamage(awake); + if (getActiveChar().isStunned() && (Rnd.get(10) == 0)) { + getActiveChar().stopStunning(true); + } + } + + if (value > 0) { + setCurrentHp(Math.max(getCurrentHp() - value, 0)); + } + + if ((getActiveChar().getCurrentHp() < 0.5) && getActiveChar().isMortal()) // Die + { + getActiveChar().abortAttack(); + getActiveChar().abortCast(); + + if (general().debug()) { + LOG.debug("char is dead."); + } + + getActiveChar().doDie(attacker); + } + } + + public void reduceMp(double value) { + setCurrentMp(Math.max(getCurrentMp() - value, 0)); + } + + /** + * Start the HP/MP/CP Regeneration task.
          + * Actions: + *
            + *
          • Calculate the regen task period
          • + *
          • Launch the HP/MP/CP Regeneration task with Medium priority
          • + *
          + */ + public final synchronized void startHpMpRegeneration() { + if ((_regTask == null) && !getActiveChar().isDead()) { + if (general().debug()) { + LOG.debug("HP/MP regen started"); + } + + // Get the Regeneration period + int period = Formulas.getRegeneratePeriod(getActiveChar()); + + // Create the HP/MP/CP Regeneration task + _regTask = ThreadPoolManager.getInstance().scheduleEffectAtFixedRate(new RegenTask(), period, period); + } + } + + /** + * Stop the HP/MP/CP Regeneration task.
          + * Actions: + *
            + *
          • Set the RegenActive flag to False
          • + *
          • Stop the HP/MP/CP Regeneration task
          • + *
          + */ + public final synchronized void stopHpMpRegeneration() { + if (_regTask != null) { + if (general().debug()) { + LOG.debug("HP/MP regen stop"); + } + + // Stop the HP/MP/CP Regeneration task + _regTask.cancel(false); + _regTask = null; + + // Set the RegenActive flag to false + _flagsRegenActive = 0; + } + } + + // place holder, only PcStatus has CP + public double getCurrentCp() { + return 0; + } + + // place holder, only PcStatus has CP + public void setCurrentCp(double newCp) { + } + + public final double getCurrentHp() { + return _currentHp; + } + + public final void setCurrentHp(double newHp) { + setCurrentHp(newHp, true); + } + + /** + * Sets the current hp of this character. + * @param newHp the new hp + * @param broadcastPacket if true StatusUpdate packet will be broadcasted. + * @return @{code true} if hp was changed, @{code false} otherwise. + */ + public boolean setCurrentHp(double newHp, boolean broadcastPacket) { + // Get the Max HP of the L2Character + int currentHp = (int) getCurrentHp(); + final double maxHp = getActiveChar().getMaxHp(); + + synchronized (this) { + if (getActiveChar().isDead()) { + return false; + } + + if (newHp >= maxHp) { + // Set the RegenActive flag to false + _currentHp = maxHp; + _flagsRegenActive &= ~REGEN_FLAG_HP; + + // Stop the HP/MP/CP Regeneration task + if (_flagsRegenActive == 0) { + stopHpMpRegeneration(); + } + } else { + // Set the RegenActive flag to true + _currentHp = newHp; + _flagsRegenActive |= REGEN_FLAG_HP; + + // Start the HP/MP/CP Regeneration task with Medium priority + startHpMpRegeneration(); + } + } + + boolean hpWasChanged = currentHp != _currentHp; + + // Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + if (hpWasChanged && broadcastPacket) { + getActiveChar().broadcastStatusUpdate(); + } + + return hpWasChanged; + } + + public final void setCurrentHpMp(double newHp, double newMp) { + boolean hpOrMpWasChanged = setCurrentHp(newHp, false); + hpOrMpWasChanged |= setCurrentMp(newMp, false); + if (hpOrMpWasChanged) { + getActiveChar().broadcastStatusUpdate(); + } + } + + public final double getCurrentMp() { + return _currentMp; + } + + public final void setCurrentMp(double newMp) { + setCurrentMp(newMp, true); + } + + /** + * Sets the current mp of this character. + * @param newMp the new mp + * @param broadcastPacket if true StatusUpdate packet will be broadcasted. + * @return @{code true} if mp was changed, @{code false} otherwise. + */ + public final boolean setCurrentMp(double newMp, boolean broadcastPacket) { + // Get the Max MP of the L2Character + int currentMp = (int) getCurrentMp(); + final int maxMp = getActiveChar().getMaxMp(); + + synchronized (this) { + if (getActiveChar().isDead()) { + return false; + } + + if (newMp >= maxMp) { + // Set the RegenActive flag to false + _currentMp = maxMp; + _flagsRegenActive &= ~REGEN_FLAG_MP; + + // Stop the HP/MP/CP Regeneration task + if (_flagsRegenActive == 0) { + stopHpMpRegeneration(); + } + } else { + // Set the RegenActive flag to true + _currentMp = newMp; + _flagsRegenActive |= REGEN_FLAG_MP; + + // Start the HP/MP/CP Regeneration task with Medium priority + startHpMpRegeneration(); + } + } + + boolean mpWasChanged = currentMp != _currentMp; + + // Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + if (mpWasChanged && broadcastPacket) { + getActiveChar().broadcastStatusUpdate(); + } + + return mpWasChanged; + } + + protected void doRegeneration() { + // Modify the current HP of the L2Character and broadcast Server->Client packet StatusUpdate + if (getCurrentHp() < getActiveChar().getMaxRecoverableHp()) { + setCurrentHp(getCurrentHp() + Formulas.calcHpRegen(getActiveChar()), false); + } + + // Modify the current MP of the L2Character and broadcast Server->Client packet StatusUpdate + if (getCurrentMp() < getActiveChar().getMaxRecoverableMp()) { + setCurrentMp(getCurrentMp() + Formulas.calcMpRegen(getActiveChar()), false); + } + + if ((getCurrentHp() >= getActiveChar().getMaxRecoverableHp()) && (getCurrentMp() >= getActiveChar().getMaxMp())) { + stopHpMpRegeneration(); + } + + if (getActiveChar().isInActiveRegion()) { + getActiveChar().broadcastStatusUpdate(); + } + } + + /** Task of HP/MP regeneration */ + class RegenTask implements Runnable { + @Override + public void run() { + try { + doRegeneration(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + public L2Character getActiveChar() { + return _activeChar; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/DoorStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/DoorStatus.java new file mode 100644 index 00000000..d320fe19 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/DoorStatus.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; + +public class DoorStatus extends CharStatus { + public DoorStatus(L2DoorInstance activeChar) { + super(activeChar); + } + + @Override + public L2DoorInstance getActiveChar() { + return (L2DoorInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/FolkStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/FolkStatus.java new file mode 100644 index 00000000..67342137 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/FolkStatus.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import static com.l2jserver.gameserver.config.Configuration.clanhall; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2ClanHallManagerInstance; +import com.l2jserver.gameserver.model.actor.instance.L2NpcInstance; + +public class FolkStatus extends NpcStatus { + public FolkStatus(L2Npc activeChar) { + super(activeChar); + } + + @Override + public final void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public final void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHpConsumption) { + } + + @Override + public final void reduceMp(double value) { + // If Clan Hall buff are free and it's a Clan Hall Manager MP won't get reduced. + if (clanhall().mpBuffFree() && (getActiveChar() instanceof L2ClanHallManagerInstance)) { + return; + } + super.reduceMp(value); + } + + @Override + public L2NpcInstance getActiveChar() { + return (L2NpcInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/NpcStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/NpcStatus.java new file mode 100644 index 00000000..f48cba23 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/NpcStatus.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +public class NpcStatus extends CharStatus { + public NpcStatus(L2Npc activeChar) { + super(activeChar); + } + + @Override + public void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHpConsumption) { + if (getActiveChar().isDead()) { + return; + } + + if (attacker != null) { + final L2PcInstance attackerPlayer = attacker.getActingPlayer(); + if ((attackerPlayer != null) && attackerPlayer.isInDuel()) { + attackerPlayer.setDuelState(DuelState.INTERRUPTED); + } + + // Add attackers to npc's attacker list + getActiveChar().addAttackerToAttackByList(attacker); + } + + super.reduceHp(value, attacker, awake, isDOT, isHpConsumption); + } + + @Override + public L2Npc getActiveChar() { + return (L2Npc) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/PcStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/PcStatus.java new file mode 100644 index 00000000..8ff14cac --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/PcStatus.java @@ -0,0 +1,328 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import static com.l2jserver.gameserver.config.Configuration.customs; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.enums.PrivateStoreType; +import com.l2jserver.gameserver.instancemanager.DuelManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.stat.PcStat; +import com.l2jserver.gameserver.model.stats.Formulas; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +public class PcStatus extends PlayableStatus { + private double _currentCp = 0; // Current CP of the L2PcInstance + + public PcStatus(L2PcInstance activeChar) { + super(activeChar); + } + + @Override + public final void reduceCp(int value) { + if (getCurrentCp() > value) { + setCurrentCp(getCurrentCp() - value); + } else { + setCurrentCp(0); + } + } + + @Override + public final void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false, false); + } + + @Override + public final void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHPConsumption) { + reduceHp(value, attacker, awake, isDOT, isHPConsumption, false); + } + + public final void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHPConsumption, boolean ignoreCP) { + if (getActiveChar().isDead()) { + return; + } + + // If OFFLINE_MODE_NO_DAMAGE is enabled and player is offline and he is in store/craft mode, no damage is taken. + if (customs().offlineModeNoDamage() && (getActiveChar().getClient() != null) && getActiveChar().getClient().isDetached() + && ((customs().offlineTradeEnable() && ((getActiveChar().getPrivateStoreType() == PrivateStoreType.SELL) || (getActiveChar().getPrivateStoreType() == PrivateStoreType.BUY))) + || (customs().offlineCraftEnable() && (getActiveChar().isInCraftMode() || (getActiveChar().getPrivateStoreType() == PrivateStoreType.MANUFACTURE))))) { + return; + } + + if ((getActiveChar().isInvul() || getActiveChar().isHpBlocked()) && !(isDOT || isHPConsumption)) { + return; + } + + if (!isHPConsumption) { + getActiveChar().stopEffectsOnDamage(awake); + // Attacked players in craft/shops stand up. + if (getActiveChar().isInCraftMode() || getActiveChar().isInStoreMode()) { + getActiveChar().setPrivateStoreType(PrivateStoreType.NONE); + getActiveChar().standUp(); + getActiveChar().broadcastUserInfo(); + } else if (getActiveChar().isSitting()) { + getActiveChar().standUp(); + } + + if (!isDOT) { + if (getActiveChar().isStunned() && (Rnd.get(10) == 0)) { + getActiveChar().stopStunning(true); + } + } + } + + int fullValue = (int) value; + int tDmg = 0; + + if ((attacker != null) && (attacker != getActiveChar())) { + final L2PcInstance attackerPlayer = attacker.getActingPlayer(); + + if (attackerPlayer != null) { + if (attackerPlayer.isGM() && !attackerPlayer.getAccessLevel().canGiveDamage()) { + return; + } + + if (getActiveChar().isInDuel()) { + if (getActiveChar().getDuelState() == DuelState.DEAD) { + return; + } else if (getActiveChar().getDuelState() == DuelState.WINNER) { + return; + } + + // cancel duel if player got hit by another player, that is not part of the duel + if (attackerPlayer.getDuelId() != getActiveChar().getDuelId()) { + getActiveChar().setDuelState(DuelState.INTERRUPTED); + } + } + } + + // Check and calculate transferred damage + final L2Summon summon = getActiveChar().getSummon(); + if (getActiveChar().hasServitor() && Util.checkIfInRange(1000, getActiveChar(), summon, true)) { + tDmg = ((int) value * (int) getActiveChar().getStat().calcStat(Stats.TRANSFER_DAMAGE_PERCENT, 0, null, null)) / 100; + + // Only transfer dmg up to current HP, it should not be killed + tDmg = Math.min((int) summon.getCurrentHp() - 1, tDmg); + if (tDmg > 0) { + summon.reduceCurrentHp(tDmg, attacker, null); + value -= tDmg; + fullValue = (int) value; // reduce the announced value here as player will get a message about summon damage + } + } + + int mpDam = ((int) value * (int) getActiveChar().getStat().calcStat(Stats.MANA_SHIELD_PERCENT, 0, null, null)) / 100; + if (mpDam > 0) { + mpDam = (int) (value - mpDam); + if (mpDam > getActiveChar().getCurrentMp()) { + getActiveChar().sendPacket(SystemMessageId.MP_BECAME_0_ARCANE_SHIELD_DISAPPEARING); + getActiveChar().stopSkillEffects(true, 1556); + value = mpDam - getActiveChar().getCurrentMp(); + getActiveChar().setCurrentMp(0); + } else { + getActiveChar().reduceCurrentMp(mpDam); + SystemMessage smsg = SystemMessage.getSystemMessage(SystemMessageId.ARCANE_SHIELD_DECREASED_YOUR_MP_BY_S1_INSTEAD_OF_HP); + smsg.addInt(mpDam); + getActiveChar().sendPacket(smsg); + return; + } + } + + final L2PcInstance caster = getActiveChar().getTransferringDamageTo(); + if ((caster != null) && (getActiveChar().getParty() != null) && Util.checkIfInRange(1000, getActiveChar(), caster, true) && !caster.isDead() && (getActiveChar() != caster) && getActiveChar().getParty().getMembers().contains(caster)) { + int transferDmg = ((int) value * (int) getActiveChar().getStat().calcStat(Stats.TRANSFER_DAMAGE_TO_PLAYER, 0, null, null)) / 100; + transferDmg = Math.min((int) caster.getCurrentHp() - 1, transferDmg); + if (transferDmg > 0) { + int membersInRange = 0; + for (L2PcInstance member : caster.getParty().getMembers()) { + if (Util.checkIfInRange(1000, member, caster, false) && (member != caster)) { + membersInRange++; + } + } + + if ((attacker instanceof L2Playable) && (caster.getCurrentCp() > 0)) { + if (caster.getCurrentCp() > transferDmg) { + caster.getStatus().reduceCp(transferDmg); + } else { + transferDmg = (int) (transferDmg - caster.getCurrentCp()); + caster.getStatus().reduceCp((int) caster.getCurrentCp()); + } + } + + if (membersInRange > 0) { + caster.reduceCurrentHp(transferDmg / membersInRange, attacker, null); + value -= transferDmg; + fullValue = (int) value; + } + } + } + + if (!ignoreCP && (attacker instanceof L2Playable)) { + if (getCurrentCp() >= value) { + setCurrentCp(getCurrentCp() - value); // Set Cp to diff of Cp vs value + value = 0; // No need to subtract anything from Hp + } else { + value -= getCurrentCp(); // Get diff from value vs Cp; will apply diff to Hp + setCurrentCp(0, false); // Set Cp to 0 + } + } + + if ((fullValue > 0) && !isDOT) { + // Send a System Message to the L2PcInstance + SystemMessage smsg = SystemMessage.getSystemMessage(SystemMessageId.C1_RECEIVED_DAMAGE_OF_S3_FROM_C2); + smsg.addString(getActiveChar().getName()); + smsg.addCharName(attacker); + smsg.addInt(fullValue); + getActiveChar().sendPacket(smsg); + + if ((tDmg > 0) && (attackerPlayer != null)) { + smsg = SystemMessage.getSystemMessage(SystemMessageId.GIVEN_S1_DAMAGE_TO_YOUR_TARGET_AND_S2_DAMAGE_TO_SERVITOR); + smsg.addInt(fullValue); + smsg.addInt(tDmg); + attackerPlayer.sendPacket(smsg); + } + } + } + + if (value > 0) { + value = getCurrentHp() - value; + if (value <= 0) { + if (getActiveChar().isInDuel()) { + getActiveChar().disableAllSkills(); + stopHpMpRegeneration(); + if (attacker != null) { + attacker.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + attacker.sendPacket(ActionFailed.STATIC_PACKET); + attacker.setTarget(null); + attacker.abortAttack(); + } + + // let the DuelManager know of his defeat + DuelManager.getInstance().onPlayerDefeat(getActiveChar()); + value = 1; + } else { + value = 0; + } + } + setCurrentHp(value); + } + + if ((getActiveChar().getCurrentHp() < 0.5) && !isHPConsumption) { + getActiveChar().abortAttack(); + getActiveChar().abortCast(); + + if (getActiveChar().isInOlympiadMode()) { + stopHpMpRegeneration(); + getActiveChar().setIsDead(true); + getActiveChar().setIsPendingRevive(true); + if (getActiveChar().hasSummon()) { + getActiveChar().getSummon().getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE, null); + } + return; + } + + getActiveChar().doDie(attacker); + } + } + + @Override + public final double getCurrentCp() { + return _currentCp; + } + + @Override + public final void setCurrentCp(double newCp) { + setCurrentCp(newCp, true); + } + + public final void setCurrentCp(double newCp, boolean broadcastPacket) { + // Get the Max CP of the L2Character + int currentCp = (int) getCurrentCp(); + int maxCp = getActiveChar().getStat().getMaxCp(); + + synchronized (this) { + if (getActiveChar().isDead()) { + return; + } + + if (newCp < 0) { + newCp = 0; + } + + if (newCp >= maxCp) { + // Set the RegenActive flag to false + _currentCp = maxCp; + _flagsRegenActive &= ~REGEN_FLAG_CP; + + // Stop the HP/MP/CP Regeneration task + if (_flagsRegenActive == 0) { + stopHpMpRegeneration(); + } + } else { + // Set the RegenActive flag to true + _currentCp = newCp; + _flagsRegenActive |= REGEN_FLAG_CP; + + // Start the HP/MP/CP Regeneration task with Medium priority + startHpMpRegeneration(); + } + } + + // Send the Server->Client packet StatusUpdate with current HP and MP to all other L2PcInstance to inform + if ((currentCp != _currentCp) && broadcastPacket) { + getActiveChar().broadcastStatusUpdate(); + } + } + + @Override + protected void doRegeneration() { + final PcStat charstat = getActiveChar().getStat(); + + // Modify the current CP of the L2Character and broadcast Server->Client packet StatusUpdate + if (getCurrentCp() < charstat.getMaxRecoverableCp()) { + setCurrentCp(getCurrentCp() + Formulas.calcCpRegen(getActiveChar()), false); + } + + // Modify the current HP of the L2Character and broadcast Server->Client packet StatusUpdate + if (getCurrentHp() < charstat.getMaxRecoverableHp()) { + setCurrentHp(getCurrentHp() + Formulas.calcHpRegen(getActiveChar()), false); + } + + // Modify the current MP of the L2Character and broadcast Server->Client packet StatusUpdate + if (getCurrentMp() < charstat.getMaxRecoverableMp()) { + setCurrentMp(getCurrentMp() + Formulas.calcMpRegen(getActiveChar()), false); + } + + getActiveChar().broadcastStatusUpdate(); // send the StatusUpdate packet + } + + @Override + public L2PcInstance getActiveChar() { + return (L2PcInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/PetStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/PetStatus.java new file mode 100644 index 00000000..ff6675e0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/PetStatus.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class PetStatus extends SummonStatus { + private int _currentFed = 0; // Current Fed of the L2PetInstance + + public PetStatus(L2PetInstance activeChar) { + super(activeChar); + } + + @Override + public final void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public final void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHpConsumption) { + if (getActiveChar().isDead()) { + return; + } + + super.reduceHp(value, attacker, awake, isDOT, isHpConsumption); + + if (attacker != null) { + if (!isDOT && (getActiveChar().getOwner() != null)) { + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PET_RECEIVED_S2_DAMAGE_BY_C1); + sm.addCharName(attacker); + sm.addInt((int) value); + getActiveChar().sendPacket(sm); + } + getActiveChar().getAI().notifyEvent(CtrlEvent.EVT_ATTACKED, attacker); + } + } + + public int getCurrentFed() { + return _currentFed; + } + + public void setCurrentFed(int value) { + _currentFed = value; + } + + @Override + public L2PetInstance getActiveChar() { + return (L2PetInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/PlayableStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/PlayableStatus.java new file mode 100644 index 00000000..975a8b1b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/PlayableStatus.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.model.actor.L2Playable; + +public class PlayableStatus extends CharStatus { + public PlayableStatus(L2Playable activeChar) { + super(activeChar); + } + + @Override + public L2Playable getActiveChar() { + return (L2Playable) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/SiegeFlagStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/SiegeFlagStatus.java new file mode 100644 index 00000000..9584427f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/SiegeFlagStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2SiegeFlagInstance; + +public class SiegeFlagStatus extends NpcStatus { + public SiegeFlagStatus(L2SiegeFlagInstance activeChar) { + super(activeChar); + } + + @Override + public void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHpConsumption) { + if (getActiveChar().isAdvancedHeadquarter()) { + value /= 2.; + } + + super.reduceHp(value, attacker, awake, isDOT, isHpConsumption); + } + + @Override + public L2SiegeFlagInstance getActiveChar() { + return (L2SiegeFlagInstance) super.getActiveChar(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/StaticObjStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/StaticObjStatus.java new file mode 100644 index 00000000..52c93839 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/StaticObjStatus.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; + +public class StaticObjStatus extends CharStatus { + public StaticObjStatus(L2StaticObjectInstance activeChar) { + super(activeChar); + } + + @Override + public L2StaticObjectInstance getActiveChar() { + return (L2StaticObjectInstance) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/status/SummonStatus.java b/src/main/java/com/l2jserver/gameserver/model/actor/status/SummonStatus.java new file mode 100644 index 00000000..55c4a181 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/status/SummonStatus.java @@ -0,0 +1,101 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.status; + +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.util.Util; + +public class SummonStatus extends PlayableStatus { + public SummonStatus(L2Summon activeChar) { + super(activeChar); + } + + @Override + public void reduceHp(double value, L2Character attacker) { + reduceHp(value, attacker, true, false, false); + } + + @Override + public void reduceHp(double value, L2Character attacker, boolean awake, boolean isDOT, boolean isHPConsumption) { + if ((attacker == null) || getActiveChar().isDead()) { + return; + } + + final L2PcInstance attackerPlayer = attacker.getActingPlayer(); + if ((attackerPlayer != null) && ((getActiveChar().getOwner() == null) || (getActiveChar().getOwner().getDuelId() != attackerPlayer.getDuelId()))) { + attackerPlayer.setDuelState(DuelState.INTERRUPTED); + } + + final L2PcInstance caster = getActiveChar().getTransferringDamageTo(); + if (getActiveChar().getOwner().getParty() != null) { + if ((caster != null) && Util.checkIfInRange(1000, getActiveChar(), caster, true) && !caster.isDead() && getActiveChar().getParty().getMembers().contains(caster)) { + int transferDmg = ((int) value * (int) getActiveChar().getStat().calcStat(Stats.TRANSFER_DAMAGE_TO_PLAYER, 0, null, null)) / 100; + transferDmg = Math.min((int) caster.getCurrentHp() - 1, transferDmg); + if (transferDmg > 0) { + int membersInRange = 0; + for (L2PcInstance member : caster.getParty().getMembers()) { + if (Util.checkIfInRange(1000, member, caster, false) && (member != caster)) { + membersInRange++; + } + } + if ((attacker instanceof L2Playable) && (caster.getCurrentCp() > 0)) { + if (caster.getCurrentCp() > transferDmg) { + caster.getStatus().reduceCp(transferDmg); + } else { + transferDmg = (int) (transferDmg - caster.getCurrentCp()); + caster.getStatus().reduceCp((int) caster.getCurrentCp()); + } + } + if (membersInRange > 0) { + caster.reduceCurrentHp(transferDmg / membersInRange, attacker, null); + value -= transferDmg; + } + } + } + } else if ((caster != null) && (caster == getActiveChar().getOwner()) && Util.checkIfInRange(1000, getActiveChar(), caster, true) && !caster.isDead()) { + // when no party, transfer only to owner (caster) + int transferDmg = ((int) value * (int) getActiveChar().getStat().calcStat(Stats.TRANSFER_DAMAGE_TO_PLAYER, 0, null, null)) / 100; + transferDmg = Math.min((int) caster.getCurrentHp() - 1, transferDmg); + if (transferDmg > 0) { + if ((attacker instanceof L2Playable) && (caster.getCurrentCp() > 0)) { + if (caster.getCurrentCp() > transferDmg) { + caster.getStatus().reduceCp(transferDmg); + } else { + transferDmg = (int) (transferDmg - caster.getCurrentCp()); + caster.getStatus().reduceCp((int) caster.getCurrentCp()); + } + } + + caster.reduceCurrentHp(transferDmg, attacker, null); + value -= transferDmg; + } + } + super.reduceHp(value, attacker, awake, isDOT, isHPConsumption); + } + + @Override + public L2Summon getActiveChar() { + return (L2Summon) super.getActiveChar(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/CommandChannelTimer.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/CommandChannelTimer.java new file mode 100644 index 00000000..78ebce41 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/CommandChannelTimer.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.attackable; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.L2Attackable; + +/** + * @author xban1x + */ +public final class CommandChannelTimer implements Runnable { + private final L2Attackable _attackable; + + public CommandChannelTimer(L2Attackable attackable) { + _attackable = attackable; + } + + @Override + public void run() { + if (_attackable == null) { + return; + } + + if ((System.currentTimeMillis() - _attackable.getCommandChannelLastAttack()) > character().getRaidLootRightsInterval()) { + _attackable.setCommandChannelTimer(null); + _attackable.setFirstCommandChannelAttacked(null); + _attackable.setCommandChannelLastAttack(0); + } else { + ThreadPoolManager.getInstance().scheduleGeneral(this, 10000); // 10sec + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/OnKillNotifyTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/OnKillNotifyTask.java new file mode 100644 index 00000000..a377dc02 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/attackable/OnKillNotifyTask.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.attackable; + +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.quest.Quest; + +/** + * @author xban1x + */ +public final class OnKillNotifyTask implements Runnable { + private final L2Attackable _attackable; + private final Quest _quest; + private final L2PcInstance _killer; + private final boolean _isSummon; + + public OnKillNotifyTask(L2Attackable attackable, Quest quest, L2PcInstance killer, boolean isSummon) { + _attackable = attackable; + _quest = quest; + _killer = killer; + _isSummon = isSummon; + } + + @Override + public void run() { + if ((_quest != null) && (_attackable != null) && (_killer != null)) { + _quest.onKill(_attackable, _killer, _isSummon); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/FlyToLocationTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/FlyToLocationTask.java new file mode 100644 index 00000000..41059236 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/FlyToLocationTask.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.character; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.network.serverpackets.FlyToLocation; +import com.l2jserver.gameserver.network.serverpackets.FlyToLocation.FlyType; + +/** + * Task dedicated to fly a player to the location + * @author xban1x + */ +public final class FlyToLocationTask implements Runnable { + private final L2Character _character; + private final Location _targetLocation; + private final FlyType _type; + + public FlyToLocationTask(L2Character character, L2Object target, FlyType type) { + _character = character; + _targetLocation = target.getLocation(); + _type = type; + } + + @Override + public void run() { + if (_character != null) { + _character.broadcastPacket(new FlyToLocation(_character, _targetLocation, _type)); + _character.setLocation(_targetLocation); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/HitTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/HitTask.java new file mode 100644 index 00000000..02cc46c7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/HitTask.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.character; + +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Task launching the function onHitTimer().
          + * Actions: + *
            + *
          • If the attacker/target is dead or use fake death, notify the AI with EVT_CANCEL and send a Server->Client packet ActionFailed (if attacker is a L2PcInstance)
          • + *
          • If attack isn't aborted, send a message system (critical hit, missed...) to attacker/target if they are L2PcInstance
          • + *
          • If attack isn't aborted and hit isn't missed, reduce HP of the target and calculate reflection damage to reduce HP of attacker if necessary
          • + *
          • if attack isn't aborted and hit isn't missed, manage attack or cast break of the target (calculating rate, sending message...)
          • + *
          + * @author xban1x + */ +public final class HitTask implements Runnable { + private final L2Character _character; + private final L2Character _hitTarget; + private final int _damage; + private final boolean _crit; + private final boolean _miss; + private final byte _shld; + private final boolean _soulshot; + + public HitTask(L2Character character, L2Character target, int damage, boolean crit, boolean miss, boolean soulshot, byte shld) { + _character = character; + _hitTarget = target; + _damage = damage; + _crit = crit; + _shld = shld; + _miss = miss; + _soulshot = soulshot; + } + + @Override + public void run() { + if (_character != null) { + _character.onHitTimer(_hitTarget, _damage, _crit, _miss, _soulshot, _shld); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/MagicUseTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/MagicUseTask.java new file mode 100644 index 00000000..5afa401e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/MagicUseTask.java @@ -0,0 +1,101 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.character; + +import java.util.List; + +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Task dedicated to magic use of character + * @author xban1x + */ +public final class MagicUseTask implements Runnable { + private final L2Character _character; + private List _targets; + private final Skill _skill; + private int _count; + private int _skillTime; + private int _phase; + private final boolean _simultaneously; + + public MagicUseTask(L2Character character, List targets, Skill s, int hit, boolean simultaneous) { + _character = character; + _targets = targets; + _skill = s; + _count = 0; + _phase = 1; + _skillTime = hit; + _simultaneously = simultaneous; + } + + @Override + public void run() { + if (_character == null) { + return; + } + switch (_phase) { + case 1 -> _character.onMagicLaunchedTimer(this); + case 2 -> _character.onMagicHitTimer(this); + case 3 -> _character.onMagicFinalizer(this); + } + } + + public int getCount() { + return _count; + } + + public int getPhase() { + return _phase; + } + + public Skill getSkill() { + return _skill; + } + + public int getSkillTime() { + return _skillTime; + } + + public List getTargets() { + return _targets; + } + + public boolean isSimultaneous() { + return _simultaneously; + } + + public void setCount(int count) { + _count = count; + } + + public void setPhase(int phase) { + _phase = phase; + } + + public void setSkillTime(int skillTime) { + _skillTime = skillTime; + } + + public void setTargets(List targets) { + _targets = targets; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/NotifyAITask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/NotifyAITask.java new file mode 100644 index 00000000..3f20751e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/NotifyAITask.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.character; + +import com.l2jserver.gameserver.ai.CtrlEvent; +import com.l2jserver.gameserver.model.actor.L2Character; + +/** + * Task dedicated to notify character's AI + * @author xban1x + */ +public final class NotifyAITask implements Runnable { + private final L2Character _character; + private final CtrlEvent _event; + + public NotifyAITask(L2Character character, CtrlEvent event) { + _character = character; + _event = event; + } + + @Override + public void run() { + if (_character != null) { + _character.getAI().notifyEvent(_event); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/QueuedMagicUseTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/QueuedMagicUseTask.java new file mode 100644 index 00000000..c930f18a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/character/QueuedMagicUseTask.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.character; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Task dedicated to queued magic use of character + * @author xban1x + */ +public final class QueuedMagicUseTask implements Runnable { + private final L2PcInstance _currPlayer; + private final Skill _queuedSkill; + private final boolean _isCtrlPressed; + private final boolean _isShiftPressed; + + public QueuedMagicUseTask(L2PcInstance currPlayer, Skill queuedSkill, boolean isCtrlPressed, boolean isShiftPressed) { + _currPlayer = currPlayer; + _queuedSkill = queuedSkill; + _isCtrlPressed = isCtrlPressed; + _isShiftPressed = isShiftPressed; + } + + @Override + public void run() { + if (_currPlayer != null) { + _currPlayer.useMagic(_queuedSkill, _isCtrlPressed, _isShiftPressed); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicAction.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicAction.java new file mode 100644 index 00000000..32c73c3e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicAction.java @@ -0,0 +1,159 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.cubics; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2CubicInstance; +import com.l2jserver.gameserver.model.effects.L2EffectType; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; +import com.l2jserver.gameserver.taskmanager.AttackStanceTaskManager; + +/** + * Cubic action task. + * @author Zoey76 + */ +public final class CubicAction implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(CubicAction.class); + + private final L2CubicInstance _cubic; + private final AtomicInteger _currentCount = new AtomicInteger(); + private final int _chance; + + public CubicAction(L2CubicInstance cubic, int chance) { + _cubic = cubic; + _chance = chance; + } + + @Override + public void run() { + if (_cubic == null) { + return; + } + + try { + if (_cubic.getOwner().isDead() || !_cubic.getOwner().isOnline()) { + _cubic.stopAction(); + _cubic.getOwner().getCubics().remove(_cubic.getId()); + _cubic.getOwner().broadcastUserInfo(); + _cubic.cancelDisappear(); + return; + } + + if (!AttackStanceTaskManager.getInstance().hasAttackStanceTask(_cubic.getOwner())) { + if (_cubic.getOwner().hasSummon()) { + if (!AttackStanceTaskManager.getInstance().hasAttackStanceTask(_cubic.getOwner().getSummon())) { + _cubic.stopAction(); + return; + } + } else { + _cubic.stopAction(); + return; + } + } + + // The cubic has already reached its limit and it will stay idle until its duration ends. + if ((_cubic.getCubicMaxCount() > -1) && (_currentCount.get() >= _cubic.getCubicMaxCount())) { + _cubic.stopAction(); + return; + } + + // Smart Cubic debuff cancel is 100% + boolean useCubicCure = false; + if ((_cubic.getId() >= L2CubicInstance.SMART_CUBIC_EVATEMPLAR) && (_cubic.getId() <= L2CubicInstance.SMART_CUBIC_SPECTRALMASTER)) { + for (BuffInfo info : _cubic.getOwner().getEffectList().getDebuffs()) { + if (!info.getSkill().isIrreplaceableBuff()) { + useCubicCure = true; + info.getEffected().getEffectList().stopSkillEffects(true, info.getSkill()); + } + } + } + + if (useCubicCure) { + // Smart Cubic debuff cancel is needed, no other skill is used in this activation period + MagicSkillUse msu = new MagicSkillUse(_cubic.getOwner(), _cubic.getOwner(), L2CubicInstance.SKILL_CUBIC_CURE, 1, 0, 0); + _cubic.getOwner().broadcastPacket(msu); + + // The cubic has done an action, increase the current count + _currentCount.incrementAndGet(); + } else if (Rnd.get(1, 100) < _chance) { + final Skill skill = _cubic.getSkills().get(Rnd.get(_cubic.getSkills().size())); + if (skill == null) { + return; + } + + if (skill.getId() == L2CubicInstance.SKILL_CUBIC_HEAL) { + // friendly skill, so we look a target in owner's party + _cubic.cubicTargetForHeal(); + } else { + // offensive skill, we look for an enemy target + _cubic.getCubicTarget(); + if (!L2CubicInstance.isInCubicRange(_cubic.getOwner(), _cubic.getTarget())) { + _cubic.setTarget(null); + } + } + L2Character target = _cubic.getTarget(); + if ((target != null) && !target.isDead()) { + if (general().debug()) { + LOG.info("Action.run();"); + LOG.info("Cubic ID: {} Target: {} distance: {}", _cubic.getId(), target.getName(), target.calculateDistance(_cubic.getOwner(), true, false)); + } + + _cubic.getOwner().broadcastPacket(new MagicSkillUse(_cubic.getOwner(), target, skill.getId(), skill.getLevel(), 0, 0)); + + final List targets = List.of(target); + + if (skill.isContinuous()) { + _cubic.useCubicContinuous(skill, targets); + } else { + skill.activateSkill(_cubic, targets); + } + + if (skill.hasEffectType(L2EffectType.MAGICAL_ATTACK)) { + _cubic.useCubicMdam(skill, targets); + } else if (skill.hasEffectType(L2EffectType.HP_DRAIN)) { + _cubic.useCubicDrain(skill, targets); + } else if (skill.hasEffectType(L2EffectType.STUN, L2EffectType.ROOT)) { + _cubic.useCubicDisabler(skill, targets); + } else if (skill.hasEffectType(L2EffectType.DMG_OVER_TIME)) { + _cubic.useCubicContinuous(skill, targets); + } else if (skill.hasEffectType(L2EffectType.AGGRESSION)) { + _cubic.useCubicDisabler(skill, targets); + } + + // The cubic has done an action, increase the current count + _currentCount.incrementAndGet(); + } + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicDisappear.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicDisappear.java new file mode 100644 index 00000000..d3f67b4f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicDisappear.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.cubics; + +import com.l2jserver.gameserver.model.actor.instance.L2CubicInstance; + +/** + * Cubic disappear task. + * @author Zoey76 + */ +public final class CubicDisappear implements Runnable { + private final L2CubicInstance _cubic; + + public CubicDisappear(L2CubicInstance cubic) { + _cubic = cubic; + } + + @Override + public void run() { + if (_cubic != null) { + _cubic.stopAction(); + _cubic.getOwner().getCubics().remove(_cubic.getId()); + _cubic.getOwner().broadcastUserInfo(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicHeal.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicHeal.java new file mode 100644 index 00000000..e758fc60 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/cubics/CubicHeal.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.cubics; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2CubicInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.MagicSkillUse; + +/** + * Cubic heal task. + * @author Zoey76 + */ +public class CubicHeal implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(CubicHeal.class); + + private final L2CubicInstance _cubic; + + public CubicHeal(L2CubicInstance cubic) { + _cubic = cubic; + } + + @Override + public void run() { + if (_cubic == null) { + return; + } + + if (_cubic.getOwner().isDead() || !_cubic.getOwner().isOnline()) { + _cubic.stopAction(); + _cubic.getOwner().getCubics().remove(_cubic.getId()); + _cubic.getOwner().broadcastUserInfo(); + _cubic.cancelDisappear(); + return; + } + + try { + final Skill skill = _cubic.getSkills().stream().filter(s -> s.getId() == L2CubicInstance.SKILL_CUBIC_HEAL).findFirst().orElse(null); + if (skill == null) { + return; + } + + _cubic.cubicTargetForHeal(); + final L2Character target = _cubic.getTarget(); + if ((target != null) && !target.isDead()) { + skill.activateSkill(_cubic, List.of(target)); + _cubic.getOwner().broadcastPacket(new MagicSkillUse(_cubic.getOwner(), target, skill.getId(), skill.getLevel(), 0, 0)); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTask.java new file mode 100644 index 00000000..f4854754 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTask.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.npc.trap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2TrapInstance; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; + +/** + * Trap task. + * @author Zoey76 + */ +public class TrapTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(TrapTask.class); + + private static final int TICK = 1000; // 1s + private final L2TrapInstance _trap; + + public TrapTask(L2TrapInstance trap) { + _trap = trap; + } + + @Override + public void run() { + try { + if (!_trap.isTriggered()) { + if (_trap.hasLifeTime()) { + _trap.setRemainingTime(_trap.getRemainingTime() - TICK); + if (_trap.getRemainingTime() < (_trap.getLifeTime() - 15000)) { + _trap.broadcastPacket(new SocialAction(_trap.getObjectId(), 2)); + } + if (_trap.getRemainingTime() <= 0) { + // TODO(Zoey76): Handle ex AURA, FRONT_AURA, BEHIND_AURA skills. + // _trap.triggerTrap(_trap); + _trap.unSummon(); + return; + } + } + + for (L2Character target : _trap.getKnownList().getKnownCharacters()) { + if (_trap.checkTarget(target)) { + _trap.triggerTrap(target); + break; + } + } + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + _trap.unSummon(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTriggerTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTriggerTask.java new file mode 100644 index 00000000..ef81e5f6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapTriggerTask.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.npc.trap; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.actor.instance.L2TrapInstance; + +/** + * Trap trigger task. + * @author Zoey76 + */ +public class TrapTriggerTask implements Runnable { + private final L2TrapInstance _trap; + + public TrapTriggerTask(L2TrapInstance trap) { + _trap = trap; + } + + @Override + public void run() { + try { + _trap.doCast(_trap.getSkill()); + ThreadPoolManager.getInstance().scheduleGeneral(new TrapUnsummonTask(_trap), _trap.getSkill().getHitTime() + 300); + } catch (Exception e) { + _trap.unSummon(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapUnsummonTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapUnsummonTask.java new file mode 100644 index 00000000..03f37c4b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/trap/TrapUnsummonTask.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.npc.trap; + +import com.l2jserver.gameserver.model.actor.instance.L2TrapInstance; + +/** + * Trap unsummon task. + * @author Zoey76 + */ +public class TrapUnsummonTask implements Runnable { + private final L2TrapInstance _trap; + + public TrapUnsummonTask(L2TrapInstance trap) { + _trap = trap; + } + + @Override + public void run() { + _trap.unSummon(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/walker/ArrivedTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/walker/ArrivedTask.java new file mode 100644 index 00000000..05738d3a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/npc/walker/ArrivedTask.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.npc.walker; + +import com.l2jserver.gameserver.instancemanager.WalkingManager; +import com.l2jserver.gameserver.model.WalkInfo; +import com.l2jserver.gameserver.model.actor.L2Npc; + +/** + * Walker arrive task. + * @author GKR + */ +public class ArrivedTask implements Runnable { + private final WalkInfo _walk; + private final L2Npc _npc; + + public ArrivedTask(L2Npc npc, WalkInfo walk) { + _npc = npc; + _walk = walk; + } + + @Override + public void run() { + _walk.setBlocked(false); + WalkingManager.getInstance().startMoving(_npc, _walk.getRoute().getName()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/DismountTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/DismountTask.java new file mode 100644 index 00000000..15cca3fb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/DismountTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to dismount player from pet. + * @author UnAfraid + */ +public class DismountTask implements Runnable { + private final L2PcInstance _player; + + public DismountTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.dismount(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/FameTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/FameTask.java new file mode 100644 index 00000000..d64d57a9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/FameTask.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.customs; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; + +/** + * Task dedicated to reward player with fame while standing on siege zone. + * @author UnAfraid + */ +public class FameTask implements Runnable { + private final L2PcInstance _player; + private final int _value; + + public FameTask(L2PcInstance player, int value) { + _player = player; + _value = value; + } + + @Override + public void run() { + if ((_player == null) || (_player.isDead() && !character().fameForDeadPlayers())) { + return; + } + if (((_player.getClient() == null) || _player.getClient().isDetached()) && !customs().offlineFame()) { + return; + } + _player.setFame(_player.getFame() + _value); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.ACQUIRED_S1_REPUTATION_SCORE); + sm.addInt(_value); + _player.sendPacket(sm); + _player.sendPacket(new UserInfo(_player)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/GameGuardCheckTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/GameGuardCheckTask.java new file mode 100644 index 00000000..ea6b4ce4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/GameGuardCheckTask.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.L2GameClient; +import com.l2jserver.gameserver.network.serverpackets.LeaveWorld; + +/** + * Task dedicated to verify client's game guard. + * @author UnAfraid + */ +public class GameGuardCheckTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(GameGuardCheckTask.class); + + private final L2PcInstance _player; + + public GameGuardCheckTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if ((_player != null)) { + L2GameClient client = _player.getClient(); + if ((client != null) && !client.isAuthedGG() && _player.isOnline()) { + AdminData.getInstance().broadcastMessageToGMs("Client " + client + " failed to reply GameGuard query and is being kicked!"); + LOG.info("Client {} failed to reply GameGuard query and is being kicked!", client); + + client.close(LeaveWorld.STATIC_PACKET); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/IllegalPlayerActionTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/IllegalPlayerActionTask.java new file mode 100644 index 00000000..d9f80387 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/IllegalPlayerActionTask.java @@ -0,0 +1,83 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.AdminData; +import com.l2jserver.gameserver.enums.IllegalActionPunishmentType; +import com.l2jserver.gameserver.instancemanager.PunishmentManager; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.punishment.PunishmentAffect; +import com.l2jserver.gameserver.model.punishment.PunishmentTask; +import com.l2jserver.gameserver.model.punishment.PunishmentType; + +/** + * Task that handles illegal player actions. + */ +public final class IllegalPlayerActionTask implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger("audit"); + + private final String _message; + + private final IllegalActionPunishmentType _punishment; + + private final L2PcInstance _actor; + + public IllegalPlayerActionTask(L2PcInstance actor, String message, IllegalActionPunishmentType punishment) { + _message = message; + _punishment = punishment; + _actor = actor; + + switch (punishment) { + case KICK -> _actor.sendMessage("You will be kicked for illegal action, GM informed."); + case KICK_BAN -> { + if (!_actor.isGM()) { + _actor.setAccessLevel(-1); + _actor.setAccountAccesslevel(-1); + } + _actor.sendMessage("You are banned for illegal action, GM informed."); + } + case JAIL -> { + _actor.sendMessage("Illegal action performed!"); + _actor.sendMessage("You will be teleported to GM Consultation Service area and jailed."); + } + } + } + + @Override + public void run() { + LOG.info("Illegal action [{}] by player {}, action taken {}!", _message, _actor, _punishment); + + AdminData.getInstance().broadcastMessageToGMs(_message); + if (!_actor.isGM()) { + switch (_punishment) { + case BROADCAST -> { + } + case KICK -> _actor.logout(false); + case KICK_BAN -> PunishmentManager.getInstance().startPunishment(new PunishmentTask(_actor.getObjectId(), PunishmentAffect.CHARACTER, PunishmentType.BAN, System.currentTimeMillis() + general().getDefaultPunishParam(), _message, getClass().getSimpleName())); + case JAIL -> PunishmentManager.getInstance().startPunishment(new PunishmentTask(_actor.getObjectId(), PunishmentAffect.CHARACTER, PunishmentType.JAIL, System.currentTimeMillis() + general().getDefaultPunishParam(), _message, getClass().getSimpleName())); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/InventoryEnableTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/InventoryEnableTask.java new file mode 100644 index 00000000..5a8a30a8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/InventoryEnableTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to enable player's inventory. + * @author UnAfraid + */ +public class InventoryEnableTask implements Runnable { + private final L2PcInstance _player; + + public InventoryEnableTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.setInventoryBlockingStatus(false); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/LookingForFishTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/LookingForFishTask.java new file mode 100644 index 00000000..ed0bf659 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/LookingForFishTask.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated for looking for fish. + * @author UnAfraid + */ +public class LookingForFishTask implements Runnable { + private final L2PcInstance _player; + private final boolean _isNoob, _isUpperGrade; + private final int _fishGroup; + private final double _fishGutsCheck; + private final long _endTaskTime; + + public LookingForFishTask(L2PcInstance player, int startCombatTime, double fishGutsCheck, int fishGroup, boolean isNoob, boolean isUpperGrade) { + _player = player; + _fishGutsCheck = fishGutsCheck; + _endTaskTime = System.currentTimeMillis() + (startCombatTime * 1000) + 10000; + _fishGroup = fishGroup; + _isNoob = isNoob; + _isUpperGrade = isUpperGrade; + } + + @Override + public void run() { + if (_player != null) { + if (System.currentTimeMillis() >= _endTaskTime) { + _player.endFishing(false); + return; + } + if (_fishGroup == -1) { + return; + } + int check = Rnd.get(100); + if (_fishGutsCheck > check) { + _player.stopLookingForFishTask(); + _player.startFishCombat(_isNoob, _isUpperGrade); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PetFeedTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PetFeedTask.java new file mode 100644 index 00000000..3ea1a5e3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PetFeedTask.java @@ -0,0 +1,94 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.data.xml.impl.PetDataTable; +import com.l2jserver.gameserver.handler.IItemHandler; +import com.l2jserver.gameserver.handler.ItemHandler; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Task dedicated for feeding player's pet. + * @author UnAfraid + */ +public class PetFeedTask implements Runnable { + private static final Logger _log = LoggerFactory.getLogger(PetFeedTask.class); + + private final L2PcInstance _player; + + public PetFeedTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + try { + if (!_player.isMounted() || (_player.getMountNpcId() == 0) || (PetDataTable.getInstance().getPetData(_player.getMountNpcId()) == null)) { + _player.stopFeed(); + return; + } + + if (_player.getCurrentFeed() > _player.getFeedConsume()) { + // eat + _player.setCurrentFeed(_player.getCurrentFeed() - _player.getFeedConsume()); + } else { + // go back to pet control item, or simply said, unsummon it + _player.setCurrentFeed(0); + _player.stopFeed(); + _player.dismount(); + _player.sendPacket(SystemMessageId.OUT_OF_FEED_MOUNT_CANCELED); + return; + } + + final List foodIds = PetDataTable.getInstance().getPetData(_player.getMountNpcId()).getFood(); + if (foodIds.isEmpty()) { + return; + } + + L2ItemInstance food = null; + for (int id : foodIds) { + // TODO: possibly pet inv? + food = _player.getInventory().getItemByItemId(id); + if (food != null) { + break; + } + } + + if ((food != null) && _player.isHungry()) { + IItemHandler handler = ItemHandler.getInstance().getHandler(food.getEtcItem()); + if (handler != null) { + handler.useItem(_player, food, false); + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.PET_TOOK_S1_BECAUSE_HE_WAS_HUNGRY); + sm.addItemName(food.getId()); + _player.sendPacket(sm); + } + } + } catch (Exception e) { + _log.error("Could not feed mounted Pet NPC ID {}, a feed task error has occurred", _player.getMountNpcId(), e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PvPFlagTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PvPFlagTask.java new file mode 100644 index 00000000..9c43bae9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/PvPFlagTask.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to update player's current pvp status. + * @author UnAfraid + */ +public class PvPFlagTask implements Runnable { + private final L2PcInstance _player; + + public PvPFlagTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player == null) { + return; + } + + if (System.currentTimeMillis() > _player.getPvpFlagLasts()) { + _player.stopPvPFlag(); + } else if (System.currentTimeMillis() > (_player.getPvpFlagLasts() - 20000)) { + _player.updatePvPFlag(2); + } else { + _player.updatePvPFlag(1); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoBonusTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoBonusTask.java new file mode 100644 index 00000000..776e8097 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoBonusTask.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import java.util.Objects; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author UnAfraid + * @author Maneco2 + * @since 2.6.3.0 + */ +public class RecoBonusTask implements Runnable { + private final L2PcInstance _player; + + public RecoBonusTask(L2PcInstance player) { + Objects.requireNonNull(player); + _player = player; + } + + @Override + public void run() { + _player.getRecSystem().finishBonusTask(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoGiveTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoGiveTask.java new file mode 100644 index 00000000..8c75ffb8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RecoGiveTask.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; + +/** + * Task dedicated to increase player's recommendation bonus. + * @author UnAfraid + */ +public class RecoGiveTask implements Runnable { + private final L2PcInstance _player; + + public RecoGiveTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + // 10 recommendations to give out after 2 hours of being logged in + // 1 more recommendation to give out every hour after that. + int recoToGive = 1; + if (!_player.getRecSystem().isTwoHoursGiven()) { + recoToGive = 10; + _player.getRecSystem().setTwoHoursGiven(true); + } + + _player.getRecSystem().setLeft(_player.getRecSystem().getLeft() + recoToGive); + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.YOU_OBTAINED_S1_RECOMMENDATIONS); + sm.addInt(recoToGive); + _player.sendPacket(sm); + _player.sendPacket(new UserInfo(_player)); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RentPetTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RentPetTask.java new file mode 100644 index 00000000..c12b3f85 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/RentPetTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to dismount player from rented pet. + * @author UnAfraid + */ +public class RentPetTask implements Runnable { + private final L2PcInstance _player; + + public RentPetTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.stopRentPet(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetChargesTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetChargesTask.java new file mode 100644 index 00000000..67e9104f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetChargesTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to reset player's current charges. + * @author UnAfraid + */ +public class ResetChargesTask implements Runnable { + private final L2PcInstance _player; + + public ResetChargesTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.clearCharges(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetSoulsTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetSoulsTask.java new file mode 100644 index 00000000..05230588 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/ResetSoulsTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to reset player's current souls. + * @author UnAfraid + */ +public class ResetSoulsTask implements Runnable { + private final L2PcInstance _player; + + public ResetSoulsTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.clearSouls(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/SitDownTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/SitDownTask.java new file mode 100644 index 00000000..0a1fea3c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/SitDownTask.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to put player to sit down. + * @author UnAfraid + */ +public class SitDownTask implements Runnable { + private final L2PcInstance _player; + + public SitDownTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.stopStunning(false); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/StandUpTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/StandUpTask.java new file mode 100644 index 00000000..2dbd9d7a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/StandUpTask.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated to put player to stand up. + * @author UnAfraid + */ +public class StandUpTask implements Runnable { + private final L2PcInstance _player; + + public StandUpTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + _player.setIsSitting(false); + _player.getAI().setIntention(CtrlIntention.AI_INTENTION_IDLE); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportTask.java new file mode 100644 index 00000000..5a191714 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportTask.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author UnAfraid + */ +public class TeleportTask implements Runnable { + private final L2PcInstance _activeChar; + private final Location _loc; + + public TeleportTask(L2PcInstance player, Location loc) { + _activeChar = player; + _loc = loc; + } + + @Override + public void run() { + if ((_activeChar != null) && _activeChar.isOnline()) { + _activeChar.teleToLocation(_loc, true); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportWatchdogTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportWatchdogTask.java new file mode 100644 index 00000000..2e7e59ee --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/TeleportWatchdogTask.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * Task dedicated watch for player teleportation. + * @author UnAfraid + */ +public class TeleportWatchdogTask implements Runnable { + private final L2PcInstance _player; + + public TeleportWatchdogTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if ((_player == null) || !_player.isTeleporting()) { + return; + } + _player.onTeleported(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/VitalityTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/VitalityTask.java new file mode 100644 index 00000000..4dc4fa95 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/VitalityTask.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import static com.l2jserver.gameserver.config.Configuration.vitality; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.stat.PcStat; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.serverpackets.ExVitalityPointInfo; + +/** + * Task dedicated to reward player with vitality. + * @author UnAfraid + */ +public class VitalityTask implements Runnable { + private final L2PcInstance _player; + + public VitalityTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (!_player.isInsideZone(ZoneId.PEACE)) { + return; + } + + if (_player.getVitalityPoints() >= PcStat.MAX_VITALITY_POINTS) { + return; + } + + _player.updateVitalityPoints(vitality().getRateRecoveryPeaceZone(), false, false); + _player.sendPacket(new ExVitalityPointInfo(_player.getVitalityPoints())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WarnUserTakeBreakTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WarnUserTakeBreakTask.java new file mode 100644 index 00000000..246bea55 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WarnUserTakeBreakTask.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Task dedicated to warn user to take a break. + * @author UnAfraid + */ +public class WarnUserTakeBreakTask implements Runnable { + private final L2PcInstance _player; + + public WarnUserTakeBreakTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + if (_player.isOnline()) { + _player.sendPacket(SystemMessageId.PLAYING_FOR_LONG_TIME); + } else { + _player.stopWarnUserTakeBreak(); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WaterTask.java b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WaterTask.java new file mode 100644 index 00000000..cc859dec --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/tasks/player/WaterTask.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.tasks.player; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Task dedicated to make damage to the player while drowning. + * @author UnAfraid + */ +public class WaterTask implements Runnable { + private final L2PcInstance _player; + + public WaterTask(L2PcInstance player) { + _player = player; + } + + @Override + public void run() { + if (_player != null) { + double reduceHp = _player.getMaxHp() / 100.0; + + if (reduceHp < 1) { + reduceHp = 1; + } + + _player.reduceCurrentHp(reduceHp, _player, false, false, null); + // reduced hp, because not rest + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.DROWN_DAMAGE_S1); + sm.addInt((int) reduceHp); + _player.sendPacket(sm); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2CharTemplate.java b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2CharTemplate.java new file mode 100644 index 00000000..8ea95d7a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2CharTemplate.java @@ -0,0 +1,585 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.templates; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.events.ListenersContainer; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.MoveType; + +/** + * Character template. + * @author Zoey76 + */ +public class L2CharTemplate extends ListenersContainer { + // BaseStats + private int _baseSTR; + private int _baseCON; + private int _baseDEX; + private int _baseINT; + private int _baseWIT; + private int _baseMEN; + private float _baseHpMax; + private float _baseCpMax; + private float _baseMpMax; + private float _baseHpReg; + private float _baseMpReg; + private int _basePAtk; + private int _baseMAtk; + private int _basePDef; + private int _baseMDef; + private int _basePAtkSpd; + private int _baseMAtkSpd; + private int _baseAttackRange; + private int _randomDamage; + private WeaponType _baseAttackType; + private int _baseShldDef; + private int _baseShldRate; + private int _baseCritRate; + private int _baseMCritRate; + private int _baseReuseDelay; + // SpecialStats + private int _baseBreath; + private int _baseFire; + private int _baseWind; + private int _baseWater; + private int _baseEarth; + private int _baseHoly; + private int _baseDark; + private double _baseFireRes; + private double _baseWindRes; + private double _baseWaterRes; + private double _baseEarthRes; + private double _baseHolyRes; + private double _baseDarkRes; + private double _baseElementRes; + /** For client info use {@link #_fCollisionRadius} */ + private int _collisionRadius; + /** For client info use {@link #_fCollisionHeight} */ + private int _collisionHeight; + + private double _fCollisionRadius; + private double _fCollisionHeight; + + private final double[] _moveType = new double[MoveType.values().length]; + /** The creature's race. */ + private Race _race; + + public L2CharTemplate(StatsSet set) { + set(set); + } + + public void set(StatsSet set) { + // Base stats + _baseSTR = set.getInt("baseSTR", 0); + _baseCON = set.getInt("baseCON", 0); + _baseDEX = set.getInt("baseDEX", 0); + _baseINT = set.getInt("baseINT", 0); + _baseWIT = set.getInt("baseWIT", 0); + _baseMEN = set.getInt("baseMEN", 0); + _baseHpMax = set.getFloat("baseHpMax", 0); + _baseCpMax = set.getFloat("baseCpMax", 0); + _baseMpMax = set.getFloat("baseMpMax", 0); + _baseHpReg = set.getFloat("baseHpReg", 0); + _baseMpReg = set.getFloat("baseMpReg", 0); + _basePAtk = set.getInt("basePAtk", 0); + _baseMAtk = set.getInt("baseMAtk", 0); + _basePDef = set.getInt("basePDef", 0); + _baseMDef = set.getInt("baseMDef", 0); + _basePAtkSpd = set.getInt("basePAtkSpd", 300); + _baseMAtkSpd = set.getInt("baseMAtkSpd", 333); + _baseShldDef = set.getInt("baseShldDef", 0); + _baseAttackRange = set.getInt("baseAtkRange", 40); + _randomDamage = set.getInt("baseRndDam", 0); + _baseAttackType = set.getEnum("baseAtkType", WeaponType.class, WeaponType.FIST); + _baseShldRate = set.getInt("baseShldRate", 0); + _baseCritRate = set.getInt("baseCritRate", 4); + _baseMCritRate = set.getInt("baseMCritRate", 0); + _baseReuseDelay = set.getInt("reuseDelay", 0); + + // SpecialStats + _baseBreath = set.getInt("baseBreath", 100); + _baseFire = set.getInt("baseFire", 0); + _baseWind = set.getInt("baseWind", 0); + _baseWater = set.getInt("baseWater", 0); + _baseEarth = set.getInt("baseEarth", 0); + _baseHoly = set.getInt("baseHoly", 0); + _baseDark = set.getInt("baseDark", 0); + _baseFireRes = set.getInt("baseFireRes", 0); + _baseWindRes = set.getInt("baseWindRes", 0); + _baseWaterRes = set.getInt("baseWaterRes", 0); + _baseEarthRes = set.getInt("baseEarthRes", 0); + _baseHolyRes = set.getInt("baseHolyRes", 0); + _baseDarkRes = set.getInt("baseDarkRes", 0); + _baseElementRes = set.getInt("baseElementRes", 0); + + // Geometry + _fCollisionHeight = set.getDouble("collisionHeight", 0); + _fCollisionRadius = set.getDouble("collisionRadius", 0); + _collisionRadius = (int) _fCollisionRadius; + _collisionHeight = (int) _fCollisionHeight; + + // speed. + Arrays.fill(_moveType, 1); + setBaseMoveSpeed(MoveType.RUN, set.getDouble("baseRunSpd", 120)); + setBaseMoveSpeed(MoveType.WALK, set.getDouble("baseWalkSpd", 50)); + setBaseMoveSpeed(MoveType.FAST_SWIM, set.getDouble("baseSwimRunSpd", getBaseMoveSpeed(MoveType.RUN))); + setBaseMoveSpeed(MoveType.SLOW_SWIM, set.getDouble("baseSwimWalkSpd", getBaseMoveSpeed(MoveType.WALK))); + setBaseMoveSpeed(MoveType.FAST_FLY, set.getDouble("baseFlyRunSpd", getBaseMoveSpeed(MoveType.RUN))); + setBaseMoveSpeed(MoveType.SLOW_FLY, set.getDouble("baseFlyWalkSpd", getBaseMoveSpeed(MoveType.WALK))); + } + + /** + * @return the baseHpMax + */ + public float getBaseHpMax() { + return _baseHpMax; + } + + /** + * @return the _baseFire + */ + public int getBaseFire() { + return _baseFire; + } + + /** + * @return the _baseWind + */ + public int getBaseWind() { + return _baseWind; + } + + /** + * @return the _baseWater + */ + public int getBaseWater() { + return _baseWater; + } + + /** + * @return the _baseEarth + */ + public int getBaseEarth() { + return _baseEarth; + } + + /** + * @return the _baseHoly + */ + public int getBaseHoly() { + return _baseHoly; + } + + /** + * @return the _baseDark + */ + public int getBaseDark() { + return _baseDark; + } + + /** + * @return the _baseFireRes + */ + public double getBaseFireRes() { + return _baseFireRes; + } + + /** + * @return the _baseWindRes + */ + public double getBaseWindRes() { + return _baseWindRes; + } + + /** + * @return the _baseWaterRes + */ + public double getBaseWaterRes() { + return _baseWaterRes; + } + + /** + * @return the _baseEarthRes + */ + public double getBaseEarthRes() { + return _baseEarthRes; + } + + /** + * @return the _baseHolyRes + */ + public double getBaseHolyRes() { + return _baseHolyRes; + } + + /** + * @return the _baseDarkRes + */ + public double getBaseDarkRes() { + return _baseDarkRes; + } + + /** + * @return the _baseElementRes + */ + public double getBaseElementRes() { + return _baseElementRes; + } + + /** + * @return the baseSTR + */ + public int getBaseSTR() { + return _baseSTR; + } + + /** + * @return the baseCON + */ + public int getBaseCON() { + return _baseCON; + } + + /** + * @return the baseDEX + */ + public int getBaseDEX() { + return _baseDEX; + } + + /** + * @return the baseINT + */ + public int getBaseINT() { + return _baseINT; + } + + /** + * @return the baseWIT + */ + public int getBaseWIT() { + return _baseWIT; + } + + /** + * @return the baseMEN + */ + public int getBaseMEN() { + return _baseMEN; + } + + /** + * @return the baseCpMax + */ + public float getBaseCpMax() { + return _baseCpMax; + } + + /** + * @return the baseMpMax + */ + public float getBaseMpMax() { + return _baseMpMax; + } + + /** + * @return the baseHpReg + */ + public float getBaseHpReg() { + return _baseHpReg; + } + + /** + * @return the baseMpReg + */ + public float getBaseMpReg() { + return _baseMpReg; + } + + /** + * @return the basePAtk + */ + public int getBasePAtk() { + return _basePAtk; + } + + /** + * @return the baseMAtk + */ + public int getBaseMAtk() { + return _baseMAtk; + } + + /** + * @return the basePDef + */ + public int getBasePDef() { + return _basePDef; + } + + /** + * @return the baseMDef + */ + public int getBaseMDef() { + return _baseMDef; + } + + /** + * @return the basePAtkSpd + */ + public int getBasePAtkSpd() { + return _basePAtkSpd; + } + + /** + * @return the baseMAtkSpd + */ + public int getBaseMAtkSpd() { + return _baseMAtkSpd; + } + + /** + * @return the random damage + */ + public int getRandomDamage() { + return _randomDamage; + } + + /** + * @return the baseShldDef + */ + public int getBaseShldDef() { + return _baseShldDef; + } + + /** + * @return the baseShldRate + */ + public int getBaseShldRate() { + return _baseShldRate; + } + + /** + * @return the baseCritRate + */ + public int getBaseCritRate() { + return _baseCritRate; + } + + /** + * @return the baseMCritRate + */ + public int getBaseMCritRate() { + return _baseMCritRate; + } + + /** + * @return the baseReuseDelay + */ + public int getBaseReuseDelay() { + return _baseReuseDelay; + } + + public void setBaseMoveSpeed(MoveType type, double val) { + _moveType[type.ordinal()] = val; + } + + public double getBaseMoveSpeed(MoveType mt) { + return _moveType[mt.ordinal()]; + } + + /** + * @return the baseBreath + */ + public int getBaseBreath() { + return _baseBreath; + } + + /** + * @return the collisionRadius + */ + public int getCollisionRadius() { + return _collisionRadius; + } + + /** + * @return the collisionHeight + */ + public int getCollisionHeight() { + return _collisionHeight; + } + + /** + * @return the fCollisionRadius + */ + public double getfCollisionRadius() { + return _fCollisionRadius; + } + + /** + * @return the fCollisionHeight + */ + public double getfCollisionHeight() { + return _fCollisionHeight; + } + + /** + * @param baseFire the baseFire to set + */ + public void setBaseFire(int baseFire) { + _baseFire = baseFire; + } + + /** + * @param baseWater the baseWater to set + */ + public void setBaseWater(int baseWater) { + _baseWater = baseWater; + } + + /** + * @param baseEarth the baseEarth to set + */ + public void setBaseEarth(int baseEarth) { + _baseEarth = baseEarth; + } + + /** + * @param baseWind the baseWind to set + */ + public void setBaseWind(int baseWind) { + _baseWind = baseWind; + } + + /** + * @param baseHoly the baseHoly to set + */ + public void setBaseHoly(int baseHoly) { + _baseHoly = baseHoly; + } + + /** + * @param baseDark the baseDark to set + */ + public void setBaseDark(int baseDark) { + _baseDark = baseDark; + } + + /** + * @param baseFireRes the baseFireRes to set + */ + public void setBaseFireRes(double baseFireRes) { + _baseFireRes = baseFireRes; + } + + /** + * @param baseWaterRes the baseWaterRes to set + */ + public void setBaseWaterRes(double baseWaterRes) { + _baseWaterRes = baseWaterRes; + } + + /** + * @param baseEarthRes the baseEarthRes to set + */ + public void setBaseEarthRes(double baseEarthRes) { + _baseEarthRes = baseEarthRes; + } + + /** + * @param baseWindRes the baseWindRes to set + */ + public void setBaseWindRes(double baseWindRes) { + _baseWindRes = baseWindRes; + } + + /** + * @param baseHolyRes the baseHolyRes to set + */ + public void setBaseHolyRes(double baseHolyRes) { + _baseHolyRes = baseHolyRes; + } + + /** + * @param baseDarkRes the baseDarkRes to set + */ + public void setBaseDarkRes(double baseDarkRes) { + _baseDarkRes = baseDarkRes; + } + + /** + * @param baseElementRes + */ + public void setBaseElementRes(double baseElementRes) { + _baseElementRes = baseElementRes; + } + + /** + * @return the base attack type (Sword, Fist, Blunt, etc..) + */ + public WeaponType getBaseAttackType() { + return _baseAttackType; + } + + /** + * Sets base attack type. + * @param type + */ + public void setBaseAttackType(WeaponType type) { + _baseAttackType = type; + } + + /** + * @return the baseAtkRange + */ + public int getBaseAttackRange() { + return _baseAttackRange; + } + + /** + * Sets base attack range. + * @param val + */ + public void setBaseAttackRange(int val) { + _baseAttackRange = val; + } + + /** + * Overridden in L2NpcTemplate + * @return the characters skills + */ + public Map getSkills() { + return Collections.emptyMap(); + } + + public Race getRace() { + return _race; + } + + public void setRace(Race race) { + _race = race; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2DoorTemplate.java b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2DoorTemplate.java new file mode 100644 index 00000000..99cbc69a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2DoorTemplate.java @@ -0,0 +1,222 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.templates; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; + +/** + * Doors template. + * @author JIV + */ +public class L2DoorTemplate extends L2CharTemplate implements IIdentifiable { + private final int _doorId; + private final int[] _nodeX; + private final int[] _nodeY; + private final int _nodeZ; + private final int _height; + private final int _posX; + private final int _posY; + private final int _posZ; + private final int _emmiter; + private final int _childDoorId; + private final String _name; + private final String _groupName; + private final boolean _showHp; + private final boolean _isWall; + // -1 close, 0 nothing, 1 open + private final byte _masterDoorClose; + private final byte _masterDoorOpen; + + private final boolean _isTargetable; + private final boolean _default_status; + + private int _openTime; + private int _randomTime; + private final int _closeTime; + private final int _level; + private final int _openType; + private final boolean _checkCollision; + private final boolean _isAttackableDoor; + private final int _clanhallId; + private final boolean _stealth; + + public L2DoorTemplate(StatsSet set) { + super(set); + _doorId = set.getInt("id"); + _name = set.getString("name"); + + // position + String[] pos = set.getString("pos").split(";"); + _posX = Integer.parseInt(pos[0]); + _posY = Integer.parseInt(pos[1]); + _posZ = Integer.parseInt(pos[2]); + _height = set.getInt("height"); + _nodeZ = set.getInt("nodeZ"); + _nodeX = new int[4]; // 4 * x + _nodeY = new int[4]; // 4 * y + for (int i = 0; i < 4; i++) { + String[] split = set.getString("node" + (i + 1)).split(","); + _nodeX[i] = Integer.parseInt(split[0]); + _nodeY[i] = Integer.parseInt(split[1]); + } + + // optional + _emmiter = set.getInt("emitter_id", 0); + _showHp = set.getBoolean("hp_showable", true); + _isWall = set.getBoolean("is_wall", false); + _groupName = set.getString("group", null); + + _childDoorId = set.getInt("child_id_event", -1); + // true if door is opening + String masterevent = set.getString("master_close_event", "act_nothing"); + _masterDoorClose = (byte) (masterevent.equals("act_open") ? 1 : masterevent.equals("act_close") ? -1 : 0); + + masterevent = set.getString("master_open_event", "act_nothing"); + _masterDoorOpen = (byte) (masterevent.equals("act_open") ? 1 : masterevent.equals("act_close") ? -1 : 0); + + _isTargetable = set.getBoolean("targetable", true); + _default_status = set.getString("default_status", "close").equals("open"); + _closeTime = set.getInt("close_time", -1); + _level = set.getInt("level", 0); + _openType = set.getInt("open_method", 0); + _checkCollision = set.getBoolean("check_collision", true); + if ((_openType & L2DoorInstance.OPEN_BY_TIME) == L2DoorInstance.OPEN_BY_TIME) { + _openTime = set.getInt("open_time"); + _randomTime = set.getInt("random_time", -1); + } + _isAttackableDoor = set.getBoolean("is_attackable", false); + _clanhallId = set.getInt("clanhall_id", 0); + _stealth = set.getBoolean("stealth", false); + } + + /** + * Gets the door ID. + * @return the door ID + */ + @Override + public int getId() { + return _doorId; + } + + public String getName() { + return _name; + } + + public int[] getNodeX() { + return _nodeX; + } + + public int[] getNodeY() { + return _nodeY; + } + + public int getNodeZ() { + return _nodeZ; + } + + public int getHeight() { + return _height; + } + + public int getX() { + return _posX; + } + + public int getY() { + return _posY; + } + + public int getZ() { + return _posZ; + } + + public int getEmmiter() { + return _emmiter; + } + + public int getChildDoorId() { + return _childDoorId; + } + + public String getGroupName() { + return _groupName; + } + + public boolean isShowHp() { + return _showHp; + } + + public boolean isWall() { + return _isWall; + } + + public byte getMasterDoorOpen() { + return _masterDoorOpen; + } + + public byte getMasterDoorClose() { + return _masterDoorClose; + } + + public boolean isTargetable() { + return _isTargetable; + } + + public boolean isOpenByDefault() { + return _default_status; + } + + public int getOpenTime() { + return _openTime; + } + + public int getRandomTime() { + return _randomTime; + } + + public int getCloseTime() { + return _closeTime; + } + + public int getLevel() { + return _level; + } + + public int getOpenType() { + return _openType; + } + + public boolean isCheckCollision() { + return _checkCollision; + } + + public boolean isAttackable() { + return _isAttackableDoor; + } + + public int getClanHallId() { + return _clanhallId; + } + + public boolean isStealth() { + return _stealth; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2NpcTemplate.java b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2NpcTemplate.java new file mode 100644 index 00000000..5399cdf5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2NpcTemplate.java @@ -0,0 +1,472 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.templates; + +import static com.l2jserver.gameserver.config.Configuration.npc; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.enums.AISkillScope; +import com.l2jserver.gameserver.enums.AIType; +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.enums.Sex; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.DropListScope; +import com.l2jserver.gameserver.model.drops.IDropItem; +import com.l2jserver.gameserver.model.holders.ItemHolder; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * NPC template. + * @author NosBit + */ +public final class L2NpcTemplate extends L2CharTemplate implements IIdentifiable { + private int _id; + private int _displayId; + private byte _level; + private String _type; + private String _name; + private boolean _usingServerSideName; + private String _title; + private boolean _usingServerSideTitle; + private StatsSet _parameters; + private Sex _sex; + private int _chestId; + private int _rhandId; + private int _lhandId; + private int _weaponEnchant; + private double _expRate; + private double _sp; + private double _raidPoints; + private boolean _unique; + private boolean _attackable; + private boolean _targetable; + private boolean _undying; + private boolean _showName; + private boolean _flying; + private boolean _canMove; + private boolean _noSleepMode; + private boolean _passableDoor; + private boolean _hasSummoner; + private boolean _canBeSown; + private int _corpseTime; + private AIType _aiType; + private int _aggroRange; + private int _clanHelpRange; + private boolean _isChaos; + private boolean _isAggressive; + private int _soulShot; + private int _spiritShot; + private int _soulShotChance; + private int _spiritShotChance; + private int _minSkillChance; + private int _maxSkillChance; + private Map _skills; + private Map> _aiSkillLists; + private Set _clans; + private Set _ignoreClanNpcIds; + private Map> _dropLists; + private double _collisionRadiusGrown; + private double _collisionHeightGrown; + + /** + * Constructor of L2Character. + * @param set The StatsSet object to transfer data to the method + */ + public L2NpcTemplate(StatsSet set) { + super(set); + } + + @Override + public void set(StatsSet set) { + super.set(set); + _id = set.getInt("id"); + _displayId = set.getInt("displayId", _id); + _level = set.getByte("level", (byte) 70); + _type = set.getString("type", "L2Npc"); + _name = set.getString("name", ""); + _usingServerSideName = set.getBoolean("usingServerSideName", false); + _title = set.getString("title", ""); + _usingServerSideTitle = set.getBoolean("usingServerSideTitle", false); + setRace(set.getEnum("race", Race.class, Race.NONE)); + _sex = set.getEnum("sex", Sex.class, Sex.ETC); + + _chestId = set.getInt("chestId", 0); + _rhandId = set.getInt("rhandId", 0); + _lhandId = set.getInt("lhandId", 0); + _weaponEnchant = set.getInt("weaponEnchant", 0); + + _expRate = set.getDouble("expRate", 0); + _sp = set.getDouble("sp", 0); + _raidPoints = set.getDouble("raidPoints", 0); + + _unique = set.getBoolean("unique", false); + _attackable = set.getBoolean("attackable", true); + _targetable = set.getBoolean("targetable", true); + _undying = set.getBoolean("undying", true); + _showName = set.getBoolean("showName", true); + _flying = set.getBoolean("flying", false); + _canMove = set.getBoolean("canMove", true); + _noSleepMode = set.getBoolean("noSleepMode", false); + _passableDoor = set.getBoolean("passableDoor", false); + _hasSummoner = set.getBoolean("hasSummoner", false); + _canBeSown = set.getBoolean("canBeSown", false); + + _corpseTime = set.getInt("corpseTime", npc().getDefaultCorpseTime()); + + _aiType = set.getEnum("aiType", AIType.class, AIType.FIGHTER); + _aggroRange = set.getInt("aggroRange", 0); + _clanHelpRange = set.getInt("clanHelpRange", 0); + _isChaos = set.getBoolean("isChaos", false); + _isAggressive = set.getBoolean("isAggressive", true); + + _soulShot = set.getInt("soulShot", 0); + _spiritShot = set.getInt("spiritShot", 0); + _soulShotChance = set.getInt("shotShotChance", 0); + _spiritShotChance = set.getInt("spiritShotChance", 0); + + _minSkillChance = set.getInt("minSkillChance", 7); + _maxSkillChance = set.getInt("maxSkillChance", 15); + + _collisionRadiusGrown = set.getDouble("collisionRadiusGrown", 0); + _collisionHeightGrown = set.getDouble("collisionHeightGrown", 0); + } + + @Override + public int getId() { + return _id; + } + + public int getDisplayId() { + return _displayId; + } + + public byte getLevel() { + return _level; + } + + public String getType() { + return _type; + } + + public boolean isType(String type) { + return getType().equalsIgnoreCase(type); + } + + public String getName() { + return _name; + } + + public boolean isUsingServerSideName() { + return _usingServerSideName; + } + + public String getTitle() { + return _title; + } + + public boolean isUsingServerSideTitle() { + return _usingServerSideTitle; + } + + public StatsSet getParameters() { + return _parameters; + } + + public void setParameters(StatsSet set) { + _parameters = set; + } + + public Sex getSex() { + return _sex; + } + + public int getChestId() { + return _chestId; + } + + public int getRHandId() { + return _rhandId; + } + + public int getLHandId() { + return _lhandId; + } + + public int getWeaponEnchant() { + return _weaponEnchant; + } + + public double getExpRate() { + return _expRate; + } + + public double getSP() { + return _sp; + } + + public double getRaidPoints() { + return _raidPoints; + } + + public boolean isUnique() { + return _unique; + } + + public boolean isAttackable() { + return _attackable; + } + + public boolean isTargetable() { + return _targetable; + } + + public boolean isUndying() { + return _undying; + } + + public boolean isShowName() { + return _showName; + } + + public boolean isFlying() { + return _flying; + } + + public boolean canMove() { + return _canMove; + } + + public boolean isNoSleepMode() { + return _noSleepMode; + } + + public boolean isPassableDoor() { + return _passableDoor; + } + + public boolean hasSummoner() { + return _hasSummoner; + } + + public boolean canBeSown() { + return _canBeSown; + } + + public int getCorpseTime() { + return _corpseTime; + } + + public AIType getAIType() { + return _aiType; + } + + public int getAggroRange() { + return _aggroRange; + } + + public int getClanHelpRange() { + return _clanHelpRange; + } + + public boolean isChaos() { + return _isChaos; + } + + public boolean isAggressive() { + return _isAggressive; + } + + public int getSoulShot() { + return _soulShot; + } + + public int getSpiritShot() { + return _spiritShot; + } + + public int getSoulShotChance() { + return _soulShotChance; + } + + public int getSpiritShotChance() { + return _spiritShotChance; + } + + public int getMinSkillChance() { + return _minSkillChance; + } + + public int getMaxSkillChance() { + return _maxSkillChance; + } + + @Override + public Map getSkills() { + return _skills; + } + + public void setSkills(Map skills) { + _skills = skills != null ? Collections.unmodifiableMap(skills) : Collections.emptyMap(); + } + + public List getAISkills(AISkillScope aiSkillScope) { + return _aiSkillLists.getOrDefault(aiSkillScope, Collections.emptyList()); + } + + public void setAISkillLists(Map> aiSkillLists) { + _aiSkillLists = aiSkillLists != null ? Collections.unmodifiableMap(aiSkillLists) : Collections.emptyMap(); + } + + public Set getClans() { + return _clans; + } + + /** + * @param clans A sorted array of clan ids + */ + public void setClans(Set clans) { + _clans = clans != null ? Collections.unmodifiableSet(clans) : null; + } + + /** + * @param clanName clan name to check if it belongs to this NPC template clans. + * @param clanNames clan names to check if they belong to this NPC template clans. + * @return {@code true} if at least one of the clan names belong to this NPC template clans, {@code false} otherwise. + */ + public boolean isClan(String clanName, String... clanNames) { + // Using local variable for the sake of reloading since it can be turned to null. + final var clans = _clans; + + if (clans == null) { + return false; + } + + int clanId = NpcData.getInstance().getClanId("ALL"); + if (clans.contains(clanId)) { + return true; + } + + clanId = NpcData.getInstance().getClanId(clanName); + if (clans.contains(clanId)) { + return true; + } + + for (String name : clanNames) { + clanId = NpcData.getInstance().getClanId(name); + if (clans.contains(clanId)) { + return true; + } + } + return false; + } + + /** + * @param clans A set of clan names to check if they belong to this NPC template clans. + * @return {@code true} if at least one of the clan names belong to this NPC template clans, {@code false} otherwise. + */ + public boolean isClan(Set clans) { + // Using local variable for the sake of reloading since it can be turned to null. + final Set clanSet = _clans; + + if ((clanSet == null) || (clans == null)) { + return false; + } + + int clanId = NpcData.getInstance().getClanId("ALL"); + if (clanSet.contains(clanId)) { + return true; + } + + for (Integer id : clans) { + if (clanSet.contains(id)) { + return true; + } + } + return false; + } + + public Set getIgnoreClanNpcIds() { + return _ignoreClanNpcIds; + } + + /** + * @param ignoreClanNpcIds the ignore clan npc ids + */ + public void setIgnoreClanNpcIds(Set ignoreClanNpcIds) { + _ignoreClanNpcIds = ignoreClanNpcIds != null ? Collections.unmodifiableSet(ignoreClanNpcIds) : null; + } + + public Map> getDropLists() { + return _dropLists; + } + + public void setDropLists(Map> dropLists) { + _dropLists = dropLists != null ? Collections.unmodifiableMap(dropLists) : null; + } + + public List getDropList(DropListScope dropListScope) { + final var dropLists = _dropLists; + return dropLists != null ? dropLists.get(dropListScope) : null; + } + + public Collection calculateDrops(DropListScope dropListScope, L2Character victim, L2Character killer) { + final var dropList = getDropList(dropListScope); + if (dropList == null) { + return null; + } + + Collection calculatedDrops = null; + for (var dropItem : dropList) { + final var drops = dropItem.calculateDrops(victim, killer); + if ((drops == null) || drops.isEmpty()) { + continue; + } + + if (calculatedDrops == null) { + calculatedDrops = new LinkedList<>(); + } + + calculatedDrops.addAll(drops); + } + + return calculatedDrops; + } + + public double getCollisionRadiusGrown() { + return _collisionRadiusGrown; + } + + public double getCollisionHeightGrown() { + return _collisionHeightGrown; + } + + @Override + public String toString() { + return getName() + " [" + getId() + "]"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2PcTemplate.java b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2PcTemplate.java new file mode 100644 index 00000000..bcf93671 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/templates/L2PcTemplate.java @@ -0,0 +1,183 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.templates; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.base.ClassId; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; + +/** + * @author mkizub + * @author Zoey76 + */ +public class L2PcTemplate extends L2CharTemplate { + private final ClassId _classId; + + private final float[] _baseHp; + private final float[] _baseMp; + private final float[] _baseCp; + + private final double[] _baseHpReg; + private final double[] _baseMpReg; + private final double[] _baseCpReg; + + private final double _fCollisionHeightFemale; + private final double _fCollisionRadiusFemale; + + private final int _baseSafeFallHeight; + + private final Map _baseSlotDef; + + public L2PcTemplate(StatsSet set) { + super(set); + _classId = ClassId.getClassId(set.getInt("classId")); + setRace(_classId.getRace()); + _baseHp = new float[character().getMaxPlayerLevel() + 1]; + _baseMp = new float[character().getMaxPlayerLevel() + 1]; + _baseCp = new float[character().getMaxPlayerLevel() + 1]; + _baseHpReg = new double[character().getMaxPlayerLevel() + 1]; + _baseMpReg = new double[character().getMaxPlayerLevel() + 1]; + _baseCpReg = new double[character().getMaxPlayerLevel() + 1]; + + _baseSlotDef = new HashMap<>(12); + _baseSlotDef.put(Inventory.PAPERDOLL_CHEST, set.getInt("basePDefchest", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_LEGS, set.getInt("basePDeflegs", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_HEAD, set.getInt("basePDefhead", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_FEET, set.getInt("basePDeffeet", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_GLOVES, set.getInt("basePDefgloves", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_UNDER, set.getInt("basePDefunderwear", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_CLOAK, set.getInt("basePDefcloak", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_REAR, set.getInt("baseMDefrear", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_LEAR, set.getInt("baseMDeflear", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_RFINGER, set.getInt("baseMDefrfinger", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_LFINGER, set.getInt("baseMDefrfinger", 0)); + _baseSlotDef.put(Inventory.PAPERDOLL_NECK, set.getInt("baseMDefneck", 0)); + + _fCollisionRadiusFemale = set.getDouble("collisionFemaleradius"); + _fCollisionHeightFemale = set.getDouble("collisionFemaleheight"); + + _baseSafeFallHeight = set.getInt("baseSafeFall", 333); + } + + /** + * @return the template class Id. + */ + public ClassId getClassId() { + return _classId; + } + + /** + * Sets the value of level upgain parameter. + * @param paramName name of parameter + * @param level corresponding character level + * @param val value of parameter + */ + public void setUpgainValue(String paramName, int level, double val) { + switch (paramName) { + case "hp" -> _baseHp[level] = (float) val; + case "mp" -> _baseMp[level] = (float) val; + case "cp" -> _baseCp[level] = (float) val; + case "hpRegen" -> _baseHpReg[level] = val; + case "mpRegen" -> _baseMpReg[level] = val; + case "cpRegen" -> _baseCpReg[level] = val; + } + } + + /** + * @param level character level to return value + * @return the baseHpMax for given character level + */ + public float getBaseHpMax(int level) { + return _baseHp[level]; + } + + /** + * @param level character level to return value + * @return the baseMpMax for given character level + */ + public float getBaseMpMax(int level) { + return _baseMp[level]; + } + + /** + * @param level character level to return value + * @return the baseCpMax for given character level + */ + public float getBaseCpMax(int level) { + return _baseCp[level]; + } + + /** + * @param level character level to return value + * @return the base HP Regeneration for given character level + */ + public double getBaseHpRegen(int level) { + return _baseHpReg[level]; + } + + /** + * @param level character level to return value + * @return the base MP Regeneration for given character level + */ + public double getBaseMpRegen(int level) { + return _baseMpReg[level]; + } + + /** + * @param level character level to return value + * @return the base HP Regeneration for given character level + */ + public double getBaseCpRegen(int level) { + return _baseCpReg[level]; + } + + /** + * @param slotId id of inventory slot to return value + * @return defence value of character for EMPTY given slot + */ + public int getBaseDefBySlot(int slotId) { + return _baseSlotDef.getOrDefault(slotId, 0); + } + + /** + * @return the template collision height for female characters. + */ + public double getFCollisionHeightFemale() { + return _fCollisionHeightFemale; + } + + /** + * @return the template collision radius for female characters. + */ + public double getFCollisionRadiusFemale() { + return _fCollisionRadiusFemale; + } + + /** + * @return the safe fall height. + */ + public int getSafeFallHeight() { + return _baseSafeFallHeight; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/transform/Transform.java b/src/main/java/com/l2jserver/gameserver/model/actor/transform/Transform.java new file mode 100644 index 00000000..519d2b74 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/transform/Transform.java @@ -0,0 +1,337 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.transform; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.holders.AdditionalItemHolder; +import com.l2jserver.gameserver.model.holders.AdditionalSkillHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.network.serverpackets.ExBasicActionList; + +/** + * @author UnAfraid + */ +public final class Transform implements IIdentifiable { + private final int _id; + private final int _displayId; + private final TransformType _type; + private final boolean _canSwim; + private final int _spawnHeight; + private final boolean _canAttack; + private final String _name; + private final String _title; + + private TransformTemplate _maleTemplate; + private TransformTemplate _femaleTemplate; + + public Transform(StatsSet set) { + _id = set.getInt("id"); + _displayId = set.getInt("displayId", _id); + _type = set.getEnum("type", TransformType.class, TransformType.COMBAT); + _canSwim = set.getInt("can_swim", 0) == 1; + _canAttack = set.getInt("normal_attackable", 1) == 1; + _spawnHeight = set.getInt("spawn_height", 0); + _name = set.getString("setName", null); + _title = set.getString("setTitle", null); + } + + /** + * Gets the transformation ID. + * @return the transformation ID + */ + @Override + public int getId() { + return _id; + } + + public int getDisplayId() { + return _displayId; + } + + public TransformType getType() { + return _type; + } + + public boolean canSwim() { + return _canSwim; + } + + public boolean canAttack() { + return _canAttack; + } + + public int getSpawnHeight() { + return _spawnHeight; + } + + /** + * @return name that's going to be set to the player while is transformed with current transformation + */ + public String getName() { + return _name; + } + + /** + * @return title that's going to be set to the player while is transformed with current transformation + */ + public String getTitle() { + return _title; + } + + public TransformTemplate getTemplate(L2PcInstance player) { + return player != null ? (player.getAppearance().getSex() ? _femaleTemplate : _maleTemplate) : null; + } + + public void setTemplate(boolean male, TransformTemplate template) { + if (male) { + _maleTemplate = template; + } else { + _femaleTemplate = template; + } + } + + /** + * @return {@code true} if transform type is mode change, {@code false} otherwise + */ + public boolean isStance() { + return _type == TransformType.MODE_CHANGE; + } + + /** + * @return {@code true} if transform type is combat, {@code false} otherwise + */ + public boolean isCombat() { + return _type == TransformType.COMBAT; + } + + /** + * @return {@code true} if transform type is non combat, {@code false} otherwise + */ + public boolean isNonCombat() { + return _type == TransformType.NON_COMBAT; + } + + /** + * @return {@code true} if transform type is flying, {@code false} otherwise + */ + public boolean isFlying() { + return _type == TransformType.FLYING; + } + + /** + * @return {@code true} if transform type is cursed, {@code false} otherwise + */ + public boolean isCursed() { + return _type == TransformType.CURSED; + } + + /** + * @return {@code true} if transform type is raiding, {@code false} otherwise + */ + public boolean isRiding() { + return _type == TransformType.RIDING_MODE; + } + + /** + * @return {@code true} if transform type is pure stat, {@code false} otherwise + */ + public boolean isPureStats() { + return _type == TransformType.PURE_STAT; + } + + public double getCollisionHeight(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + return template != null ? template.getCollisionHeight() : player.getCollisionHeight(); + } + + public double getCollisionRadius(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + return template != null ? template.getCollisionRadius() : player.getCollisionRadius(); + } + + public int getBaseAttackRange(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + return template != null ? template.getBaseAttackRange() : player.getTemplate().getBaseAttackRange(); + } + + public void onTransform(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + if (template != null) { + // Start flying. + if (isFlying()) { + player.setIsFlying(true); + } + + if (getName() != null) { + player.getAppearance().setVisibleName(getName()); + } + if (getTitle() != null) { + player.getAppearance().setVisibleTitle(getTitle()); + } + + // Set inventory blocks if needed. + if (!template.getAdditionalItems().isEmpty()) { + final List allowed = new ArrayList<>(); + final List notAllowed = new ArrayList<>(); + for (AdditionalItemHolder holder : template.getAdditionalItems()) { + if (holder.isAllowedToUse()) { + allowed.add(holder.getId()); + } else { + notAllowed.add(holder.getId()); + } + } + + if (!allowed.isEmpty()) { + final int[] items = new int[allowed.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = allowed.get(i); + } + player.getInventory().setInventoryBlock(items, 1); + } + + if (!notAllowed.isEmpty()) { + final int[] items = new int[notAllowed.size()]; + for (int i = 0; i < items.length; i++) { + items[i] = notAllowed.get(i); + } + player.getInventory().setInventoryBlock(items, 2); + } + } + + // Send basic action list. + if (template.hasBasicActionList()) { + player.sendPacket(template.getBasicActionList()); + } + } + } + + public void onUntransform(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + if (template != null) { + // Stop flying. + if (isFlying()) { + player.setIsFlying(false); + } + + if (getName() != null) { + player.getAppearance().setVisibleName(null); + } + if (getTitle() != null) { + player.getAppearance().setVisibleTitle(null); + } + + // Remove common skills. + if (!template.getSkills().isEmpty()) { + for (SkillHolder holder : template.getSkills()) { + final Skill skill = holder.getSkill(); + if (!SkillTreesData.getInstance().isSkillAllowed(player, skill)) { + player.removeSkill(skill, false, skill.isPassive()); + } + } + } + + // Remove skills depending on level. + if (!template.getAdditionalSkills().isEmpty()) { + for (AdditionalSkillHolder holder : template.getAdditionalSkills()) { + final Skill skill = holder.getSkill(); + if ((player.getLevel() >= holder.getMinLevel()) && !SkillTreesData.getInstance().isSkillAllowed(player, skill)) { + player.removeSkill(skill, false, skill.isPassive()); + } + } + } + + // Remove transformation skills. + player.removeAllTransformSkills(); + + // Remove inventory blocks if needed. + if (!template.getAdditionalItems().isEmpty()) { + player.getInventory().unblock(); + } + + player.sendPacket(ExBasicActionList.STATIC_PACKET); + } + } + + public void onLevelUp(L2PcInstance player) { + final TransformTemplate template = getTemplate(player); + if (template != null) { + // Add skills depending on level. + if (!template.getAdditionalSkills().isEmpty()) { + for (AdditionalSkillHolder holder : template.getAdditionalSkills()) { + if (player.getLevel() >= holder.getMinLevel()) { + if (player.getSkillLevel(holder.getSkillId()) < holder.getSkillLvl()) { + player.addSkill(holder.getSkill(), false); + player.addTransformSkill(holder.getSkill()); + } + } + } + } + } + } + + public double getStat(L2PcInstance player, Stats stats) { + double val = 0; + final TransformTemplate template = getTemplate(player); + if (template != null) { + val = template.getStats(stats); + final TransformLevelData data = template.getData(player.getLevel()); + if (data != null) { + val = data.getStats(stats); + } + } + return val; + } + + /** + * @param player + * @param slot + * @return + */ + public int getBaseDefBySlot(L2PcInstance player, int slot) { + final TransformTemplate template = getTemplate(player); + if (template != null) { + return template.getDefense(slot); + } + return player.getTemplate().getBaseDefBySlot(slot); + } + + /** + * @param player + * @return + */ + public double getLevelMod(L2PcInstance player) { + double val = -1; + final TransformTemplate template = getTemplate(player); + if (template != null) { + final TransformLevelData data = template.getData(player.getLevel()); + if (data != null) { + val = data.getLevelMod(); + } + } + return val; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformLevelData.java b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformLevelData.java new file mode 100644 index 00000000..1b793a3f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformLevelData.java @@ -0,0 +1,67 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.transform; + +import java.util.HashMap; +import java.util.Map; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.stats.Stats; + +/** + * @author UnAfraid + */ +public final class TransformLevelData { + private final int _level; + private final double _levelMod; + private Map _stats; + + public TransformLevelData(StatsSet set) { + _level = set.getInt("val"); + _levelMod = set.getDouble("levelMod"); + addStats(Stats.MAX_HP, set.getDouble("hp")); + addStats(Stats.MAX_MP, set.getDouble("mp")); + addStats(Stats.MAX_CP, set.getDouble("cp")); + addStats(Stats.REGENERATE_HP_RATE, set.getDouble("hpRegen")); + addStats(Stats.REGENERATE_MP_RATE, set.getDouble("mpRegen")); + addStats(Stats.REGENERATE_CP_RATE, set.getDouble("cpRegen")); + } + + private void addStats(Stats stat, double val) { + if (_stats == null) { + _stats = new HashMap<>(); + } + _stats.put(stat.ordinal(), val); + } + + public double getStats(Stats stats) { + if ((_stats == null) || !_stats.containsKey(stats.ordinal())) { + return 0; + } + return _stats.get(stats.ordinal()); + } + + public int getLevel() { + return _level; + } + + public double getLevelMod() { + return _levelMod; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformTemplate.java b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformTemplate.java new file mode 100644 index 00000000..4f06b250 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformTemplate.java @@ -0,0 +1,211 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.transform; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.holders.AdditionalItemHolder; +import com.l2jserver.gameserver.model.holders.AdditionalSkillHolder; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.type.WeaponType; +import com.l2jserver.gameserver.model.stats.MoveType; +import com.l2jserver.gameserver.model.stats.Stats; +import com.l2jserver.gameserver.network.serverpackets.ExBasicActionList; + +/** + * @author UnAfraid + */ +public final class TransformTemplate { + private final double _collisionRadius; + private final double _collisionHeight; + private final WeaponType _baseAttackType; + private final int _baseAttackRange; + private final double _baseRandomDamage; + private List _skills; + private List _additionalSkills; + private List _additionalItems; + private Map _baseDefense; + private Map _baseStats; + private Map _baseSpeed; + + private ExBasicActionList _list; + private final Map _data = new LinkedHashMap<>(100); + + public TransformTemplate(StatsSet set) { + _collisionRadius = set.getDouble("radius", 0); + _collisionHeight = set.getDouble("height", 0); + _baseAttackType = set.getEnum("attackType", WeaponType.class, WeaponType.FIST); + _baseAttackRange = set.getInt("range", 40); + _baseRandomDamage = set.getDouble("randomDamage", 0); + + addSpeed(MoveType.WALK, set.getDouble("walk", 0)); + addSpeed(MoveType.RUN, set.getDouble("run", 0)); + addSpeed(MoveType.SLOW_SWIM, set.getDouble("waterWalk", 0)); + addSpeed(MoveType.FAST_SWIM, set.getDouble("waterRun", 0)); + addSpeed(MoveType.SLOW_FLY, set.getDouble("flyWalk", 0)); + addSpeed(MoveType.FAST_FLY, set.getDouble("flyRun", 0)); + + addStats(Stats.POWER_ATTACK, set.getDouble("pAtk", 0)); + addStats(Stats.MAGIC_ATTACK, set.getDouble("mAtk", 0)); + addStats(Stats.POWER_ATTACK_RANGE, set.getInt("range", 0)); + addStats(Stats.POWER_ATTACK_SPEED, set.getInt("attackSpeed", 0)); + addStats(Stats.CRITICAL_RATE, set.getInt("critRate", 0)); + addStats(Stats.STAT_STR, set.getInt("str", 0)); + addStats(Stats.STAT_INT, set.getInt("int", 0)); + addStats(Stats.STAT_CON, set.getInt("con", 0)); + addStats(Stats.STAT_DEX, set.getInt("dex", 0)); + addStats(Stats.STAT_WIT, set.getInt("wit", 0)); + addStats(Stats.STAT_MEN, set.getInt("men", 0)); + + addDefense(Inventory.PAPERDOLL_CHEST, set.getInt("chest", 0)); + addDefense(Inventory.PAPERDOLL_LEGS, set.getInt("legs", 0)); + addDefense(Inventory.PAPERDOLL_HEAD, set.getInt("head", 0)); + addDefense(Inventory.PAPERDOLL_FEET, set.getInt("feet", 0)); + addDefense(Inventory.PAPERDOLL_GLOVES, set.getInt("gloves", 0)); + addDefense(Inventory.PAPERDOLL_UNDER, set.getInt("underwear", 0)); + addDefense(Inventory.PAPERDOLL_CLOAK, set.getInt("cloak", 0)); + addDefense(Inventory.PAPERDOLL_REAR, set.getInt("rear", 0)); + addDefense(Inventory.PAPERDOLL_LEAR, set.getInt("lear", 0)); + addDefense(Inventory.PAPERDOLL_RFINGER, set.getInt("rfinger", 0)); + addDefense(Inventory.PAPERDOLL_LFINGER, set.getInt("lfinger", 0)); + addDefense(Inventory.PAPERDOLL_NECK, set.getInt("neck", 0)); + } + + private void addSpeed(MoveType type, double val) { + if (_baseSpeed == null) { + _baseSpeed = new HashMap<>(); + } + _baseSpeed.put(type.ordinal(), val); + } + + public double getBaseMoveSpeed(MoveType type) { + if ((_baseSpeed == null) || !_baseSpeed.containsKey(type.ordinal())) { + return 0; + } + return _baseSpeed.get(type.ordinal()); + } + + private void addDefense(int type, int val) { + if (_baseDefense == null) { + _baseDefense = new HashMap<>(); + } + _baseDefense.put(type, val); + } + + public int getDefense(int type) { + if ((_baseDefense == null) || !_baseDefense.containsKey(type)) { + return 0; + } + return _baseDefense.get(type); + } + + private void addStats(Stats stats, double val) { + if (_baseStats == null) { + _baseStats = new HashMap<>(); + } + _baseStats.put(stats.ordinal(), val); + } + + public double getStats(Stats stats) { + if ((_baseStats == null) || !_baseStats.containsKey(stats.ordinal())) { + return 0; + } + return _baseStats.get(stats.ordinal()); + } + + public double getCollisionRadius() { + return _collisionRadius; + } + + public double getCollisionHeight() { + return _collisionHeight; + } + + public WeaponType getBaseAttackType() { + return _baseAttackType; + } + + public int getBaseAttackRange() { + return _baseAttackRange; + } + + public double getBaseRandomDamage() { + return _baseRandomDamage; + } + + public void addSkill(SkillHolder holder) { + if (_skills == null) { + _skills = new ArrayList<>(); + } + _skills.add(holder); + } + + public List getSkills() { + return _skills != null ? _skills : Collections.emptyList(); + } + + public void addAdditionalSkill(AdditionalSkillHolder holder) { + if (_additionalSkills == null) { + _additionalSkills = new ArrayList<>(); + } + _additionalSkills.add(holder); + } + + public List getAdditionalSkills() { + return _additionalSkills != null ? _additionalSkills : Collections.emptyList(); + } + + public void addAdditionalItem(AdditionalItemHolder holder) { + if (_additionalItems == null) { + _additionalItems = new ArrayList<>(); + } + _additionalItems.add(holder); + } + + public List getAdditionalItems() { + return _additionalItems != null ? _additionalItems : Collections.emptyList(); + } + + public void setBasicActionList(ExBasicActionList list) { + _list = list; + } + + public ExBasicActionList getBasicActionList() { + return _list; + } + + public boolean hasBasicActionList() { + return _list != null; + } + + public void addLevelData(TransformLevelData data) { + _data.put(data.getLevel(), data); + } + + public TransformLevelData getData(int level) { + return _data.get(level); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformType.java b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformType.java new file mode 100644 index 00000000..f7761799 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/actor/transform/TransformType.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.actor.transform; + +/** + * @author UnAfraid + */ +public enum TransformType { + MODE_CHANGE, + NON_COMBAT, + CURSED, + RIDING_MODE, + PURE_STAT, + FLYING, + COMBAT +} diff --git a/src/main/java/com/l2jserver/gameserver/model/announce/Announcement.java b/src/main/java/com/l2jserver/gameserver/model/announce/Announcement.java new file mode 100644 index 00000000..f8b27e04 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/announce/Announcement.java @@ -0,0 +1,146 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.announce; + +import static java.sql.Statement.RETURN_GENERATED_KEYS; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; + +/** + * Announcement. + * @author UnAfraid + */ +public class Announcement implements IAnnouncement { + private static final Logger LOG = LoggerFactory.getLogger(Announcement.class); + + private static final String INSERT_QUERY = "INSERT INTO announcements (type, content, author) VALUES (?, ?, ?)"; + private static final String UPDATE_QUERY = "UPDATE announcements SET type = ?, content = ?, author = ? WHERE id = ?"; + private static final String DELETE_QUERY = "DELETE FROM announcements WHERE id = ?"; + + protected int _id; + + private AnnouncementType _type; + + private String _content; + + private String _author; + + public Announcement(AnnouncementType type, String content, String author) { + _type = type; + _content = content; + _author = author; + } + + public Announcement(int id, AnnouncementType type, String content, String author) { + this(type, content, author); + _id = id; + } + + @Override + public int getId() { + return _id; + } + + @Override + public AnnouncementType getType() { + return _type; + } + + @Override + public void setType(AnnouncementType type) { + _type = type; + } + + @Override + public String getContent() { + return _content; + } + + @Override + public void setContent(String content) { + _content = content; + } + + @Override + public String getAuthor() { + return _author; + } + + @Override + public void setAuthor(String author) { + _author = author; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean storeMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_QUERY, RETURN_GENERATED_KEYS)) { + ps.setInt(1, _type.ordinal()); + ps.setString(2, _content); + ps.setString(3, _author); + ps.execute(); + try (var rs = ps.getGeneratedKeys()) { + if (rs.next()) { + _id = rs.getInt(1); + } + } + } catch (Exception e) { + LOG.warn("Couldn't store announcement: ", e); + return false; + } + return true; + } + + @Override + public boolean updateMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_QUERY)) { + ps.setInt(1, _type.ordinal()); + ps.setString(2, _content); + ps.setString(3, _author); + ps.setInt(4, _id); + ps.execute(); + } catch (Exception e) { + LOG.warn("Couldn't store announcement: ", e); + return false; + } + return true; + } + + @Override + public boolean deleteMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(DELETE_QUERY)) { + ps.setInt(1, _id); + ps.execute(); + } catch (Exception e) { + LOG.warn("Couldn't remove announcement: ", e); + return false; + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/announce/AnnouncementType.java b/src/main/java/com/l2jserver/gameserver/model/announce/AnnouncementType.java new file mode 100644 index 00000000..bef85981 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/announce/AnnouncementType.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.announce; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author UnAfraid + */ +public enum AnnouncementType { + NORMAL, + CRITICAL, + EVENT, + AUTO_NORMAL, + AUTO_CRITICAL; + + private static final Logger LOG = LoggerFactory.getLogger(AnnouncementType.class); + + public static AnnouncementType findById(int id) { + for (AnnouncementType type : values()) { + if (type.ordinal() == id) { + return type; + } + } + LOG.warn("Nonexistent id specified: {}!", id, new IllegalStateException()); + return NORMAL; + } + + public static AnnouncementType findByName(String name) { + for (AnnouncementType type : values()) { + if (type.name().equalsIgnoreCase(name)) { + return type; + } + } + LOG.warn("Nonexistent name specified: {}!", name, new IllegalStateException()); + return NORMAL; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/announce/AutoAnnouncement.java b/src/main/java/com/l2jserver/gameserver/model/announce/AutoAnnouncement.java new file mode 100644 index 00000000..0ac0d4b5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/announce/AutoAnnouncement.java @@ -0,0 +1,154 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.announce; + +import static java.sql.Statement.RETURN_GENERATED_KEYS; + +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.config.Configuration; +import com.l2jserver.gameserver.util.Broadcast; + +/** + * Auto-announcements. + * @author UnAfraid + */ +public final class AutoAnnouncement extends Announcement implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(AutoAnnouncement.class); + + private static final String INSERT_QUERY = "INSERT INTO announcements (`type`, `content`, `author`, `initial`, `delay`, `repeat`) VALUES (?, ?, ?, ?, ?, ?)"; + private static final String UPDATE_QUERY = "UPDATE announcements SET `type` = ?, `content` = ?, `author` = ?, `initial` = ?, `delay` = ?, `repeat` = ? WHERE id = ?"; + + private long _initial; + private long _delay; + private int _repeat; + private int _currentState; + + private ScheduledFuture _task; + + public AutoAnnouncement(AnnouncementType type, String content, String author, long initial, long delay, int repeat) { + super(type, content, author); + _initial = initial; + _delay = delay; + _repeat = repeat; + restartMe(); + } + + public long getInitial() { + return _initial; + } + + public void setInitial(long initial) { + _initial = initial; + } + + public long getDelay() { + return _delay; + } + + public void setDelay(long delay) { + _delay = delay; + } + + public int getRepeat() { + return _repeat; + } + + public void setRepeat(int repeat) { + _repeat = repeat; + } + + @Override + public boolean storeMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(INSERT_QUERY, RETURN_GENERATED_KEYS)) { + ps.setInt(1, getType().ordinal()); + ps.setString(2, getContent()); + ps.setString(3, getAuthor()); + ps.setLong(4, getInitial()); + ps.setLong(5, getDelay()); + ps.setInt(6, getRepeat()); + ps.execute(); + try (var rs = ps.getGeneratedKeys()) { + if (rs.next()) { + _id = rs.getInt(1); + } + } + } catch (Exception e) { + LOG.warn("Couldn't store announcement: ", e); + return false; + } + return true; + } + + @Override + public boolean updateMe() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement(UPDATE_QUERY)) { + ps.setInt(1, getType().ordinal()); + ps.setString(2, getContent()); + ps.setString(3, getAuthor()); + ps.setLong(4, getInitial()); + ps.setLong(5, getDelay()); + ps.setLong(6, getRepeat()); + ps.setLong(7, getId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Couldn't update announcement: ", e); + return false; + } + return true; + } + + @Override + public boolean deleteMe() { + if ((_task != null) && !_task.isCancelled()) { + _task.cancel(false); + } + return super.deleteMe(); + } + + public void restartMe() { + if ((_task != null) && !_task.isCancelled()) { + _task.cancel(false); + } + _currentState = _repeat; + _task = ThreadPoolManager.getInstance().scheduleGeneral(this, _initial); + } + + @Override + public void run() { + if ((_currentState == -1) || (_currentState > 0)) { + for (String content : getContent().split(Configuration.EOL)) { + Broadcast.toAllOnlinePlayers(content, (getType() == AnnouncementType.AUTO_CRITICAL)); + } + + if (_currentState != -1) { + _currentState--; + } + + _task = ThreadPoolManager.getInstance().scheduleGeneral(this, _delay); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/announce/EventAnnouncement.java b/src/main/java/com/l2jserver/gameserver/model/announce/EventAnnouncement.java new file mode 100644 index 00000000..67017675 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/announce/EventAnnouncement.java @@ -0,0 +1,95 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.announce; + +import java.util.Date; + +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.script.DateRange; + +/** + * @author UnAfraid + */ +public class EventAnnouncement implements IAnnouncement { + private final int _id; + private final DateRange _range; + private String _content; + + public EventAnnouncement(DateRange range, String content) { + _id = IdFactory.getInstance().getNextId(); + _range = range; + _content = content; + } + + @Override + public int getId() { + return _id; + } + + @Override + public AnnouncementType getType() { + return AnnouncementType.EVENT; + } + + @Override + public void setType(AnnouncementType type) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isValid() { + return _range.isWithinRange(new Date()); + } + + @Override + public String getContent() { + return _content; + } + + @Override + public void setContent(String content) { + _content = content; + } + + @Override + public String getAuthor() { + return "N/A"; + } + + @Override + public void setAuthor(String author) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean deleteMe() { + IdFactory.getInstance().releaseId(_id); + return true; + } + + @Override + public boolean storeMe() { + return true; + } + + @Override + public boolean updateMe() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/announce/IAnnouncement.java b/src/main/java/com/l2jserver/gameserver/model/announce/IAnnouncement.java new file mode 100644 index 00000000..ef53b5db --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/announce/IAnnouncement.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.announce; + +import com.l2jserver.gameserver.model.interfaces.IDeletable; +import com.l2jserver.gameserver.model.interfaces.IStorable; +import com.l2jserver.gameserver.model.interfaces.IUpdatable; + +/** + * @author UnAfraid + */ +public interface IAnnouncement extends IStorable, IUpdatable, IDeletable { + int getId(); + + AnnouncementType getType(); + + void setType(AnnouncementType type); + + boolean isValid(); + + String getContent(); + + void setContent(String content); + + String getAuthor(); + + void setAuthor(String author); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/AcquireSkillType.java b/src/main/java/com/l2jserver/gameserver/model/base/AcquireSkillType.java new file mode 100644 index 00000000..e9fe9965 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/AcquireSkillType.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +/** + * Skill Learn types. + * @author Zoey76 + */ +public enum AcquireSkillType { + CLASS, + FISHING, + PLEDGE, + SUBPLEDGE, + TRANSFORM, + TRANSFER, + SUBCLASS, + COLLECT; + + public static AcquireSkillType getAcquireSkillType(int id) { + return values()[id]; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/ClassId.java b/src/main/java/com/l2jserver/gameserver/model/base/ClassId.java new file mode 100644 index 00000000..c3d85c6a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/ClassId.java @@ -0,0 +1,323 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.interfaces.IIdentifiable; + +/** + * This class defines all classes (ex : human fighter, darkFighter...) that a player can chose.
          + * Data: + *
            + *
          • id : The Identifier of the class
          • + *
          • isMage : True if the class is a mage class
          • + *
          • race : The race of this class
          • + *
          • parent : The parent ClassId or null if this class is the root
          • + *
          + * @since 2005/03/27 15:29:33 + */ +public enum ClassId implements IIdentifiable { + fighter(0x00, false, Race.HUMAN, null), + + warrior(0x01, false, Race.HUMAN, fighter), + gladiator(0x02, false, Race.HUMAN, warrior), + warlord(0x03, false, Race.HUMAN, warrior), + knight(0x04, false, Race.HUMAN, fighter), + paladin(0x05, false, Race.HUMAN, knight), + darkAvenger(0x06, false, Race.HUMAN, knight), + rogue(0x07, false, Race.HUMAN, fighter), + treasureHunter(0x08, false, Race.HUMAN, rogue), + hawkeye(0x09, false, Race.HUMAN, rogue), + + mage(0x0a, true, Race.HUMAN, null), + wizard(0x0b, true, Race.HUMAN, mage), + sorceror(0x0c, true, Race.HUMAN, wizard), + necromancer(0x0d, true, Race.HUMAN, wizard), + warlock(0x0e, true, true, Race.HUMAN, wizard), + cleric(0x0f, true, Race.HUMAN, mage), + bishop(0x10, true, Race.HUMAN, cleric), + prophet(0x11, true, Race.HUMAN, cleric), + + elvenFighter(0x12, false, Race.ELF, null), + elvenKnight(0x13, false, Race.ELF, elvenFighter), + templeKnight(0x14, false, Race.ELF, elvenKnight), + swordSinger(0x15, false, Race.ELF, elvenKnight), + elvenScout(0x16, false, Race.ELF, elvenFighter), + plainsWalker(0x17, false, Race.ELF, elvenScout), + silverRanger(0x18, false, Race.ELF, elvenScout), + + elvenMage(0x19, true, Race.ELF, null), + elvenWizard(0x1a, true, Race.ELF, elvenMage), + spellsinger(0x1b, true, Race.ELF, elvenWizard), + elementalSummoner(0x1c, true, true, Race.ELF, elvenWizard), + oracle(0x1d, true, Race.ELF, elvenMage), + elder(0x1e, true, Race.ELF, oracle), + + darkFighter(0x1f, false, Race.DARK_ELF, null), + palusKnight(0x20, false, Race.DARK_ELF, darkFighter), + shillienKnight(0x21, false, Race.DARK_ELF, palusKnight), + bladedancer(0x22, false, Race.DARK_ELF, palusKnight), + assassin(0x23, false, Race.DARK_ELF, darkFighter), + abyssWalker(0x24, false, Race.DARK_ELF, assassin), + phantomRanger(0x25, false, Race.DARK_ELF, assassin), + + darkMage(0x26, true, Race.DARK_ELF, null), + darkWizard(0x27, true, Race.DARK_ELF, darkMage), + spellhowler(0x28, true, Race.DARK_ELF, darkWizard), + phantomSummoner(0x29, true, true, Race.DARK_ELF, darkWizard), + shillienOracle(0x2a, true, Race.DARK_ELF, darkMage), + shillenElder(0x2b, true, Race.DARK_ELF, shillienOracle), + + orcFighter(0x2c, false, Race.ORC, null), + orcRaider(0x2d, false, Race.ORC, orcFighter), + destroyer(0x2e, false, Race.ORC, orcRaider), + orcMonk(0x2f, false, Race.ORC, orcFighter), + tyrant(0x30, false, Race.ORC, orcMonk), + + orcMage(0x31, true, Race.ORC, null), + orcShaman(0x32, true, Race.ORC, orcMage), + overlord(0x33, true, Race.ORC, orcShaman), + warcryer(0x34, true, Race.ORC, orcShaman), + + dwarvenFighter(0x35, false, Race.DWARF, null), + scavenger(0x36, false, Race.DWARF, dwarvenFighter), + bountyHunter(0x37, false, Race.DWARF, scavenger), + artisan(0x38, false, Race.DWARF, dwarvenFighter), + warsmith(0x39, false, Race.DWARF, artisan), + + /* + * Dummy Entries (id's already in decimal format) btw FU NCSoft for the amount of work you put me through to do this!! + */ + dummyEntry1(58, false, null, null), + dummyEntry2(59, false, null, null), + dummyEntry3(60, false, null, null), + dummyEntry4(61, false, null, null), + dummyEntry5(62, false, null, null), + dummyEntry6(63, false, null, null), + dummyEntry7(64, false, null, null), + dummyEntry8(65, false, null, null), + dummyEntry9(66, false, null, null), + dummyEntry10(67, false, null, null), + dummyEntry11(68, false, null, null), + dummyEntry12(69, false, null, null), + dummyEntry13(70, false, null, null), + dummyEntry14(71, false, null, null), + dummyEntry15(72, false, null, null), + dummyEntry16(73, false, null, null), + dummyEntry17(74, false, null, null), + dummyEntry18(75, false, null, null), + dummyEntry19(76, false, null, null), + dummyEntry20(77, false, null, null), + dummyEntry21(78, false, null, null), + dummyEntry22(79, false, null, null), + dummyEntry23(80, false, null, null), + dummyEntry24(81, false, null, null), + dummyEntry25(82, false, null, null), + dummyEntry26(83, false, null, null), + dummyEntry27(84, false, null, null), + dummyEntry28(85, false, null, null), + dummyEntry29(86, false, null, null), + dummyEntry30(87, false, null, null), + /* + * Of Dummy entries + */ + + /* + * Now the bad boys! new class ids :)) (3rd classes) + */ + duelist(0x58, false, Race.HUMAN, gladiator), + dreadnought(0x59, false, Race.HUMAN, warlord), + phoenixKnight(0x5a, false, Race.HUMAN, paladin), + hellKnight(0x5b, false, Race.HUMAN, darkAvenger), + sagittarius(0x5c, false, Race.HUMAN, hawkeye), + adventurer(0x5d, false, Race.HUMAN, treasureHunter), + archmage(0x5e, true, Race.HUMAN, sorceror), + soultaker(0x5f, true, Race.HUMAN, necromancer), + arcanaLord(0x60, true, true, Race.HUMAN, warlock), + cardinal(0x61, true, Race.HUMAN, bishop), + hierophant(0x62, true, Race.HUMAN, prophet), + + evaTemplar(0x63, false, Race.ELF, templeKnight), + swordMuse(0x64, false, Race.ELF, swordSinger), + windRider(0x65, false, Race.ELF, plainsWalker), + moonlightSentinel(0x66, false, Race.ELF, silverRanger), + mysticMuse(0x67, true, Race.ELF, spellsinger), + elementalMaster(0x68, true, true, Race.ELF, elementalSummoner), + evaSaint(0x69, true, Race.ELF, elder), + + shillienTemplar(0x6a, false, Race.DARK_ELF, shillienKnight), + spectralDancer(0x6b, false, Race.DARK_ELF, bladedancer), + ghostHunter(0x6c, false, Race.DARK_ELF, abyssWalker), + ghostSentinel(0x6d, false, Race.DARK_ELF, phantomRanger), + stormScreamer(0x6e, true, Race.DARK_ELF, spellhowler), + spectralMaster(0x6f, true, true, Race.DARK_ELF, phantomSummoner), + shillienSaint(0x70, true, Race.DARK_ELF, shillenElder), + + titan(0x71, false, Race.ORC, destroyer), + grandKhavatari(0x72, false, Race.ORC, tyrant), + dominator(0x73, true, Race.ORC, overlord), + doomcryer(0x74, true, Race.ORC, warcryer), + + fortuneSeeker(0x75, false, Race.DWARF, bountyHunter), + maestro(0x76, false, Race.DWARF, warsmith), + + dummyEntry31(0x77, false, null, null), + dummyEntry32(0x78, false, null, null), + dummyEntry33(0x79, false, null, null), + dummyEntry34(0x7a, false, null, null), + + maleSoldier(0x7b, false, Race.KAMAEL, null), + femaleSoldier(0x7C, false, Race.KAMAEL, null), + trooper(0x7D, false, Race.KAMAEL, maleSoldier), + warder(0x7E, false, Race.KAMAEL, femaleSoldier), + berserker(0x7F, false, Race.KAMAEL, trooper), + maleSoulbreaker(0x80, false, Race.KAMAEL, trooper), + femaleSoulbreaker(0x81, false, Race.KAMAEL, warder), + arbalester(0x82, false, Race.KAMAEL, warder), + doombringer(0x83, false, Race.KAMAEL, berserker), + maleSoulhound(0x84, false, Race.KAMAEL, maleSoulbreaker), + femaleSoulhound(0x85, false, Race.KAMAEL, femaleSoulbreaker), + trickster(0x86, false, Race.KAMAEL, arbalester), + inspector(0x87, false, Race.KAMAEL, warder), // DS: yes, both male/female inspectors use skills from warder + judicator(0x88, false, Race.KAMAEL, inspector); + + /** The Identifier of the Class */ + private final int _id; + + /** True if the class is a mage class */ + private final boolean _isMage; + + /** True if the class is a summoner class */ + private final boolean _isSummoner; + + /** The Race object of the class */ + private final Race _race; + + /** The parent ClassId or null if this class is a root */ + private final ClassId _parent; + + /** + * Class constructor. + * @param pId the class Id. + * @param pIsMage {code true} if the class is mage class. + * @param race the race related to the class. + * @param pParent the parent class Id. + */ + ClassId(int pId, boolean pIsMage, Race race, ClassId pParent) { + _id = pId; + _isMage = pIsMage; + _isSummoner = false; + _race = race; + _parent = pParent; + } + + /** + * Class constructor. + * @param pId the class Id. + * @param pIsMage {code true} if the class is mage class. + * @param pIsSummoner {code true} if the class is summoner class. + * @param race the race related to the class. + * @param pParent the parent class Id. + */ + ClassId(int pId, boolean pIsMage, boolean pIsSummoner, Race race, ClassId pParent) { + _id = pId; + _isMage = pIsMage; + _isSummoner = pIsSummoner; + _race = race; + _parent = pParent; + } + + /** + * Gets the ID of the class. + * @return the ID of the class + */ + @Override + public final int getId() { + return _id; + } + + /** + * @return {code true} if the class is a mage class. + */ + public final boolean isMage() { + return _isMage; + } + + /** + * @return {code true} if the class is a summoner class. + */ + public final boolean isSummoner() { + return _isSummoner; + } + + /** + * @return the Race object of the class. + */ + public final Race getRace() { + return _race; + } + + /** + * @param cid the parent ClassId to check. + * @return {code true} if this Class is a child of the selected ClassId. + */ + public final boolean childOf(ClassId cid) { + if (_parent == null) { + return false; + } + + if (_parent == cid) { + return true; + } + + return _parent.childOf(cid); + + } + + /** + * @param cid the parent ClassId to check. + * @return {code true} if this Class is equal to the selected ClassId or a child of the selected ClassId. + */ + public final boolean equalsOrChildOf(ClassId cid) { + return (this == cid) || childOf(cid); + } + + /** + * @return the child level of this Class (0=root, 1=child leve 1...) + */ + public final int level() { + if (_parent == null) { + return 0; + } + + return 1 + _parent.level(); + } + + public final ClassId getParent() { + return _parent; + } + + public static ClassId getClassId(int cId) { + try { + return ClassId.values()[cId]; + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/ClassInfo.java b/src/main/java/com/l2jserver/gameserver/model/base/ClassInfo.java new file mode 100644 index 00000000..ac784e29 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/ClassInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +import java.util.regex.Matcher; + +/** + * This class will hold the information of the player classes. + * @author Zoey76 + */ +public final class ClassInfo { + private final ClassId _classId; + private final String _className; + private final ClassId _parentClassId; + + /** + * Constructor for ClassInfo. + * @param classId the class ID + * @param className the in game class name + * @param parentClassId the parent class for the given {@code classId} + */ + public ClassInfo(ClassId classId, String className, ClassId parentClassId) { + _classId = classId; + _className = className; + _parentClassId = parentClassId; + } + + public ClassId getClassId() { + return _classId; + } + + public String getClassName() { + return _className; + } + + private int getClassClientId() { + int classClientId = _classId.getId(); + if ((classClientId >= 0) && (classClientId <= 57)) { + classClientId += 247; + } else if ((classClientId >= 88) && (classClientId <= 118)) { + classClientId += 1071; + } else if ((classClientId >= 123) && (classClientId <= 136)) { + classClientId += 1438; + } + return classClientId; + } + + /** + * @return the class client ID formatted to be displayed on a HTML. + */ + public String getClientCode() { + return "&$" + getClassClientId() + ";"; + } + + public String getEscapedClientCode() { + return Matcher.quoteReplacement(getClientCode()); + } + + public ClassId getParentClassId() { + return _parentClassId; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/base/ClassLevel.java b/src/main/java/com/l2jserver/gameserver/model/base/ClassLevel.java new file mode 100644 index 00000000..ce13b9be --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/ClassLevel.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +/** + * @since 2004/06/27 08:12:59 + */ +public enum ClassLevel { + First, + Second, + Third, + Fourth +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/ClassType.java b/src/main/java/com/l2jserver/gameserver/model/base/ClassType.java new file mode 100644 index 00000000..40dc1797 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/ClassType.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +/** + * ClassType Enum + * @author Tempy + */ +public enum ClassType { + Fighter, + Mystic, + Priest +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/PlayerClass.java b/src/main/java/com/l2jserver/gameserver/model/base/PlayerClass.java new file mode 100644 index 00000000..16c71aaa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/PlayerClass.java @@ -0,0 +1,313 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.model.base.ClassLevel.First; +import static com.l2jserver.gameserver.model.base.ClassLevel.Fourth; +import static com.l2jserver.gameserver.model.base.ClassLevel.Second; +import static com.l2jserver.gameserver.model.base.ClassLevel.Third; +import static com.l2jserver.gameserver.model.base.ClassType.Fighter; +import static com.l2jserver.gameserver.model.base.ClassType.Mystic; +import static com.l2jserver.gameserver.model.base.ClassType.Priest; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Set; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author luisantonioa + */ +public enum PlayerClass { + HumanFighter(Race.HUMAN, Fighter, First), + Warrior(Race.HUMAN, Fighter, Second), + Gladiator(Race.HUMAN, Fighter, Third), + Warlord(Race.HUMAN, Fighter, Third), + HumanKnight(Race.HUMAN, Fighter, Second), + Paladin(Race.HUMAN, Fighter, Third), + DarkAvenger(Race.HUMAN, Fighter, Third), + Rogue(Race.HUMAN, Fighter, Second), + TreasureHunter(Race.HUMAN, Fighter, Third), + Hawkeye(Race.HUMAN, Fighter, Third), + HumanMystic(Race.HUMAN, Mystic, First), + HumanWizard(Race.HUMAN, Mystic, Second), + Sorceror(Race.HUMAN, Mystic, Third), + Necromancer(Race.HUMAN, Mystic, Third), + Warlock(Race.HUMAN, Mystic, Third), + Cleric(Race.HUMAN, Priest, Second), + Bishop(Race.HUMAN, Priest, Third), + Prophet(Race.HUMAN, Priest, Third), + + ElvenFighter(Race.ELF, Fighter, First), + ElvenKnight(Race.ELF, Fighter, Second), + TempleKnight(Race.ELF, Fighter, Third), + Swordsinger(Race.ELF, Fighter, Third), + ElvenScout(Race.ELF, Fighter, Second), + Plainswalker(Race.ELF, Fighter, Third), + SilverRanger(Race.ELF, Fighter, Third), + ElvenMystic(Race.ELF, Mystic, First), + ElvenWizard(Race.ELF, Mystic, Second), + Spellsinger(Race.ELF, Mystic, Third), + ElementalSummoner(Race.ELF, Mystic, Third), + ElvenOracle(Race.ELF, Priest, Second), + ElvenElder(Race.ELF, Priest, Third), + + DarkElvenFighter(Race.DARK_ELF, Fighter, First), + PalusKnight(Race.DARK_ELF, Fighter, Second), + ShillienKnight(Race.DARK_ELF, Fighter, Third), + Bladedancer(Race.DARK_ELF, Fighter, Third), + Assassin(Race.DARK_ELF, Fighter, Second), + AbyssWalker(Race.DARK_ELF, Fighter, Third), + PhantomRanger(Race.DARK_ELF, Fighter, Third), + DarkElvenMystic(Race.DARK_ELF, Mystic, First), + DarkElvenWizard(Race.DARK_ELF, Mystic, Second), + Spellhowler(Race.DARK_ELF, Mystic, Third), + PhantomSummoner(Race.DARK_ELF, Mystic, Third), + ShillienOracle(Race.DARK_ELF, Priest, Second), + ShillienElder(Race.DARK_ELF, Priest, Third), + + OrcFighter(Race.ORC, Fighter, First), + OrcRaider(Race.ORC, Fighter, Second), + Destroyer(Race.ORC, Fighter, Third), + OrcMonk(Race.ORC, Fighter, Second), + Tyrant(Race.ORC, Fighter, Third), + OrcMystic(Race.ORC, Mystic, First), + OrcShaman(Race.ORC, Mystic, Second), + Overlord(Race.ORC, Mystic, Third), + Warcryer(Race.ORC, Mystic, Third), + + DwarvenFighter(Race.DWARF, Fighter, First), + DwarvenScavenger(Race.DWARF, Fighter, Second), + BountyHunter(Race.DWARF, Fighter, Third), + DwarvenArtisan(Race.DWARF, Fighter, Second), + Warsmith(Race.DWARF, Fighter, Third), + + dummyEntry1(null, null, null), + dummyEntry2(null, null, null), + dummyEntry3(null, null, null), + dummyEntry4(null, null, null), + dummyEntry5(null, null, null), + dummyEntry6(null, null, null), + dummyEntry7(null, null, null), + dummyEntry8(null, null, null), + dummyEntry9(null, null, null), + dummyEntry10(null, null, null), + dummyEntry11(null, null, null), + dummyEntry12(null, null, null), + dummyEntry13(null, null, null), + dummyEntry14(null, null, null), + dummyEntry15(null, null, null), + dummyEntry16(null, null, null), + dummyEntry17(null, null, null), + dummyEntry18(null, null, null), + dummyEntry19(null, null, null), + dummyEntry20(null, null, null), + dummyEntry21(null, null, null), + dummyEntry22(null, null, null), + dummyEntry23(null, null, null), + dummyEntry24(null, null, null), + dummyEntry25(null, null, null), + dummyEntry26(null, null, null), + dummyEntry27(null, null, null), + dummyEntry28(null, null, null), + dummyEntry29(null, null, null), + dummyEntry30(null, null, null), + /* + * (3rd classes) + */ + duelist(Race.HUMAN, Fighter, Fourth), + dreadnought(Race.HUMAN, Fighter, Fourth), + phoenixKnight(Race.HUMAN, Fighter, Fourth), + hellKnight(Race.HUMAN, Fighter, Fourth), + sagittarius(Race.HUMAN, Fighter, Fourth), + adventurer(Race.HUMAN, Fighter, Fourth), + archmage(Race.HUMAN, Mystic, Fourth), + soultaker(Race.HUMAN, Mystic, Fourth), + arcanaLord(Race.HUMAN, Mystic, Fourth), + cardinal(Race.HUMAN, Priest, Fourth), + hierophant(Race.HUMAN, Priest, Fourth), + + evaTemplar(Race.ELF, Fighter, Fourth), + swordMuse(Race.ELF, Fighter, Fourth), + windRider(Race.ELF, Fighter, Fourth), + moonlightSentinel(Race.ELF, Fighter, Fourth), + mysticMuse(Race.ELF, Mystic, Fourth), + elementalMaster(Race.ELF, Mystic, Fourth), + evaSaint(Race.ELF, Priest, Fourth), + + shillienTemplar(Race.DARK_ELF, Fighter, Fourth), + spectralDancer(Race.DARK_ELF, Fighter, Fourth), + ghostHunter(Race.DARK_ELF, Fighter, Fourth), + ghostSentinel(Race.DARK_ELF, Fighter, Fourth), + stormScreamer(Race.DARK_ELF, Mystic, Fourth), + spectralMaster(Race.DARK_ELF, Mystic, Fourth), + shillienSaint(Race.DARK_ELF, Priest, Fourth), + + titan(Race.ORC, Fighter, Fourth), + grandKhavatari(Race.ORC, Fighter, Fourth), + dominator(Race.ORC, Mystic, Fourth), + doomcryer(Race.ORC, Mystic, Fourth), + + fortuneSeeker(Race.DWARF, Fighter, Fourth), + maestro(Race.DWARF, Fighter, Fourth), + + dummyEntry31(null, null, null), + dummyEntry32(null, null, null), + dummyEntry33(null, null, null), + dummyEntry34(null, null, null), + + maleSoldier(Race.KAMAEL, Fighter, First), + femaleSoldier(Race.KAMAEL, Fighter, First), + trooper(Race.KAMAEL, Fighter, Second), + warder(Race.KAMAEL, Fighter, Second), + berserker(Race.KAMAEL, Fighter, Third), + maleSoulbreaker(Race.KAMAEL, Fighter, Third), + femaleSoulbreaker(Race.KAMAEL, Fighter, Third), + arbalester(Race.KAMAEL, Fighter, Third), + doombringer(Race.KAMAEL, Fighter, Fourth), + maleSoulhound(Race.KAMAEL, Fighter, Fourth), + femaleSoulhound(Race.KAMAEL, Fighter, Fourth), + trickster(Race.KAMAEL, Fighter, Fourth), + inspector(Race.KAMAEL, Fighter, Third), + judicator(Race.KAMAEL, Fighter, Fourth); + + private final Race _race; + private final ClassLevel _level; + private final ClassType _type; + + private static final Set mainSubclassSet; + private static final Set neverSubclassed = EnumSet.of(Overlord, Warsmith); + + private static final Set subclasseSet1 = EnumSet.of(DarkAvenger, Paladin, TempleKnight, ShillienKnight); + private static final Set subclasseSet2 = EnumSet.of(TreasureHunter, AbyssWalker, Plainswalker); + private static final Set subclasseSet3 = EnumSet.of(Hawkeye, SilverRanger, PhantomRanger); + private static final Set subclasseSet4 = EnumSet.of(Warlock, ElementalSummoner, PhantomSummoner); + private static final Set subclasseSet5 = EnumSet.of(Sorceror, Spellsinger, Spellhowler); + + private static final EnumMap> subclassSetMap = new EnumMap<>(PlayerClass.class); + + static { + Set subclasses = getSet(null, Third); + subclasses.removeAll(neverSubclassed); + + mainSubclassSet = subclasses; + + subclassSetMap.put(DarkAvenger, subclasseSet1); + subclassSetMap.put(Paladin, subclasseSet1); + subclassSetMap.put(TempleKnight, subclasseSet1); + subclassSetMap.put(ShillienKnight, subclasseSet1); + + subclassSetMap.put(TreasureHunter, subclasseSet2); + subclassSetMap.put(AbyssWalker, subclasseSet2); + subclassSetMap.put(Plainswalker, subclasseSet2); + + subclassSetMap.put(Hawkeye, subclasseSet3); + subclassSetMap.put(SilverRanger, subclasseSet3); + subclassSetMap.put(PhantomRanger, subclasseSet3); + + subclassSetMap.put(Warlock, subclasseSet4); + subclassSetMap.put(ElementalSummoner, subclasseSet4); + subclassSetMap.put(PhantomSummoner, subclasseSet4); + + subclassSetMap.put(Sorceror, subclasseSet5); + subclassSetMap.put(Spellsinger, subclasseSet5); + subclassSetMap.put(Spellhowler, subclasseSet5); + } + + PlayerClass(Race race, ClassType pType, ClassLevel pLevel) { + _race = race; + _level = pLevel; + _type = pType; + } + + public final Set getAvailableSubclasses(L2PcInstance player) { + Set subclasses = null; + + if (_level == Third) { + if (player.getRace() != Race.KAMAEL) { + subclasses = EnumSet.copyOf(mainSubclassSet); + + subclasses.remove(this); + + switch (player.getRace()) { + case ELF -> subclasses.removeAll(getSet(Race.DARK_ELF, Third)); + case DARK_ELF -> subclasses.removeAll(getSet(Race.ELF, Third)); + } + + subclasses.removeAll(getSet(Race.KAMAEL, Third)); + + Set unavailableClasses = subclassSetMap.get(this); + + if (unavailableClasses != null) { + subclasses.removeAll(unavailableClasses); + } + + } else { + subclasses = getSet(Race.KAMAEL, Third); + subclasses.remove(this); + // Check sex, male subclasses female and vice versa + // If server owner set MaxSubclass > 3 some kamael's cannot take 4 sub + // So, in that situation we must skip sex check + if (character().getMaxSubclass() <= 3) { + if (player.getAppearance().getSex()) { + subclasses.removeAll(EnumSet.of(femaleSoulbreaker)); + } else { + subclasses.removeAll(EnumSet.of(maleSoulbreaker)); + } + } + if (!player.getSubClasses().containsKey(2) || (player.getSubClasses().get(2).getLevel() < 75)) { + subclasses.removeAll(EnumSet.of(inspector)); + } + } + } + return subclasses; + } + + public static EnumSet getSet(Race race, ClassLevel level) { + EnumSet allOf = EnumSet.noneOf(PlayerClass.class); + + for (PlayerClass playerClass : EnumSet.allOf(PlayerClass.class)) { + if ((race == null) || playerClass.isOfRace(race)) { + if ((level == null) || playerClass.isOfLevel(level)) { + allOf.add(playerClass); + } + } + } + return allOf; + } + + public final boolean isOfRace(Race pRace) { + return _race == pRace; + } + + public final boolean isOfType(ClassType pType) { + return _type == pType; + } + + public final boolean isOfLevel(ClassLevel pLevel) { + return _level == pLevel; + } + + public final ClassLevel getLevel() { + return _level; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/PlayerState.java b/src/main/java/com/l2jserver/gameserver/model/base/PlayerState.java new file mode 100644 index 00000000..a2eb9c12 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/PlayerState.java @@ -0,0 +1,31 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +public enum PlayerState { + RESTING, + MOVING, + RUNNING, + STANDING, + FLYING, + BEHIND, + FRONT, + CHAOTIC, + OLYMPIAD +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/SocialClass.java b/src/main/java/com/l2jserver/gameserver/model/base/SocialClass.java new file mode 100644 index 00000000..e6c2acda --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/SocialClass.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +/** + * Enumerated for Social Classes. + * @author Zoey76 + */ +public enum SocialClass { + VAGABOND, + VASSAL, + APPRENTICE, + HEIR, + KNIGHT, + ELDER, + BARON, + VISCOUNT, + COUNT, + MARQUIS, + DUKE, + GRAND_DUKE, + DISTINGUISHED_KING, + EMPEROR +} diff --git a/src/main/java/com/l2jserver/gameserver/model/base/SubClass.java b/src/main/java/com/l2jserver/gameserver/model/base/SubClass.java new file mode 100644 index 00000000..b4401568 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/base/SubClass.java @@ -0,0 +1,98 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.base; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import com.l2jserver.gameserver.data.json.ExperienceData; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.stat.PcStat; + +/** + * Character Sub-Class Definition
          + * Used to store key information about a character's sub-class. + * @author Zealar + */ +public final class SubClass { + private PlayerClass _class; + private int _classIndex = 1; + private final PcStat _stats; + + public SubClass(L2PcInstance activeChar) { + _stats = new PcStat(activeChar); + _stats.setExp(ExperienceData.getInstance().getExpForLevel(character().getBaseSubclassLevel())); + _stats.setLevel(character().getBaseSubclassLevel()); + } + + public PlayerClass getClassDefinition() { + return _class; + } + + public void setClassIndex(int classIndex) { + _classIndex = classIndex; + } + + /** + * First Sub-Class is index 1. + * @return int _classIndex + */ + public int getClassIndex() { + return _classIndex; + } + + public void setClassId(int classId) { + _class = PlayerClass.values()[classId]; + } + + public int getClassId() { + return _class.ordinal(); + } + + public long getExp() { + return _stats.getExp(); + } + + public int getLevel() { + return _stats.getLevel(); + } + + public int getSp() { + return _stats.getSp(); + } + + public void setSp(int sp) { + _stats.setSp(sp); + } + + public void setExp(long exp) { + _stats.setExp(exp); + } + + public void setLevel(int level) { + _stats.setLevel(level); + } + + public void addExp(long exp) { + _stats.addExp(exp); + } + + public PcStat getStat() { + return _stats; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/buylist/L2BuyList.java b/src/main/java/com/l2jserver/gameserver/model/buylist/L2BuyList.java new file mode 100644 index 00000000..3b044e3c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/buylist/L2BuyList.java @@ -0,0 +1,68 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.buylist; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author NosBit + */ +public final class L2BuyList { + private final int _listId; + private final Map _products = new LinkedHashMap<>(); + private Set _allowedNpcs = null; + + public L2BuyList(int listId) { + _listId = listId; + } + + public int getListId() { + return _listId; + } + + public Collection getProducts() { + return _products.values(); + } + + public Product getProductByItemId(int itemId) { + return _products.get(itemId); + } + + public void addProduct(Product product) { + _products.put(product.getItemId(), product); + } + + public void addAllowedNpc(int npcId) { + if (_allowedNpcs == null) { + _allowedNpcs = new HashSet<>(); + } + _allowedNpcs.add(npcId); + } + + public boolean isNpcAllowed(int npcId) { + if (_allowedNpcs == null) { + return false; + } + return _allowedNpcs.contains(npcId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/buylist/Product.java b/src/main/java/com/l2jserver/gameserver/model/buylist/Product.java new file mode 100644 index 00000000..9d92936c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/buylist/Product.java @@ -0,0 +1,166 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.buylist; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.model.items.L2Item; + +/** + * @author NosBit + */ +public final class Product { + private static final Logger LOG = LoggerFactory.getLogger(Product.class); + + private final int _buyListId; + + private final L2Item _item; + + private final long _price; + + private final double _baseTax; + + private final long _restockDelay; + + private final long _maxCount; + + private AtomicLong _count = null; + + private ScheduledFuture _restockTask = null; + + public Product(int buyListId, L2Item item, long price, double baseTax, long restockDelay, long maxCount) { + _buyListId = buyListId; + _item = item; + _price = price; + _baseTax = baseTax; + _restockDelay = restockDelay * 60000; + _maxCount = maxCount; + if (hasLimitedStock()) { + _count = new AtomicLong(maxCount); + } + } + + public int getBuyListId() { + return _buyListId; + } + + public L2Item getItem() { + return _item; + } + + public int getItemId() { + return getItem().getId(); + } + + public long getPrice() { + return _price; + } + + public double getBaseTax() { + return _baseTax; + } + + public long getRestockDelay() { + return _restockDelay; + } + + public long getMaxCount() { + return _maxCount; + } + + public long getCount() { + if (_count == null) { + return 0; + } + long count = _count.get(); + return count > 0 ? count : 0; + } + + public void setCount(long currentCount) { + if (_count == null) { + _count = new AtomicLong(); + } + _count.set(currentCount); + } + + public boolean decreaseCount(long val) { + if (_count == null) { + return false; + } + if ((_restockTask == null) || _restockTask.isDone()) { + _restockTask = ThreadPoolManager.getInstance().scheduleGeneral(new RestockTask(), getRestockDelay()); + } + boolean result = _count.addAndGet(-val) >= 0; + save(); + return result; + } + + public boolean hasLimitedStock() { + return getMaxCount() > -1; + } + + public void restartRestockTask(long nextRestockTime) { + long remainTime = nextRestockTime - System.currentTimeMillis(); + if (remainTime > 0) { + _restockTask = ThreadPoolManager.getInstance().scheduleGeneral(new RestockTask(), remainTime); + } else { + restock(); + } + } + + public void restock() { + setCount(getMaxCount()); + save(); + } + + protected final class RestockTask implements Runnable { + @Override + public void run() { + restock(); + } + } + + private void save() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO `buylists`(`buylist_id`, `item_id`, `count`, `next_restock_time`) VALUES(?, ?, ?, ?) ON DUPLICATE KEY UPDATE `count` = ?, `next_restock_time` = ?")) { + ps.setInt(1, getBuyListId()); + ps.setInt(2, getItemId()); + ps.setLong(3, getCount()); + ps.setLong(5, getCount()); + if ((_restockTask != null) && (_restockTask.getDelay(TimeUnit.MILLISECONDS) > 0)) { + long nextRestockTime = System.currentTimeMillis() + _restockTask.getDelay(TimeUnit.MILLISECONDS); + ps.setLong(4, nextRestockTime); + ps.setLong(6, nextRestockTime); + } else { + ps.setLong(4, 0); + ps.setLong(6, 0); + } + ps.executeUpdate(); + } catch (Exception e) { + LOG.warn("Failed to save Product buylist_id:{} item_id:{}", getBuyListId(), getItemId(), e); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/cleft/CleftCombatAerialTarget.java b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftCombatAerialTarget.java new file mode 100644 index 00000000..71230eb7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftCombatAerialTarget.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.cleft; + +/** + * @author HorridoJoho + * @since 2.6.3.0 + */ +public final class CleftCombatAerialTarget { + private final int _id; + private final String _name; + + public CleftCombatAerialTarget(int id, String name) { + _id = id; + _name = name; + } + + public int getId() { + return _id; + } + + public String getName() { + return _name; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/cleft/CleftMatch.java b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftMatch.java new file mode 100644 index 00000000..df7de840 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftMatch.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.cleft; + +import com.l2jserver.gameserver.enums.Team; + +/** + * @author HorridoJoho + * @since 2.6.3.0 + */ +public final class CleftMatch { + private final int _minTeamMembers; + private final boolean _isBalanced; + private final CleftTeam _blueTeam; + private final CleftTeam _redTeam; + + public CleftMatch(int minTeamMembers, boolean isBalanced) { + _minTeamMembers = minTeamMembers; + _isBalanced = isBalanced; + _blueTeam = new CleftTeam(Team.BLUE); + _redTeam = new CleftTeam(Team.RED); + } + + public int getMinTeamMembers() { + return _minTeamMembers; + } + + public boolean isBalanced() { + return _isBalanced; + } + + public CleftTeam getBlueTeam() { + return _blueTeam; + } + + public CleftTeam getRedTeam() { + return _redTeam; + } + + public int getRemainingSeconds() { + // TODO: + return 0; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/cleft/CleftParticipant.java b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftParticipant.java new file mode 100644 index 00000000..8d631651 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftParticipant.java @@ -0,0 +1,96 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.cleft; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author HorridoJoho + * @since 2.6.3.0 + */ +public final class CleftParticipant { + private volatile L2PcInstance _player; + private volatile CleftTeam _team; + private final AtomicInteger _kills; + private final AtomicInteger _deaths; + private final AtomicInteger _towers; + + public CleftParticipant(L2PcInstance player, CleftTeam team) { + Objects.requireNonNull(player); + Objects.requireNonNull(team); + + _player = player; + _team = team; + _kills = new AtomicInteger(0); + _deaths = new AtomicInteger(0); + _towers = new AtomicInteger(0); + } + + public CleftParticipant(L2PcInstance player, CleftTeam team, int kills, int deaths, int towers) { + this(player, team); + _kills.set(kills); + _deaths.set(deaths); + _towers.set(towers); + } + + public void setPlayer(L2PcInstance player) { + Objects.requireNonNull(player); + _player = player; + } + + public void setTeam(CleftTeam team) { + Objects.requireNonNull(team); + _team = team; + } + + public int addKill() { + return _kills.incrementAndGet(); + } + + public int addDeath() { + return _deaths.incrementAndGet(); + } + + public int addTower() { + return _towers.incrementAndGet(); + } + + public L2PcInstance getPlayer() { + return _player; + } + + public CleftTeam getTeam() { + return _team; + } + + public int getKills() { + return _kills.get(); + } + + public int getDeaths() { + return _deaths.get(); + } + + public int getTowers() { + return _towers.get(); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTeam.java b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTeam.java new file mode 100644 index 00000000..f6fc5795 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTeam.java @@ -0,0 +1,88 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.cleft; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author HorridoJoho + * @since 2.6.3.0 + */ +public final class CleftTeam { + private final Team _team; + private final Map _participants; + private final AtomicInteger _points; + private CleftCombatAerialTarget _combatAerialTarget; + + public CleftTeam(Team team) { + Objects.requireNonNull(team); + if (team == Team.NONE) { + throw new IllegalArgumentException("Team.NONE not allowed!"); + } + + _team = team; + _participants = Collections.synchronizedMap(new LinkedHashMap<>()); + _points = new AtomicInteger(0); + } + + public CleftTeam(Team team, Map participants, int points) { + this(team); + _participants.putAll(participants); + _points.set(points); + } + + public void addPoints(int points) { + _points.addAndGet(points); + } + + public void addParticipant(L2PcInstance player) { + _participants.put(player.getObjectId(), new CleftParticipant(player, this)); + } + + public CleftParticipant removeParticipant(L2PcInstance player) { + return _participants.remove(player.getObjectId()); + } + + public void setCombarAerialTarget(int id, String name) { + _combatAerialTarget = new CleftCombatAerialTarget(id, name); + } + + public Team getTeam() { + return _team; + } + + public Map getParticipants() { + return Collections.unmodifiableMap(_participants); + } + + public int getPoints() { + return _points.get(); + } + + public CleftCombatAerialTarget getCombatAerialTarget() { + return _combatAerialTarget; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTowerType.java b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTowerType.java new file mode 100644 index 00000000..bfb9d073 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/cleft/CleftTowerType.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.cleft; + +/** + * @author HorridoJoho + * @since 2.6.3.0 + */ +public enum CleftTowerType { + +} diff --git a/src/main/java/com/l2jserver/gameserver/model/clientstrings/Builder.java b/src/main/java/com/l2jserver/gameserver/model/clientstrings/Builder.java new file mode 100644 index 00000000..6883a4c6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/clientstrings/Builder.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.clientstrings; + +import java.util.ArrayList; + +/** + * @author Forsaiken + * @author Zoey76 + */ +public abstract class Builder { + public abstract String toString(final Object param); + + public abstract String toString(final Object... params); + + public abstract int getIndex(); + + public static Builder newBuilder(final String text) { + final ArrayList builders = new ArrayList<>(); + + int index1 = 0, index2 = 0, paramId, subTextLen; + + final char[] array = text.toCharArray(); + final int arrayLength = array.length; + + char c, c2, c3; + for (; index1 < arrayLength; index1++) { + c = array[index1]; + if ((c == '$') && (index1 < (arrayLength - 2))) { + c2 = array[index1 + 1]; + if ((c2 == 'c') || (c2 == 's') || (c2 == 'p') || (c2 == 'C') || (c2 == 'S') || (c2 == 'P')) { + c3 = array[index1 + 2]; + if (Character.isDigit(c3)) { + paramId = Character.getNumericValue(c3); + subTextLen = index1 - index2; + if (subTextLen != 0) { + builders.add(new BuilderText(new String(array, index2, subTextLen))); + } + + builders.add(new BuilderObject(paramId)); + index1 += 2; + index2 = index1 + 1; + } + } + } + } + + if (arrayLength >= index1) { + subTextLen = index1 - index2; + if (subTextLen != 0) { + builders.add(new BuilderText(new String(array, index2, subTextLen))); + } + } + + if (builders.size() == 1) { + return builders.get(0); + } + return new BuilderContainer(builders.toArray(new Builder[builders.size()])); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderContainer.java b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderContainer.java new file mode 100644 index 00000000..617a2c10 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderContainer.java @@ -0,0 +1,72 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.clientstrings; + +/** + * @author Forsaiken + */ +final class BuilderContainer extends Builder { + private final Builder[] _builders; + + BuilderContainer(final Builder[] builders) { + _builders = builders; + } + + @Override + public String toString(final Object param) { + return toString(new Object[] { + param + }); + } + + @Override + public final String toString(final Object... params) { + final int buildersLength = _builders.length; + final int paramsLength = params.length; + final String[] builds = new String[buildersLength]; + + int buildTextLen = 0; + if (paramsLength != 0) { + for (int i = buildersLength; i-- > 0;) { + var builder = _builders[i]; + var paramIndex = builder.getIndex(); + var build = (paramIndex != -1) && (paramIndex < paramsLength) ? builder.toString(params[paramIndex]) : builder.toString(); + buildTextLen += build.length(); + builds[i] = build; + } + } else { + for (int i = buildersLength; i-- > 0;) { + var build = _builders[i].toString(); + buildTextLen += build.length(); + builds[i] = build; + } + } + + final var sb = new StringBuilder(buildTextLen); + for (int i = 0; i < buildersLength; i++) { + sb.append(builds[i]); + } + return sb.toString(); + } + + @Override + public int getIndex() { + return -1; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderObject.java b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderObject.java new file mode 100644 index 00000000..782d41b7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderObject.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.clientstrings; + +/** + * @author Forsaiken + */ +final class BuilderObject extends Builder { + private final int _index; + + BuilderObject(final int id) { + if ((id < 1) || (id > 9)) { + throw new RuntimeException("Illegal Id: " + id); + } + _index = id - 1; + } + + @Override + public String toString(final Object param) { + return param == null ? "null" : param.toString(); + } + + @Override + public final String toString(final Object... params) { + if ((params == null) || (params.length == 0)) { + return "null"; + } + return params[0].toString(); + } + + @Override + public int getIndex() { + return _index; + } + + @Override + public String toString() { + return "[PARAM-" + (_index + 1) + "]"; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderText.java b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderText.java new file mode 100644 index 00000000..f64946f1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/clientstrings/BuilderText.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.clientstrings; + +/** + * @author Forsaiken + */ +final class BuilderText extends Builder { + private final String _text; + + BuilderText(final String text) { + _text = text; + } + + @Override + public String toString(final Object param) { + return toString(); + } + + @Override + public final String toString(final Object... params) { + return toString(); + } + + @Override + public int getIndex() { + return -1; + } + + @Override + public String toString() { + return _text; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/clientstrings/Localization.java b/src/main/java/com/l2jserver/gameserver/model/clientstrings/Localization.java new file mode 100644 index 00000000..d514d090 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/clientstrings/Localization.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.clientstrings; + +/** + * Localization. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class Localization { + private final String lang; + private final Builder builder; + + public Localization(String lang, String text) { + this.lang = lang; + this.builder = Builder.newBuilder(text); + } + + public String getLanguage() { + return lang; + } + + public final String build(Object... params) { + return builder.toString(params); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/Condition.java b/src/main/java/com/l2jserver/gameserver/model/conditions/Condition.java new file mode 100644 index 00000000..9b4be10f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/Condition.java @@ -0,0 +1,133 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class Condition. + * @author mkizub + */ +public abstract class Condition implements ConditionListener { + private ConditionListener _listener; + private String _msg; + private int _msgId; + private boolean _addName = false; + private boolean _result; + + /** + * Sets the message. + * @param msg the new message + */ + public final void setMessage(String msg) { + _msg = msg; + } + + /** + * Gets the message. + * @return the message + */ + public final String getMessage() { + return _msg; + } + + /** + * Sets the message id. + * @param msgId the new message id + */ + public final void setMessageId(int msgId) { + _msgId = msgId; + } + + /** + * Gets the message id. + * @return the message id + */ + public final int getMessageId() { + return _msgId; + } + + /** + * Adds the name. + */ + public final void addName() { + _addName = true; + } + + /** + * Checks if is adds the name. + * @return true, if is adds the name + */ + public final boolean isAddName() { + return _addName; + } + + /** + * Sets the listener. + * @param listener the new listener + */ + void setListener(ConditionListener listener) { + _listener = listener; + notifyChanged(); + } + + /** + * Gets the listener. + * @return the listener + */ + final ConditionListener getListener() { + return _listener; + } + + public final boolean test(L2Character caster, L2Character target, Skill skill) { + return test(caster, target, skill, null); + } + + public final boolean test(L2Character caster, L2Character target, L2Item item) { + return test(caster, target, null, null); + } + + public final boolean test(L2Character caster, L2Character target, Skill skill, L2Item item) { + boolean res = testImpl(caster, target, skill, item); + if ((_listener != null) && (res != _result)) { + _result = res; + notifyChanged(); + } + return res; + } + + /** + * Test the condition. + * @param effector the effector + * @param effected the effected + * @param skill the skill + * @param item the item + * @return {@code true} if successful, {@code false} otherwise + */ + public abstract boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item); + + @Override + public void notifyChanged() { + if (_listener != null) { + _listener.notifyChanged(); + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCategoryType.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCategoryType.java new file mode 100644 index 00000000..79df9138 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCategoryType.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.Set; + +import com.l2jserver.gameserver.enums.CategoryType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Condition Category Type implementation. + * @author Adry_85 + */ +public class ConditionCategoryType extends Condition { + private final Set _categoryTypes; + + public ConditionCategoryType(Set categoryTypes) { + _categoryTypes = categoryTypes; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + for (CategoryType type : _categoryTypes) { + if (effector.isInCategory(type)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionChangeWeapon.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionChangeWeapon.java new file mode 100644 index 00000000..3a76efc7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionChangeWeapon.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Change Weapon condition. + * @author nBd + */ +public class ConditionChangeWeapon extends Condition { + private final boolean _required; + + public ConditionChangeWeapon(boolean required) { + _required = required; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + if (_required) { + final L2Weapon weaponItem = effector.getActiveWeaponItem(); + if (weaponItem == null) { + return false; + } + + if (weaponItem.getChangeWeaponId() == 0) { + return false; + } + return !effector.getActingPlayer().isEnchanting(); + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCheckAbnormal.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCheckAbnormal.java new file mode 100644 index 00000000..6464ad18 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionCheckAbnormal.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.AbnormalType; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Check abnormal type and level. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class ConditionCheckAbnormal extends Condition { + private final AbnormalType type; + private final int level; + private final boolean present; + + public ConditionCheckAbnormal(AbnormalType type, int level, boolean present) { + this.type = type; + this.level = level; + this.present = present; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final var info = effected.getEffectList().getBuffInfoByAbnormalType(type); + return present == ((info != null) && (level <= info.getSkill().getAbnormalLvl())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameChance.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameChance.java new file mode 100644 index 00000000..eb8ed2f3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameChance.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionGameChance. + * @author Advi + */ +public class ConditionGameChance extends Condition { + private final int _chance; + + public ConditionGameChance(int chance) { + _chance = chance; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return Rnd.get(100) < _chance; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameTime.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameTime.java new file mode 100644 index 00000000..acf8324f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionGameTime.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.GameTimeController; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionGameTime. + * @author mkizub + */ +public class ConditionGameTime extends Condition { + + public enum CheckGameTime { + NIGHT + } + + private final CheckGameTime _check; + private final boolean _required; + + public ConditionGameTime(CheckGameTime check, boolean required) { + _check = check; + _required = required; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (_check == CheckGameTime.NIGHT) { + return GameTimeController.getInstance().isNight() == _required; + } + return !_required; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionInventory.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionInventory.java new file mode 100644 index 00000000..534a6639 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionInventory.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionInventory. + * @author mkizub + */ +public abstract class ConditionInventory extends Condition { + + protected final int _slot; + + public ConditionInventory(int slot) { + _slot = slot; + } + + @Override + public abstract boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionItemId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionItemId.java new file mode 100644 index 00000000..45444c40 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionItemId.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionItemId. + * @author mkizub + */ +public final class ConditionItemId extends Condition { + private final int _itemId; + + public ConditionItemId(int itemId) { + _itemId = itemId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (item != null) && (item.getId() == _itemId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionListener.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionListener.java new file mode 100644 index 00000000..32338fce --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionListener.java @@ -0,0 +1,32 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +/** + * The listener interface for receiving condition events.
          + * The class that is interested in processing a condition event implements this interface,
          + * and the object created with that class is registered with a component using the component's
          + * addConditionListener method.
          + * When the condition event occurs, that object's appropriate method is invoked. + * @author mkizub + */ +public interface ConditionListener { + + void notifyChanged(); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicAnd.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicAnd.java new file mode 100644 index 00000000..2e525696 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicAnd.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionLogicAnd. + * @author mkizub + */ +public class ConditionLogicAnd extends Condition { + private static final Condition[] EMPTY_CONDITIONS = new Condition[0]; + public Condition[] conditions = EMPTY_CONDITIONS; + + public void add(Condition condition) { + if (condition == null) { + return; + } + if (getListener() != null) { + condition.setListener(this); + } + final int len = conditions.length; + final Condition[] tmp = new Condition[len + 1]; + System.arraycopy(conditions, 0, tmp, 0, len); + tmp[len] = condition; + conditions = tmp; + } + + @Override + void setListener(ConditionListener listener) { + if (listener != null) { + for (Condition c : conditions) { + c.setListener(this); + } + } else { + for (Condition c : conditions) { + c.setListener(null); + } + } + super.setListener(listener); + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + for (Condition c : conditions) { + if (!c.test(effector, effected, skill, item)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicNot.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicNot.java new file mode 100644 index 00000000..23b354dd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicNot.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionLogicNot. + * @author mkizub + */ +public class ConditionLogicNot extends Condition { + private final Condition _condition; + + public ConditionLogicNot(Condition condition) { + _condition = condition; + if (getListener() != null) { + _condition.setListener(this); + } + } + + @Override + void setListener(ConditionListener listener) { + if (listener != null) { + _condition.setListener(this); + } else { + _condition.setListener(null); + } + super.setListener(listener); + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return !_condition.test(effector, effected, skill, item); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicOr.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicOr.java new file mode 100644 index 00000000..5df09fa3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionLogicOr.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionLogicOr. + * @author mkizub + */ +public class ConditionLogicOr extends Condition { + private static final Condition[] EMPTY_CONDITIONS = new Condition[0]; + public Condition[] conditions = EMPTY_CONDITIONS; + + public void add(Condition condition) { + if (condition == null) { + return; + } + if (getListener() != null) { + condition.setListener(this); + } + final int len = conditions.length; + final Condition[] tmp = new Condition[len + 1]; + System.arraycopy(conditions, 0, tmp, 0, len); + tmp[len] = condition; + conditions = tmp; + } + + @Override + void setListener(ConditionListener listener) { + if (listener != null) { + for (Condition c : conditions) { + c.setListener(this); + } + } else { + for (Condition c : conditions) { + c.setListener(null); + } + } + super.setListener(listener); + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + for (Condition c : conditions) { + if (c.test(effector, effected, skill, item)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionMinDistance.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionMinDistance.java new file mode 100644 index 00000000..c8eb1f7c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionMinDistance.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionMinDistance. + * @author Didldak + */ +public class ConditionMinDistance extends Condition { + private final int _sqDistance; + + public ConditionMinDistance(int sqDistance) { + _sqDistance = sqDistance; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effected != null) && (effector.calculateDistance(effected, true, true) >= _sqDistance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpCompanion.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpCompanion.java new file mode 100644 index 00000000..c53ed2cd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpCompanion.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.model.conditions.ConditionOpCompanion.CompanionType.PET; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_CANNOT_BE_USED; +import static com.l2jserver.gameserver.network.serverpackets.SystemMessage.getSystemMessage; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Operator Companion condition. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class ConditionOpCompanion extends Condition { + private final CompanionType type; + + public ConditionOpCompanion(String type) { + this.type = CompanionType.valueOf(type); + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (type == PET) { + if (!effected.isPet()) { + effector.sendPacket(getSystemMessage(S1_CANNOT_BE_USED).addSkillName(skill)); + return false; + } + return true; + } + return false; + } + + public enum CompanionType { + PET + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpExistNpc.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpExistNpc.java new file mode 100644 index 00000000..e10ade93 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpExistNpc.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.Set; + +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Exist NPC condition. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class ConditionOpExistNpc extends Condition { + + private final Set npcIds; + private final int radius; + private final boolean present; + + public ConditionOpExistNpc(Set npcIds, int radius, boolean present) { + this.npcIds = npcIds; + this.radius = radius; + this.present = present; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean isPresent = false; + for (var obj : L2World.getInstance().getVisibleObjects(effector, radius)) { + if (!obj.isNpc()) { + continue; + } + + if (npcIds.contains(obj.getId())) { + isPresent = true; + break; + } + } + return isPresent == present; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpResurrection.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpResurrection.java new file mode 100644 index 00000000..6fbbb93f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionOpResurrection.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.model.zone.ZoneId.SIEGE; +import static com.l2jserver.gameserver.network.SystemMessageId.CANNOT_BE_RESURRECTED_DURING_SIEGE; +import static com.l2jserver.gameserver.network.SystemMessageId.REJECT_RESURRECTION; +import static com.l2jserver.gameserver.network.SystemMessageId.RES_HAS_ALREADY_BEEN_PROPOSED; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Operator Resurrection condition. + * @author Zoey76 + * @version 2.6.3.0 + */ +public class ConditionOpResurrection extends Condition { + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effected == null) || !(effected.isPlayer() || effected.isPet()) || !effected.isDead()) { + return false; + } + + if (effected.isResurrectionBlocked()) { + effector.sendPacket(REJECT_RESURRECTION); + return false; + } + + final L2PcInstance player = effected.getActingPlayer(); + if (effected.isPlayer() && player.isReviveRequested()) { + effector.sendPacket(RES_HAS_ALREADY_BEEN_PROPOSED); + return false; + } + + if (effected.isPet() && player.isRevivingPet()) { + effector.sendPacket(RES_HAS_ALREADY_BEEN_PROPOSED); + return false; + } + + if (player.isInsideZone(SIEGE) && !player.isInSiege()) { + effector.sendPacket(CANNOT_BE_RESURRECTED_DURING_SIEGE); + return false; + } + + if (player.isFestivalParticipant()) { + effector.sendMessage("You may not resurrect participants in a festival."); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveEffectId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveEffectId.java new file mode 100644 index 00000000..af62be29 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveEffectId.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionPlayerActiveEffectId. + */ +public class ConditionPlayerActiveEffectId extends Condition { + + private final int _effectId; + private final int _effectLvl; + + public ConditionPlayerActiveEffectId(int effectId) { + _effectId = effectId; + _effectLvl = -1; + } + + public ConditionPlayerActiveEffectId(int effectId, int effectLevel) { + _effectId = effectId; + _effectLvl = effectLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final BuffInfo info = effector.getEffectList().getBuffInfoBySkillId(_effectId); + return ((info != null) && ((_effectLvl == -1) || (_effectLvl <= info.getSkill().getLevel()))); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveSkillId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveSkillId.java new file mode 100644 index 00000000..e7a7ce4b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerActiveSkillId.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionPlayerActiveSkillId. + * @author DrHouse + */ +public class ConditionPlayerActiveSkillId extends Condition { + private final int _skillId; + private final int _skillLevel; + + public ConditionPlayerActiveSkillId(int skillId) { + _skillId = skillId; + _skillLevel = -1; + } + + public ConditionPlayerActiveSkillId(int skillId, int skillLevel) { + _skillId = skillId; + _skillLevel = skillLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + for (Skill sk : effector.getAllSkills()) { + if (sk != null) { + if (sk.getId() == _skillId) { + if ((_skillLevel == -1) || (_skillLevel <= sk.getLevel())) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionEnergy.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionEnergy.java new file mode 100644 index 00000000..7eb4a6a1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionEnergy.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.model.itemcontainer.Inventory.PAPERDOLL_LBRACELET; + +import com.l2jserver.gameserver.agathion.repository.AgathionRepository; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Condition agathion energy. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ConditionPlayerAgathionEnergy extends Condition { + + private final int energy; + + public ConditionPlayerAgathionEnergy(int energy) { + this.energy = energy; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (!effector.isPlayer()) { + return false; + } + + final var player = effector.getActingPlayer(); + final var agathionInfo = AgathionRepository.getInstance().getByNpcId(player.getAgathionId()); + if ((agathionInfo == null) || (agathionInfo.maxEnergy() <= 0)) { + return false; + } + + final var agathionItem = player.getInventory().getPaperdollItem(PAPERDOLL_LBRACELET); + if ((agathionItem == null) || (agathionInfo.itemId() != agathionItem.getId())) { + return false; + } + return agathionItem.getAgathionRemainingEnergy() >= energy; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionId.java new file mode 100644 index 00000000..06fc3da3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerAgathionId.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Condition Player Agathion Id. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ConditionPlayerAgathionId extends Condition { + + private final int _agathionId; + + public ConditionPlayerAgathionId(int agathionId) { + _agathionId = agathionId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return effector.isPlayer() && (effector.getActingPlayer().getAgathionId() == _agathionId); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerBaseStats.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerBaseStats.java new file mode 100644 index 00000000..8046dc20 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerBaseStats.java @@ -0,0 +1,74 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionPlayerBaseStats. + * @author mkizub + */ +public class ConditionPlayerBaseStats extends Condition { + + private final BaseStat _stat; + private final int _value; + + /** + * Instantiates a new condition player base stats. + * @param player the player + * @param stat the stat + * @param value the value + */ + public ConditionPlayerBaseStats(L2Character player, BaseStat stat, int value) { + _stat = stat; + _value = value; + } + + /** + * Test impl. + * @return true, if successful + */ + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + final L2PcInstance player = effector.getActingPlayer(); + return switch (_stat) { + case Int -> player.getINT() >= _value; + case Str -> player.getSTR() >= _value; + case Con -> player.getCON() >= _value; + case Dex -> player.getDEX() >= _value; + case Men -> player.getMEN() >= _value; + case Wit -> player.getWIT() >= _value; + }; + } +} + +enum BaseStat { + Int, + Str, + Con, + Dex, + Men, + Wit +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCallPc.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCallPc.java new file mode 100644 index 00000000..b220aa9d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCallPc.java @@ -0,0 +1,61 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.TvTEvent; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Call Pc condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCallPc extends Condition { + private final boolean _val; + + public ConditionPlayerCallPc(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canCallPlayer = true; + final L2PcInstance player = effector.getActingPlayer(); + + if (player == null) { + canCallPlayer = false; + } else if (player.isInOlympiadMode()) { + player.sendPacket(SystemMessageId.YOU_MAY_NOT_SUMMON_FROM_YOUR_CURRENT_LOCATION); + canCallPlayer = false; + } else if (player.inObserverMode()) { + canCallPlayer = false; + } else if (!TvTEvent.onEscapeUse(player.getObjectId())) { + player.sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + canCallPlayer = false; + } else if (player.isInsideZone(ZoneId.NO_SUMMON_FRIEND) || player.isInsideZone(ZoneId.JAIL) || player.isFlyingMounted()) { + player.sendPacket(SystemMessageId.YOUR_TARGET_IS_IN_AN_AREA_WHICH_BLOCKS_SUMMONING); + canCallPlayer = false; + } + return (_val == canCallPlayer); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateBase.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateBase.java new file mode 100644 index 00000000..8b5ce447 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateBase.java @@ -0,0 +1,93 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.config.Configuration.fortSiege; + +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Player Can Create Base condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanCreateBase extends Condition { + private final boolean _val; + + public ConditionPlayerCanCreateBase(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return !_val; + } + + final L2PcInstance player = effector.getActingPlayer(); + boolean canCreateBase = true; + if (player.isAlikeDead() || player.isCursedWeaponEquipped() || (player.getClan() == null)) { + canCreateBase = false; + } + + final Castle castle = CastleManager.getInstance().getCastle(player); + final Fort fort = FortManager.getInstance().getFort(player); + final SystemMessage sm; + if ((castle == null) && (fort == null)) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canCreateBase = false; + } else if (((castle != null) && !castle.getSiege().isInProgress()) || ((fort != null) && !fort.getSiege().isInProgress())) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canCreateBase = false; + } else if (((castle != null) && (castle.getSiege().getAttackerClan(player.getClan()) == null)) || ((fort != null) && (fort.getSiege().getAttackerClan(player.getClan()) == null))) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canCreateBase = false; + } else if (!player.isClanLeader()) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canCreateBase = false; + } else if (((castle != null) && (castle.getSiege().getAttackerClan(player.getClan()).getNumFlags() >= SiegeManager.getInstance().getFlagMaxCount())) || ((fort != null) && (fort.getSiege().getAttackerClan(player.getClan()).getNumFlags() >= fortSiege().getMaxFlags()))) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canCreateBase = false; + } else if (!player.isInsideZone(ZoneId.HQ)) { + player.sendPacket(SystemMessageId.NOT_SET_UP_BASE_HERE); + canCreateBase = false; + } + return (_val == canCreateBase); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateOutpost.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateOutpost.java new file mode 100644 index 00000000..00a1e8aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanCreateOutpost.java @@ -0,0 +1,83 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Can Create Outpost condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanCreateOutpost extends Condition { + private final boolean _val; + + public ConditionPlayerCanCreateOutpost(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return !_val; + } + + final L2PcInstance player = effector.getActingPlayer(); + boolean canCreateOutpost = true; + if (player.isAlikeDead() || player.isCursedWeaponEquipped() || (player.getClan() == null)) { + canCreateOutpost = false; + } + + final Castle castle = CastleManager.getInstance().getCastle(player); + final Fort fort = FortManager.getInstance().getFort(player); + if ((castle == null) && (fort == null)) { + canCreateOutpost = false; + } + + if (((fort != null) && (fort.getResidenceId() == 0)) || ((castle != null) && (castle.getResidenceId() == 0))) { + player.sendMessage("You must be on fort or castle ground to construct an outpost or flag."); + canCreateOutpost = false; + } else if (((fort != null) && !fort.getZone().isActive()) || ((castle != null) && !castle.getZone().isActive())) { + player.sendMessage("You can only construct an outpost or flag on siege field."); + canCreateOutpost = false; + } else if (!player.isClanLeader()) { + player.sendMessage("You must be a clan leader to construct an outpost or flag."); + canCreateOutpost = false; + } else if (TerritoryWarManager.getInstance().getHQForClan(player.getClan()) != null) { + player.sendPacket(SystemMessageId.NOT_ANOTHER_HEADQUARTERS); + canCreateOutpost = false; + } else if (TerritoryWarManager.getInstance().getFlagForClan(player.getClan()) != null) { + player.sendPacket(SystemMessageId.A_FLAG_IS_ALREADY_BEING_DISPLAYED_ANOTHER_FLAG_CANNOT_BE_DISPLAYED); + canCreateOutpost = false; + } else if (!player.isInsideZone(ZoneId.HQ)) { + player.sendPacket(SystemMessageId.NOT_SET_UP_BASE_HERE); + canCreateOutpost = false; + } + return (_val == canCreateOutpost); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanEscape.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanEscape.java new file mode 100644 index 00000000..120f8bc4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanEscape.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.instancemanager.GrandBossManager; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.TvTEvent; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Player Can Escape condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanEscape extends Condition { + private final boolean _val; + + public ConditionPlayerCanEscape(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canTeleport = true; + final L2PcInstance player = effector.getActingPlayer(); + if (player == null) { + canTeleport = false; + } else if (!TvTEvent.onEscapeUse(player.getObjectId())) { + canTeleport = false; + } else if (player.isInDuel()) { + canTeleport = false; + } else if (player.isAfraid()) { + canTeleport = false; + } else if (player.isCombatFlagEquipped()) { + canTeleport = false; + } else if (player.isFlying() || player.isFlyingMounted()) { + canTeleport = false; + } else if (player.isInOlympiadMode()) { + canTeleport = false; + } else if ((GrandBossManager.getInstance().getZone(player) != null) && !player.canOverrideCond(PcCondOverride.SKILL_CONDITIONS)) { + canTeleport = false; + } + return (_val == canTeleport); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanRefuelAirship.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanRefuelAirship.java new file mode 100644 index 00000000..6e8eaea7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanRefuelAirship.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2ControllableAirShipInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Player Can Refuel Airship condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanRefuelAirship extends Condition { + private final int _val; + + public ConditionPlayerCanRefuelAirship(int val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canRefuelAirship = true; + final L2PcInstance player = effector.getActingPlayer(); + if ((player == null) || (player.getAirShip() == null) || !(player.getAirShip() instanceof L2ControllableAirShipInstance) || ((player.getAirShip().getFuel() + _val) > player.getAirShip().getMaxFuel())) { + canRefuelAirship = false; + } + return canRefuelAirship; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummon.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummon.java new file mode 100644 index 00000000..ac5a7c93 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummon.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import com.l2jserver.gameserver.data.sql.impl.CharSummonTable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Can Summon condition implementation. + * @author Zoey76 + */ +public class ConditionPlayerCanSummon extends Condition { + private final boolean _val; + + public ConditionPlayerCanSummon(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final L2PcInstance player = effector.getActingPlayer(); + if (player == null) { + return false; + } + + boolean canSummon = true; + if (character().restoreServitorOnReconnect() && CharSummonTable.getInstance().getServitors().containsKey(player.getObjectId())) { + player.sendPacket(SystemMessageId.SUMMON_ONLY_ONE); + canSummon = false; + } else if (character().restorePetOnReconnect() && CharSummonTable.getInstance().getPets().containsKey(player.getObjectId())) { + player.sendPacket(SystemMessageId.SUMMON_ONLY_ONE); + canSummon = false; + } else if (player.hasSummon()) { + player.sendPacket(SystemMessageId.SUMMON_ONLY_ONE); + canSummon = false; + } else if (player.isFlyingMounted() || player.isMounted()) { + canSummon = false; + } + return (_val == canSummon); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummonSiegeGolem.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummonSiegeGolem.java new file mode 100644 index 00000000..6939e969 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSummonSiegeGolem.java @@ -0,0 +1,75 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.SevenSigns; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Can Summon Siege Golem implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanSummonSiegeGolem extends Condition { + private final boolean _val; + + public ConditionPlayerCanSummonSiegeGolem(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return !_val; + } + + final L2PcInstance player = effector.getActingPlayer(); + boolean canSummonSiegeGolem = true; + if (player.isAlikeDead() || player.isCursedWeaponEquipped() || (player.getClan() == null)) { + canSummonSiegeGolem = false; + } + + final Castle castle = CastleManager.getInstance().getCastle(player); + final Fort fort = FortManager.getInstance().getFort(player); + if ((castle == null) && (fort == null)) { + canSummonSiegeGolem = false; + } + + if (((fort != null) && (fort.getResidenceId() == 0)) || ((castle != null) && (castle.getResidenceId() == 0))) { + player.sendPacket(SystemMessageId.INCORRECT_TARGET); + canSummonSiegeGolem = false; + } else if (((castle != null) && !castle.getSiege().isInProgress()) || ((fort != null) && !fort.getSiege().isInProgress())) { + player.sendPacket(SystemMessageId.INCORRECT_TARGET); + canSummonSiegeGolem = false; + } else if ((player.getClanId() != 0) && (((castle != null) && (castle.getSiege().getAttackerClan(player.getClanId()) == null)) || ((fort != null) && (fort.getSiege().getAttackerClan(player.getClanId()) == null)))) { + player.sendPacket(SystemMessageId.INCORRECT_TARGET); + canSummonSiegeGolem = false; + } else if ((SevenSigns.getInstance().checkSummonConditions(player))) { + canSummonSiegeGolem = false; + } + return (_val == canSummonSiegeGolem); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSweep.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSweep.java new file mode 100644 index 00000000..6d8856d1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanSweep.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.config.Configuration.npc; +import static com.l2jserver.gameserver.network.SystemMessageId.SWEEPER_FAILED_TARGET_NOT_SPOILED; + +import com.l2jserver.gameserver.model.actor.L2Attackable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Checks Sweeper conditions: + *
            + *
          • Minimum checks, player not null, skill not null.
          • + *
          • Checks if the target isn't null, is dead and spoiled.
          • + *
          • Checks if the sweeper player is the target spoiler, or is in the spoiler party.
          • + *
          • Checks if the corpse is too old.
          • + *
          • Checks inventory limit and weight max load won't be exceed after sweep.
          • + *
          + * If two or more conditions aren't meet at the same time, one message per condition will be shown. + * @author Zoey76 + */ +public class ConditionPlayerCanSweep extends Condition { + private final boolean _val; + + public ConditionPlayerCanSweep(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canSweep = false; + final var sweeper = effector.getActingPlayer(); + if (sweeper != null) { + if (skill != null) { + final var targets = skill.getTargets(sweeper); + for (var object : targets) { + if (!object.isAttackable()) { + continue; + } + + final var target = (L2Attackable) object; + if (!target.isDead()) { + continue; + } + + if (target.isSpoiled()) { + canSweep = target.checkSpoilOwner(sweeper, true); + canSweep &= !target.isOldCorpse(sweeper, npc().getCorpseConsumeSkillAllowedTimeBeforeDecay(), true); + canSweep &= sweeper.getInventory().checkInventorySlotsAndWeight(target.getSpoilLootItems(), true, true); + } else { + sweeper.sendPacket(SWEEPER_FAILED_TARGET_NOT_SPOILED); + } + } + } + } + return (_val == canSweep); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeCastle.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeCastle.java new file mode 100644 index 00000000..5a8ac5dc --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeCastle.java @@ -0,0 +1,65 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * Player Can Take Castle condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanTakeCastle extends Condition { + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return false; + } + + final L2PcInstance player = effector.getActingPlayer(); + + if (player.isAlikeDead() || player.isCursedWeaponEquipped() || !player.isClanLeader()) { + return false; + } + + Castle castle = CastleManager.getInstance().getCastle(player); + SystemMessage sm; + if ((castle == null) || (castle.getResidenceId() <= 0) || !castle.getSiege().isInProgress() || (castle.getSiege().getAttackerClan(player.getClan()) == null)) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + return false; + } else if (!castle.getArtefacts().contains(effected)) { + player.sendPacket(SystemMessageId.INCORRECT_TARGET); + return false; + } else if (!Util.checkIfInRange(skill.getCastRange(), player, effected, true)) { + player.sendPacket(SystemMessageId.DIST_TOO_FAR_CASTING_STOPPED); + return false; + } + castle.getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.OPPONENT_STARTED_ENGRAVING), false); + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeFort.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeFort.java new file mode 100644 index 00000000..a196a343 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTakeFort.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.util.Util; + +/** + * Player Can Take Fort condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanTakeFort extends Condition { + private final boolean _val; + + public ConditionPlayerCanTakeFort(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return !_val; + } + + final L2PcInstance player = effector.getActingPlayer(); + boolean canTakeFort = true; + if (player.isAlikeDead() || player.isCursedWeaponEquipped() || !player.isClanLeader()) { + canTakeFort = false; + } + + final Fort fort = FortManager.getInstance().getFort(player); + final SystemMessage sm; + if ((fort == null) || (fort.getResidenceId() <= 0) || !fort.getSiege().isInProgress() || (fort.getSiege().getAttackerClan(player.getClan()) == null)) { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CANNOT_BE_USED); + sm.addSkillName(skill); + player.sendPacket(sm); + canTakeFort = false; + } else if (fort.getFlagPole() != effected) { + player.sendPacket(SystemMessageId.INCORRECT_TARGET); + canTakeFort = false; + } else if (!Util.checkIfInRange(200, player, effected, true)) { + player.sendPacket(SystemMessageId.DIST_TOO_FAR_CASTING_STOPPED); + canTakeFort = false; + } + return (_val == canTakeFort); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTransform.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTransform.java new file mode 100644 index 00000000..710ee33d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanTransform.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Can Transform condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanTransform extends Condition { + private final boolean _val; + + public ConditionPlayerCanTransform(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canTransform = true; + final L2PcInstance player = effector.getActingPlayer(); + if ((player == null) || player.isAlikeDead() || player.isCursedWeaponEquipped()) { + canTransform = false; + } else if (player.isSitting()) { + player.sendPacket(SystemMessageId.CANNOT_TRANSFORM_WHILE_SITTING); + canTransform = false; + } else if (player.isTransformed() || player.isInStance()) { + player.sendPacket(SystemMessageId.YOU_ALREADY_POLYMORPHED_AND_CANNOT_POLYMORPH_AGAIN); + canTransform = false; + } else if (player.isInWater()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_POLYMORPH_INTO_THE_DESIRED_FORM_IN_WATER); + canTransform = false; + } else if (player.isFlyingMounted() || player.isMounted()) { + player.sendPacket(SystemMessageId.YOU_CANNOT_POLYMORPH_WHILE_RIDING_A_PET); + canTransform = false; + } + return (_val == canTransform); + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanUntransform.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanUntransform.java new file mode 100644 index 00000000..ba8cb5dd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCanUntransform.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Player Can Untransform condition implementation. + * @author Adry_85 + */ +public class ConditionPlayerCanUntransform extends Condition { + private final boolean _val; + + public ConditionPlayerCanUntransform(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean canUntransform = true; + final L2PcInstance player = effector.getActingPlayer(); + if (player == null) { + canUntransform = false; + } else if (player.isAlikeDead() || player.isCursedWeaponEquipped()) { + canUntransform = false; + } else if ((player.isTransformed() || player.isInStance()) && player.isFlyingMounted() && !player.isInsideZone(ZoneId.LANDING)) { + player.sendPacket(SystemMessageId.TOO_HIGH_TO_PERFORM_THIS_ACTION); // TODO: check if message is retail like. + canUntransform = false; + } + return (_val == canUntransform); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCharges.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCharges.java new file mode 100644 index 00000000..1e74da0c --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCharges.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Player charges condition. + */ +public class ConditionPlayerCharges extends Condition { + private final int _charges; + + public ConditionPlayerCharges(int charges) { + _charges = charges; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() != null) && (effector.getActingPlayer().getCharges() >= _charges); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerClassIdRestriction.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerClassIdRestriction.java new file mode 100644 index 00000000..60d5bf93 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerClassIdRestriction.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Class Id restriction condition. + */ +public class ConditionPlayerClassIdRestriction extends Condition { + private final List _classIds; + + public ConditionPlayerClassIdRestriction(List classId) { + _classIds = classId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() != null) && (_classIds.contains(effector.getActingPlayer().getClassId().getId())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCloakStatus.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCloakStatus.java new file mode 100644 index 00000000..1af07bbe --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCloakStatus.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Cloak status condition. + */ +public class ConditionPlayerCloakStatus extends Condition { + private final boolean _val; + + public ConditionPlayerCloakStatus(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() != null) && (effector.getActingPlayer().getInventory().canEquipCloak() == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCp.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCp.java new file mode 100644 index 00000000..0e4546ae --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerCp.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * CP condition. + */ +public class ConditionPlayerCp extends Condition { + + private final int _cp; + + public ConditionPlayerCp(int cp) { + _cp = cp; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector != null) && (((effector.getCurrentCp() * 100) / effector.getMaxCp()) >= _cp); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerFlyMounted.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerFlyMounted.java new file mode 100644 index 00000000..ddfebc3f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerFlyMounted.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Fly mounted condition. + * @author kerberos + */ +public class ConditionPlayerFlyMounted extends Condition { + private final boolean _val; + + public ConditionPlayerFlyMounted(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() == null) || (effector.getActingPlayer().isFlyingMounted() == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerGrade.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerGrade.java new file mode 100644 index 00000000..35807b07 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerGrade.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Grade condition. + * @author Gigiikun + */ +public final class ConditionPlayerGrade extends Condition { + // conditional values + public static final int COND_NO_GRADE = 0x0001; + public static final int COND_D_GRADE = 0x0002; + public static final int COND_C_GRADE = 0x0004; + public static final int COND_B_GRADE = 0x0008; + public static final int COND_A_GRADE = 0x0010; + public static final int COND_S_GRADE = 0x0020; + public static final int COND_S80_GRADE = 0x0040; + public static final int COND_S84_GRADE = 0x0080; + + private final int _value; + + public ConditionPlayerGrade(int value) { + _value = value; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() != null) && (_value == (byte) effector.getActingPlayer().getExpertiseLevel()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasAgathion.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasAgathion.java new file mode 100644 index 00000000..9de121a3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasAgathion.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Condition has agathion. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ConditionPlayerHasAgathion extends Condition { + + private final boolean expectedValue; + + public ConditionPlayerHasAgathion(boolean expectedValue) { + this.expectedValue = expectedValue; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return expectedValue == (effector.isPlayer() && (effector.getActingPlayer().getAgathionId() > 0)); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasCastle.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasCastle.java new file mode 100644 index 00000000..54a5b564 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasCastle.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Has castle condition. + * @author MrPoke + */ +public final class ConditionPlayerHasCastle extends Condition { + + private final int _castle; + + public ConditionPlayerHasCastle(int castle) { + _castle = castle; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + final L2Clan clan = effector.getActingPlayer().getClan(); + if (clan == null) { + return _castle == 0; + } + + // Any castle + if (_castle == -1) { + return clan.getCastleId() > 0; + } + return clan.getCastleId() == _castle; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasClanHall.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasClanHall.java new file mode 100644 index 00000000..27c197aa --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasClanHall.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Has clan hall condition. + * @author MrPoke + */ +public final class ConditionPlayerHasClanHall extends Condition { + private final List _clanHalls; + + public ConditionPlayerHasClanHall(List clanHalls) { + _clanHalls = clanHalls; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + final L2Clan clan = effector.getActingPlayer().getClan(); + if (clan == null) { + return ((_clanHalls.size() == 1) && (_clanHalls.get(0) == 0)); + } + + // All Clan Hall + if ((_clanHalls.size() == 1) && (_clanHalls.get(0) == -1)) { + return clan.getHideoutId() > 0; + } + return _clanHalls.contains(clan.getHideoutId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasFort.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasFort.java new file mode 100644 index 00000000..2602d13e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasFort.java @@ -0,0 +1,54 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Has fort condition. + * @author MrPoke + */ +public final class ConditionPlayerHasFort extends Condition { + private final int _fort; + + public ConditionPlayerHasFort(int fort) { + _fort = fort; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + final L2Clan clan = effector.getActingPlayer().getClan(); + if (clan == null) { + return _fort == 0; + } + + // Any fortress + if (_fort == -1) { + return clan.getFortId() > 0; + } + return clan.getFortId() == _fort; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasPet.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasPet.java new file mode 100644 index 00000000..cf490567 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasPet.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PetInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Has pet condition. + */ +public class ConditionPlayerHasPet extends Condition { + private final List _controlItemIds; + + public ConditionPlayerHasPet(List itemIds) { + if ((itemIds.size() == 1) && (itemIds.get(0) == 0)) { + _controlItemIds = null; + } else { + _controlItemIds = itemIds; + } + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector.getActingPlayer() == null) || (!(effector.getActingPlayer().getSummon() instanceof L2PetInstance))) { + return false; + } + + if (_controlItemIds == null) { + return true; + } + + final L2ItemInstance controlItem = ((L2PetInstance) effector.getActingPlayer().getSummon()).getControlItem(); + return (controlItem != null) && _controlItemIds.contains(controlItem.getId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasServitor.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasServitor.java new file mode 100644 index 00000000..5688c58b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHasServitor.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.SystemMessageId; + +/** + * Has servitor condition. + * @author Zealar + */ +public class ConditionPlayerHasServitor extends Condition { + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector.getActingPlayer() == null)) { + return false; + } + + if (!effector.getActingPlayer().hasSummon()) { + effector.sendPacket(SystemMessageId.CANNOT_USE_SKILL_WITHOUT_SERVITOR); + return false; + } + + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHp.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHp.java new file mode 100644 index 00000000..3dedf89d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerHp.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * HP condition. + * @author mr + */ +public class ConditionPlayerHp extends Condition { + private final int _hp; + + public ConditionPlayerHp(int hp) { + _hp = hp; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector != null) && (((effector.getCurrentHp() * 100) / effector.getMaxHp()) <= _hp); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInsideZoneId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInsideZoneId.java new file mode 100644 index 00000000..bf7aadf5 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInsideZoneId.java @@ -0,0 +1,53 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.L2ZoneType; + +/** + * Inside zone Id condition. + * @author UnAfraid + */ +public class ConditionPlayerInsideZoneId extends Condition { + private final List _zones; + + public ConditionPlayerInsideZoneId(List zones) { + _zones = zones; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + for (L2ZoneType zone : ZoneManager.getInstance().getZones(effector)) { + if (_zones.contains(zone.getId())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInstanceId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInstanceId.java new file mode 100644 index 00000000..cc8833c3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInstanceId.java @@ -0,0 +1,56 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.ArrayList; + +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.instancezone.InstanceWorld; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Instance Id condition. + */ +public class ConditionPlayerInstanceId extends Condition { + private final ArrayList _instanceIds; + + public ConditionPlayerInstanceId(ArrayList instanceIds) { + _instanceIds = instanceIds; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + + final int instanceId = effector.getInstanceId(); + if (instanceId <= 0) { + return false; // player not in instance + } + + final InstanceWorld world = InstanceManager.getInstance().getPlayerWorld(effector.getActingPlayer()); + if ((world == null) || (world.getInstanceId() != instanceId)) { + return false; // player in the different instance + } + return _instanceIds.contains(world.getTemplateId()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInvSize.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInvSize.java new file mode 100644 index 00000000..10303a62 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerInvSize.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Inventory size condition. + * @author Kerberos + */ +public class ConditionPlayerInvSize extends Condition { + + private final int _size; + + public ConditionPlayerInvSize(int size) { + _size = size; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() != null) { + return effector.getActingPlayer().getInventory().getSize(false) <= (effector.getActingPlayer().getInventoryLimit() - _size); + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsClanLeader.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsClanLeader.java new file mode 100644 index 00000000..1157d6a6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsClanLeader.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Clan leader condition. + */ +public class ConditionPlayerIsClanLeader extends Condition { + private final boolean _val; + + public ConditionPlayerIsClanLeader(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + return (effector.getActingPlayer().isClanLeader() == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsHero.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsHero.java new file mode 100644 index 00000000..61b71b6d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerIsHero.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Hero condition. + */ +public class ConditionPlayerIsHero extends Condition { + private final boolean _val; + + public ConditionPlayerIsHero(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + return (effector.getActingPlayer().isHero() == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLandingZone.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLandingZone.java new file mode 100644 index 00000000..b51f1664 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLandingZone.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; + +/** + * Landing zone condition. + * @author kerberos + */ +public class ConditionPlayerLandingZone extends Condition { + + private final boolean _val; + + public ConditionPlayerLandingZone(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return effector.isInsideZone(ZoneId.LANDING) == _val; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevel.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevel.java new file mode 100644 index 00000000..f7f7bae1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevel.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Level condition. + * @author mkizub + */ +public class ConditionPlayerLevel extends Condition { + private final int _level; + + public ConditionPlayerLevel(int level) { + _level = level; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return effector.getLevel() >= _level; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevelRange.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevelRange.java new file mode 100644 index 00000000..7b4d4aff --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerLevelRange.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Level range condition. + * @author Zoey76 + */ +public class ConditionPlayerLevelRange extends Condition { + + private final int minimumLevel; + + private final int maximumLevel; + + public ConditionPlayerLevelRange(int minimumLevel, int maximumLevel) { + this.minimumLevel = minimumLevel; + this.maximumLevel = maximumLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final int level = effector.getLevel(); + return (level >= minimumLevel) && (level <= maximumLevel); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerMp.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerMp.java new file mode 100644 index 00000000..7fabfea1 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerMp.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * MP condition. + */ +public class ConditionPlayerMp extends Condition { + private final int _mp; + + public ConditionPlayerMp(int mp) { + _mp = mp; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return ((effector.getCurrentMp() * 100) / effector.getMaxMp()) <= _mp; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPkCount.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPkCount.java new file mode 100644 index 00000000..e19de502 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPkCount.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * PK count condition. + */ +public class ConditionPlayerPkCount extends Condition { + public final int _pk; + + public ConditionPlayerPkCount(int pk) { + _pk = pk; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + return effector.getActingPlayer().getPkKills() <= _pk; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPledgeClass.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPledgeClass.java new file mode 100644 index 00000000..9ffc937d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerPledgeClass.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Pledge class condition. + * @author MrPoke + */ +public final class ConditionPlayerPledgeClass extends Condition { + private final int _pledgeClass; + + public ConditionPlayerPledgeClass(int pledgeClass) { + _pledgeClass = pledgeClass; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector.getActingPlayer() == null) || (effector.getActingPlayer().getClan() == null)) { + return false; + } + return (_pledgeClass == -1) ? effector.getActingPlayer().isClanLeader() : (effector.getActingPlayer().getPledgeClass() >= _pledgeClass); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRace.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRace.java new file mode 100644 index 00000000..d0855ce8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRace.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.Set; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Race condition. + * @author Zoey76 + */ +public class ConditionPlayerRace extends Condition { + private final Set races; + + public ConditionPlayerRace(Race[] races) { + this.races = Set.of(races); + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return effector.isPlayer() && races.contains(effector.getActingPlayer().getRace()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRangeFromNpc.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRangeFromNpc.java new file mode 100644 index 00000000..7e95260a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerRangeFromNpc.java @@ -0,0 +1,58 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.util.Util; + +/** + * Exist NPC condition. + * @author UnAfraid + * @author Zoey76 + */ +public class ConditionPlayerRangeFromNpc extends Condition { + + private final int[] _npcIds; + + private final int _radius; + + private final boolean _val; + + public ConditionPlayerRangeFromNpc(int[] npcIds, int radius, boolean val) { + _npcIds = npcIds; + _radius = radius; + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + boolean existNpc = false; + if ((_npcIds != null) && (_npcIds.length > 0) && (_radius > 0)) { + for (L2Character target : effector.getKnownList().getKnownCharactersInRadius(_radius)) { + if (target.isNpc() && Util.contains(_npcIds, target.getId())) { + existNpc = true; + break; + } + } + } + return existNpc == _val; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSex.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSex.java new file mode 100644 index 00000000..500ca3bd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSex.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Sex condition. + */ +public class ConditionPlayerSex extends Condition { + // male 0 female 1 + private final int _sex; + + public ConditionPlayerSex(int sex) { + _sex = sex; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + return (effector.getActingPlayer().getAppearance().getSex() ? 1 : 0) == _sex; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSiegeSide.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSiegeSide.java new file mode 100644 index 00000000..6cb99e3e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSiegeSide.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Siege side condition. + */ +public class ConditionPlayerSiegeSide extends Condition { + private final int _siegeSide; + + public ConditionPlayerSiegeSide(int side) { + _siegeSide = side; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return false; + } + return effector.getActingPlayer().getSiegeSide() == _siegeSide; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSouls.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSouls.java new file mode 100644 index 00000000..447a1454 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSouls.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Souls condition. + */ +public class ConditionPlayerSouls extends Condition { + private final int _souls; + + public ConditionPlayerSouls(int souls) { + _souls = souls; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effector.getActingPlayer() != null) && (effector.getActingPlayer().getChargedSouls() >= _souls); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerState.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerState.java new file mode 100644 index 00000000..e375e221 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerState.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.base.PlayerState; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * State condition. + * @author mkizub + */ +public class ConditionPlayerState extends Condition { + private final PlayerState _check; + private final boolean _required; + + public ConditionPlayerState(PlayerState check, boolean required) { + _check = check; + _required = required; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final L2PcInstance player = effector.getActingPlayer(); + switch (_check) { + case RESTING: + if (player != null) { + return (player.isSitting() == _required); + } + return !_required; + case MOVING: + return effector.isMoving() == _required; + case RUNNING: + return effector.isRunning() == _required; + case STANDING: + if (player != null) { + return (_required != (player.isSitting() || player.isMoving())); + } + return (_required != effector.isMoving()); + case FLYING: + return (effector.isFlying() == _required); + case BEHIND: + return (effector.isBehindTarget() == _required); + case FRONT: + return (effector.isInFrontOfTarget() == _required); + case CHAOTIC: + if (player != null) { + return ((player.getKarma() > 0) == _required); + } + return !_required; + case OLYMPIAD: + if (player != null) { + return (player.isInOlympiadMode() == _required); + } + return !_required; + } + return !_required; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSubclass.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSubclass.java new file mode 100644 index 00000000..fd6b0860 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerSubclass.java @@ -0,0 +1,42 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Subclass condition. + */ +public class ConditionPlayerSubclass extends Condition { + private final boolean _val; + + public ConditionPlayerSubclass(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return true; + } + return effector.getActingPlayer().isSubClassActive() == _val; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTransformationId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTransformationId.java new file mode 100644 index 00000000..957d1898 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTransformationId.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * This condition becomes true whether the player is transformed and the transformation Id match the parameter or
          + * the parameter is -1 which returns true if player is transformed regardless the transformation Id. + * @author Zoey76 + */ +public class ConditionPlayerTransformationId extends Condition { + private final int _id; + + public ConditionPlayerTransformationId(int id) { + _id = id; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final L2PcInstance player = effector.getActingPlayer(); + if (player == null) { + return false; + } + if (_id == -1) { + return player.isTransformed(); + } + return player.getTransformationId() == _id; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTvTEvent.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTvTEvent.java new file mode 100644 index 00000000..5e983073 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerTvTEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.TvTEvent; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * TvT Event condition. + */ +public class ConditionPlayerTvTEvent extends Condition { + private final boolean _val; + + public ConditionPlayerTvTEvent(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final L2PcInstance player = effector.getActingPlayer(); + if ((player == null) || !TvTEvent.isStarted()) { + return !_val; + } + return (TvTEvent.isPlayerParticipant(player.getObjectId()) == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerVehicleMounted.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerVehicleMounted.java new file mode 100644 index 00000000..1f797f8a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerVehicleMounted.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Vehicle mounted condition. + * @author Nyaran + */ +public class ConditionPlayerVehicleMounted extends Condition { + private final boolean _val; + + public ConditionPlayerVehicleMounted(boolean val) { + _val = val; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector.getActingPlayer() == null) { + return true; + } + return (effector.getActingPlayer().isInVehicle() == _val); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerWeight.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerWeight.java new file mode 100644 index 00000000..be236829 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionPlayerWeight.java @@ -0,0 +1,47 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Weight condition. + * @author Kerberos + */ +public class ConditionPlayerWeight extends Condition { + + private final int _weight; + + public ConditionPlayerWeight(int weight) { + _weight = weight; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final L2PcInstance player = effector.getActingPlayer(); + if ((player != null) && (player.getMaxLoad() > 0)) { + int weightproc = (((player.getCurrentLoad() - player.getBonusWeightPenalty()) * 100) / player.getMaxLoad()); + return (weightproc < _weight) || player.getDietMode(); + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSiegeZone.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSiegeZone.java new file mode 100644 index 00000000..b1908b2b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSiegeZone.java @@ -0,0 +1,124 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.entity.Castle; +import com.l2jserver.gameserver.model.entity.Fort; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Siege zone condition. + * @author Gigiikun + */ +public final class ConditionSiegeZone extends Condition { + // conditional values + public static final int COND_NOT_ZONE = 0x0001; + public static final int COND_CAST_ATTACK = 0x0002; + public static final int COND_CAST_DEFEND = 0x0004; + public static final int COND_CAST_NEUTRAL = 0x0008; + public static final int COND_FORT_ATTACK = 0x0010; + public static final int COND_FORT_DEFEND = 0x0020; + public static final int COND_FORT_NEUTRAL = 0x0040; + public static final int COND_TW_CHANNEL = 0x0080; + public static final int COND_TW_PROGRESS = 0x0100; + + private final int _value; + private final boolean _self; + + public ConditionSiegeZone(int value, boolean self) { + _value = value; + _self = self; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + L2Character target = _self ? effector : effected; + Castle castle = CastleManager.getInstance().getCastle(target); + Fort fort = FortManager.getInstance().getFort(target); + + if (((_value & COND_TW_PROGRESS) != 0) && !TerritoryWarManager.getInstance().isTWInProgress()) { + return false; + } else if (((_value & COND_TW_CHANNEL) != 0) && !TerritoryWarManager.getInstance().isTWChannelOpen()) { + return false; + } else if ((castle == null) && (fort == null)) { + return (_value & COND_NOT_ZONE) != 0; + } + if (castle != null) { + return checkIfOk(target, castle, _value); + } + return checkIfOk(target, fort, _value); + } + + /** + * Check if ok. + * @param activeChar the active char + * @param castle the castle + * @param value the value + * @return true, if successful + */ + public static boolean checkIfOk(L2Character activeChar, Castle castle, int value) { + if (!(activeChar instanceof L2PcInstance player)) { + return false; + } + + if (((castle == null) || (castle.getResidenceId() <= 0))) { + return (value & COND_NOT_ZONE) != 0; + } else if (!castle.getZone().isActive()) { + return (value & COND_NOT_ZONE) != 0; + } else if (((value & COND_CAST_ATTACK) != 0) && player.isRegisteredOnThisSiegeField(castle.getResidenceId()) && (player.getSiegeState() == 1)) { + return true; + } else if (((value & COND_CAST_DEFEND) != 0) && player.isRegisteredOnThisSiegeField(castle.getResidenceId()) && (player.getSiegeState() == 2)) { + return true; + } else { + return ((value & COND_CAST_NEUTRAL) != 0) && (player.getSiegeState() == 0); + } + } + + /** + * Check if ok. + * @param activeChar the active char + * @param fort the fort + * @param value the value + * @return true, if successful + */ + public static boolean checkIfOk(L2Character activeChar, Fort fort, int value) { + if (!(activeChar instanceof L2PcInstance player)) { + return false; + } + + if (((fort == null) || (fort.getResidenceId() <= 0))) { + return (value & COND_NOT_ZONE) != 0; + } else if (!fort.getZone().isActive()) { + return (value & COND_NOT_ZONE) != 0; + } else if (((value & COND_FORT_ATTACK) != 0) && player.isRegisteredOnThisSiegeField(fort.getResidenceId()) && (player.getSiegeState() == 1)) { + return true; + } else if (((value & COND_FORT_DEFEND) != 0) && player.isRegisteredOnThisSiegeField(fort.getResidenceId()) && (player.getSiegeState() == 2)) { + return true; + } else { + return ((value & COND_FORT_NEUTRAL) != 0) && (player.getSiegeState() == 0); + } + } + +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemId.java new file mode 100644 index 00000000..73a28ea4 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemId.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Slot item Id condition. + * @author mkizub + */ +public final class ConditionSlotItemId extends ConditionInventory { + private final int _itemId; + private final int _enchantLevel; + + public ConditionSlotItemId(int slot, int itemId, int enchantLevel) { + super(slot); + _itemId = itemId; + _enchantLevel = enchantLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return false; + } + + final L2ItemInstance itemSlot = effector.getInventory().getPaperdollItem(_slot); + if (itemSlot == null) { + return _itemId == 0; + } + return (itemSlot.getId() == _itemId) && (itemSlot.getEnchantLevel() >= _enchantLevel); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemType.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemType.java new file mode 100644 index 00000000..93784edd --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionSlotItemType.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Slot item type condition. + * @author mkizub + */ +public final class ConditionSlotItemType extends ConditionInventory { + private final int _mask; + + public ConditionSlotItemType(int slot, int mask) { + super(slot); + _mask = mask; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return false; + } + + final L2ItemInstance itemSlot = effector.getInventory().getPaperdollItem(_slot); + if (itemSlot == null) { + return false; + } + return (itemSlot.getItem().getItemMask() & _mask) != 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAbnormal.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAbnormal.java new file mode 100644 index 00000000..b3c8b996 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAbnormal.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target abnormal condition. + * @author janiii + */ +public class ConditionTargetAbnormal extends Condition { + private final int _abnormalId; + + public ConditionTargetAbnormal(int abnormalId) { + _abnormalId = abnormalId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effected.getAbnormalVisualEffects() & _abnormalId) != 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveEffectId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveEffectId.java new file mode 100644 index 00000000..d4a7118f --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveEffectId.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target active effect Id condition. + */ +public class ConditionTargetActiveEffectId extends Condition { + private final int _effectId; + private final int _effectLvl; + + public ConditionTargetActiveEffectId(int effectId) { + _effectId = effectId; + _effectLvl = -1; + } + + public ConditionTargetActiveEffectId(int effectId, int effectLevel) { + _effectId = effectId; + _effectLvl = effectLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final BuffInfo info = effected.getEffectList().getBuffInfoBySkillId(_effectId); + return (info != null) && ((_effectLvl == -1) || (_effectLvl <= info.getSkill().getLevel())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveSkillId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveSkillId.java new file mode 100644 index 00000000..9f6c8fe3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetActiveSkillId.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target active skill Id condition. + */ +public class ConditionTargetActiveSkillId extends Condition { + private final int _skillId; + private final int _skillLevel; + + public ConditionTargetActiveSkillId(int skillId) { + _skillId = skillId; + _skillLevel = -1; + } + + public ConditionTargetActiveSkillId(int skillId, int skillLevel) { + _skillId = skillId; + _skillLevel = skillLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + for (Skill sk : effected.getAllSkills()) { + if (sk != null) { + if (sk.getId() == _skillId) { + if ((_skillLevel == -1) || (_skillLevel <= sk.getLevel())) { + return true; + } + } + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAggro.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAggro.java new file mode 100644 index 00000000..71ce92d3 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetAggro.java @@ -0,0 +1,49 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2MonsterInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target aggro condition. + * @author mkizub + */ +public class ConditionTargetAggro extends Condition { + + private final boolean _isAggro; + + public ConditionTargetAggro(boolean isAggro) { + _isAggro = isAggro; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effected instanceof L2MonsterInstance) { + return ((L2MonsterInstance) effected).isAggressive() == _isAggro; + } + if (effected instanceof L2PcInstance) { + return ((L2PcInstance) effected).getKarma() > 0; + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetClassIdRestriction.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetClassIdRestriction.java new file mode 100644 index 00000000..2c5a3718 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetClassIdRestriction.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target class Id restriction condition. + */ +public class ConditionTargetClassIdRestriction extends Condition { + private final List _classIds; + + public ConditionTargetClassIdRestriction(List classId) { + _classIds = classId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (!(effected instanceof L2PcInstance)) { + return false; + } + return (_classIds.contains((effected.getActingPlayer()).getClassId().getId())); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetInvSize.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetInvSize.java new file mode 100644 index 00000000..5f3f2378 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetInvSize.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target inventory size condition. + * @author Zoey76 + */ +public class ConditionTargetInvSize extends Condition { + private final int _size; + + public ConditionTargetInvSize(int size) { + _size = size; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effected != null) && effected.isPlayer()) { + final L2PcInstance target = effected.getActingPlayer(); + return target.getInventory().getSize(false) <= (target.getInventoryLimit() - _size); + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevel.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevel.java new file mode 100644 index 00000000..d906be90 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevel.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target level condition. + * @author mkizub + */ +public class ConditionTargetLevel extends Condition { + + private final int _level; + + public ConditionTargetLevel(int level) { + _level = level; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effected == null) { + return false; + } + return effected.getLevel() >= _level; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevelRange.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevelRange.java new file mode 100644 index 00000000..730fb45a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetLevelRange.java @@ -0,0 +1,48 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target level range condition. + * @author UnAfraid + * @author Zoey76 + */ +public class ConditionTargetLevelRange extends Condition { + private final int minimumLevel; + + private final int maximumLevel; + + public ConditionTargetLevelRange(int minimumLevel, int maximumLevel) { + this.minimumLevel = minimumLevel; + this.maximumLevel = maximumLevel; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effected == null) { + return false; + } + final int level = effected.getLevel(); + return (level >= minimumLevel) && (level <= maximumLevel); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetMyParty.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetMyParty.java new file mode 100644 index 00000000..b93a889b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetMyParty.java @@ -0,0 +1,70 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import static com.l2jserver.gameserver.network.SystemMessageId.CANNOT_USE_ON_YOURSELF; +import static com.l2jserver.gameserver.network.SystemMessageId.S1_CANNOT_BE_USED; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * Condition Target My Party. + * @author Zoey76 + * @version 2.6.2.0 + */ +public class ConditionTargetMyParty extends Condition { + + private static final String EXCEPT_ME = "EXCEPT_ME"; + + private final String type; + + public ConditionTargetMyParty(String type) { + this.type = type; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + final var player = effector.getActingPlayer(); + if (player == null) { + return false; + } + + if (EXCEPT_ME.equals(type) && (player == effected)) { + effector.sendPacket(CANNOT_USE_ON_YOURSELF); + return false; + } + + if (player.isInParty()) { + if (!player.isInPartyWith(effected)) { + effector.sendPacket(SystemMessage.getSystemMessage(S1_CANNOT_BE_USED).addSkillName(skill)); + return false; + } + } else { + final var effectedPlayer = effected.getActingPlayer(); + if (player != effectedPlayer) { + effector.sendPacket(SystemMessage.getSystemMessage(S1_CANNOT_BE_USED).addSkillName(skill)); + return false; + } + } + return true; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNone.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNone.java new file mode 100644 index 00000000..2486b56e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNone.java @@ -0,0 +1,39 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * None condition. + * @author mkizub + */ +public class ConditionTargetNone extends Condition { + + public ConditionTargetNone() { + // Do nothing. + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effected == null); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcId.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcId.java new file mode 100644 index 00000000..8f23ff62 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcId.java @@ -0,0 +1,44 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target NPC Id condition. + */ +public class ConditionTargetNpcId extends Condition { + private final List _npcIds; + + public ConditionTargetNpcId(List npcIds) { + _npcIds = npcIds; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effected != null) && (effected.isNpc() || effected.isDoor())) { + return _npcIds.contains(effected.getId()); + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcType.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcType.java new file mode 100644 index 00000000..7b836b0d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetNpcType.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.enums.InstanceType; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target NPC type condition. + */ +public class ConditionTargetNpcType extends Condition { + private final InstanceType[] _npcType; + + public ConditionTargetNpcType(InstanceType[] type) { + _npcType = type; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effected == null) { + return false; + } + return effected.getInstanceType().isTypes(_npcType); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetPlayable.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetPlayable.java new file mode 100644 index 00000000..7e98aa33 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetPlayable.java @@ -0,0 +1,35 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.L2Playable; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target playable condition. + * @author DS + */ +public class ConditionTargetPlayable extends Condition { + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (effected instanceof L2Playable); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetRace.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetRace.java new file mode 100644 index 00000000..97d427b2 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetRace.java @@ -0,0 +1,41 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.enums.Race; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target race condition. + * @author Zealar + */ +public class ConditionTargetRace extends Condition { + private final Race _race; + + public ConditionTargetRace(Race race) { + _race = race; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return _race == effected.getRace(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetUsesWeaponKind.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetUsesWeaponKind.java new file mode 100644 index 00000000..c06cc239 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetUsesWeaponKind.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.L2Weapon; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Target uses weapon kind condition. + * @author mkizub + */ +public class ConditionTargetUsesWeaponKind extends Condition { + private final int _weaponMask; + + public ConditionTargetUsesWeaponKind(int weaponMask) { + _weaponMask = weaponMask; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effected == null) { + return false; + } + + L2Weapon weapon = effected.getActiveWeaponItem(); + if (weapon == null) { + return false; + } + + return (weapon.getItemType().mask() & _weaponMask) != 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetWeight.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetWeight.java new file mode 100644 index 00000000..2aae1c03 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionTargetWeight.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * The Class ConditionTargetWeight. + * @author Zoey76 + */ +public class ConditionTargetWeight extends Condition { + private final int _weight; + + /** + * Instantiates a new condition player weight. + * @param weight the weight + */ + public ConditionTargetWeight(int weight) { + _weight = weight; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effected != null) && effected.isPlayer()) { + final L2PcInstance target = effected.getActingPlayer(); + if (!target.getDietMode() && (target.getMaxLoad() > 0)) { + int weightproc = (((target.getCurrentLoad() - target.getBonusWeightPenalty()) * 100) / target.getMaxLoad()); + return (weightproc < _weight); + } + } + return false; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingItemType.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingItemType.java new file mode 100644 index 00000000..856df8a9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingItemType.java @@ -0,0 +1,84 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.items.type.ArmorType; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Using item type condition. + * @author mkizub + */ +public final class ConditionUsingItemType extends Condition { + private final boolean _armor; + private final int _mask; + + public ConditionUsingItemType(int mask) { + _mask = mask; + _armor = (_mask & (ArmorType.MAGIC.mask() | ArmorType.LIGHT.mask() | ArmorType.HEAVY.mask())) != 0; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (effector == null) { + return false; + } + + if (!effector.isPlayer()) { + return !_armor && ((_mask & effector.getAttackType().mask()) != 0); + } + + final Inventory inv = effector.getInventory(); + // If ConditionUsingItemType is one between Light, Heavy or Magic + if (_armor) { + // Get the itemMask of the worn chest (if exists) + L2ItemInstance chest = inv.getPaperdollItem(Inventory.PAPERDOLL_CHEST); + if (chest == null) { + return false; + } + int chestMask = chest.getItem().getItemMask(); + + // If chest armor is different from the condition one return false + if ((_mask & chestMask) == 0) { + return false; + } + + // So from here, chest armor matches conditions + + int chestBodyPart = chest.getItem().getBodyPart(); + // return True if chest armor is a Full Armor + if (chestBodyPart == L2Item.SLOT_FULL_ARMOR) { + return true; + } + // check legs armor + L2ItemInstance legs = inv.getPaperdollItem(Inventory.PAPERDOLL_LEGS); + if (legs == null) { + return false; + } + int legMask = legs.getItem().getItemMask(); + // return true if legs armor matches too + return (_mask & legMask) != 0; + } + return (_mask & inv.getWearedMask()) != 0; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSkill.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSkill.java new file mode 100644 index 00000000..d723d749 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSkill.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Using skill condition. + * @author mkizub + */ +public final class ConditionUsingSkill extends Condition { + private final int _skillId; + + public ConditionUsingSkill(int skillId) { + _skillId = skillId; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if (skill == null) { + return false; + } + return skill.getId() == _skillId; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSlotType.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSlotType.java new file mode 100644 index 00000000..a9a58c13 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionUsingSlotType.java @@ -0,0 +1,45 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * Using slot type condition. + * @author NosBit + */ +public class ConditionUsingSlotType extends Condition { + private final int _mask; + + public ConditionUsingSlotType(int mask) { + _mask = mask; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + if ((effector == null) || !effector.isPlayer()) { + return false; + } + + return (effector.getActiveWeaponItem().getBodyPart() & _mask) != 0; + } + +} diff --git a/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionWithSkill.java b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionWithSkill.java new file mode 100644 index 00000000..88d0ad11 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/conditions/ConditionWithSkill.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.conditions; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.items.L2Item; +import com.l2jserver.gameserver.model.skills.Skill; + +/** + * With skill condition. + * @author Steuf + */ +public class ConditionWithSkill extends Condition { + private final boolean _skill; + + public ConditionWithSkill(boolean skill) { + _skill = skill; + } + + @Override + public boolean testImpl(L2Character effector, L2Character effected, Skill skill, L2Item item) { + return (skill != null) == _skill; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/DropListScope.java b/src/main/java/com/l2jserver/gameserver/model/drops/DropListScope.java new file mode 100644 index 00000000..a941e513 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/DropListScope.java @@ -0,0 +1,59 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +import com.l2jserver.gameserver.model.drops.strategy.IAmountMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IChanceMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IGroupedItemDropCalculationStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IKillerChanceModifierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IPreciseDeterminationStrategy; + +/** + * @author NosBit + */ +public enum DropListScope implements IDropItemFactory, IGroupedDropItemFactory { + DEATH((itemId, min, max, chance) -> new GeneralDropItem(itemId, min, max, chance, IAmountMultiplierStrategy.DROP, IChanceMultiplierStrategy.DROP), GroupedGeneralDropItem::new), + CORPSE((itemId, min, max, chance) -> new GeneralDropItem(itemId, min, max, chance, IAmountMultiplierStrategy.SPOIL, IChanceMultiplierStrategy.SPOIL), DEATH), + + /** + * This droplist scope isn't affected by ANY rates, nor Champion, etc... + */ + STATIC( + (itemId, min, max, chance) -> new GeneralDropItem(itemId, min, max, chance, IAmountMultiplierStrategy.STATIC, IChanceMultiplierStrategy.STATIC, IPreciseDeterminationStrategy.ALWAYS, IKillerChanceModifierStrategy.NO_RULES), + chance -> new GroupedGeneralDropItem(chance, IGroupedItemDropCalculationStrategy.DEFAULT_STRATEGY, IKillerChanceModifierStrategy.NO_RULES, IPreciseDeterminationStrategy.ALWAYS)), + QUEST((itemId, min, max, chance) -> new GeneralDropItem(itemId, min, max, chance, IAmountMultiplierStrategy.QUEST, IChanceMultiplierStrategy.QUEST, IPreciseDeterminationStrategy.ALWAYS, IKillerChanceModifierStrategy.NO_RULES), STATIC); + + private final IDropItemFactory _factory; + private final IGroupedDropItemFactory _groupFactory; + + DropListScope(IDropItemFactory factory, IGroupedDropItemFactory groupFactory) { + _factory = factory; + _groupFactory = groupFactory; + } + + @Override + public IDropItem newDropItem(int itemId, long min, long max, double chance) { + return _factory.newDropItem(itemId, min, max, chance); + } + + @Override + public GroupedGeneralDropItem newGroupedDropItem(double chance) { + return _groupFactory.newGroupedDropItem(chance); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/GeneralDropItem.java b/src/main/java/com/l2jserver/gameserver/model/drops/GeneralDropItem.java new file mode 100644 index 00000000..b7bcd596 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/GeneralDropItem.java @@ -0,0 +1,227 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.strategy.IAmountMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IChanceMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IDropCalculationStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IKillerChanceModifierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.INonGroupedKillerChanceModifierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IPreciseDeterminationStrategy; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author NosBit + */ +public final class GeneralDropItem implements IDropItem { + private final int _itemId; + private final long _min; + private final long _max; + private final double _chance; + + protected final IAmountMultiplierStrategy _amountStrategy; + protected final IChanceMultiplierStrategy _chanceStrategy; + protected final IPreciseDeterminationStrategy _preciseStrategy; + protected final INonGroupedKillerChanceModifierStrategy _killerStrategy; + protected final IDropCalculationStrategy _dropCalculationStrategy; + + /** + * @param itemId the item id + * @param min the min count + * @param max the max count + * @param chance the chance of this drop item + */ + public GeneralDropItem(int itemId, long min, long max, double chance) { + this(itemId, min, max, chance, 1, 1); + } + + public GeneralDropItem(int itemId, long min, long max, double chance, double defaultAmountMultiplier, double defaultChanceMultiplier) { + this(itemId, min, max, defaultChanceMultiplier, IAmountMultiplierStrategy.DEFAULT_STRATEGY(defaultAmountMultiplier), IChanceMultiplierStrategy.DEFAULT_STRATEGY(defaultChanceMultiplier)); + } + + public GeneralDropItem(int itemId, long min, long max, double chance, IAmountMultiplierStrategy amountMultiplierStrategy, IChanceMultiplierStrategy chanceMultiplierStrategy) { + this(itemId, min, max, chance, amountMultiplierStrategy, chanceMultiplierStrategy, IPreciseDeterminationStrategy.DEFAULT, IKillerChanceModifierStrategy.DEFAULT_NONGROUP_STRATEGY); + } + + public GeneralDropItem(int itemId, long min, long max, double chance, IAmountMultiplierStrategy amountMultiplierStrategy, IChanceMultiplierStrategy chanceMultiplierStrategy, IPreciseDeterminationStrategy preciseStrategy, INonGroupedKillerChanceModifierStrategy killerStrategy) { + this(itemId, min, max, chance, amountMultiplierStrategy, chanceMultiplierStrategy, preciseStrategy, killerStrategy, IDropCalculationStrategy.DEFAULT_STRATEGY); + } + + public GeneralDropItem(int itemId, long min, long max, double chance, IAmountMultiplierStrategy amountMultiplierStrategy, IChanceMultiplierStrategy chanceMultiplierStrategy, IPreciseDeterminationStrategy preciseStrategy, INonGroupedKillerChanceModifierStrategy killerStrategy, IDropCalculationStrategy dropCalculationStrategy) { + _itemId = itemId; + _min = min; + _max = max; + _chance = chance; + _amountStrategy = amountMultiplierStrategy; + _chanceStrategy = chanceMultiplierStrategy; + _preciseStrategy = preciseStrategy; + _killerStrategy = killerStrategy; + _dropCalculationStrategy = dropCalculationStrategy; + + } + + /** + * @return the _amountStrategy + */ + public IAmountMultiplierStrategy getAmountStrategy() { + return _amountStrategy; + } + + /** + * @return the _chanceStrategy + */ + public IChanceMultiplierStrategy getChanceStrategy() { + return _chanceStrategy; + } + + /** + * @return the _preciseStrategy + */ + public IPreciseDeterminationStrategy getPreciseStrategy() { + return _preciseStrategy; + } + + /** + * @return the _killerStrategy + */ + public INonGroupedKillerChanceModifierStrategy getKillerChanceModifierStrategy() { + return _killerStrategy; + } + + /** + * @return the _dropCalculationStrategy + */ + public IDropCalculationStrategy getDropCalculationStrategy() { + return _dropCalculationStrategy; + } + + /** + * Gets the item id + * @return the item id + */ + public int getItemId() { + return _itemId; + } + + /** + * Gets the base min drop count + * @return the min + */ + public long getMin() { + return _min; + } + + /** + * Gets the min drop count modified by server rates + * @param victim the victim who drops the item + * @return the min modified by any rates. + */ + public long getMin(L2Character victim) { + return (long) (getMin() * getAmountMultiplier(victim)); + } + + /** + * Gets the base max drop count + * @return the max + */ + public long getMax() { + return _max; + } + + /** + * Gets the max drop count modified by server rates + * @param victim the victim who drops the item + * @return the max modified by any rates. + */ + public long getMax(L2Character victim) { + return (long) (getMax() * getAmountMultiplier(victim)); + } + + /** + * Gets the chance of this drop item. + * @return the chance + */ + public double getChance() { + return _chance; + } + + /** + * Gets the general chance to drop this item modified by rates.
          + * This shall be used in calculating chance within drop groups. + * @param victim the victim who drops the item + * @return the chance modified by any rates. + */ + public double getChance(L2Character victim) { + return getChance() * getChanceMultiplier(victim); + } + + /** + * Gets the chance of dropping this item for current killer and victim (modified by server rates and another rules based on killer)
          + * This shall be used to calculate chance outside of drop groups. + * @param victim the victim who drops the item + * @param killer who kills the victim + * @return a chance to drop modified by deep blue drop rules + */ + public double getChance(L2Character victim, L2Character killer) { + return (getKillerChanceModifier(victim, killer) * getChance(victim)); + } + + @Override + public List calculateDrops(L2Character victim, L2Character killer) { + return _dropCalculationStrategy.calculateDrops(this, victim, killer); + } + + /** + * @return true if chance over 100% should be handled + */ + public boolean isPreciseCalculated() { + return _preciseStrategy.isPreciseCalculated(this); + } + + /** + * This handles by default deep blue drop rules. It may also be used to handle another drop chance rules based on killer + * @param victim the victim who drops the item + * @param killer who kills the victim + * @return a number between 0 and 1 (usually) + */ + protected double getKillerChanceModifier(L2Character victim, L2Character killer) { + return _killerStrategy.getKillerChanceModifier(this, victim, killer); + } + + /** + * This gets standard server rates for this item + * @param victim who drops the item + * @return + */ + protected double getAmountMultiplier(L2Character victim) { + return _amountStrategy.getAmountMultiplier(this, victim); + } + + /** + * This gets standard server rates for this item + * @param victim who drops the item + * @return + */ + protected double getChanceMultiplier(L2Character victim) { + return _chanceStrategy.getChanceMultiplier(this, victim); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/GroupedGeneralDropItem.java b/src/main/java/com/l2jserver/gameserver/model/drops/GroupedGeneralDropItem.java new file mode 100644 index 00000000..9c12f712 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/GroupedGeneralDropItem.java @@ -0,0 +1,235 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +import static com.l2jserver.gameserver.config.Configuration.general; +import static com.l2jserver.gameserver.model.drops.strategy.IKillerChanceModifierStrategy.NO_RULES; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.strategy.IAmountMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IChanceMultiplierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IGroupedItemDropCalculationStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IKillerChanceModifierStrategy; +import com.l2jserver.gameserver.model.drops.strategy.IPreciseDeterminationStrategy; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author NosBit + */ +public final class GroupedGeneralDropItem implements IDropItem { + + private final double _chance; + private List _items; + protected final IGroupedItemDropCalculationStrategy _dropCalculationStrategy; + protected final IKillerChanceModifierStrategy _killerChanceModifierStrategy; + protected final IPreciseDeterminationStrategy _preciseStrategy; + + /** + * @param chance the chance of this drop item. + */ + public GroupedGeneralDropItem(double chance) { + this(chance, general().preciseDropMultipleGroupRolls() ? IGroupedItemDropCalculationStrategy.PRECISE_MULTIPLE_GROUP_ROLLS : IGroupedItemDropCalculationStrategy.DEFAULT_STRATEGY, IKillerChanceModifierStrategy.DEFAULT_STRATEGY, IPreciseDeterminationStrategy.DEFAULT); + } + + /** + * @param chance the chance of this drop item. + * @param dropStrategy to calculate drops. + * @param killerStrategy + * @param preciseStrategy + */ + public GroupedGeneralDropItem(double chance, IGroupedItemDropCalculationStrategy dropStrategy, IKillerChanceModifierStrategy killerStrategy, IPreciseDeterminationStrategy preciseStrategy) { + _chance = chance; + _dropCalculationStrategy = dropStrategy; + _killerChanceModifierStrategy = killerStrategy; + _preciseStrategy = preciseStrategy; + } + + /** + * Gets the chance of this drop item. + * @return the chance + */ + public double getChance() { + return _chance; + } + + /** + * Gets the items. + * @return the items + */ + public List getItems() { + return _items; + } + + /** + * @return the strategy + */ + public IGroupedItemDropCalculationStrategy getDropCalculationStrategy() { + return _dropCalculationStrategy; + } + + /** + * @return the _killerChanceModifierStrategy + */ + public IKillerChanceModifierStrategy getKillerChanceModifierStrategy() { + return _killerChanceModifierStrategy; + } + + /** + * @return the _preciseStrategy + */ + public IPreciseDeterminationStrategy getPreciseStrategy() { + return _preciseStrategy; + } + + /** + * Sets an item list to this drop item. + * @param items the item list + */ + public void setItems(List items) { + _items = Collections.unmodifiableList(items); + } + + /** + * Returns a list of items in the group with chance multiplied by chance of the group + * @return the list of items with modified chances + */ + public List extractMe() { + List items = new ArrayList<>(); + for (final GeneralDropItem item : getItems()) { + // precise and killer strategies of the group + items.add(new GeneralDropItem(item.getItemId(), item.getMin(), item.getMax(), (item.getChance() * getChance()) / 100, item.getAmountStrategy(), item.getChanceStrategy(), getPreciseStrategy(), getKillerChanceModifierStrategy(), item.getDropCalculationStrategy())); + } + return items; + } + + /** + * statically normalizes a group, useful when need to convert legacy SQL data + * @return a new group with items, which have a sum of getChance() of 100% + */ + public GroupedGeneralDropItem normalizeMe() { + double sumchance = 0; + for (GeneralDropItem item : getItems()) { + sumchance += (item.getChance() * getChance()) / 100; + } + final double sumchance1 = sumchance; + GroupedGeneralDropItem group = new GroupedGeneralDropItem(sumchance1, getDropCalculationStrategy(), NO_RULES, getPreciseStrategy()); + List items = new ArrayList<>(); + for (final GeneralDropItem item : getItems()) { + // modify only the chance, leave all other rules intact + items.add(new GeneralDropItem(item.getItemId(), item.getMin(), item.getMax(), (item.getChance() * getChance()) / sumchance1, item.getAmountStrategy(), item.getChanceStrategy(), item.getPreciseStrategy(), item.getKillerChanceModifierStrategy(), item.getDropCalculationStrategy())); + } + group.setItems(items); + return group; + } + + /** + * Creates a normalized group taking into account all drop modifiers, needed when handling a group which has items with different chance rates + * @param victim + * @param killer + * @return a new normalized group with all drop modifiers applied + */ + public GroupedGeneralDropItem normalizeMe(L2Character victim, L2Character killer) { + return normalizeMe(victim, killer, true, 1); + } + + /** + * Creates a normalized group taking into account all drop modifiers, needed when handling a group which has items with different chance rates + * @param victim + * @param killer + * @param chanceModifier an additional chance modifier + * @return a new normalized group with all drop modifiers applied + */ + public GroupedGeneralDropItem normalizeMe(L2Character victim, L2Character killer, double chanceModifier) { + return normalizeMe(victim, killer, true, chanceModifier); + } + + /** + * Creates a normalized group taking into account all drop modifiers, needed when handling a group which has items with different chance rates + * @param victim + * @return a new normalized group with all victim modifiers applied + */ + public GroupedGeneralDropItem normalizeMe(L2Character victim) { + return normalizeMe(victim, null, false, 1); + } + + /** + * Creates a normalized group taking into account all drop modifiers, needed when handling a group which has items with different chance rates + * @param victim + * @param chanceModifier an additional chance modifier + * @return a new normalized group with all victim modifiers applied + */ + public GroupedGeneralDropItem normalizeMe(L2Character victim, double chanceModifier) { + return normalizeMe(victim, null, false, chanceModifier); + } + + /** + * Creates a normalized group taking into account all drop modifiers, needed when handling a group which has items with different chance rates + * @param victim + * @param killer + * @param applyKillerModifier if to modify chance by {@link GroupedGeneralDropItem#getKillerChanceModifier(L2Character, L2Character)} + * @param chanceModifier an additional chance modifier + * @return a new normalized group with all drop modifiers applied + */ + private GroupedGeneralDropItem normalizeMe(L2Character victim, L2Character killer, boolean applyKillerModifier, double chanceModifier) { + if (applyKillerModifier) { + chanceModifier *= (getKillerChanceModifier(victim, killer)); + } + double sumchance = 0; + for (GeneralDropItem item : getItems()) { + sumchance += (item.getChance(victim) * getChance() * chanceModifier) / 100; + } + + // to discard further deep blue calculations + GroupedGeneralDropItem group = new GroupedGeneralDropItem(sumchance, getDropCalculationStrategy(), NO_RULES, getPreciseStrategy()); + List items = new ArrayList<>(); + for (GeneralDropItem item : getItems()) { + // the item is made almost "static" + items.add(new GeneralDropItem(item.getItemId(), item.getMin(victim), item.getMax(victim), // + (item.getChance(victim) * getChance() * chanceModifier) / sumchance, // + IAmountMultiplierStrategy.STATIC, IChanceMultiplierStrategy.STATIC, getPreciseStrategy(), // + NO_RULES, item.getDropCalculationStrategy())); + } + group.setItems(items); + return group; + + } + + @Override + public List calculateDrops(L2Character victim, L2Character killer) { + return _dropCalculationStrategy.calculateDrops(this, victim, killer); + } + + /** + * This handles by default deep blue drop rules. It may also be used to handle another drop chance rules based on killer + * @param victim the victim who drops the item + * @param killer who kills the victim + * @return a number between 0 and 1 (usually) + */ + public double getKillerChanceModifier(L2Character victim, L2Character killer) { + return _killerChanceModifierStrategy.getKillerChanceModifier(this, victim, killer); + } + + public boolean isPreciseCalculated() { + return _preciseStrategy.isPreciseCalculated(this); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/IDropItem.java b/src/main/java/com/l2jserver/gameserver/model/drops/IDropItem.java new file mode 100644 index 00000000..75f8ea62 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/IDropItem.java @@ -0,0 +1,38 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +import java.util.List; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author NosBit + */ +public interface IDropItem { + + /** + * Calculates drops of this drop item. + * @param victim the victim + * @param killer the killer + * @return {@code null} or empty collection if there are no drops, a collection containing all items to drop otherwise + */ + List calculateDrops(L2Character victim, L2Character killer); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/IDropItemFactory.java b/src/main/java/com/l2jserver/gameserver/model/drops/IDropItemFactory.java new file mode 100644 index 00000000..4118c9f0 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/IDropItemFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +/** + * @author Battlecruiser + */ +public interface IDropItemFactory { + /** + * @param itemId the item id + * @param min the min count + * @param max the max count + * @param chance the chance of this drop item + * @return the drop item created by this factory + */ + IDropItem newDropItem(int itemId, long min, long max, double chance); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/IGroupedDropItemFactory.java b/src/main/java/com/l2jserver/gameserver/model/drops/IGroupedDropItemFactory.java new file mode 100644 index 00000000..393a0e1a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/IGroupedDropItemFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops; + +/** + * @author Battlecruiser + */ +public interface IGroupedDropItemFactory { + GroupedGeneralDropItem newGroupedDropItem(double chance); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IAmountMultiplierStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IAmountMultiplierStrategy.java new file mode 100644 index 00000000..e334b979 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IAmountMultiplierStrategy.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.rates; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; + +/** + * @author Battlecruiser + */ +public interface IAmountMultiplierStrategy { + IAmountMultiplierStrategy DROP = DEFAULT_STRATEGY(rates().getDeathDropAmountMultiplier()); + IAmountMultiplierStrategy SPOIL = DEFAULT_STRATEGY(rates().getCorpseDropAmountMultiplier()); + IAmountMultiplierStrategy STATIC = (item, victim) -> 1; + IAmountMultiplierStrategy QUEST = DEFAULT_STRATEGY(rates().getQuestDropAmountMultiplier()); + + static IAmountMultiplierStrategy DEFAULT_STRATEGY(final double defaultMultiplier) { + return (item, victim) -> { + double multiplier = 1; + if (victim.isChampion()) { + multiplier *= item.getItemId() != Inventory.ADENA_ID ? customs().getChampionRewardsAmount() : customs().getChampionAdenasRewardsAmount(); + } + + Float dropAmountMultiplier = rates().getDropAmountMultiplierByItemId().get(item.getItemId()); + if (dropAmountMultiplier != null) { + multiplier *= dropAmountMultiplier; + } else if (ItemTable.getInstance().getTemplate(item.getItemId()).hasExImmediateEffect()) { + multiplier *= rates().getHerbDropAmountMultiplier(); + } else if (victim.isRaid()) { + multiplier *= rates().getRaidDropAmountMultiplier(); + } else { + multiplier *= defaultMultiplier; + } + return multiplier; + }; + } + + double getAmountMultiplier(GeneralDropItem item, L2Character victim); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IChanceMultiplierStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IChanceMultiplierStrategy.java new file mode 100644 index 00000000..b20a57c9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IChanceMultiplierStrategy.java @@ -0,0 +1,69 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import static com.l2jserver.gameserver.config.Configuration.customs; +import static com.l2jserver.gameserver.config.Configuration.rates; + +import com.l2jserver.gameserver.datatables.ItemTable; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; + +/** + * @author Battlecruiser + */ +public interface IChanceMultiplierStrategy { + IChanceMultiplierStrategy DROP = DEFAULT_STRATEGY(rates().getDeathDropChanceMultiplier()); + IChanceMultiplierStrategy SPOIL = DEFAULT_STRATEGY(rates().getCorpseDropChanceMultiplier()); + IChanceMultiplierStrategy STATIC = (item, victim) -> 1; + + IChanceMultiplierStrategy QUEST = (item, victim) -> { + double championmult; + if ((item.getItemId() == Inventory.ADENA_ID) || (item.getItemId() == Inventory.ANCIENT_ADENA_ID)) { + championmult = customs().getChampionAdenasRewardsChance(); + } else { + championmult = customs().getChampionRewardsChance(); + } + + return (customs().championEnable() && (victim != null) && victim.isChampion()) ? (rates().getQuestDropChanceMultiplier() * championmult) : rates().getQuestDropChanceMultiplier(); + }; + + static IChanceMultiplierStrategy DEFAULT_STRATEGY(final double defaultMultiplier) { + return (item, victim) -> { + float multiplier = 1; + if (victim.isChampion()) { + multiplier *= item.getItemId() != Inventory.ADENA_ID ? customs().getChampionRewardsChance() : customs().getChampionAdenasRewardsChance(); + } + Float dropChanceMultiplier = rates().getDropChanceMultiplierByItemId().get(item.getItemId()); + if (dropChanceMultiplier != null) { + multiplier *= dropChanceMultiplier; + } else if (ItemTable.getInstance().getTemplate(item.getItemId()).hasExImmediateEffect()) { + multiplier *= rates().getHerbDropChanceMultiplier(); + } else if (victim.isRaid()) { + multiplier *= rates().getRaidDropChanceMultiplier(); + } else { + multiplier *= defaultMultiplier; + } + return multiplier; + }; + } + + double getChanceMultiplier(GeneralDropItem item, L2Character victim); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IDropCalculationStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IDropCalculationStrategy.java new file mode 100644 index 00000000..16c863d7 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IDropCalculationStrategy.java @@ -0,0 +1,50 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import java.util.List; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author Battlecruiser + */ +public interface IDropCalculationStrategy { + IDropCalculationStrategy DEFAULT_STRATEGY = (item, victim, killer) -> { + double chance = item.getChance(victim, killer); + if (chance > (Rnd.nextDouble() * 100)) { + int amountMultiply = 1; + if (item.isPreciseCalculated() && (chance > 100)) { + amountMultiply = (int) chance / 100; + if ((chance % 100) > (Rnd.nextDouble() * 100)) { + amountMultiply++; + } + } + + return List.of(new ItemHolder(item.getItemId(), Rnd.get(item.getMin(victim), item.getMax(victim)) * amountMultiply)); + } + + return null; + }; + + List calculateDrops(GeneralDropItem item, L2Character victim, L2Character killer); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IGroupedItemDropCalculationStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IGroupedItemDropCalculationStrategy.java new file mode 100644 index 00000000..1f8cbbf8 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IGroupedItemDropCalculationStrategy.java @@ -0,0 +1,142 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.drops.GroupedGeneralDropItem; +import com.l2jserver.gameserver.model.drops.IDropItem; +import com.l2jserver.gameserver.model.holders.ItemHolder; + +/** + * @author Battlecruiser + */ +public interface IGroupedItemDropCalculationStrategy { + Logger LOG = LoggerFactory.getLogger(IGroupedItemDropCalculationStrategy.class); + /** + * The default strategy used in L2J to calculate drops. When the group's chance raises over 100% and group has precise calculation, the dropped item's amount increases. + */ + IGroupedItemDropCalculationStrategy DEFAULT_STRATEGY = new IGroupedItemDropCalculationStrategy() { + private final Map singleItemCache = new ConcurrentHashMap<>(); + + private GeneralDropItem getSingleItem(GroupedGeneralDropItem dropItem) { + final GeneralDropItem item1 = dropItem.getItems().iterator().next(); + singleItemCache.putIfAbsent(dropItem, new GeneralDropItem(item1.getItemId(), item1.getMin(), item1.getMax(), (item1.getChance() * dropItem.getChance()) / 100, // + item1.getAmountStrategy(), item1.getChanceStrategy(), dropItem.getPreciseStrategy(), dropItem.getKillerChanceModifierStrategy(), item1.getDropCalculationStrategy())); + return singleItemCache.get(dropItem); + } + + @Override + public List calculateDrops(GroupedGeneralDropItem dropItem, L2Character victim, L2Character killer) { + if (dropItem.getItems().size() == 1) { + return getSingleItem(dropItem).calculateDrops(victim, killer); + } + + GroupedGeneralDropItem normalized = dropItem.normalizeMe(victim, killer); + if (normalized.getChance() > (Rnd.nextDouble() * 100)) { + final double random = (Rnd.nextDouble() * 100); + double totalChance = 0; + for (GeneralDropItem item2 : normalized.getItems()) { + // Grouped item chance rates should not be modified (the whole magic was already done by normalizing thus the items' chance sum is always 100%). + totalChance += item2.getChance(); + if (totalChance > random) { + int amountMultiply = 1; + if (dropItem.isPreciseCalculated() && (normalized.getChance() >= 100)) { + amountMultiply = (int) (normalized.getChance()) / 100; + if ((normalized.getChance() % 100) > (Rnd.nextDouble() * 100)) { + amountMultiply++; + } + } + + return Collections.singletonList(new ItemHolder(item2.getItemId(), Rnd.get(item2.getMin(victim), item2.getMax(victim)) * amountMultiply)); + } + } + } + return null; + } + }; + + /** + * This strategy calculates a group's drop by calculating drops of its individual items and merging its results. + */ + IGroupedItemDropCalculationStrategy DISBAND_GROUP = (item, victim, killer) -> { + List dropped = new ArrayList<>(); + for (IDropItem dropItem : item.extractMe()) { + dropped.addAll(dropItem.calculateDrops(victim, killer)); + } + return dropped.isEmpty() ? null : dropped; + }; + + /** + * This strategy when group has precise calculation rolls multiple times over group to determine drops when group's chance raises over 100% instead of just multiplying the dropped item's amount. Thus it can produce different items from group at once. + */ + IGroupedItemDropCalculationStrategy PRECISE_MULTIPLE_GROUP_ROLLS = (item, victim, killer) -> { + if (!item.isPreciseCalculated()) { + // if item hasn't precise calculation there's no change from DEFAULT_STRATEGY + return DEFAULT_STRATEGY.calculateDrops(item, victim, victim); + } + GroupedGeneralDropItem newItem = new GroupedGeneralDropItem(item.getChance(), DEFAULT_STRATEGY, item.getKillerChanceModifierStrategy(), IPreciseDeterminationStrategy.NEVER); + newItem.setItems(item.getItems()); + GroupedGeneralDropItem normalized = newItem.normalizeMe(victim, killer); + // Let's determine the number of rolls. + int rolls = (int) (normalized.getChance() / 100); + if ((Rnd.nextDouble() * 100) < (normalized.getChance() % 100)) { + rolls++; + } + + List dropped = new ArrayList<>(rolls); + for (int i = 0; i < rolls; i++) { + // As further normalizing on already normalized drop group does nothing, we can just pass the calculation to DEFAULT_STRATEGY with precise calculation disabled as we handle it. + List drops = normalized.calculateDrops(victim, killer); + if (drops != null) { + dropped.addAll(drops); + } + } + + if (general().preciseDropMultipleRollsAggregateDrops()) { + Map countByItemId = new HashMap<>(); + for (ItemHolder drop : dropped) { + Long currentCount = countByItemId.getOrDefault(drop.getId(), 0L); + countByItemId.put(drop.getId(), currentCount + drop.getCount()); + } + + dropped.clear(); + for (Map.Entry entry : countByItemId.entrySet()) { + dropped.add(new ItemHolder(entry.getKey(), entry.getValue())); + } + } + + return dropped.isEmpty() ? null : dropped; + }; + + List calculateDrops(GroupedGeneralDropItem item, L2Character victim, L2Character killer); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IKillerChanceModifierStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IKillerChanceModifierStrategy.java new file mode 100644 index 00000000..cdf29bcb --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IKillerChanceModifierStrategy.java @@ -0,0 +1,63 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import static com.l2jserver.gameserver.config.Configuration.npc; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; +import com.l2jserver.gameserver.model.drops.IDropItem; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.util.Util; + +/** + * @author Battlecruiser + */ +public interface IKillerChanceModifierStrategy extends INonGroupedKillerChanceModifierStrategy { + IKillerChanceModifierStrategy DEFAULT_STRATEGY = (item, victim, killer) -> { + int levelDifference = victim.getLevel() - killer.getLevel(); + if ((victim.isRaid()) && npc().useDeepBlueDropRulesRaid()) { + // FIXME: Config? + return Math.max(0, Math.min(1, (levelDifference * 0.15) + 1)); + } else if (npc().useDeepBlueDropRules()) { + + return Util.map(levelDifference, -npc().getDropItemMaxLevelDifference(), -npc().getDropItemMinLevelDifference(), npc().getDropItemMinLevelGapChance(), 100.0) / 100; + } + return 1; + }; + INonGroupedKillerChanceModifierStrategy DEFAULT_NONGROUP_STRATEGY = (item, victim, killer) -> { + if (((!(victim.isRaid())) && npc().useDeepBlueDropRules()) || ((victim.isRaid()) && npc().useDeepBlueDropRulesRaid())) { + int levelDifference = victim.getLevel() - killer.getLevel(); + if (item.getItemId() == Inventory.ADENA_ID) { + return Util.map(levelDifference, -npc().getDropAdenaMaxLevelDifference(), -npc().getDropAdenaMinLevelDifference(), npc().getDropAdenaMinLevelGapChance(), 100.0) / 100; + } + return Util.map(levelDifference, -npc().getDropItemMaxLevelDifference(), -npc().getDropItemMinLevelDifference(), npc().getDropItemMinLevelGapChance(), 100.0) / 100; + } + return 1; + }; + + IKillerChanceModifierStrategy NO_RULES = (item, victim, killer) -> 1; + + double getKillerChanceModifier(IDropItem item, L2Character victim, L2Character killer); + + @Override + default double getKillerChanceModifier(GeneralDropItem item, L2Character victim, L2Character killer) { + return getKillerChanceModifier((IDropItem) item, victim, killer); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/INonGroupedKillerChanceModifierStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/INonGroupedKillerChanceModifierStrategy.java new file mode 100644 index 00000000..cfe2a38a --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/INonGroupedKillerChanceModifierStrategy.java @@ -0,0 +1,29 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.drops.GeneralDropItem; + +/** + * @author Battlecruiser + */ +public interface INonGroupedKillerChanceModifierStrategy { + double getKillerChanceModifier(GeneralDropItem item, L2Character victim, L2Character killer); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IPreciseDeterminationStrategy.java b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IPreciseDeterminationStrategy.java new file mode 100644 index 00000000..cd90910b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/drops/strategy/IPreciseDeterminationStrategy.java @@ -0,0 +1,40 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.drops.strategy; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import com.l2jserver.gameserver.model.drops.IDropItem; + +/** + * @author Battlecruiser + */ +public interface IPreciseDeterminationStrategy { + IPreciseDeterminationStrategy ALWAYS = dropItem -> true; + + IPreciseDeterminationStrategy DEFAULT = dropItem -> general().preciseDropCalculation(); + + IPreciseDeterminationStrategy NEVER = dropItem -> false; + + /** + * @param dropItem + * @return true if drop calculation strategy should use precise rules + */ + boolean isPreciseCalculated(IDropItem dropItem); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/effects/AbstractEffect.java b/src/main/java/com/l2jserver/gameserver/model/effects/AbstractEffect.java new file mode 100644 index 00000000..2a9b01ce --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/effects/AbstractEffect.java @@ -0,0 +1,267 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.effects; + +import static com.l2jserver.gameserver.config.Configuration.character; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.handler.EffectHandler; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.L2Character; +import com.l2jserver.gameserver.model.conditions.Condition; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.stats.functions.AbstractFunction; +import com.l2jserver.gameserver.model.stats.functions.FuncTemplate; + +/** + * Abstract effect implementation.
          + * Instant effects should not override {@link #onExit(BuffInfo)}.
          + * Instant effects should not override {@link #canStart(BuffInfo)}, all checks should be done {@link #onStart(BuffInfo)}.
          + * Do not call super class methods {@link #onStart(BuffInfo)} nor {@link #onExit(BuffInfo)}. + * @since Changeset 6249 the "effect steal constructor" is deprecated. + * @author Zoey76 + */ +public abstract class AbstractEffect { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractEffect.class); + + // Conditions + /** Attach condition. */ + private final Condition _attachCond; + // Apply condition + // private final Condition _applyCond; // TODO: Use or cleanup. + private List _funcTemplates; + /** Effect name. */ + private final String _name; + /** Ticks. */ + private int _ticks; + + /** + * Abstract effect constructor. + * @param attachCond the attach condition + * @param applyCond the apply condition + * @param set the attributes + * @param params the parameters + */ + protected AbstractEffect(Condition attachCond, Condition applyCond, StatsSet set, StatsSet params) { + _attachCond = attachCond; + // _applyCond = applyCond; + _name = set.getString("name"); + } + + /** + * Creates an effect given the parameters. + * @param attachCond the attach condition + * @param applyCond the apply condition + * @param set the attributes + * @param params the parameters + * @return the new effect + */ + public static AbstractEffect createEffect(Condition attachCond, Condition applyCond, StatsSet set, StatsSet params) { + final var name = set.getString("name"); + final var handler = EffectHandler.getInstance().getHandler(name); + if (handler == null) { + LOG.warn("Requested unexistent effect handler {} in skill Id {}!", name, set.getInt("id")); + return null; + } + + try { + final var constructor = handler.getConstructor(Condition.class, Condition.class, StatsSet.class, StatsSet.class); + return constructor.newInstance(attachCond, applyCond, set, params); + } catch (NoSuchMethodException | SecurityException ex) { + LOG.warn("Requested unexistent constructor for effect handler {} in skill Id {}!", name, set.getInt("id"), ex); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + LOG.warn("Unable to initialize effect handler {} in skill Id {}!", name, set.getInt("id"), ex); + } + return null; + } + + /** + * Tests the attach condition. + * @param caster the caster + * @param target the target + * @param skill the skill + * @return {@code true} if there isn't a condition to test or it's passed, {@code false} otherwise + */ + public boolean testConditions(L2Character caster, L2Character target, Skill skill) { + return (_attachCond == null) || _attachCond.test(caster, target, skill); + } + + /** + * Attaches a function template. + * @param f the function + */ + public void attach(FuncTemplate f) { + if (_funcTemplates == null) { + _funcTemplates = new ArrayList<>(1); + } + _funcTemplates.add(f); + } + + /** + * Gets the effect name. + * @return the name + */ + public String getName() { + return _name; + } + + /** + * Gets the effect ticks + * @return the ticks + */ + public int getTicks() { + return _ticks; + } + + /** + * Sets the effect ticks + * @param ticks the ticks + */ + protected void setTicks(int ticks) { + _ticks = ticks; + } + + public double getTicksMultiplier() { + return (getTicks() * character().getEffectTickRatio()) / 1000f; + } + + public List getFuncTemplates() { + return _funcTemplates; + } + + /** + * Calculates whether this effects land or not.
          + * If it lands will be scheduled and added to the character effect list.
          + * Override in effect implementation to change behavior.
          + * Warning: Must be used only for instant effects continuous effects will not call this they have their success handled by activate_rate. + * @param info the buff info + * @return {@code true} if this effect land, {@code false} otherwise + */ + public boolean calcSuccess(BuffInfo info) { + return true; + } + + /** + * Get this effect's type.
          + * TODO: Remove. + * @return the effect type + */ + public L2EffectType getEffectType() { + return L2EffectType.NONE; + } + + /** + * Verify if the buff can start.
          + * Used for continuous effects. + * @param info the buff info + * @return {@code true} if all the start conditions are meet, {@code false} otherwise + */ + public boolean canStart(BuffInfo info) { + return true; + } + + /** + * Called on effect start. + * @param info the buff info + */ + public void onStart(BuffInfo info) { + + } + + /** + * Called on each tick.
          + * If the abnormal time is lesser than zero it will last forever. + * @param info the buff info + * @return if {@code true} this effect will continue forever, if {@code false} it will stop after abnormal time has passed + */ + public boolean onActionTime(BuffInfo info) { + return false; + } + + /** + * Called when the effect is exited. + * @param info the buff info + */ + public void onExit(BuffInfo info) { + + } + + /** + * Get this effect's stats functions. + * @param caster the caster + * @param target the target + * @param skill the skill + * @return a list of stat functions. + */ + public List getStatFuncs(L2Character caster, L2Character target, Skill skill) { + if (getFuncTemplates() == null) { + return List.of(); + } + + final var functions = new ArrayList(getFuncTemplates().size()); + for (var functionTemplate : getFuncTemplates()) { + final var function = functionTemplate.getFunc(caster, target, skill, this); + if (function != null) { + functions.add(function); + } + } + return functions; + } + + /** + * Get the effect flags. + * @return bit flag for current effect + */ + public int getEffectFlags() { + return EffectFlag.NONE.getMask(); + } + + @Override + public String toString() { + return "Effect " + _name; + } + + public void decreaseForce() { + + } + + public void increaseEffect() { + + } + + public boolean checkCondition(Object obj) { + return true; + } + + /** + * Verify if this effect is an instant effect. + * @return {@code true} if this effect is instant, {@code false} otherwise + */ + public boolean isInstant() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/effects/EffectFlag.java b/src/main/java/com/l2jserver/gameserver/model/effects/EffectFlag.java new file mode 100644 index 00000000..9b20eed9 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/effects/EffectFlag.java @@ -0,0 +1,52 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.effects; + +/** + * @author UnAfraid + */ +public enum EffectFlag { + NONE, + BLOCK_BUFF, + BLOCK_DEBUFF, + BLOCK_HP, + BLOCK_MP, + RESURRECTION_SPECIAL, + NOBLESS_BLESSING, + SILENT_MOVE, + PROTECTION_BLESSING, + RELAXING, + FEAR, + CONFUSED, + MUTED, + PSYCHICAL_MUTED, + PSYCHICAL_ATTACK_MUTED, + DISARMED, + ROOTED, + SLEEP, + STUNNED, + BETRAYED, + SINGLE_TARGET, + BLOCK_RESURRECTION, + SERVITOR_SHARE; + + public int getMask() { + return 1 << ordinal(); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/effects/EffectTaskInfo.java b/src/main/java/com/l2jserver/gameserver/model/effects/EffectTaskInfo.java new file mode 100644 index 00000000..55c0398d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/effects/EffectTaskInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.effects; + +import java.util.concurrent.ScheduledFuture; + +/** + * Effect Task Info DTO. + * @author Zoey76 + */ +public class EffectTaskInfo { + private final EffectTickTask _effectTask; + private final ScheduledFuture _scheduledFuture; + + public EffectTaskInfo(EffectTickTask effectTask, ScheduledFuture scheduledFuture) { + _effectTask = effectTask; + _scheduledFuture = scheduledFuture; + } + + public EffectTickTask getEffectTask() { + return _effectTask; + } + + public ScheduledFuture getScheduledFuture() { + return _scheduledFuture; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/effects/EffectTickTask.java b/src/main/java/com/l2jserver/gameserver/model/effects/EffectTickTask.java new file mode 100644 index 00000000..79437633 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/effects/EffectTickTask.java @@ -0,0 +1,55 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.effects; + +import java.util.concurrent.atomic.AtomicInteger; + +import com.l2jserver.gameserver.model.skills.BuffInfo; + +/** + * Effect tick task. + * @author Zoey76 + */ +public class EffectTickTask implements Runnable { + private final BuffInfo _info; + private final AbstractEffect _effect; + private final AtomicInteger _tickCount = new AtomicInteger(); + + public EffectTickTask(BuffInfo info, AbstractEffect effect) { + _info = info; + _effect = effect; + } + + public BuffInfo getBuffInfo() { + return _info; + } + + public AbstractEffect getEffect() { + return _effect; + } + + public int getTickCount() { + return _tickCount.get(); + } + + @Override + public void run() { + _info.onTick(_effect, _tickCount.incrementAndGet()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/effects/L2EffectType.java b/src/main/java/com/l2jserver/gameserver/model/effects/L2EffectType.java new file mode 100644 index 00000000..302f999e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/effects/L2EffectType.java @@ -0,0 +1,60 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.effects; + +/** + * Effect types. + * @author nBd + */ +public enum L2EffectType { + AGGRESSION, + BUFF, + BONUS_TIME_LIMIT_UP, + CHAT_BLOCK, + CP, + DEBUFF, + DISPEL, + DMG_OVER_TIME, + FAKE_DEATH, + FEAR, + FISHING, + FISHING_START, + HATE, + HP, + HP_DRAIN, + MAGICAL_ATTACK, + MANAHEAL_BY_LEVEL, + MANAHEAL_PERCENT, + MUTE, + NOBLESSE_BLESSING, + NONE, + PHYSICAL_ATTACK, + REBALANCE_HP, + REFUEL_AIRSHIP, + RELAXING, + RESURRECTION, + RESURRECTION_SPECIAL, + ROOT, + SLEEP, + STEAL_ABNORMAL, + STUN, + SUMMON, + SUMMON_NPC, + TELEPORT, +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/AbstractResidence.java b/src/main/java/com/l2jserver/gameserver/model/entity/AbstractResidence.java new file mode 100644 index 00000000..903373ee --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/AbstractResidence.java @@ -0,0 +1,109 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import java.util.ArrayList; +import java.util.List; + +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.model.L2SkillLearn; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.events.ListenersContainer; +import com.l2jserver.gameserver.model.holders.SkillHolder; +import com.l2jserver.gameserver.model.interfaces.INamable; +import com.l2jserver.gameserver.model.zone.type.L2ResidenceZone; + +/** + * @author xban1x + */ +public abstract class AbstractResidence extends ListenersContainer implements INamable { + private final int _residenceId; + private String _name; + + private L2ResidenceZone _zone = null; + private final List _residentialSkills = new ArrayList<>(); + + public AbstractResidence(int residenceId) { + _residenceId = residenceId; + initResidentialSkills(); + } + + protected abstract void load(); + + protected abstract void initResidenceZone(); + + protected void initResidentialSkills() { + final List residentialSkills = SkillTreesData.getInstance().getAvailableResidentialSkills(getResidenceId()); + for (L2SkillLearn s : residentialSkills) { + _residentialSkills.add(new SkillHolder(s.getSkillId(), s.getSkillLevel())); + } + } + + public final int getResidenceId() { + return _residenceId; + } + + @Override + public final String getName() { + return _name; + } + + // TODO: Remove it later when both castles and forts are loaded from same table. + public final void setName(String name) { + _name = name; + } + + public L2ResidenceZone getResidenceZone() { + return _zone; + } + + protected void setResidenceZone(L2ResidenceZone zone) { + _zone = zone; + } + + public final List getResidentialSkills() { + return _residentialSkills; + } + + public void giveResidentialSkills(L2PcInstance player) { + if (!_residentialSkills.isEmpty()) { + for (SkillHolder sh : _residentialSkills) { + player.addSkill(sh.getSkill(), false); + } + } + } + + public void removeResidentialSkills(L2PcInstance player) { + if (!_residentialSkills.isEmpty()) { + for (SkillHolder sh : _residentialSkills) { + player.removeSkill(sh.getSkill(), false); + } + } + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof AbstractResidence) && (((AbstractResidence) obj).getResidenceId() == getResidenceId()); + } + + @Override + public String toString() { + return getName() + "(" + getResidenceId() + ")"; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Auction.java b/src/main/java/com/l2jserver/gameserver/model/entity/Auction.java new file mode 100644 index 00000000..4da90e38 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Auction.java @@ -0,0 +1,495 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.model.itemcontainer.Inventory.ADENA_ID; + +import java.util.Calendar; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.enums.AuctionItemType; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.instancemanager.AuctionManager; +import com.l2jserver.gameserver.instancemanager.ClanHallManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.ItemContainer; +import com.l2jserver.gameserver.network.SystemMessageId; + +public class Auction { + protected static final Logger LOG = LoggerFactory.getLogger(Auction.class); + + private final int _id; + private long _endDate; + private int _highestBidderId = 0; + private String _highestBidderName = ""; + private long _highestBidderMaxBid = 0; + private int _itemId = 0; + private String _itemName = ""; + private int _itemObjectId = 0; + private final long _itemQuantity = 0; + private String _itemType = ""; + private int _sellerId = 0; + private String _sellerClanName = ""; + private String _sellerName = ""; + private long _currentBid = 0; + private long _startingBid = 0; + + private final Map _bidders = new ConcurrentHashMap<>(); + + private static final String[] ItemTypeName = { + "ClanHall" + }; + + public static class Bidder { + private final String _name; // TODO replace with objid + private final String _clanName; + private long _bid; + private final Calendar _timeBid; + + public Bidder(String name, String clanName, long bid, long timeBid) { + _name = name; + _clanName = clanName; + _bid = bid; + _timeBid = Calendar.getInstance(); + _timeBid.setTimeInMillis(timeBid); + } + + public String getName() { + return _name; + } + + public String getClanName() { + return _clanName; + } + + public long getBid() { + return _bid; + } + + public Calendar getTimeBid() { + return _timeBid; + } + + public void setTimeBid(long timeBid) { + _timeBid.setTimeInMillis(timeBid); + } + + public void setBid(long bid) { + _bid = bid; + } + } + + public class AutoEndTask implements Runnable { + public AutoEndTask() { + } + + @Override + public void run() { + try { + endAuction(); + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + public Auction(int auctionId) { + _id = auctionId; + load(); + startAutoTask(); + } + + public Auction(int itemId, L2Clan Clan, long delay, long bid, String name) { + _id = itemId; + _endDate = System.currentTimeMillis() + delay; + _itemId = itemId; + _itemName = name; + _itemType = "ClanHall"; + _sellerId = Clan.getLeaderId(); + _sellerName = Clan.getLeaderName(); + _sellerClanName = Clan.getName(); + _startingBid = bid; + } + + private void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("Select * from auction where id = ?")) { + ps.setInt(1, getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _currentBid = rs.getLong("currentBid"); + _endDate = rs.getLong("endDate"); + _itemId = rs.getInt("itemId"); + _itemName = rs.getString("itemName"); + _itemObjectId = rs.getInt("itemObjectId"); + _itemType = rs.getString("itemType"); + _sellerId = rs.getInt("sellerId"); + _sellerClanName = rs.getString("sellerClanName"); + _sellerName = rs.getString("sellerName"); + _startingBid = rs.getLong("startingBid"); + } + } + loadBid(); + } catch (Exception e) { + LOG.warn("Exception: Auction.load(): {}", e.getMessage(), e); + } + } + + private void loadBid() { + _highestBidderId = 0; + _highestBidderName = ""; + _highestBidderMaxBid = 0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT bidderId, bidderName, maxBid, clan_name, time_bid FROM auction_bid WHERE auctionId = ? ORDER BY maxBid DESC")) { + ps.setInt(1, getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + if (rs.isFirst()) { + _highestBidderId = rs.getInt("bidderId"); + _highestBidderName = rs.getString("bidderName"); + _highestBidderMaxBid = rs.getLong("maxBid"); + } + _bidders.put(rs.getInt("bidderId"), new Bidder(rs.getString("bidderName"), rs.getString("clan_name"), rs.getLong("maxBid"), rs.getLong("time_bid"))); + } + } + } catch (Exception e) { + LOG.warn("Exception: Auction.loadBid(): {}", e.getMessage(), e); + } + } + + /** Task Manage */ + private void startAutoTask() { + long currentTime = System.currentTimeMillis(); + long taskDelay = 0; + if (_endDate <= currentTime) { + _endDate = currentTime + (7 * 24 * 3600000); + saveAuctionDate(); + } else { + taskDelay = _endDate - currentTime; + } + ThreadPoolManager.getInstance().scheduleGeneral(new AutoEndTask(), taskDelay); + } + + public static String getItemTypeName(AuctionItemType value) { + return ItemTypeName[value.ordinal()]; + } + + private void saveAuctionDate() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("Update auction set endDate = ? where id = ?")) { + ps.setLong(1, _endDate); + ps.setInt(2, _id); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: saveAuctionDate(): {}", e.getMessage(), e); + } + } + + /** + * Set a bid + */ + public synchronized void setBid(L2PcInstance bidder, long bid) { + long requiredAdena = bid; + if (getHighestBidderName().equals(bidder.getClan().getLeaderName())) { + requiredAdena = bid - getHighestBidderMaxBid(); + } + if (((getHighestBidderId() > 0) && (bid > getHighestBidderMaxBid())) || ((getHighestBidderId() == 0) && (bid >= getStartingBid()))) { + if (takeItem(bidder, requiredAdena)) { + updateInDB(bidder, bid); + bidder.getClan().setAuctionBidAt(_id, true); + return; + } + } + if ((bid < getStartingBid()) || (bid <= getHighestBidderMaxBid())) { + bidder.sendPacket(SystemMessageId.BID_PRICE_MUST_BE_HIGHER); + } + } + + /** + * Returns the item to the clan warehouse. + * @param clanName the clan name + * @param quantity the Adena value + * @param penalty if {@code true} fees are applied + */ + private void returnItem(String clanName, long quantity, boolean penalty) { + if (penalty) { + quantity *= 0.9; // take 10% tax fee if needed + } + + final L2Clan clan = ClanTable.getInstance().getClanByName(clanName); + if (clan == null) { + LOG.warn("Clan {} doesn't exist!", clanName); + return; + } + + final ItemContainer cwh = clan.getWarehouse(); + if (cwh == null) { + LOG.warn("There has been a problem with {}'s clan warehouse!", clanName); + return; + } + + // avoid overflow on return + final long limit = character().getMaxAdena() - cwh.getAdena(); + quantity = Math.min(quantity, limit); + + cwh.addItem("Outbidded", ADENA_ID, quantity, null, null); + } + + /** + * Take Item in WHC + */ + private boolean takeItem(L2PcInstance bidder, long quantity) { + if ((bidder.getClan() != null) && (bidder.getClan().getWarehouse().getAdena() >= quantity)) { + bidder.getClan().getWarehouse().destroyItemByItemId("Buy", ADENA_ID, quantity, bidder, bidder); + return true; + } + bidder.sendPacket(SystemMessageId.NOT_ENOUGH_ADENA_IN_CWH); + return false; + } + + /** + * Update auction in DB + */ + private void updateInDB(L2PcInstance bidder, long bid) { + try (var con = ConnectionFactory.getInstance().getConnection()) { + if (_bidders.get(bidder.getClanId()) != null) { + try (var ps = con.prepareStatement("UPDATE auction_bid SET bidderId=?, bidderName=?, maxBid=?, time_bid=? WHERE auctionId=? AND bidderId=?")) { + ps.setInt(1, bidder.getClanId()); + ps.setString(2, bidder.getClan().getLeaderName()); + ps.setLong(3, bid); + ps.setLong(4, System.currentTimeMillis()); + ps.setInt(5, getId()); + ps.setInt(6, bidder.getClanId()); + ps.execute(); + } + } else { + try (var ps = con.prepareStatement("INSERT INTO auction_bid (id, auctionId, bidderId, bidderName, maxBid, clan_name, time_bid) VALUES (?, ?, ?, ?, ?, ?, ?)")) { + ps.setInt(1, IdFactory.getInstance().getNextId()); + ps.setInt(2, getId()); + ps.setInt(3, bidder.getClanId()); + ps.setString(4, bidder.getName()); + ps.setLong(5, bid); + ps.setString(6, bidder.getClan().getName()); + ps.setLong(7, System.currentTimeMillis()); + ps.execute(); + } + if (L2World.getInstance().getPlayer(_highestBidderName) != null) { + L2World.getInstance().getPlayer(_highestBidderName).sendMessage("You have been out bidded"); + } + } + + _highestBidderId = bidder.getClanId(); + _highestBidderMaxBid = bid; + _highestBidderName = bidder.getClan().getLeaderName(); + if (_bidders.get(_highestBidderId) == null) { + _bidders.put(_highestBidderId, new Bidder(_highestBidderName, bidder.getClan().getName(), bid, Calendar.getInstance().getTimeInMillis())); + } else { + _bidders.get(_highestBidderId).setBid(bid); + _bidders.get(_highestBidderId).setTimeBid(Calendar.getInstance().getTimeInMillis()); + } + bidder.sendPacket(SystemMessageId.BID_IN_CLANHALL_AUCTION); + } catch (Exception e) { + LOG.error("Exception: Auction.updateInDB(L2PcInstance bidder, int bid): {}", e.getMessage(), e); + } + } + + private void removeBids() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM auction_bid WHERE auctionId=?")) { + ps.setInt(1, getId()); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Auction.deleteFromDB(): {}", e.getMessage(), e); + } + + for (Bidder b : _bidders.values()) { + if (ClanTable.getInstance().getClanByName(b.getClanName()).getHideoutId() == 0) { + returnItem(b.getClanName(), b.getBid(), true); // 10 % tax + } else { + if (L2World.getInstance().getPlayer(b.getName()) != null) { + L2World.getInstance().getPlayer(b.getName()).sendMessage("Congratulation you have won ClanHall!"); + } + } + ClanTable.getInstance().getClanByName(b.getClanName()).setAuctionBidAt(0, true); + } + _bidders.clear(); + } + + public void deleteAuctionFromDB() { + AuctionManager.getInstance().getAuctions().remove(this); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM auction WHERE itemId=?")) { + ps.setInt(1, _itemId); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Auction.deleteFromDB(): {}", e.getMessage(), e); + } + } + + public void endAuction() { + if (ClanHallManager.getInstance().loaded()) { + if ((_highestBidderId == 0) && (_sellerId == 0)) { + startAutoTask(); + return; + } + if ((_highestBidderId == 0) && (_sellerId > 0)) { + // If seller haven't sold ClanHall, auction removed, THIS MUST BE CONFIRMED + int aucId = AuctionManager.getInstance().getAuctionIndex(_id); + AuctionManager.getInstance().getAuctions().remove(aucId); + return; + } + if (_sellerId > 0) { + returnItem(_sellerClanName, _highestBidderMaxBid, true); + returnItem(_sellerClanName, ClanHallManager.getInstance().getAuctionableHallById(_itemId).getLease(), false); + } + deleteAuctionFromDB(); + L2Clan Clan = ClanTable.getInstance().getClanByName(_bidders.get(_highestBidderId).getClanName()); + _bidders.remove(_highestBidderId); + Clan.setAuctionBidAt(0, true); + removeBids(); + ClanHallManager.getInstance().setOwner(_itemId, Clan); + } else { + // Task waiting ClanHallManager is loaded every 3s + ThreadPoolManager.getInstance().scheduleGeneral(new AutoEndTask(), 3000); + } + } + + /** + * Cancel bid + */ + public synchronized void cancelBid(int bidder) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM auction_bid WHERE auctionId=? AND bidderId=?")) { + ps.setInt(1, getId()); + ps.setInt(2, bidder); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Auction.cancelBid(String bidder): {}", e.getMessage(), e); + } + + returnItem(_bidders.get(bidder).getClanName(), _bidders.get(bidder).getBid(), true); + ClanTable.getInstance().getClanByName(_bidders.get(bidder).getClanName()).setAuctionBidAt(0, true); + _bidders.clear(); + loadBid(); + } + + /** Cancel auction */ + public void cancelAuction() { + deleteAuctionFromDB(); + removeBids(); + } + + /** Confirm an auction */ + public void confirmAuction() { + AuctionManager.getInstance().getAuctions().add(this); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO auction (id, sellerId, sellerName, sellerClanName, itemType, itemId, itemObjectId, itemName, itemQuantity, startingBid, currentBid, endDate) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")) { + ps.setInt(1, getId()); + ps.setInt(2, _sellerId); + ps.setString(3, _sellerName); + ps.setString(4, _sellerClanName); + ps.setString(5, _itemType); + ps.setInt(6, _itemId); + ps.setInt(7, _itemObjectId); + ps.setString(8, _itemName); + ps.setLong(9, _itemQuantity); + ps.setLong(10, _startingBid); + ps.setLong(11, _currentBid); + ps.setLong(12, _endDate); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Auction.load(): {}", e.getMessage(), e); + } + } + + public final int getId() { + return _id; + } + + public final long getCurrentBid() { + return _currentBid; + } + + public final long getEndDate() { + return _endDate; + } + + public final int getHighestBidderId() { + return _highestBidderId; + } + + public final String getHighestBidderName() { + return _highestBidderName; + } + + public final long getHighestBidderMaxBid() { + return _highestBidderMaxBid; + } + + public final int getItemId() { + return _itemId; + } + + public final String getItemName() { + return _itemName; + } + + public final int getItemObjectId() { + return _itemObjectId; + } + + public final long getItemQuantity() { + return _itemQuantity; + } + + public final String getItemType() { + return _itemType; + } + + public final int getSellerId() { + return _sellerId; + } + + public final String getSellerName() { + return _sellerName; + } + + public final String getSellerClanName() { + return _sellerClanName; + } + + public final long getStartingBid() { + return _startingBid; + } + + public final Map getBidders() { + return _bidders; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/BlockCheckerEngine.java b/src/main/java/com/l2jserver/gameserver/model/entity/BlockCheckerEngine.java new file mode 100644 index 00000000..d9f98e56 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/BlockCheckerEngine.java @@ -0,0 +1,652 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.instancemanager.HandysBlockCheckerManager; +import com.l2jserver.gameserver.model.ArenaParticipantsHolder; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2BlockInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.PcInventory; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExBasicActionList; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameChangePoints; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameCloseUI; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameEnd; +import com.l2jserver.gameserver.network.serverpackets.ExCubeGameExtendedChangePoints; +import com.l2jserver.gameserver.network.serverpackets.RelationChanged; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * @author BiggBoss + */ +public final class BlockCheckerEngine { + private static final Logger LOG = LoggerFactory.getLogger(BlockCheckerEngine.class); + + // The object which holds all basic members info + private ArenaParticipantsHolder _holder; + // Maps to hold player of each team and his points + private final Map _redTeamPoints = new ConcurrentHashMap<>(); + private final Map _blueTeamPoints = new ConcurrentHashMap<>(); + // The initial points of the event + private int _redPoints = 15; + private int _bluePoints = 15; + // Current used arena + private int _arena = -1; + // All blocks + private final List _spawns = new CopyOnWriteArrayList<>(); + // Sets if the red team won the event at the end of this (used for packets) + private boolean _isRedWinner; + // Time when the event starts. Used on packet sending + private long _startedTime; + // The needed arena coordinates + // Arena X: team1X, team1Y, team2X, team2Y, ArenaCenterX, ArenaCenterY + private static final int[][] _arenaCoordinates = { + // Arena 0 - Team 1 XY, Team 2 XY - CENTER XY + { + -58368, + -62745, + -57751, + -62131, + -58053, + -62417 + }, + // Arena 1 - Team 1 XY, Team 2 XY - CENTER XY + { + -58350, + -63853, + -57756, + -63266, + -58053, + -63551 + }, + // Arena 2 - Team 1 XY, Team 2 XY - CENTER XY + { + -57194, + -63861, + -56580, + -63249, + -56886, + -63551 + }, + // Arena 3 - Team 1 XY, Team 2 XY - CENTER XY + { + -57200, + -62727, + -56584, + -62115, + -56850, + -62391 + } + }; + // Common z coordinate + private static final int _zCoord = -2405; + // List of dropped items in event (for later deletion) + private final List _drops = new CopyOnWriteArrayList<>(); + // Default arena + private static final byte DEFAULT_ARENA = -1; + // Event is started + private boolean _isStarted = false; + // Event end + private ScheduledFuture _task; + // Preserve from exploit reward by logging out + private boolean _abnormalEnd = false; + + public BlockCheckerEngine(ArenaParticipantsHolder holder, int arena) { + _holder = holder; + if ((arena > -1) && (arena < 4)) { + _arena = arena; + } + + for (L2PcInstance player : holder.getRedPlayers()) { + _redTeamPoints.put(player, 0); + } + for (L2PcInstance player : holder.getBluePlayers()) { + _blueTeamPoints.put(player, 0); + } + } + + /** + * Updates the player holder before the event starts to synchronize all info + */ + public void updatePlayersOnStart(ArenaParticipantsHolder holder) { + _holder = holder; + } + + /** + * Returns the current holder object of this object engine + * @return HandysBlockCheckerManager.ArenaParticipantsHolder + */ + public ArenaParticipantsHolder getHolder() { + return _holder; + } + + /** + * Will return the id of the arena used by this event + * @return false; + */ + public int getArena() { + return _arena; + } + + /** + * Returns the time when the event started + * @return long + */ + public long getStarterTime() { + return _startedTime; + } + + /** + * Returns the current red team points + * @return int + */ + public int getRedPoints() { + synchronized (this) { + return _redPoints; + } + } + + /** + * Returns the current blue team points + * @return int + */ + public int getBluePoints() { + synchronized (this) { + return _bluePoints; + } + } + + /** + * Returns the player points + */ + public int getPlayerPoints(L2PcInstance player, boolean isRed) { + if (!_redTeamPoints.containsKey(player) && !_blueTeamPoints.containsKey(player)) { + return 0; + } + + if (isRed) { + return _redTeamPoints.get(player); + } + return _blueTeamPoints.get(player); + } + + /** + * Increases player points for his teams + */ + public synchronized void increasePlayerPoints(L2PcInstance player, int team) { + if (player == null) { + return; + } + + if (team == 0) { + int points = _redTeamPoints.get(player) + 1; + _redTeamPoints.put(player, points); + _redPoints++; + _bluePoints--; + } else { + int points = _blueTeamPoints.get(player) + 1; + _blueTeamPoints.put(player, points); + _bluePoints++; + _redPoints--; + } + } + + /** + * Will add a new drop into the list of dropped items + */ + public void addNewDrop(L2ItemInstance item) { + if (item != null) { + _drops.add(item); + } + } + + /** + * Will return true if the event is already started + */ + public boolean isStarted() { + return _isStarted; + } + + /** + * Will send all packets for the event members with the relation info + */ + private void broadcastRelationChanged(L2PcInstance plr) { + for (L2PcInstance p : _holder.getAllPlayers()) { + p.sendPacket(new RelationChanged(plr, plr.getRelation(p), plr.isAutoAttackable(p))); + } + } + + /** + * Called when a there is an empty team. The event will end. + */ + public void endEventAbnormally() { + try { + synchronized (this) { + _isStarted = false; + + if (_task != null) { + _task.cancel(true); + } + + _abnormalEnd = true; + + ThreadPoolManager.getInstance().executeGeneral(new EndEvent()); + + if (general().debug()) { + LOG.info("Handys Block Checker Event at arena {} ended due lack of players!", _arena); + } + } + } catch (Exception e) { + LOG.error("Couldn't end Block Checker event at {}", _arena, e); + } + } + + /** + * This inner class set-ups all player and arena parameters to start the event + */ + public class StartEvent implements Runnable { + // In event used skills + private final Skill _freeze, _transformationRed, _transformationBlue; + // Common and unparametizer packet + private final ExCubeGameCloseUI _closeUserInterface = new ExCubeGameCloseUI(); + + public StartEvent() { + // Initialize all used skills + _freeze = SkillData.getInstance().getSkill(6034, 1); + _transformationRed = SkillData.getInstance().getSkill(6035, 1); + _transformationBlue = SkillData.getInstance().getSkill(6036, 1); + } + + /** + * Will set up all player parameters and port them to their respective location based on their teams + */ + private void setUpPlayers() { + // Set current arena as being used + HandysBlockCheckerManager.getInstance().setArenaBeingUsed(_arena); + + // Initialize packets avoiding create a new one per player + _redPoints = _spawns.size() / 2; + _bluePoints = _spawns.size() / 2; + final ExCubeGameChangePoints initialPoints = new ExCubeGameChangePoints(300, _bluePoints, _redPoints); + ExCubeGameExtendedChangePoints clientSetUp; + + for (L2PcInstance player : _holder.getAllPlayers()) { + if (player == null) { + continue; + } + + // Send the secret client packet set up + boolean isRed = _holder.getRedPlayers().contains(player); + + clientSetUp = new ExCubeGameExtendedChangePoints(300, _bluePoints, _redPoints, isRed, player, 0); + player.sendPacket(clientSetUp); + + player.sendPacket(ActionFailed.STATIC_PACKET); + + // Teleport Player - Array access + // Team 0 * 2 = 0; 0 = 0, 0 + 1 = 1. + // Team 1 * 2 = 2; 2 = 2, 2 + 1 = 3 + int tc = _holder.getPlayerTeam(player) * 2; + // Get x and y coordinates + int x = _arenaCoordinates[_arena][tc]; + int y = _arenaCoordinates[_arena][tc + 1]; + player.teleToLocation(x, y, _zCoord); + // Set the player team + if (isRed) { + _redTeamPoints.put(player, 0); + player.setTeam(Team.RED); + } else { + _blueTeamPoints.put(player, 0); + player.setTeam(Team.BLUE); + } + player.stopAllEffects(); + + if (player.hasSummon()) { + player.getSummon().unSummon(player); + } + + // Give the player start up effects + // Freeze + _freeze.applyEffects(player, player); + // Transformation + if (_holder.getPlayerTeam(player) == 0) { + _transformationRed.applyEffects(player, player); + } else { + _transformationBlue.applyEffects(player, player); + } + // Set the current player arena + player.setBlockCheckerArena((byte) _arena); + player.setInsideZone(ZoneId.PVP, true); + // Send needed packets + player.sendPacket(initialPoints); + player.sendPacket(_closeUserInterface); + // ExBasicActionList + player.sendPacket(ExBasicActionList.STATIC_PACKET); + broadcastRelationChanged(player); + } + } + + @Override + public void run() { + // Wrong arena passed, stop event + if (_arena == -1) { + LOG.error("Couldn't set up the arena Id for the Block Checker event, cancelling event..."); + return; + } + _isStarted = true; + // Spawn the blocks + ThreadPoolManager.getInstance().executeGeneral(new SpawnRound(16, 1)); + // Start up player parameters + setUpPlayers(); + // Set the started time + _startedTime = System.currentTimeMillis() + 300000; + } + } + + /** + * This class spawns the second round of boxes and schedules the event end + */ + private class SpawnRound implements Runnable { + int _numOfBoxes; + int _round; + + SpawnRound(int numberOfBoxes, int round) { + _numOfBoxes = numberOfBoxes; + _round = round; + } + + @Override + public void run() { + if (!_isStarted) { + return; + } + + // Schedule second spawn round + // Schedule third spawn round + // Schedule Event End Count Down + switch (_round) { + case 1 -> _task = ThreadPoolManager.getInstance().scheduleGeneral(new SpawnRound(20, 2), 60000); + case 2 -> _task = ThreadPoolManager.getInstance().scheduleGeneral(new SpawnRound(14, 3), 60000); + case 3 -> _task = ThreadPoolManager.getInstance().scheduleGeneral(new EndEvent(), 180000); + } + // random % 2, if == 0 will spawn a red block + // if != 0, will spawn a blue block + byte random = 2; + // Spawn blocks + try { + // Creates 50 new blocks + for (int i = 0; i < _numOfBoxes; i++) { + final L2Spawn spawn = new L2Spawn(18672); + spawn.setX(_arenaCoordinates[_arena][4] + Rnd.get(-400, 400)); + spawn.setY(_arenaCoordinates[_arena][5] + Rnd.get(-400, 400)); + spawn.setZ(_zCoord); + spawn.setAmount(1); + spawn.setHeading(1); + spawn.setRespawnDelay(1); + SpawnTable.getInstance().addNewSpawn(spawn, false); + spawn.init(); + L2BlockInstance block = (L2BlockInstance) spawn.getLastSpawn(); + // switch color + block.setRed((random % 2) == 0); + + block.disableCoreAI(true); + _spawns.add(spawn); + random++; + } + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + + // Spawn the block carrying girl + if ((_round == 1) || (_round == 2)) { + try { + final L2Spawn girlSpawn = new L2Spawn(18676); + girlSpawn.setX(_arenaCoordinates[_arena][4] + Rnd.get(-400, 400)); + girlSpawn.setY(_arenaCoordinates[_arena][5] + Rnd.get(-400, 400)); + girlSpawn.setZ(_zCoord); + girlSpawn.setAmount(1); + girlSpawn.setHeading(1); + girlSpawn.setRespawnDelay(1); + SpawnTable.getInstance().addNewSpawn(girlSpawn, false); + girlSpawn.init(); + // Schedule his deletion after 9 secs of spawn + ThreadPoolManager.getInstance().scheduleGeneral(new CarryingGirlUnspawn(girlSpawn), 9000); + } catch (Exception e) { + LOG.warn("Couldn't Spawn Block Checker NPCs! Wrong instance type at npc table?", e); + } + } + + _redPoints += _numOfBoxes / 2; + _bluePoints += _numOfBoxes / 2; + + int timeLeft = (int) ((getStarterTime() - System.currentTimeMillis()) / 1000); + ExCubeGameChangePoints changePoints = new ExCubeGameChangePoints(timeLeft, getBluePoints(), getRedPoints()); + getHolder().broadCastPacketToTeam(changePoints); + } + } + + private static class CarryingGirlUnspawn implements Runnable { + private final L2Spawn _spawn; + + protected CarryingGirlUnspawn(L2Spawn spawn) { + _spawn = spawn; + } + + @Override + public void run() { + if (_spawn == null) { + LOG.warn("HBCE: Block Carrying Girl is null"); + return; + } + SpawnTable.getInstance().deleteSpawn(_spawn, false); + _spawn.stopRespawn(); + _spawn.getLastSpawn().deleteMe(); + } + } + + /* + * private class CountDown implements Runnable { + * @Override public void run() { _holder.broadCastPacketToTeam(SystemMessage.getSystemMessage(SystemMessageId.BLOCK_CHECKER_ENDS_5)); ThreadPoolManager.getInstance().scheduleGeneral(new EndEvent(), 5000); } } + */ + + /** + * This class erase all event parameters on player and port them back near Handy. Also, unspawn blocks, runs a garbage collector and set as free the used arena + */ + protected class EndEvent implements Runnable { + // Garbage collector and arena free setter + private void clearMe() { + HandysBlockCheckerManager.getInstance().clearPaticipantQueueByArenaId(_arena); + _holder.clearPlayers(); + _blueTeamPoints.clear(); + _redTeamPoints.clear(); + HandysBlockCheckerManager.getInstance().setArenaFree(_arena); + + for (L2Spawn spawn : _spawns) { + spawn.stopRespawn(); + spawn.getLastSpawn().deleteMe(); + SpawnTable.getInstance().deleteSpawn(spawn, false); + } + _spawns.clear(); + + for (L2ItemInstance item : _drops) { + // a player has it, it will be deleted later + if (!item.isVisible() || (item.getOwnerId() != 0)) { + continue; + } + + item.decayMe(); + L2World.getInstance().removeObject(item); + } + _drops.clear(); + } + + /** + * Reward players after event. Tie - No Reward + */ + private void rewardPlayers() { + if (_redPoints == _bluePoints) { + return; + } + + _isRedWinner = _redPoints > _bluePoints; + + if (_isRedWinner) { + rewardAsWinner(true); + rewardAsLooser(false); + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.TEAM_C1_WON); + msg.addString("Red Team"); + _holder.broadCastPacketToTeam(msg); + } else { + rewardAsWinner(false); + rewardAsLooser(true); + SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.TEAM_C1_WON); + msg.addString("Blue Team"); + _holder.broadCastPacketToTeam(msg); + } + } + + /** + * Reward the specified team as a winner team 1) Higher score - 8 extra 2) Higher score - 5 extra + */ + private void rewardAsWinner(boolean isRed) { + Map tempPoints = isRed ? _redTeamPoints : _blueTeamPoints; + + // Main give + for (Entry points : tempPoints.entrySet()) { + if (points.getKey() == null) { + continue; + } + + if (points.getValue() >= 10) { + points.getKey().addItem("Block Checker", 13067, 2, points.getKey(), true); + } else { + tempPoints.remove(points.getKey()); + } + } + + int first = 0, second = 0; + L2PcInstance winner1 = null, winner2 = null; + for (Entry entry : tempPoints.entrySet()) { + L2PcInstance pc = entry.getKey(); + int pcPoints = entry.getValue(); + if (pcPoints > first) { + // Move old data + second = first; + winner2 = winner1; + // Set new data + first = pcPoints; + winner1 = pc; + } else if (pcPoints > second) { + second = pcPoints; + winner2 = pc; + } + } + if (winner1 != null) { + winner1.addItem("Block Checker", 13067, 8, winner1, true); + } + if (winner2 != null) { + winner2.addItem("Block Checker", 13067, 5, winner2, true); + } + } + + /** + * Will reward the looser team with the predefined rewards Player got >= 10 points: 2 coins Player got < 10 points: 0 coins + */ + private void rewardAsLooser(boolean isRed) { + Map tempPoints = isRed ? _redTeamPoints : _blueTeamPoints; + + for (Entry entry : tempPoints.entrySet()) { + L2PcInstance player = entry.getKey(); + if ((player != null) && (entry.getValue() >= 10)) { + player.addItem("Block Checker", 13067, 2, player, true); + } + } + } + + /** + * Teleport players back, give status back and send final packet + */ + private void setPlayersBack() { + final ExCubeGameEnd end = new ExCubeGameEnd(_isRedWinner); + + for (L2PcInstance player : _holder.getAllPlayers()) { + if (player == null) { + continue; + } + + player.stopAllEffects(); + // Remove team aura + player.setTeam(Team.NONE); + // Set default arena + player.setBlockCheckerArena(DEFAULT_ARENA); + // Remove the event items + PcInventory inv = player.getInventory(); + if (inv.getItemByItemId(13787) != null) { + long count = inv.getInventoryItemCount(13787, 0); + inv.destroyItemByItemId("Handys Block Checker", 13787, count, player, player); + } + if (inv.getItemByItemId(13788) != null) { + long count = inv.getInventoryItemCount(13788, 0); + inv.destroyItemByItemId("Handys Block Checker", 13788, count, player, player); + } + broadcastRelationChanged(player); + // Teleport Back + player.teleToLocation(-57478, -60367, -2370); + player.setInsideZone(ZoneId.PVP, false); + // Send end packet + player.sendPacket(end); + player.broadcastUserInfo(); + } + } + + @Override + public void run() { + if (!_abnormalEnd) { + rewardPlayers(); + } + setPlayersBack(); + clearMe(); + _isStarted = false; + _abnormalEnd = false; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Castle.java b/src/main/java/com/l2jserver/gameserver/model/entity/Castle.java new file mode 100644 index 00000000..e39bba65 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Castle.java @@ -0,0 +1,956 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.clan; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.data.xml.impl.SkillTreesData; +import com.l2jserver.gameserver.datatables.SkillData; +import com.l2jserver.gameserver.enums.MountType; +import com.l2jserver.gameserver.enums.audio.Music; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.CastleManorManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.SiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager.Territory; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2SkillLearn; +import com.l2jserver.gameserver.model.TowerSpawn; +import com.l2jserver.gameserver.model.actor.instance.L2ArtefactInstance; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.skills.Skill; +import com.l2jserver.gameserver.model.zone.type.L2CastleZone; +import com.l2jserver.gameserver.model.zone.type.L2ResidenceTeleportZone; +import com.l2jserver.gameserver.model.zone.type.L2SiegeZone; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowInfoUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public final class Castle extends AbstractResidence { + private static final Logger LOG = LoggerFactory.getLogger(Castle.class); + + private final List _doors = new ArrayList<>(); + private int _ownerId = 0; + private Siege _siege = null; + private Calendar _siegeDate; + private boolean _isTimeRegistrationOver = true; // true if Castle Lords set the time, or 24h is elapsed after the siege + private Calendar _siegeTimeRegistrationEndDate; // last siege end date + 1 day + private int _taxPercent = 0; + private double _taxRate = 0; + private int _nextTaxPercent = 0; + private long _nextTaxTime = 0; + private long _treasury = 0; + private boolean _showNpcCrest = false; + private L2SiegeZone _zone = null; + private L2ResidenceTeleportZone _teleZone; + private L2Clan _formerOwner = null; + private final List _artefacts = new ArrayList<>(1); + private final Map _function; + private int _ticketBuyCount = 0; + + /** Castle Functions */ + public static final int FUNC_RESTORE_HP = 1; + public static final int FUNC_RESTORE_MP = 2; + public static final int FUNC_RESTORE_CP = 3; + public static final int FUNC_RESTORE_EXP = 4; + public static final int FUNC_TELEPORT = 5; + public static final int FUNC_BROADCAST = 6; + public static final int FUNC_DECO_CURTAINS = 7; + public static final int FUNC_DECO_HANGING = 8; + public static final int FUNC_SUPPORT = 9; + public static final int FUNC_DECO_OUTERFLAG = 10; + public static final int FUNC_DECO_FRONTPLATEFORM = 11; + public static final int FUNC_ITEM_CREATE = 12; + + public class CastleFunction { + private final int _type; + private int _lvl; + protected int _fee; + protected int _tempFee; + private final long _rate; + private long _endDate; + protected boolean _inDebt; + public boolean _cwh; + + public CastleFunction(int type, int lvl, int lease, int tempLease, long rate, long time, boolean cwh) { + _type = type; + _lvl = lvl; + _fee = lease; + _tempFee = tempLease; + _rate = rate; + _endDate = time; + initializeTask(cwh); + } + + public int getType() { + return _type; + } + + public int getLvl() { + return _lvl; + } + + public int getLease() { + return _fee; + } + + public long getRate() { + return _rate; + } + + public long getEndTime() { + return _endDate; + } + + public void setLvl(int lvl) { + _lvl = lvl; + } + + public void setLease(int lease) { + _fee = lease; + } + + public void setEndTime(long time) { + _endDate = time; + } + + private void initializeTask(boolean cwh) { + if (getOwnerId() <= 0) { + return; + } + long currentTime = System.currentTimeMillis(); + if (_endDate > currentTime) { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), _endDate - currentTime); + } else { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), 0); + } + } + + private class FunctionTask implements Runnable { + public FunctionTask(boolean cwh) { + _cwh = cwh; + } + + @Override + public void run() { + try { + if (getOwnerId() <= 0) { + return; + } + if ((ClanTable.getInstance().getClan(getOwnerId()).getWarehouse().getAdena() >= _fee) || !_cwh) { + int fee = _fee; + if (getEndTime() == -1) { + fee = _tempFee; + } + + setEndTime(System.currentTimeMillis() + getRate()); + dbSave(); + if (_cwh) { + ClanTable.getInstance().getClan(getOwnerId()).getWarehouse().destroyItemByItemId("CS_function_fee", Inventory.ADENA_ID, fee, null, null); + } + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(true), getRate()); + } else { + removeFunction(getType()); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + public void dbSave() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO castle_functions (castle_id, type, lvl, lease, rate, endTime) VALUES (?,?,?,?,?,?)")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, getType()); + ps.setInt(3, getLvl()); + ps.setInt(4, getLease()); + ps.setLong(5, getRate()); + ps.setLong(6, getEndTime()); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Castle.updateFunctions(int type, int lvl, int lease, long rate, long time, boolean addNew): {}", e.getMessage(), e); + } + } + } + + public Castle(int castleId) { + super(castleId); + load(); + /* + * if (getResidenceId() == 7 || castleId == 9) // Goddard and Schuttgart _nbArtifact = 2; + */ + _function = new ConcurrentHashMap<>(); + initResidenceZone(); + if (getOwnerId() != 0) { + loadFunctions(); + loadDoorUpgrade(); + } + } + + /** + * Return function with id + * @param type + * @return + */ + public CastleFunction getFunction(int type) { + return _function.get(type); + } + + public synchronized void engrave(L2Clan clan, L2Object target) { + if (!_artefacts.contains(target)) { + return; + } + setOwner(clan); + final SystemMessage msg = SystemMessage.getSystemMessage(SystemMessageId.CLAN_S1_ENGRAVED_RULER); + msg.addString(clan.getName()); + getSiege().announceToPlayer(msg, true); + } + + /** + * Add amount to castle instance's treasury (warehouse). + */ + public void addToTreasury(long amount) { + // check if owned + if (getOwnerId() <= 0) { + return; + } + + if (getName().equalsIgnoreCase("Schuttgart") || getName().equalsIgnoreCase("Goddard")) { + Castle rune = CastleManager.getInstance().getCastle("rune"); + if (rune != null) { + long runeTax = (long) (amount * rune.getTaxRate()); + if (rune.getOwnerId() > 0) { + rune.addToTreasury(runeTax); + } + amount -= runeTax; + } + } + if (!getName().equalsIgnoreCase("aden") && !getName().equalsIgnoreCase("Rune") && !getName().equalsIgnoreCase("Schuttgart") && !getName().equalsIgnoreCase("Goddard")) // If current castle instance is not Aden, Rune, Goddard or Schuttgart. + { + Castle aden = CastleManager.getInstance().getCastle("aden"); + if (aden != null) { + long adenTax = (long) (amount * aden.getTaxRate()); // Find out what Aden gets from the current castle instance's income + if (aden.getOwnerId() > 0) { + aden.addToTreasury(adenTax); // Only bother to really add the tax to the treasury if not npc owned + } + + amount -= adenTax; // Subtract Aden's income from current castle instance's income + } + } + + addToTreasuryNoTax(amount); + } + + /** + * Add amount to castle instance's treasury (warehouse), no tax paying. + */ + public boolean addToTreasuryNoTax(long amount) { + if (getOwnerId() <= 0) { + return false; + } + + if (amount < 0) { + amount *= -1; + if (_treasury < amount) { + return false; + } + _treasury -= amount; + } else { + if ((_treasury + amount) > character().getMaxAdena()) { + _treasury = character().getMaxAdena(); + } else { + _treasury += amount; + } + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle SET treasury = ? WHERE id = ?")) { + ps.setLong(1, getTreasury()); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + return true; + } + + /** + * Move non clan members off castle area and to nearest town. + */ + public void banishForeigners() { + getResidenceZone().banishForeigners(getOwnerId()); + } + + /** + * Return true if object is inside the zone + */ + public boolean checkIfInZone(int x, int y, int z) { + return getZone().isInsideZone(x, y, z); + } + + public L2SiegeZone getZone() { + if (_zone == null) { + for (L2SiegeZone zone : ZoneManager.getInstance().getAllZones(L2SiegeZone.class)) { + if (zone.getSiegeObjectId() == getResidenceId()) { + _zone = zone; + break; + } + } + } + return _zone; + } + + @Override + public L2CastleZone getResidenceZone() { + return (L2CastleZone) super.getResidenceZone(); + } + + public L2ResidenceTeleportZone getTeleZone() { + if (_teleZone == null) { + for (L2ResidenceTeleportZone zone : ZoneManager.getInstance().getAllZones(L2ResidenceTeleportZone.class)) { + if (zone.getResidenceId() == getResidenceId()) { + _teleZone = zone; + break; + } + } + } + return _teleZone; + } + + public void oustAllPlayers() { + getTeleZone().oustAllPlayers(); + } + + /** + * Get the objects distance to this castle + */ + public double getDistance(L2Object obj) { + return getZone().getDistanceToZone(obj); + } + + public void closeDoor(L2PcInstance activeChar, int doorId) { + openCloseDoor(activeChar, doorId, false); + } + + public void openDoor(L2PcInstance activeChar, int doorId) { + openCloseDoor(activeChar, doorId, true); + } + + public void openCloseDoor(L2PcInstance activeChar, int doorId, boolean open) { + if (activeChar.getClanId() != getOwnerId()) { + return; + } + + L2DoorInstance door = getDoor(doorId); + if (door != null) { + if (open) { + door.openMe(); + } else { + door.closeMe(); + } + } + } + + // This method is used to begin removing all castle upgrades + public void removeUpgrade() { + removeDoorUpgrade(); + removeTrapUpgrade(); + for (Integer fc : _function.keySet()) { + removeFunction(fc); + } + _function.clear(); + } + + // This method updates the castle tax rate + public void setOwner(L2Clan clan) { + // Remove old owner + if ((getOwnerId() > 0) && ((clan == null) || (clan.getId() != getOwnerId()))) { + L2Clan oldOwner = ClanTable.getInstance().getClan(getOwnerId()); // Try to find clan instance + if (oldOwner != null) { + if (_formerOwner == null) { + _formerOwner = oldOwner; + if (character().removeCastleCirclets()) { + CastleManager.getInstance().removeCirclet(_formerOwner, getResidenceId()); + } + } + try { + L2PcInstance oldleader = oldOwner.getLeader().getPlayerInstance(); + if (oldleader != null) { + if (oldleader.getMountType() == MountType.WYVERN) { + oldleader.dismount(); + } + } + } catch (Exception e) { + LOG.warn("Exception in setOwner: {}", e.getMessage(), e); + } + oldOwner.setCastleId(0); // Unset has castle flag for old owner + for (L2PcInstance member : oldOwner.getOnlineMembers(0)) { + removeResidentialSkills(member); + member.sendSkillList(); + } + } + } + + updateOwnerInDB(clan); // Update in database + setShowNpcCrest(false); + + // if clan have fortress, remove it + if ((clan != null) && (clan.getFortId() > 0)) { + FortManager.getInstance().getFortByOwner(clan).removeOwner(true); + } + + if (getSiege().isInProgress()) { + getSiege().midVictory(); // Mid-victory phase of siege + } + + TerritoryWarManager.getInstance().getTerritory(getResidenceId()).setOwnerClan(clan); + + if (clan != null) { + for (L2PcInstance member : clan.getOnlineMembers(0)) { + giveResidentialSkills(member); + member.sendSkillList(); + } + } + } + + public void removeOwner(L2Clan clan) { + if (clan != null) { + _formerOwner = clan; + if (character().removeCastleCirclets()) { + CastleManager.getInstance().removeCirclet(_formerOwner, getResidenceId()); + } + for (L2PcInstance member : clan.getOnlineMembers(0)) { + removeResidentialSkills(member); + member.sendSkillList(); + } + clan.setCastleId(0); + clan.broadcastToOnlineMembers(new PledgeShowInfoUpdate(clan)); + } + + updateOwnerInDB(null); + if (getSiege().isInProgress()) { + getSiege().midVictory(); + } + + for (Integer fc : _function.keySet()) { + removeFunction(fc); + } + _function.clear(); + } + + public void setTaxPercent(int taxPercent) { + _taxPercent = taxPercent; + _taxRate = _taxPercent / 100.0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle SET taxPercent = ? WHERE id = ?")) { + ps.setInt(1, taxPercent); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + } + + public void setNextTaxPercent(int taxPercent) { + _nextTaxPercent = taxPercent; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle SET next_tax = ? WHERE id = ?")) { + ps.setInt(1, taxPercent); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + } + + /** + * Respawn all doors on castle grounds. + */ + public void spawnDoor() { + spawnDoor(false); + } + + /** + * Respawn all doors on castle grounds
          + *
          + * @param isDoorWeak + */ + public void spawnDoor(boolean isDoorWeak) { + for (L2DoorInstance door : _doors) { + if (door.isDead()) { + door.doRevive(); + door.setCurrentHp((isDoorWeak) ? (door.getMaxHp() / 2.0) : (door.getMaxHp())); + } + + if (door.getOpen()) { + door.closeMe(); + } + } + } + + @Override + protected void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps1 = con.prepareStatement("SELECT * FROM castle WHERE id = ?"); + var ps2 = con.prepareStatement("SELECT clan_id FROM clan_data WHERE hasCastle = ?")) { + ps1.setInt(1, getResidenceId()); + try (var rs = ps1.executeQuery()) { + while (rs.next()) { + setName(rs.getString("name")); + + _siegeDate = Calendar.getInstance(); + _siegeDate.setTimeInMillis(rs.getLong("siegeDate")); + _siegeTimeRegistrationEndDate = Calendar.getInstance(); + _siegeTimeRegistrationEndDate.setTimeInMillis(rs.getLong("regTimeEnd")); + _isTimeRegistrationOver = rs.getBoolean("regTimeOver"); + + _taxPercent = rs.getInt("taxPercent"); + _nextTaxPercent = rs.getInt("next_tax"); + _nextTaxTime = rs.getLong("next_tax_time"); + _treasury = rs.getLong("treasury"); + + _showNpcCrest = rs.getBoolean("showNpcCrest"); + + _ticketBuyCount = rs.getInt("ticketBuyCount"); + } + } + _taxRate = _taxPercent / 100.0; + + ps2.setInt(1, getResidenceId()); + try (var rs = ps2.executeQuery()) { + while (rs.next()) { + _ownerId = rs.getInt("clan_id"); + } + } + } catch (Exception e) { + LOG.warn("Exception: loadCastleData(): {}", e.getMessage(), e); + } + } + + /** Load All Functions */ + private void loadFunctions() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM castle_functions WHERE castle_id = ?")) { + ps.setInt(1, getResidenceId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _function.put(rs.getInt("type"), new CastleFunction(rs.getInt("type"), rs.getInt("lvl"), rs.getInt("lease"), 0, rs.getLong("rate"), rs.getLong("endTime"), true)); + } + } + } catch (Exception e) { + LOG.error("Exception: Castle.loadFunctions(): {}", e.getMessage(), e); + } + } + + /** + * Remove function In List and in DB + */ + public void removeFunction(int functionType) { + _function.remove(functionType); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM castle_functions WHERE castle_id=? AND type=?")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, functionType); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Castle.removeFunctions(int functionType): {}", e.getMessage(), e); + } + } + + public boolean updateFunctions(L2PcInstance player, int type, int lvl, int lease, long rate, boolean addNew) { + if (player == null) { + return false; + } + if (lease > 0) { + if (!player.destroyItemByItemId("Consume", Inventory.ADENA_ID, lease, null, true)) { + return false; + } + } + if (addNew) { + _function.put(type, new CastleFunction(type, lvl, lease, 0, rate, 0, false)); + } else { + if ((lvl == 0) && (lease == 0)) { + removeFunction(type); + } else { + int diffLease = lease - _function.get(type).getLease(); + if (diffLease > 0) { + _function.remove(type); + _function.put(type, new CastleFunction(type, lvl, lease, 0, rate, -1, false)); + } else { + _function.get(type).setLease(lease); + _function.get(type).setLvl(lvl); + _function.get(type).dbSave(); + } + } + } + return true; + } + + public void activateInstance() { + loadDoor(); + } + + // This method loads castle door data from database + private void loadDoor() { + for (L2DoorInstance door : DoorData.getInstance().getDoors()) { + if ((door.getCastle() != null) && (door.getCastle().getResidenceId() == getResidenceId())) { + _doors.add(door); + } + } + } + + // This method loads castle door upgrade data from database + private void loadDoorUpgrade() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM castle_doorupgrade WHERE castleId=?")) { + ps.setInt(1, getResidenceId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + setDoorUpgrade(rs.getInt("doorId"), rs.getInt("ratio"), false); + } + } + } catch (Exception e) { + LOG.warn("Exception: loadCastleDoorUpgrade(): {}", e.getMessage(), e); + } + } + + private void removeDoorUpgrade() { + for (L2DoorInstance door : _doors) { + door.getStat().setUpgradeHpRatio(1); + door.setCurrentHp(door.getCurrentHp()); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM castle_doorupgrade WHERE castleId=?")) { + ps.setInt(1, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: removeDoorUpgrade(): {}", e.getMessage(), e); + } + } + + public void setDoorUpgrade(int doorId, int ratio, boolean save) { + final L2DoorInstance door = (getDoors().isEmpty()) ? DoorData.getInstance().getDoor(doorId) : getDoor(doorId); + if (door == null) { + return; + } + + door.getStat().setUpgradeHpRatio(ratio); + door.setCurrentHp(door.getMaxHp()); + + if (save) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO castle_doorupgrade (doorId, ratio, castleId) values (?,?,?)")) { + ps.setInt(1, doorId); + ps.setInt(2, ratio); + ps.setInt(3, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: setDoorUpgrade(int doorId, int ratio, int castleId): {}", e.getMessage(), e); + } + } + } + + private void updateOwnerInDB(L2Clan clan) { + if (clan != null) { + _ownerId = clan.getId(); // Update owner id property + } else { + _ownerId = 0; // Remove owner + CastleManorManager.getInstance().resetManorData(getResidenceId()); + } + + try (var con = ConnectionFactory.getInstance().getConnection()) { + // Need to remove has castle flag from clan_data, should be checked from castle table. + try (var ps = con.prepareStatement("UPDATE clan_data SET hasCastle = 0 WHERE hasCastle = ?")) { + ps.setInt(1, getResidenceId()); + ps.execute(); + } + + try (var ps = con.prepareStatement("UPDATE clan_data SET hasCastle = ? WHERE clan_id = ?")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, getOwnerId()); + ps.execute(); + } + + // Announce to clan members + if (clan != null) { + clan.setCastleId(getResidenceId()); // Set has castle flag for new owner + clan.broadcastToOnlineMembers(new PledgeShowInfoUpdate(clan)); + clan.broadcastToOnlineMembers(Music.SIEGE_VICTORY.getPacket()); + } + } catch (Exception e) { + LOG.warn("Exception: updateOwnerInDB(L2Clan clan): {}", e.getMessage(), e); + } + } + + public L2DoorInstance getDoor(int doorId) { + if (doorId <= 0) { + return null; + } + + for (L2DoorInstance door : getDoors()) { + if (door.getId() == doorId) { + return door; + } + } + return null; + } + + public List getDoors() { + return _doors; + } + + public int getOwnerId() { + return _ownerId; + } + + public L2Clan getOwner() { + return (_ownerId != 0) ? ClanTable.getInstance().getClan(_ownerId) : null; + } + + public Siege getSiege() { + if (_siege == null) { + _siege = new Siege(this); + } + return _siege; + } + + public Calendar getSiegeDate() { + return _siegeDate; + } + + public boolean getIsTimeRegistrationOver() { + return _isTimeRegistrationOver; + } + + public void setIsTimeRegistrationOver(boolean val) { + _isTimeRegistrationOver = val; + } + + public Calendar getTimeRegistrationOverDate() { + if (_siegeTimeRegistrationEndDate == null) { + _siegeTimeRegistrationEndDate = Calendar.getInstance(); + } + return _siegeTimeRegistrationEndDate; + } + + public int getTaxPercent() { + return _taxPercent; + } + + public double getTaxRate() { + return _taxRate; + } + + public int getNextTaxPercent() { + return _nextTaxPercent; + } + + public long getNextTaxTime() { + return _nextTaxTime; + } + + public long getTreasury() { + return _treasury; + } + + public boolean getShowNpcCrest() { + return _showNpcCrest; + } + + public void setShowNpcCrest(boolean showNpcCrest) { + if (_showNpcCrest != showNpcCrest) { + _showNpcCrest = showNpcCrest; + updateShowNpcCrest(); + } + } + + public void updateClansReputation() { + if (_formerOwner != null) { + if (_formerOwner != ClanTable.getInstance().getClan(getOwnerId())) { + int maxreward = Math.max(0, _formerOwner.getReputationScore()); + _formerOwner.takeReputationScore(clan().getLoseCastlePoints(), true); + L2Clan owner = ClanTable.getInstance().getClan(getOwnerId()); + if (owner != null) { + owner.addReputationScore(Math.min(clan().getTakeCastlePoints(), maxreward), true); + } + } else { + _formerOwner.addReputationScore(clan().getCastleDefendedPoints(), true); + } + } else { + L2Clan owner = ClanTable.getInstance().getClan(getOwnerId()); + if (owner != null) { + owner.addReputationScore(clan().getTakeCastlePoints(), true); + } + } + } + + public void updateShowNpcCrest() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle SET showNpcCrest = ? WHERE id = ?")) { + ps.setBoolean(1, getShowNpcCrest()); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.info("Error saving showNpcCrest for castle " + getName() + ": " + e.getMessage()); + } + } + + @Override + public void giveResidentialSkills(L2PcInstance player) { + Territory territory = TerritoryWarManager.getInstance().getTerritory(getResidenceId()); + if ((territory != null) && territory.getOwnedWardIds().contains(getResidenceId() + 80)) { + for (int wardId : territory.getOwnedWardIds()) { + final List territorySkills = SkillTreesData.getInstance().getAvailableResidentialSkills(wardId); + for (L2SkillLearn s : territorySkills) { + final Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + if (sk != null) { + player.addSkill(sk, false); + } else { + LOG.warn("Trying to add a null skill for Territory Ward Id: {}, skill Id: {} level: {}!", wardId, s.getSkillId(), s.getSkillLevel()); + } + } + } + } + super.giveResidentialSkills(player); + } + + @Override + public void removeResidentialSkills(L2PcInstance player) { + if (TerritoryWarManager.getInstance().getTerritory(getResidenceId()) != null) { + for (int wardId : TerritoryWarManager.getInstance().getTerritory(getResidenceId()).getOwnedWardIds()) { + final List territorySkills = SkillTreesData.getInstance().getAvailableResidentialSkills(wardId); + for (L2SkillLearn s : territorySkills) { + final Skill sk = SkillData.getInstance().getSkill(s.getSkillId(), s.getSkillLevel()); + if (sk != null) { + player.removeSkill(sk, false, true); + } else { + LOG.warn("Trying to remove a null skill for Territory Ward Id: {}, skill Id: {} level: {}!", wardId, s.getSkillId(), s.getSkillLevel()); + } + } + } + } + super.removeResidentialSkills(player); + } + + /** + * Register Artefact to castle + */ + public void registerArtefact(L2ArtefactInstance artefact) { + _artefacts.add(artefact); + } + + public List getArtefacts() { + return _artefacts; + } + + /** + * @return the tickets exchanged for this castle + */ + public int getTicketBuyCount() { + return _ticketBuyCount; + } + + /** + * Set the exchanged tickets count.
          + * Performs database update. + * @param count the ticket count to set + */ + public void setTicketBuyCount(int count) { + _ticketBuyCount = count; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE castle SET ticketBuyCount = ? WHERE id = ?")) { + ps.setInt(1, _ticketBuyCount); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn(e.getMessage(), e); + } + } + + public int getTrapUpgradeLevel(int towerIndex) { + final TowerSpawn spawn = SiegeManager.getInstance().getFlameTowers(getResidenceId()).get(towerIndex); + return (spawn != null) ? spawn.getUpgradeLevel() : 0; + } + + public void setTrapUpgrade(int towerIndex, int level, boolean save) { + if (save) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO castle_trapupgrade (castleId, towerIndex, level) values (?,?,?)")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, towerIndex); + ps.setInt(3, level); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: setTrapUpgradeLevel(int towerIndex, int level, int castleId): {}", e.getMessage(), e); + } + } + final TowerSpawn spawn = SiegeManager.getInstance().getFlameTowers(getResidenceId()).get(towerIndex); + if (spawn != null) { + spawn.setUpgradeLevel(level); + } + } + + private void removeTrapUpgrade() { + for (TowerSpawn ts : SiegeManager.getInstance().getFlameTowers(getResidenceId())) { + ts.setUpgradeLevel(0); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM castle_trapupgrade WHERE castleId=?")) { + ps.setInt(1, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: removeDoorUpgrade(): {}", e.getMessage(), e); + } + } + + @Override + protected void initResidenceZone() { + for (L2CastleZone zone : ZoneManager.getInstance().getAllZones(L2CastleZone.class)) { + if (zone.getResidenceId() == getResidenceId()) { + setResidenceZone(zone); + break; + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/ClanHall.java b/src/main/java/com/l2jserver/gameserver/model/entity/ClanHall.java new file mode 100644 index 00000000..c17967ab --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/ClanHall.java @@ -0,0 +1,438 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.zone.type.L2ClanHallZone; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowInfoUpdate; + +public abstract class ClanHall { + private static final Logger LOG = LoggerFactory.getLogger(ClanHall.class); + + private final int _clanHallId; + private ArrayList _doors; + private final String _name; + private int _ownerId; + private final String _desc; + private final String _location; + private L2ClanHallZone _zone; + protected boolean _isFree = true; + private final Map _functions; + + /** Clan Hall Functions */ + public static final int FUNC_RESTORE_HP = 1; + public static final int FUNC_RESTORE_MP = 2; + public static final int FUNC_RESTORE_CP = 3; + public static final int FUNC_RESTORE_EXP = 4; + public static final int FUNC_TELEPORT = 5; + public static final int FUNC_BROADCAST = 6; + public static final int FUNC_DECO_CURTAINS = 7; + public static final int FUNC_DECO_HANGING = 8; + public static final int FUNC_SUPPORT = 9; + public static final int FUNC_DECO_OUTERFLAG = 10; + public static final int FUNC_DECO_FRONTPLATEFORM = 11; + public static final int FUNC_ITEM_CREATE = 12; + + public class ClanHallFunction { + private final int _type; + private int _lvl; + protected int _fee; + protected int _tempFee; + private final long _rate; + private long _endDate; + protected boolean _inDebt; + public boolean _cwh; // first activating clanhall function is paid from player inventory, any others from clan warehouse + + public ClanHallFunction(int type, int lvl, int lease, int tempLease, long rate, long time, boolean cwh) { + _type = type; + _lvl = lvl; + _fee = lease; + _tempFee = tempLease; + _rate = rate; + _endDate = time; + initializeTask(cwh); + } + + public int getType() { + return _type; + } + + public int getLvl() { + return _lvl; + } + + public int getLease() { + return _fee; + } + + public long getRate() { + return _rate; + } + + public long getEndTime() { + return _endDate; + } + + public void setLvl(int lvl) { + _lvl = lvl; + } + + public void setLease(int lease) { + _fee = lease; + } + + public void setEndTime(long time) { + _endDate = time; + } + + private void initializeTask(boolean cwh) { + if (_isFree) { + return; + } + long currentTime = System.currentTimeMillis(); + if (_endDate > currentTime) { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), _endDate - currentTime); + } else { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), 0); + } + } + + private class FunctionTask implements Runnable { + public FunctionTask(boolean cwh) { + _cwh = cwh; + } + + @Override + public void run() { + try { + if (_isFree) { + return; + } + if ((ClanTable.getInstance().getClan(getOwnerId()).getWarehouse().getAdena() >= _fee) || !_cwh) { + int fee = _fee; + if (getEndTime() == -1) { + fee = _tempFee; + } + + setEndTime(System.currentTimeMillis() + getRate()); + dbSave(); + if (_cwh) { + ClanTable.getInstance().getClan(getOwnerId()).getWarehouse().destroyItemByItemId("CH_function_fee", Inventory.ADENA_ID, fee, null, null); + } + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(true), getRate()); + } else { + removeFunction(getType()); + } + } catch (Exception e) { + LOG.error(e.getMessage(), e); + } + } + } + + public void dbSave() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO clanhall_functions (hall_id, type, lvl, lease, rate, endTime) VALUES (?,?,?,?,?,?)")) { + ps.setInt(1, getId()); + ps.setInt(2, getType()); + ps.setInt(3, getLvl()); + ps.setInt(4, getLease()); + ps.setLong(5, getRate()); + ps.setLong(6, getEndTime()); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: ClanHall.updateFunctions(int type, int lvl, int lease, long rate, long time, boolean addNew): {}", e.getMessage(), e); + } + } + } + + public ClanHall(StatsSet set) { + _clanHallId = set.getInt("id"); + _name = set.getString("name"); + _ownerId = set.getInt("ownerId"); + _desc = set.getString("desc"); + _location = set.getString("location"); + _functions = new ConcurrentHashMap<>(); + + if (_ownerId > 0) { + L2Clan clan = ClanTable.getInstance().getClan(_ownerId); + if (clan != null) { + clan.setHideoutId(getId()); + } else { + free(); + } + } + } + + /** + * @return Id Of Clan hall + */ + public final int getId() { + return _clanHallId; + } + + /** + * @return the Clan Hall name. + */ + public final String getName() { + return _name; + } + + /** + * @return OwnerId + */ + public final int getOwnerId() { + return _ownerId; + } + + /** + * @return Desc + */ + public final String getDesc() { + return _desc; + } + + /** + * @return Location + */ + public final String getLocation() { + return _location; + } + + /** + * @return all DoorInstance + */ + public final ArrayList getDoors() { + if (_doors == null) { + _doors = new ArrayList<>(); + } + return _doors; + } + + public final L2DoorInstance getDoor(int doorId) { + if (doorId <= 0) { + return null; + } + for (L2DoorInstance door : getDoors()) { + if (door.getId() == doorId) { + return door; + } + } + return null; + } + + /** + * @param type + * @return function with id + */ + public ClanHallFunction getFunction(int type) { + return _functions.get(type); + } + + /** + * Sets this clan halls zone + */ + public void setZone(L2ClanHallZone zone) { + _zone = zone; + } + + /** + * @return true if object is inside the zone + */ + public boolean checkIfInZone(int x, int y, int z) { + return getZone().isInsideZone(x, y, z); + } + + /** + * @return the zone of this clan hall + */ + public L2ClanHallZone getZone() { + return _zone; + } + + /** Free this clan hall */ + public void free() { + _ownerId = 0; + _isFree = true; + for (Integer fc : _functions.keySet()) { + removeFunction(fc); + } + _functions.clear(); + updateDb(); + } + + /** + * Set owner if clan hall is free + */ + public void setOwner(L2Clan clan) { + // Verify that this ClanHall is Free and Clan isn't null + if ((_ownerId > 0) || (clan == null)) { + return; + } + _ownerId = clan.getId(); + _isFree = false; + clan.setHideoutId(getId()); + // Announce to Online member new ClanHall + clan.broadcastToOnlineMembers(new PledgeShowInfoUpdate(clan)); + updateDb(); + } + + /** + * Open or Close Door + */ + public void openCloseDoor(L2PcInstance activeChar, int doorId, boolean open) { + if ((activeChar != null) && (activeChar.getClanId() == getOwnerId())) { + openCloseDoor(doorId, open); + } + } + + public void openCloseDoor(int doorId, boolean open) { + openCloseDoor(getDoor(doorId), open); + } + + public void openCloseDoor(L2DoorInstance door, boolean open) { + if (door != null) { + if (open) { + door.openMe(); + } else { + door.closeMe(); + } + } + } + + public void openCloseDoors(L2PcInstance activeChar, boolean open) { + if ((activeChar != null) && (activeChar.getClanId() == getOwnerId())) { + openCloseDoors(open); + } + } + + public void openCloseDoors(boolean open) { + for (L2DoorInstance door : getDoors()) { + if (door != null) { + if (open) { + door.openMe(); + } else { + door.closeMe(); + } + } + } + } + + public void banishForeigners() { + if (_zone != null) { + _zone.banishForeigners(getOwnerId()); + } else { + LOG.warn("Zone is null for clan hall: {} {}", getId(), getName()); + } + } + + protected void loadFunctions() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM clanhall_functions WHERE hall_id = ?")) { + ps.setInt(1, getId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _functions.put(rs.getInt("type"), new ClanHallFunction(rs.getInt("type"), rs.getInt("lvl"), rs.getInt("lease"), 0, rs.getLong("rate"), rs.getLong("endTime"), true)); + } + } + } catch (Exception e) { + LOG.error("Exception: ClanHall.loadFunctions(): {}", e.getMessage(), e); + } + } + + /** + * Remove function In List and in DB + */ + public void removeFunction(int functionType) { + _functions.remove(functionType); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM clanhall_functions WHERE hall_id=? AND type=?")) { + ps.setInt(1, getId()); + ps.setInt(2, functionType); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: ClanHall.removeFunctions(int functionType): {}", e.getMessage(), e); + } + } + + public boolean updateFunctions(L2PcInstance player, int type, int lvl, int lease, long rate, boolean addNew) { + if (player == null) { + return false; + } + + if (lease > 0) { + if (!player.destroyItemByItemId("Consume", Inventory.ADENA_ID, lease, null, true)) { + return false; + } + } + if (addNew) { + _functions.put(type, new ClanHallFunction(type, lvl, lease, 0, rate, 0, false)); + } else { + if ((lvl == 0) && (lease == 0)) { + removeFunction(type); + } else { + int diffLease = lease - _functions.get(type).getLease(); + if (diffLease > 0) { + _functions.remove(type); + _functions.put(type, new ClanHallFunction(type, lvl, lease, 0, rate, -1, false)); + } else { + _functions.get(type).setLease(lease); + _functions.get(type).setLvl(lvl); + _functions.get(type).dbSave(); + } + } + } + return true; + } + + public int getGrade() { + return 0; + } + + public long getPaidUntil() { + return 0; + } + + public int getLease() { + return 0; + } + + public boolean isSiegableHall() { + return false; + } + + public boolean isFree() { + return _isFree; + } + + public abstract void updateDb(); +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Couple.java b/src/main/java/com/l2jserver/gameserver/model/entity/Couple.java new file mode 100644 index 00000000..424f324b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Couple.java @@ -0,0 +1,142 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import java.util.Calendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.idfactory.IdFactory; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; + +/** + * @author evill33t + */ +public class Couple { + private static final Logger LOG = LoggerFactory.getLogger(Couple.class); + + private int _Id = 0; + private int _player1Id = 0; + private int _player2Id = 0; + private boolean _married = false; + private Calendar _affiancedDate; + private Calendar _weddingDate; + + public Couple(int coupleId) { + _Id = coupleId; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM mods_wedding WHERE id = ?")) { + ps.setInt(1, _Id); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _player1Id = rs.getInt("player1Id"); + _player2Id = rs.getInt("player2Id"); + _married = rs.getBoolean("married"); + + _affiancedDate = Calendar.getInstance(); + _affiancedDate.setTimeInMillis(rs.getLong("affianceDate")); + + _weddingDate = Calendar.getInstance(); + _weddingDate.setTimeInMillis(rs.getLong("weddingDate")); + } + } + } catch (Exception e) { + LOG.error("Exception: Couple.load(): {}", e.getMessage(), e); + } + } + + public Couple(L2PcInstance player1, L2PcInstance player2) { + int _tempPlayer1Id = player1.getObjectId(); + int _tempPlayer2Id = player2.getObjectId(); + + _player1Id = _tempPlayer1Id; + _player2Id = _tempPlayer2Id; + + _affiancedDate = Calendar.getInstance(); + _affiancedDate.setTimeInMillis(Calendar.getInstance().getTimeInMillis()); + + _weddingDate = Calendar.getInstance(); + _weddingDate.setTimeInMillis(Calendar.getInstance().getTimeInMillis()); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO mods_wedding (id, player1Id, player2Id, married, affianceDate, weddingDate) VALUES (?, ?, ?, ?, ?, ?)")) { + _Id = IdFactory.getInstance().getNextId(); + ps.setInt(1, _Id); + ps.setInt(2, _player1Id); + ps.setInt(3, _player2Id); + ps.setBoolean(4, false); + ps.setLong(5, _affiancedDate.getTimeInMillis()); + ps.setLong(6, _weddingDate.getTimeInMillis()); + ps.execute(); + } catch (Exception e) { + LOG.error("Could not create couple: {}", e.getMessage(), e); + } + } + + public void marry() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE mods_wedding set married = ?, weddingDate = ? where id = ?")) { + ps.setBoolean(1, true); + _weddingDate = Calendar.getInstance(); + ps.setLong(2, _weddingDate.getTimeInMillis()); + ps.setInt(3, _Id); + ps.execute(); + _married = true; + } catch (Exception e) { + LOG.error("Could not marry: {}", e.getMessage(), e); + } + } + + public void divorce() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM mods_wedding WHERE id=?")) { + ps.setInt(1, _Id); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Couple.divorce(): {}", e.getMessage(), e); + } + } + + public final int getId() { + return _Id; + } + + public final int getPlayer1Id() { + return _player1Id; + } + + public final int getPlayer2Id() { + return _player2Id; + } + + public final boolean getMarried() { + return _married; + } + + public final Calendar getAffiancedDate() { + return _affiancedDate; + } + + public final Calendar getWeddingDate() { + return _weddingDate; + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/DimensionalRift.java b/src/main/java/com/l2jserver/gameserver/model/entity/DimensionalRift.java new file mode 100644 index 00000000..db40944b --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/DimensionalRift.java @@ -0,0 +1,370 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.general; + +import java.util.ArrayList; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Future; + +import com.l2jserver.commons.util.Rnd; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.instancemanager.DimensionalRiftManager; +import com.l2jserver.gameserver.instancemanager.QuestManager; +import com.l2jserver.gameserver.model.L2Party; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.quest.Quest; +import com.l2jserver.gameserver.model.quest.QuestState; +import com.l2jserver.gameserver.model.quest.State; +import com.l2jserver.gameserver.network.serverpackets.Earthquake; + +/** + * Thanks to L2Fortress and balancer.ru - kombat + */ +public class DimensionalRift { + protected byte _type; + protected L2Party _party; + protected List _completedRooms = new ArrayList<>(); + private static final long seconds_5 = 5000L; + + protected byte jumps_current = 0; + private final int MAX_DISTANCE = 1500; + + private Timer teleporterTimer; + private TimerTask teleporterTimerTask; + private Timer spawnTimer; + private TimerTask spawnTimerTask; + + private Future earthQuakeTask; + + protected byte _chosenRoom; + protected List _deadPlayers = new CopyOnWriteArrayList<>(); + protected List _waitingRoomPlayers = new CopyOnWriteArrayList<>(); + private boolean isBossRoom = false; + + private final int Q00635_IntoTheDimensionalRift = 635; + + public DimensionalRift(L2Npc npc, L2Party party, byte type, byte room) { + DimensionalRiftManager.getInstance().getRoom(type, room).setParty(party); + + _type = type; + _party = party; + _chosenRoom = room; + party.setDimensionalRift(this); + for (L2PcInstance pc : party.getMembers()) { + final Quest q635 = QuestManager.getInstance().getQuest(Q00635_IntoTheDimensionalRift); + if (q635 != null) { + QuestState qs = pc.getQuestState(q635.getName()); + if (qs == null) { + qs = q635.newQuestState(pc); + } + if (!qs.isStarted()) { + qs.setState(State.STARTED, true); + } + } + + if (npc.calculateDistance(pc, true, false) <= MAX_DISTANCE) { + teleportToRoom(pc, room); + } else { + _waitingRoomPlayers.add(pc); + } + } + + createSpawnTimer(_chosenRoom); + createTeleporterTimer(true); + } + + public byte getType() { + return _type; + } + + public byte getCurrentRoom() { + return _chosenRoom; + } + + protected void createTeleporterTimer(final boolean reasonTP) { + if (_party == null) { + return; + } + + if (teleporterTimerTask != null) { + teleporterTimerTask.cancel(); + teleporterTimerTask = null; + } + + if (teleporterTimer != null) { + teleporterTimer.cancel(); + teleporterTimer = null; + } + + if (earthQuakeTask != null) { + earthQuakeTask.cancel(false); + earthQuakeTask = null; + } + + teleporterTimer = new Timer(); + teleporterTimerTask = new TimerTask() { + @Override + public void run() { + if (_chosenRoom > -1) { + DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).unspawn().setParty(null); + } + + if (reasonTP && (jumps_current < getMaxJumps()) && (_party.getMemberCount() > _deadPlayers.size() + _waitingRoomPlayers.size())) { + jumps_current++; + + _completedRooms.add(_chosenRoom); + _chosenRoom = -1; + + for (L2PcInstance p : _party.getMembers()) { + if (!_waitingRoomPlayers.contains(p)) { + teleportToNextRoom(p); + } + } + + createTeleporterTimer(true); + createSpawnTimer(_chosenRoom); + } else { + for (L2PcInstance p : _party.getMembers()) { + if (!_waitingRoomPlayers.contains(p)) { + teleportToWaitingRoom(p); + } + } + + killRift(); + cancel(); + } + } + }; + + if (reasonTP) { + long jumpTime = calcTimeToNextJump(); + teleporterTimer.schedule(teleporterTimerTask, jumpTime); // Teleporter task, 8-10 minutes + + earthQuakeTask = ThreadPoolManager.getInstance().scheduleGeneral(() -> { + for (L2PcInstance p : _party.getMembers()) { + if (!_waitingRoomPlayers.contains(p)) { + p.sendPacket(new Earthquake(p.getX(), p.getY(), p.getZ(), 65, 9)); + } + } + }, jumpTime - 7000); + } else { + teleporterTimer.schedule(teleporterTimerTask, seconds_5); // incorrect party member invited. + } + } + + public void createSpawnTimer(final byte room) { + if (spawnTimerTask != null) { + spawnTimerTask.cancel(); + spawnTimerTask = null; + } + + if (spawnTimer != null) { + spawnTimer.cancel(); + spawnTimer = null; + } + + spawnTimer = new Timer(); + spawnTimerTask = new TimerTask() { + @Override + public void run() { + DimensionalRiftManager.getInstance().getRoom(_type, room).spawn(); + } + }; + + spawnTimer.schedule(spawnTimerTask, general().getRiftSpawnDelay()); + } + + public void partyMemberInvited() { + createTeleporterTimer(false); + } + + public void partyMemberExited(L2PcInstance player) { + _deadPlayers.remove(player); + _waitingRoomPlayers.remove(player); + + killRift(); + } + + public void manualTeleport(L2PcInstance player) { + if (!player.isInParty() || !player.getParty().isInDimensionalRift()) { + return; + } + + DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).unspawn().setParty(null); + _completedRooms.add(_chosenRoom); + _chosenRoom = -1; + + for (L2PcInstance p : _party.getMembers()) { + if (!_waitingRoomPlayers.contains(p)) { + teleportToNextRoom(p); + } + } + + DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).setParty(player.getParty()); + + createSpawnTimer(_chosenRoom); + createTeleporterTimer(true); + } + + public void manualExitRift(L2PcInstance player) { + if (!player.isInParty() || !player.getParty().isInDimensionalRift()) { + return; + } + + for (L2PcInstance p : player.getParty().getMembers()) { + if (!_waitingRoomPlayers.contains(p)) { + teleportToWaitingRoom(p); + } + } + + killRift(); + } + + protected void teleportToNextRoom(L2PcInstance player) { + if (_chosenRoom == -1) { + List emptyRooms; + do { + emptyRooms = DimensionalRiftManager.getInstance().getFreeRooms(_type); + // Do not tp in the same room a second time + emptyRooms.removeAll(_completedRooms); + // If no room left, find any empty + if (emptyRooms.isEmpty()) { + emptyRooms = DimensionalRiftManager.getInstance().getFreeRooms(_type); + } + _chosenRoom = emptyRooms.get(Rnd.get(1, emptyRooms.size()) - 1); + } + while (DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).getParty() != null); + } + + DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).setParty(player.getParty()); + checkBossRoom(_chosenRoom); + + teleportToRoom(player, _chosenRoom); + } + + protected void teleportToWaitingRoom(L2PcInstance player) { + DimensionalRiftManager.getInstance().teleportToWaitingRoom(player); + } + + public void killRift() { + _completedRooms.clear(); + + if (_party != null) { + _party.setDimensionalRift(null); + } + + _party = null; + _waitingRoomPlayers = null; + _deadPlayers = null; + + if (earthQuakeTask != null) { + earthQuakeTask.cancel(false); + earthQuakeTask = null; + } + + DimensionalRiftManager.getInstance().getRoom(_type, _chosenRoom).unspawn().setParty(null); + DimensionalRiftManager.getInstance().killRift(this); + } + + public Timer getTeleportTimer() { + return teleporterTimer; + } + + public TimerTask getTeleportTimerTask() { + return teleporterTimerTask; + } + + public Timer getSpawnTimer() { + return spawnTimer; + } + + public TimerTask getSpawnTimerTask() { + return spawnTimerTask; + } + + public void setTeleportTimer(Timer t) { + teleporterTimer = t; + } + + public void setTeleportTimerTask(TimerTask tt) { + teleporterTimerTask = tt; + } + + public void setSpawnTimer(Timer t) { + spawnTimer = t; + } + + public void setSpawnTimerTask(TimerTask st) { + spawnTimerTask = st; + } + + private long calcTimeToNextJump() { + final int time = Rnd.get((int) general().getAutoJumpsDelayMin(), (int) general().getAutoJumpsDelayMax()); + if (isBossRoom) { + return (long) (time * general().getBossRoomTimeMultiply()); + } + return time; + } + + public void memberDead(L2PcInstance player) { + if (!_deadPlayers.contains(player)) { + _deadPlayers.add(player); + } + } + + public void memberResurrected(L2PcInstance player) { + _deadPlayers.remove(player); + } + + public List getDeadPlayers() { + return _deadPlayers; + } + + public List getWaitingRoomPlayers() { + return _waitingRoomPlayers; + } + + public void checkBossRoom(byte room) { + isBossRoom = DimensionalRiftManager.getInstance().getRoom(_type, room).isBossRoom(); + } + + public Location getRoomCoord(byte room) { + return DimensionalRiftManager.getInstance().getRoom(_type, room).getTeleportCoordinates(); + } + + public int getMaxJumps() { + if ((general().getMaxRiftJumps() <= 8) && (general().getMaxRiftJumps() >= 1)) { + return general().getMaxRiftJumps(); + } + + return 4; + } + + private void teleportToRoom(L2PcInstance player, byte room) { + Location coords = getRoomCoord(room); + player.teleToLocation(coords.getX() + Rnd.get(-150, 150), coords.getY() + Rnd.get(-150, 150), coords.getZ()); + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Duel.java b/src/main/java/com/l2jserver/gameserver/model/entity/Duel.java new file mode 100644 index 00000000..ce003bf6 --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Duel.java @@ -0,0 +1,660 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.ai.CtrlIntention; +import com.l2jserver.gameserver.enums.DuelResult; +import com.l2jserver.gameserver.enums.DuelState; +import com.l2jserver.gameserver.enums.Team; +import com.l2jserver.gameserver.enums.audio.Music; +import com.l2jserver.gameserver.instancemanager.DuelManager; +import com.l2jserver.gameserver.instancemanager.InstanceManager; +import com.l2jserver.gameserver.model.Location; +import com.l2jserver.gameserver.model.actor.L2Summon; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.skills.BuffInfo; +import com.l2jserver.gameserver.model.zone.ZoneId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ActionFailed; +import com.l2jserver.gameserver.network.serverpackets.ExDuelEnd; +import com.l2jserver.gameserver.network.serverpackets.ExDuelReady; +import com.l2jserver.gameserver.network.serverpackets.ExDuelStart; +import com.l2jserver.gameserver.network.serverpackets.ExDuelUpdateUserInfo; +import com.l2jserver.gameserver.network.serverpackets.L2GameServerPacket; +import com.l2jserver.gameserver.network.serverpackets.PlaySound; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +/** + * @author Zealar + */ +public class Duel { + protected static final Logger LOG = LoggerFactory.getLogger(Duel.class); + + private static final PlaySound B04_S01 = Music.B04_S01.getPacket(); + + private static final int PARTY_DUEL_DURATION = 300; + private static final int PARTY_DUEL_PREPARE_TIME = 30; + private static final int PARTY_DUEL_TELEPORT_BACK_TIME = 10 * 1000; + private static final int PLAYER_DUEL_DURATION = 120; + private static final int DUEL_PREPARE_TIME = 5; + + private final int _duelId; + private final List _teamA; + private final L2PcInstance _leaderA; + private final List _teamB; + private final L2PcInstance _leaderB; + private final boolean _partyDuel; + private final Calendar _duelEndTime; + private int _surrenderRequest = 0; + private int _countdown; + private final Map _playerConditions = new ConcurrentHashMap<>(); + private int _duelInstanceId; + + public Duel(L2PcInstance playerA, L2PcInstance playerB, boolean partyDuel, int duelId) { + _duelId = duelId; + if (partyDuel) { + _leaderA = playerA; + _leaderB = playerB; + _teamA = new ArrayList<>(playerA.getParty().getMembers()); + _teamB = new ArrayList<>(playerB.getParty().getMembers()); + } else { + _leaderA = playerA; + _leaderB = playerB; + + _teamA = new ArrayList<>(); + _teamB = new ArrayList<>(); + + _teamA.add(playerA); + _teamB.add(playerB); + } + _partyDuel = partyDuel; + + _duelEndTime = Calendar.getInstance(); + _duelEndTime.add(Calendar.SECOND, _partyDuel ? PARTY_DUEL_DURATION : PLAYER_DUEL_DURATION); + + savePlayerConditions(); + if (_partyDuel) { + _countdown = PARTY_DUEL_PREPARE_TIME; + teleportPlayers(); + } else { + _countdown = DUEL_PREPARE_TIME; + } + // Schedule duel start + ThreadPoolManager.getInstance().scheduleGeneral(new DuelPreparationTask(this), _countdown - 3); + } + + public static class PlayerCondition { + private final L2PcInstance _player; + private L2Summon _summon; + private final double _hp; + private final double _mp; + private final double _cp; + private final boolean _paDuel; + private Location _loc; + private final List _playerEffects; + private List _petEffects; + + public PlayerCondition(L2PcInstance player, boolean partyDuel) { + _player = player; + _hp = _player.getCurrentHp(); + _mp = _player.getCurrentMp(); + _cp = _player.getCurrentCp(); + _paDuel = partyDuel; + + _playerEffects = new ArrayList<>(player.getEffectList().getEffects()); + + if (_paDuel) { + _loc = _player.getLocation(); + } + + if (player.hasSummon()) { + _summon = player.getSummon(); + _petEffects = new ArrayList<>(player.getSummon().getEffectList().getEffects()); + } + } + + public void restoreCondition() { + if (_player == null) { + return; + } + _player.setCurrentHp(_hp); + _player.setCurrentMp(_mp); + _player.setCurrentCp(_cp); + + _player.setIsInDuel(0); + _player.setTeam(Team.NONE); + _player.broadcastUserInfo(); + + if (_paDuel) { + teleportBack(); + } + _player.getEffectList().stopAllEffects(); + for (BuffInfo skill : _playerEffects) { + if ((skill != null) && (skill.getTime() > 0)) { + skill.getSkill().applyEffects(_player, _player, true, skill.getTime()); + } + } + if (_player.hasSummon()) { + _player.getSummon().getEffectList().stopAllEffects(); + if ((_summon != null) && (_summon == _player.getSummon())) { + for (BuffInfo skill : _petEffects) { + if ((skill != null) && (skill.getTime() > 0)) { + skill.getSkill().applyEffects(_summon, _summon, true, skill.getTime()); + } + } + } + } + } + + public void teleportBack() { + _player.teleToLocation(_loc); + } + + public L2PcInstance getPlayer() { + return _player; + } + } + + public static class DuelPreparationTask implements Runnable { + private final Duel _duel; + + public DuelPreparationTask(Duel duel) { + _duel = duel; + } + + @Override + public void run() { + try { + if (_duel.countdown() > 0) // duel not started yet - continue countdown + { + ThreadPoolManager.getInstance().scheduleGeneral(this, 1000); + } else { + _duel.startDuel(); + } + } catch (Exception e) { + LOG.error("There has been a problem while running a duel start task!", e); + } + } + } + + public class DuelClockTask implements Runnable { + private final Duel _duel; + + public DuelClockTask(Duel duel) { + _duel = duel; + } + + @Override + public void run() { + try { + if (_duel.checkEndDuelCondition() == DuelResult.CONTINUE) { + ThreadPoolManager.getInstance().scheduleGeneral(this, 1000); + } else { + endDuel(); + } + } catch (Exception e) { + LOG.error("There has been a problem while running a duel task!", e); + } + } + } + + /** + * Starts the duel + */ + public void startDuel() { + // Set duel state and team + + // Send duel packets + broadcastToTeam1(ExDuelReady.PARTY_DUEL); + broadcastToTeam2(ExDuelReady.PARTY_DUEL); + broadcastToTeam1(ExDuelStart.PARTY_DUEL); + broadcastToTeam2(ExDuelStart.PARTY_DUEL); + + for (L2PcInstance temp : _teamA) { + temp.cancelActiveTrade(); + temp.setIsInDuel(_duelId); + temp.setTeam(Team.BLUE); + temp.broadcastUserInfo(); + broadcastToTeam2(new ExDuelUpdateUserInfo(temp)); + } + for (L2PcInstance temp : _teamB) { + temp.cancelActiveTrade(); + temp.setIsInDuel(_duelId); + temp.setTeam(Team.RED); + temp.broadcastUserInfo(); + broadcastToTeam1(new ExDuelUpdateUserInfo(temp)); + } + + if (_partyDuel) { + // Close doors chickens cannot run from the destiny + for (L2DoorInstance door : InstanceManager.getInstance().getInstance(getDuelInstanceId()).getDoors()) { + if ((door != null) && door.getOpen()) { + door.closeMe(); + } + } + } + + // play sound + broadcastToTeam1(B04_S01); + broadcastToTeam2(B04_S01); + + // start dueling task + ThreadPoolManager.getInstance().scheduleGeneral(new DuelClockTask(this), 1000); + } + + /** + * Stops all players from attacking. Used for duel timeout / interrupt. + */ + private void stopFighting() { + for (L2PcInstance temp : _teamA) { + temp.abortCast(); + temp.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + temp.setTarget(null); + temp.sendPacket(ActionFailed.STATIC_PACKET); + } + for (L2PcInstance temp : _teamB) { + temp.abortCast(); + temp.getAI().setIntention(CtrlIntention.AI_INTENTION_ACTIVE); + temp.setTarget(null); + temp.sendPacket(ActionFailed.STATIC_PACKET); + } + } + + /** + * Save the current player condition: hp, mp, cp, location + */ + public void savePlayerConditions() { + for (L2PcInstance player : _teamA) { + _playerConditions.put(player.getObjectId(), new PlayerCondition(player, _partyDuel)); + } + for (L2PcInstance player : _teamB) { + _playerConditions.put(player.getObjectId(), new PlayerCondition(player, _partyDuel)); + } + } + + /** + * Restore player conditions + */ + public void restorePlayerConditions() { + // restore player conditions + ThreadPoolManager.getInstance().scheduleGeneral(() -> _playerConditions.values().forEach(PlayerCondition::restoreCondition), _partyDuel ? PARTY_DUEL_TELEPORT_BACK_TIME : 1000); + + ThreadPoolManager.getInstance().scheduleGeneral(this::clear, _partyDuel ? PARTY_DUEL_TELEPORT_BACK_TIME : 1000); + } + + /** + * Get the duel id + * @return id + */ + public int getId() { + return _duelId; + } + + /** + * Gets duel instance id. + * @return id the duel id + */ + public int getDuelInstanceId() { + return _duelInstanceId; + } + + /** + * Returns the remaining time + * @return remaining time + */ + public int getRemainingTime() { + return (int) (_duelEndTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis()); + } + + /** + * Get the team that requested the duel + * @return duel requester + */ + public List getTeamA() { + return _teamA; + } + + /** + * Get the team that was challenged + * @return challenged team + */ + public List getTeamB() { + return _teamB; + } + + /** + * Get the team that requested the duel + * @return duel requester + */ + public L2PcInstance getTeamLeaderA() { + return _leaderA; + } + + /** + * Get the team that was challenged + * @return challenged team + */ + public L2PcInstance getTeamLeaderB() { + return _leaderB; + } + + /** + * Get the duel looser + * @return looser + */ + public List getLooser() { + if ((_leaderA == null) || (_leaderB == null)) { + return null; + } + if (_leaderA.getDuelState() == DuelState.WINNER) { + return _teamB; + } else if (_leaderB.getDuelState() == DuelState.WINNER) { + return _teamA; + } + return null; + } + + /** + * Returns whether this is a party duel or not + * @return is party duel + */ + public boolean isPartyDuel() { + return _partyDuel; + } + + /** + * Teleports all players to a party duel instance. + */ + public void teleportPlayers() { + if (!_partyDuel) { + return; + } + + _duelInstanceId = InstanceManager.getInstance().createDynamicInstance("PartyDuel.xml"); + + Instance instance = InstanceManager.getInstance().getInstance(_duelInstanceId); + + int i = 0; + for (L2PcInstance player : _teamA) { + Location loc = instance.getEnterLocs().get(i++); + + player.teleToLocation(loc.getX(), loc.getY(), loc.getZ(), 0, _duelInstanceId, 0); + } + + i = 9; + for (L2PcInstance player : _teamB) { + Location loc = instance.getEnterLocs().get(i++); + player.teleToLocation(loc.getX(), loc.getY(), loc.getZ(), 0, _duelInstanceId, 0); + } + } + + /** + * Broadcast a packet to the challenger team + * @param packet + */ + public void broadcastToTeam1(L2GameServerPacket packet) { + if ((_teamA == null) || _teamA.isEmpty()) { + return; + } + + for (L2PcInstance temp : _teamA) { + temp.sendPacket(packet); + } + } + + /** + * Broadcast a packet to the challenged team + * @param packet + */ + public void broadcastToTeam2(L2GameServerPacket packet) { + if ((_teamB == null) || _teamB.isEmpty()) { + return; + } + + for (L2PcInstance temp : _teamB) { + temp.sendPacket(packet); + } + } + + /** + * Playback the bow animation for all looser + */ + private void playKneelAnimation() { + List looser = getLooser(); + + if (looser == null) { + return; + } + + for (L2PcInstance temp : looser) { + temp.broadcastPacket(new SocialAction(temp.getObjectId(), 7)); + } + } + + /** + * Do the countdown and send message to players if necessary + * @return current count + */ + int countdown() { + if (--_countdown > 3) { + return _countdown; + } + SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_DUEL_WILL_BEGIN_IN_S1_SECONDS); + if (_countdown > 0) { + sm.addInt(_countdown); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.LET_THE_DUEL_BEGIN); + } + + broadcastToTeam1(sm); + broadcastToTeam2(sm); + + return _countdown; + } + + /** + * The duel has reached a state in which it can no longer continue + */ + void endDuel() { + // Send end duel packet + final ExDuelEnd duelEnd = _partyDuel ? ExDuelEnd.PARTY_DUEL : ExDuelEnd.PLAYER_DUEL; + broadcastToTeam1(duelEnd); + broadcastToTeam2(duelEnd); + playKneelAnimation(); + sendEndMessages(); + restorePlayerConditions(); + } + + /** + * Clear current duel from DuelManager + */ + private void clear() { + InstanceManager.getInstance().destroyInstance(getDuelInstanceId()); + DuelManager.getInstance().removeDuel(this); + } + + /** + * Send required messages for duel end + */ + private void sendEndMessages() { + SystemMessage sm = null; + switch (checkEndDuelCondition()) { + case TEAM_1_WIN, TEAM_2_SURRENDER -> { + if (_partyDuel) { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_PARTY_HAS_WON_THE_DUEL); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_HAS_WON_THE_DUEL); + } + sm.addString(_leaderA.getName()); + } + case TEAM_1_SURRENDER, TEAM_2_WIN -> { + if (_partyDuel) { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_PARTY_HAS_WON_THE_DUEL); + } else { + sm = SystemMessage.getSystemMessage(SystemMessageId.C1_HAS_WON_THE_DUEL); + } + sm.addString(_leaderB.getName()); + } + case CANCELED, TIMEOUT -> { + stopFighting(); + sm = SystemMessage.getSystemMessage(SystemMessageId.THE_DUEL_HAS_ENDED_IN_A_TIE); + } + } + broadcastToTeam1(sm); + broadcastToTeam2(sm); + } + + /** + * Did a situation occur in which the duel has to be ended? + * @return DuelResult duel status + */ + DuelResult checkEndDuelCondition() { + // one of the players might leave during duel + if ((_teamA == null) || (_teamB == null)) { + return DuelResult.CANCELED; + } + + // got a duel surrender request? + if (_surrenderRequest != 0) { + if (_surrenderRequest == 1) { + return DuelResult.TEAM_1_SURRENDER; + } + return DuelResult.TEAM_2_SURRENDER; + } + // duel timed out + else if (getRemainingTime() <= 0) { + return DuelResult.TIMEOUT; + } + // Has a player been declared winner yet? + else if (_leaderA.getDuelState() == DuelState.WINNER) { + // If there is a Winner already there should be no more fighting going on + stopFighting(); + return DuelResult.TEAM_1_WIN; + } else if (_leaderB.getDuelState() == DuelState.WINNER) { + // If there is a Winner already there should be no more fighting going on + stopFighting(); + return DuelResult.TEAM_2_WIN; + } + + // More end duel conditions for 1on1 duels + else if (!_partyDuel) { + // Duel was interrupted e.g.: player was attacked by mobs / other players + if ((_leaderA.getDuelState() == DuelState.INTERRUPTED) || (_leaderB.getDuelState() == DuelState.INTERRUPTED)) { + return DuelResult.CANCELED; + } + + // Are the players too far apart? + if (!_leaderA.isInsideRadius(_leaderB, 2000, false, false)) { + return DuelResult.CANCELED; + } + + // is one of the players in a Siege, Peace or PvP zone? + if (_leaderA.isInsideZone(ZoneId.PEACE) || _leaderB.isInsideZone(ZoneId.PEACE) || _leaderA.isInsideZone(ZoneId.SIEGE) || _leaderB.isInsideZone(ZoneId.SIEGE) || _leaderA.isInsideZone(ZoneId.PVP) || _leaderB.isInsideZone(ZoneId.PVP)) { + return DuelResult.CANCELED; + } + } + + return DuelResult.CONTINUE; + } + + /** + * Register a surrender request + * @param player the player that surrenders. + */ + public void doSurrender(L2PcInstance player) { + // already received a surrender request + if ((_surrenderRequest != 0) || _partyDuel) { + return; + } + + // stop the fight + stopFighting(); + + if (player == _leaderA) { + _surrenderRequest = 1; + _leaderA.setDuelState(DuelState.DEAD); + _leaderB.setDuelState(DuelState.WINNER); + } else if (player == _leaderB) { + _surrenderRequest = 2; + _leaderB.setDuelState(DuelState.DEAD); + _leaderA.setDuelState(DuelState.WINNER); + } + } + + /** + * This function is called whenever a player was defeated in a duel + * @param player the player defeated. + */ + public void onPlayerDefeat(L2PcInstance player) { + // Set player as defeated + player.setDuelState(DuelState.DEAD); + player.setTeam(Team.NONE); + + if (_partyDuel) { + boolean teamDefeated = true; + + boolean isInTeamA = true; + + if (_teamA.contains(player)) { + for (L2PcInstance temp : _teamA) { + if (temp.getDuelState() == DuelState.DUELLING) { + teamDefeated = false; + break; + } + } + } else if (_teamB.contains(player)) { + isInTeamA = false; + for (L2PcInstance temp : _teamB) { + if (temp.getDuelState() == DuelState.DUELLING) { + teamDefeated = false; + break; + } + } + } + if (teamDefeated) { + List winners = (isInTeamA ? _teamB : _teamA); + for (L2PcInstance temp : winners) { + temp.setDuelState(DuelState.WINNER); + } + } + } else { + if ((player != _leaderA) && (player != _leaderB)) { + LOG.warn("Error in onPlayerDefeat(): player is not part of this 1vs1 duel!"); + } + + if (_leaderA == player) { + _leaderB.setDuelState(DuelState.WINNER); + } else { + _leaderA.setDuelState(DuelState.WINNER); + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Fort.java b/src/main/java/com/l2jserver/gameserver/model/entity/Fort.java new file mode 100644 index 00000000..42006eda --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Fort.java @@ -0,0 +1,1045 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.clan; +import static com.l2jserver.gameserver.config.Configuration.fortress; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.FortUpdater; +import com.l2jserver.gameserver.FortUpdater.UpdaterType; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.xml.impl.DoorData; +import com.l2jserver.gameserver.data.xml.impl.StaticObjectData; +import com.l2jserver.gameserver.datatables.SpawnTable; +import com.l2jserver.gameserver.enums.MountType; +import com.l2jserver.gameserver.enums.audio.Music; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.ZoneManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.instance.L2StaticObjectInstance; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.zone.type.L2FortZone; +import com.l2jserver.gameserver.model.zone.type.L2SiegeZone; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.PledgeShowInfoUpdate; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public final class Fort extends AbstractResidence { + private static final Logger LOG = LoggerFactory.getLogger(Fort.class); + + private final List _doors = new ArrayList<>(); + private L2StaticObjectInstance _flagPole = null; + private volatile FortSiege _siege = null; + private Calendar _siegeDate; + private Calendar _lastOwnedTime; + private L2SiegeZone _zone; + private int _ownerId = 0; + private L2Clan _fortOwner = null; + private int _fortType = 0; + private int _state = 0; + private int _castleId = 0; + private int _supplyLvL = 0; + private final Map _function; + private final ScheduledFuture[] _FortUpdater = new ScheduledFuture[2]; + + // Spawn Data + private boolean _isSuspiciousMerchantSpawned = false; + private final List _siegeNpcs = new CopyOnWriteArrayList<>(); + private final List _npcCommanders = new CopyOnWriteArrayList<>(); + private final List _specialEnvoys = new CopyOnWriteArrayList<>(); + + private final Map _envoyCastles = new HashMap<>(2); + private final Set _availableCastles = new HashSet<>(1); + + /** Fortress Functions */ + public static final int FUNC_RESTORE_HP = 1; + public static final int FUNC_RESTORE_MP = 2; + public static final int FUNC_RESTORE_CP = 3; + public static final int FUNC_RESTORE_EXP = 4; + public static final int FUNC_TELEPORT = 5; + public static final int FUNC_BROADCAST = 6; + public static final int FUNC_DECO_CURTAINS = 7; + public static final int FUNC_DECO_HANGING = 8; + public static final int FUNC_SUPPORT = 9; + public static final int FUNC_DECO_OUTERFLAG = 10; + public static final int FUNC_DECO_FRONTPLATEFORM = 11; + public static final int FUNC_ITEM_CREATE = 12; + + public class FortFunction { + private final int _type; + private int _lvl; + protected int _fee; + protected int _tempFee; + private final long _rate; + private long _endDate; + protected boolean _inDebt; + public boolean _cwh; + + public FortFunction(int type, int lvl, int lease, int tempLease, long rate, long time, boolean cwh) { + _type = type; + _lvl = lvl; + _fee = lease; + _tempFee = tempLease; + _rate = rate; + _endDate = time; + initializeTask(cwh); + } + + public int getType() { + return _type; + } + + public int getLvl() { + return _lvl; + } + + public int getLease() { + return _fee; + } + + public long getRate() { + return _rate; + } + + public long getEndTime() { + return _endDate; + } + + public void setLvl(int lvl) { + _lvl = lvl; + } + + public void setLease(int lease) { + _fee = lease; + } + + public void setEndTime(long time) { + _endDate = time; + } + + private void initializeTask(boolean cwh) { + if (getOwnerClan() == null) { + return; + } + long currentTime = System.currentTimeMillis(); + if (_endDate > currentTime) { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), _endDate - currentTime); + } else { + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(cwh), 0); + } + } + + private class FunctionTask implements Runnable { + public FunctionTask(boolean cwh) { + _cwh = cwh; + } + + @Override + public void run() { + try { + if (getOwnerClan() == null) { + return; + } + if ((getOwnerClan().getWarehouse().getAdena() >= _fee) || !_cwh) { + int fee = _fee; + if (getEndTime() == -1) { + fee = _tempFee; + } + + setEndTime(System.currentTimeMillis() + getRate()); + dbSave(); + if (_cwh) { + getOwnerClan().getWarehouse().destroyItemByItemId("CS_function_fee", Inventory.ADENA_ID, fee, null, null); + } + ThreadPoolManager.getInstance().scheduleGeneral(new FunctionTask(true), getRate()); + } else { + removeFunction(getType()); + } + } catch (Throwable t) { + LOG.warn(t.getMessage(), t); + } + } + } + + public void dbSave() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("REPLACE INTO fort_functions (fort_id, type, lvl, lease, rate, endTime) VALUES (?,?,?,?,?,?)")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, getType()); + ps.setInt(3, getLvl()); + ps.setInt(4, getLease()); + ps.setLong(5, getRate()); + ps.setLong(6, getEndTime()); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Fort.updateFunctions(int type, int lvl, int lease, long rate, long time, boolean addNew): {}", e.getMessage(), e); + } + } + } + + public Fort(int fortId) { + super(fortId); + load(); + loadFlagPoles(); + _function = new ConcurrentHashMap<>(); + if (getOwnerClan() != null) { + setVisibleFlag(true); + loadFunctions(); + } + initResidenceZone(); + initNpcs(); // load and spawn npcs (Always spawned) + initSiegeNpcs(); // load suspicious merchants (Despawned 10mins before siege) + // spawnSuspiciousMerchant();// spawn suspicious merchants + initNpcCommanders(); // npc Commanders (not monsters) (Spawned during siege) + spawnNpcCommanders(); // spawn npc Commanders + initSpecialEnvoys(); // envoys from castles (Spawned after fort taken) + if ((getOwnerClan() != null) && (getFortState() == 0)) { + spawnSpecialEnvoys(); + } + } + + /** + * Return function with id + */ + public FortFunction getFunction(int type) { + return _function.get(type); + } + + public void endOfSiege(L2Clan clan) { + ThreadPoolManager.getInstance().executeAi(new EndFortressSiege(this, clan)); + } + + /** + * Move non clan members off fort area and to nearest town.
          + *
          + */ + public void banishForeigners() { + getResidenceZone().banishForeigners(getOwnerClan().getId()); + } + + /** + * @return true if object is inside the zone + */ + public boolean checkIfInZone(int x, int y, int z) { + return getZone().isInsideZone(x, y, z); + } + + public L2SiegeZone getZone() { + if (_zone == null) { + for (L2SiegeZone zone : ZoneManager.getInstance().getAllZones(L2SiegeZone.class)) { + if (zone.getSiegeObjectId() == getResidenceId()) { + _zone = zone; + break; + } + } + } + return _zone; + } + + @Override + public L2FortZone getResidenceZone() { + return (L2FortZone) super.getResidenceZone(); + } + + /** + * Get the objects distance to this fort + */ + public double getDistance(L2Object obj) { + return getZone().getDistanceToZone(obj); + } + + public void closeDoor(L2PcInstance activeChar, int doorId) { + openCloseDoor(activeChar, doorId, false); + } + + public void openDoor(L2PcInstance activeChar, int doorId) { + openCloseDoor(activeChar, doorId, true); + } + + public void openCloseDoor(L2PcInstance activeChar, int doorId, boolean open) { + if (activeChar.getClan() != getOwnerClan()) { + return; + } + + L2DoorInstance door = getDoor(doorId); + if (door != null) { + if (open) { + door.openMe(); + } else { + door.closeMe(); + } + } + } + + // This method is used to begin removing all fort upgrades + public void removeUpgrade() { + removeDoorUpgrade(); + } + + /** + * This method will set owner for Fort + * @param clan + * @param updateClansReputation + * @return + */ + public boolean setOwner(L2Clan clan, boolean updateClansReputation) { + if (clan == null) { + LOG.warn("Updating Fort owner with null clan!!!"); + return false; + } + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_FORTRESS_BATTLE_OF_S1_HAS_FINISHED); + sm.addCastleId(getResidenceId()); + getSiege().announceToPlayer(sm); + + final L2Clan oldowner = getOwnerClan(); + if ((oldowner != null) && (clan != oldowner)) { + // Remove points from old owner + updateClansReputation(oldowner, true); + try { + L2PcInstance oldleader = oldowner.getLeader().getPlayerInstance(); + if (oldleader != null) { + if (oldleader.getMountType() == MountType.WYVERN) { + oldleader.dismount(); + } + } + } catch (Exception e) { + LOG.warn("Exception in setOwner: {}", e.getMessage(), e); + } + removeOwner(true); + } + setFortState(0, 0); // initialize fort state + + // if clan already have castle, don't store him in fortress + if (clan.getCastleId() > 0) { + getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.NPCS_RECAPTURED_FORTRESS)); + return false; + } + + // Give points to new owner + if (updateClansReputation) { + updateClansReputation(clan, false); + } + + spawnSpecialEnvoys(); + // if clan have already fortress, remove it + if (clan.getFortId() > 0) { + FortManager.getInstance().getFortByOwner(clan).removeOwner(true); + } + + setSupplyLvL(0); + setOwnerClan(clan); + updateOwnerInDB(); // Update in database + saveFortVariables(); + + if (getSiege().isInProgress()) { + getSiege().endSiege(); + } + + for (L2PcInstance member : clan.getOnlineMembers(0)) { + giveResidentialSkills(member); + member.sendSkillList(); + } + return true; + } + + public void removeOwner(boolean updateDB) { + L2Clan clan = getOwnerClan(); + if (clan != null) { + for (L2PcInstance member : clan.getOnlineMembers(0)) { + removeResidentialSkills(member); + member.sendSkillList(); + } + clan.setFortId(0); + clan.broadcastToOnlineMembers(new PledgeShowInfoUpdate(clan)); + setOwnerClan(null); + setSupplyLvL(0); + saveFortVariables(); + removeAllFunctions(); + if (updateDB) { + updateOwnerInDB(); + } + } + } + + public void raiseSupplyLvL() { + _supplyLvL++; + if (_supplyLvL > fortress().getMaxSupplyLevel()) { + _supplyLvL = fortress().getMaxSupplyLevel(); + } + } + + public void setSupplyLvL(int val) { + if (val <= fortress().getMaxSupplyLevel()) { + _supplyLvL = val; + } + } + + public int getSupplyLvL() { + return _supplyLvL; + } + + public void saveFortVariables() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE fort SET supplyLvL=? WHERE id = ?")) { + ps.setInt(1, _supplyLvL); + ps.setInt(2, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: saveFortVariables(): {}", e.getMessage(), e); + } + } + + /** + * Show or hide flag inside flag pole. + */ + public void setVisibleFlag(boolean val) { + L2StaticObjectInstance flagPole = getFlagPole(); + if (flagPole != null) { + flagPole.setMeshIndex(val ? 1 : 0); + } + } + + /** + * Respawn all doors on fort grounds. + */ + public void resetDoors() { + for (L2DoorInstance door : _doors) { + if (door.getOpen()) { + door.closeMe(); + } + if (door.isDead()) { + door.doRevive(); + } + if (door.getCurrentHp() < door.getMaxHp()) { + door.setCurrentHp(door.getMaxHp()); + } + } + loadDoorUpgrade(); // Check for any upgrade the doors may have + } + + public void upgradeDoor(int doorId, int hp, int pDef, int mDef) { + L2DoorInstance door = getDoor(doorId); + if (door != null) { + door.setCurrentHp(door.getMaxHp() + hp); + + saveDoorUpgrade(doorId, hp, pDef, mDef); + } + } + + // This method loads fort + @Override + protected void load() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM fort WHERE id = ?")) { + ps.setInt(1, getResidenceId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + setName(rs.getString("name")); + + _siegeDate = Calendar.getInstance(); + _lastOwnedTime = Calendar.getInstance(); + _siegeDate.setTimeInMillis(rs.getLong("siegeDate")); + _lastOwnedTime.setTimeInMillis(rs.getLong("lastOwnedTime")); + _ownerId = rs.getInt("owner"); + _fortType = rs.getInt("fortType"); + _state = rs.getInt("state"); + _castleId = rs.getInt("castleId"); + _supplyLvL = rs.getInt("supplyLvL"); + } + } + if (_ownerId > 0) { + final var clan = ClanTable.getInstance().getClan(_ownerId); + clan.setFortId(getResidenceId()); + setOwnerClan(clan); + int runCount = getOwnedTime() / (int) MILLISECONDS.toSeconds(fortress().getPeriodicUpdateFrequency()); + long initial = System.currentTimeMillis() - _lastOwnedTime.getTimeInMillis(); + while (initial > fortress().getPeriodicUpdateFrequency()) { + initial -= fortress().getPeriodicUpdateFrequency(); + } + initial = fortress().getPeriodicUpdateFrequency() - initial; + if ((fortress().getMaxKeepTime() <= 0) || (getOwnedTime() < (fortress().getMaxKeepTime() * 3600))) { + _FortUpdater[0] = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FortUpdater(this, clan, runCount, UpdaterType.PERIODIC_UPDATE), initial, fortress().getPeriodicUpdateFrequency()); // Schedule owner tasks to start running + if (fortress().getMaxKeepTime() > 0) { + _FortUpdater[1] = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FortUpdater(this, clan, runCount, UpdaterType.MAX_OWN_TIME), 3600000, 3600000); // Schedule owner tasks to remove owener + } + } else { + _FortUpdater[1] = ThreadPoolManager.getInstance().scheduleGeneral(new FortUpdater(this, clan, 0, UpdaterType.MAX_OWN_TIME), 60000); // Schedule owner tasks to remove owner + } + } else { + setOwnerClan(null); + } + } catch (Exception e) { + LOG.warn("Exception: loadFortData(): {}", e.getMessage(), e); + } + } + + /** Load All Functions */ + private void loadFunctions() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM fort_functions WHERE fort_id = ?")) { + ps.setInt(1, getResidenceId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + _function.put(rs.getInt("type"), new FortFunction(rs.getInt("type"), rs.getInt("lvl"), rs.getInt("lease"), 0, rs.getLong("rate"), rs.getLong("endTime"), true)); + } + } + } catch (Exception e) { + LOG.error("Exception: Fort.loadFunctions(): {}", e.getMessage(), e); + } + } + + /** + * Remove function In List and in DB + */ + public void removeFunction(int functionType) { + _function.remove(functionType); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM fort_functions WHERE fort_id=? AND type=?")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, functionType); + ps.execute(); + } catch (Exception e) { + LOG.error("Exception: Fort.removeFunctions(int functionType): {}", e.getMessage(), e); + } + } + + /** + * Remove all fort functions. + */ + private void removeAllFunctions() { + for (int id : _function.keySet()) { + removeFunction(id); + } + } + + public boolean updateFunctions(L2PcInstance player, int type, int lvl, int lease, long rate, boolean addNew) { + if (player == null) { + return false; + } + if (lease > 0) { + if (!player.destroyItemByItemId("Consume", Inventory.ADENA_ID, lease, null, true)) { + return false; + } + } + if (addNew) { + _function.put(type, new FortFunction(type, lvl, lease, 0, rate, 0, false)); + } else { + if ((lvl == 0) && (lease == 0)) { + removeFunction(type); + } else { + int diffLease = lease - _function.get(type).getLease(); + if (diffLease > 0) { + _function.remove(type); + _function.put(type, new FortFunction(type, lvl, lease, 0, rate, -1, false)); + } else { + _function.get(type).setLease(lease); + _function.get(type).setLvl(lvl); + _function.get(type).dbSave(); + } + } + } + return true; + } + + public void activateInstance() { + loadDoor(); + } + + // This method loads fort door data from database + private void loadDoor() { + for (L2DoorInstance door : DoorData.getInstance().getDoors()) { + if ((door.getFort() != null) && (door.getFort().getResidenceId() == getResidenceId())) { + _doors.add(door); + } + } + } + + private void loadFlagPoles() { + for (L2StaticObjectInstance obj : StaticObjectData.getInstance().getStaticObjects()) { + if ((obj.getType() == 3) && obj.getName().startsWith(getName())) { + _flagPole = obj; + break; + } + } + if (_flagPole == null) { + throw new NullPointerException("Can't find flagpole for Fort " + this); + } + } + + private void loadDoorUpgrade() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM fort_doorupgrade WHERE fortId = ?")) { + ps.setInt(1, getResidenceId()); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + upgradeDoor(rs.getInt("id"), rs.getInt("hp"), rs.getInt("pDef"), rs.getInt("mDef")); + } + } + } catch (Exception e) { + LOG.warn("Exception: loadFortDoorUpgrade(): {}", e.getMessage(), e); + } + } + + private void removeDoorUpgrade() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM fort_doorupgrade WHERE fortId = ?")) { + ps.setInt(1, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: removeDoorUpgrade(): {}", e.getMessage(), e); + } + } + + private void saveDoorUpgrade(int doorId, int hp, int pDef, int mDef) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("INSERT INTO fort_doorupgrade (doorId, hp, pDef, mDef) VALUES (?,?,?,?)")) { + ps.setInt(1, doorId); + ps.setInt(2, hp); + ps.setInt(3, pDef); + ps.setInt(4, mDef); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: saveDoorUpgrade(int doorId, int hp, int pDef, int mDef): {}", e.getMessage(), e); + } + } + + private void updateOwnerInDB() { + L2Clan clan = getOwnerClan(); + int clanId = 0; + if (clan != null) { + clanId = clan.getId(); + _lastOwnedTime.setTimeInMillis(System.currentTimeMillis()); + } else { + _lastOwnedTime.setTimeInMillis(0); + } + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE fort SET owner=?,lastOwnedTime=?,state=?,castleId=? WHERE id = ?")) { + ps.setInt(1, clanId); + ps.setLong(2, _lastOwnedTime.getTimeInMillis()); + ps.setInt(3, 0); + ps.setInt(4, 0); + ps.setInt(5, getResidenceId()); + ps.execute(); + + // Announce to clan members + if (clan != null) { + clan.setFortId(getResidenceId()); // Set has fort flag for new owner + SystemMessage sm; + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_CLAN_IS_VICTORIOUS_IN_THE_FORTRESS_BATTLE_OF_S2); + sm.addString(clan.getName()); + sm.addCastleId(getResidenceId()); + L2World.getInstance().getPlayers().forEach(p -> p.sendPacket(sm)); + clan.broadcastToOnlineMembers(new PledgeShowInfoUpdate(clan)); + clan.broadcastToOnlineMembers(Music.SIEGE_VICTORY.getPacket()); + if (_FortUpdater[0] != null) { + _FortUpdater[0].cancel(false); + } + if (_FortUpdater[1] != null) { + _FortUpdater[1].cancel(false); + } + _FortUpdater[0] = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FortUpdater(this, clan, 0, UpdaterType.PERIODIC_UPDATE), fortress().getPeriodicUpdateFrequency(), fortress().getPeriodicUpdateFrequency()); // Schedule owner tasks to start running + if (fortress().getMaxKeepTime() > 0) { + _FortUpdater[1] = ThreadPoolManager.getInstance().scheduleGeneralAtFixedRate(new FortUpdater(this, clan, 0, UpdaterType.MAX_OWN_TIME), 3600000, 3600000); // Schedule owner tasks to remove owner + } + } else { + if (_FortUpdater[0] != null) { + _FortUpdater[0].cancel(false); + } + _FortUpdater[0] = null; + if (_FortUpdater[1] != null) { + _FortUpdater[1].cancel(false); + } + _FortUpdater[1] = null; + } + } catch (Exception e) { + LOG.warn("Exception: updateOwnerInDB(L2Clan clan): {}", e.getMessage(), e); + } + } + + public int getOwnerId() { + return _ownerId; + } + + public L2Clan getOwnerClan() { + return _fortOwner; + } + + public void setOwnerClan(L2Clan clan) { + setVisibleFlag(clan != null); + _fortOwner = clan; + } + + public L2DoorInstance getDoor(int doorId) { + if (doorId <= 0) { + return null; + } + + for (L2DoorInstance door : getDoors()) { + if (door.getId() == doorId) { + return door; + } + } + return null; + } + + public List getDoors() { + return _doors; + } + + public L2StaticObjectInstance getFlagPole() { + return _flagPole; + } + + public FortSiege getSiege() { + if (_siege == null) { + synchronized (this) { + if (_siege == null) { + _siege = new FortSiege(this); + } + } + } + return _siege; + } + + public Calendar getSiegeDate() { + return _siegeDate; + } + + public void setSiegeDate(Calendar siegeDate) { + _siegeDate = siegeDate; + } + + public int getOwnedTime() { + if (_lastOwnedTime.getTimeInMillis() == 0) { + return 0; + } + + return (int) ((System.currentTimeMillis() - _lastOwnedTime.getTimeInMillis()) / 1000); + } + + public int getTimeTillRebelArmy() { + if (_lastOwnedTime.getTimeInMillis() == 0) { + return 0; + } + + return (int) (((_lastOwnedTime.getTimeInMillis() + (fortress().getMaxKeepTime() * 3600000L)) - System.currentTimeMillis()) / 1000L); + } + + public long getTimeTillNextFortUpdate() { + if (_FortUpdater[0] == null) { + return 0; + } + return _FortUpdater[0].getDelay(TimeUnit.SECONDS); + } + + public void updateClansReputation(L2Clan owner, boolean removePoints) { + if (owner != null) { + if (removePoints) { + owner.takeReputationScore(clan().getLoseFortPoints(), true); + } else { + owner.addReputationScore(clan().getTakeFortPoints(), true); + } + } + } + + private static class EndFortressSiege implements Runnable { + private final Fort _f; + private final L2Clan _clan; + + public EndFortressSiege(Fort f, L2Clan clan) { + _f = f; + _clan = clan; + } + + @Override + public void run() { + try { + _f.setOwner(_clan, true); + } catch (Exception e) { + LOG.warn("Exception in endFortressSiege {}", e.getMessage(), e); + } + } + + } + + /** + * @return Returns state of fortress.
          + *
          + * 0 - not decided yet
          + * 1 - independent
          + * 2 - contracted with castle
          + */ + public int getFortState() { + return _state; + } + + /** + * @param state + *
            + *
          • 0 - not decided yet
          • + *
          • 1 - independent
          • + *
          • 2 - contracted with castle
          • + *
          + * @param castleId the Id of the contracted castle (0 if no contract with any castle) + */ + public void setFortState(int state, int castleId) { + _state = state; + _castleId = castleId; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("UPDATE fort SET state=?,castleId=? WHERE id = ?")) { + ps.setInt(1, getFortState()); + ps.setInt(2, getContractedCastleId()); + ps.setInt(3, getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: setFortState(int state, int castleId): {}", e.getMessage(), e); + } + } + + /** + * @return the fortress type (0 - small (3 commanders), 1 - big (4 commanders + control room)) + */ + public int getFortType() { + return _fortType; + } + + /** + * @param npcId the Id of the ambassador NPC + * @return the Id of the castle this ambassador represents + */ + public int getCastleIdByAmbassador(int npcId) { + return _envoyCastles.get(npcId); + } + + /** + * @param npcId the Id of the ambassador NPC + * @return the castle this ambassador represents + */ + public Castle getCastleByAmbassador(int npcId) { + return CastleManager.getInstance().getCastleById(getCastleIdByAmbassador(npcId)); + } + + /** + * @return the Id of the castle contracted with this fortress + */ + public int getContractedCastleId() { + return _castleId; + } + + /** + * @return the castle contracted with this fortress ({@code null} if no contract with any castle) + */ + public Castle getContractedCastle() { + return CastleManager.getInstance().getCastleById(getContractedCastleId()); + } + + /** + * Check if this is a border fortress (associated with multiple castles). + * @return {@code true} if this is a border fortress (associated with more than one castle), {@code false} otherwise + */ + public boolean isBorderFortress() { + return _availableCastles.size() > 1; + } + + /** + * @return the amount of barracks in this fortress + */ + public int getFortSize() { + return getFortType() == 0 ? 3 : 5; + } + + public void spawnSuspiciousMerchant() { + if (_isSuspiciousMerchantSpawned) { + return; + } + _isSuspiciousMerchantSpawned = true; + + for (L2Spawn spawnDat : _siegeNpcs) { + spawnDat.doSpawn(); + spawnDat.startRespawn(); + } + } + + public void despawnSuspiciousMerchant() { + if (!_isSuspiciousMerchantSpawned) { + return; + } + _isSuspiciousMerchantSpawned = false; + + for (L2Spawn spawnDat : _siegeNpcs) { + spawnDat.stopRespawn(); + spawnDat.getLastSpawn().deleteMe(); + } + } + + public void spawnNpcCommanders() { + for (L2Spawn spawnDat : _npcCommanders) { + spawnDat.doSpawn(); + spawnDat.startRespawn(); + } + } + + public void despawnNpcCommanders() { + for (L2Spawn spawnDat : _npcCommanders) { + spawnDat.stopRespawn(); + spawnDat.getLastSpawn().deleteMe(); + } + } + + public void spawnSpecialEnvoys() { + for (L2Spawn spawnDat : _specialEnvoys) { + spawnDat.doSpawn(); + } + } + + private void initNpcs() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM fort_spawnlist WHERE fortId = ? AND spawnType = ?")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, 0); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + L2Spawn spawnDat = new L2Spawn(rs.getInt("npcId")); + spawnDat.setAmount(1); + spawnDat.setX(rs.getInt("x")); + spawnDat.setY(rs.getInt("y")); + spawnDat.setZ(rs.getInt("z")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(60); + SpawnTable.getInstance().addNewSpawn(spawnDat, false); + spawnDat.doSpawn(); + spawnDat.startRespawn(); + } + } + } catch (Exception e) { + LOG.warn("Fort {} initNpcs: Spawn could not be initialized: {}", getResidenceId(), e.getMessage(), e); + } + } + + private void initSiegeNpcs() { + _siegeNpcs.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT id, npcId, x, y, z, heading FROM fort_spawnlist WHERE fortId = ? AND spawnType = ? ORDER BY id")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, 2); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final L2Spawn spawnDat = new L2Spawn(rs.getInt("npcId")); + spawnDat.setAmount(1); + spawnDat.setX(rs.getInt("x")); + spawnDat.setY(rs.getInt("y")); + spawnDat.setZ(rs.getInt("z")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(60); + _siegeNpcs.add(spawnDat); + } + } + } catch (Exception e) { + LOG.warn("Fort {} initSiegeNpcs: Spawn could not be initialized: {}", getResidenceId(), e.getMessage(), e); + } + } + + private void initNpcCommanders() { + _npcCommanders.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT id, npcId, x, y, z, heading FROM fort_spawnlist WHERE fortId = ? AND spawnType = ? ORDER BY id")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, 1); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final L2Spawn spawnDat = new L2Spawn(rs.getInt("npcId")); + spawnDat.setAmount(1); + spawnDat.setX(rs.getInt("x")); + spawnDat.setY(rs.getInt("y")); + spawnDat.setZ(rs.getInt("z")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(60); + _npcCommanders.add(spawnDat); + } + } + } catch (Exception e) { + LOG.warn("Fort {} initNpcCommanders: Spawn could not be initialized: {}", getResidenceId(), e.getMessage(), e); + } + } + + private void initSpecialEnvoys() { + _specialEnvoys.clear(); + _envoyCastles.clear(); + _availableCastles.clear(); + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT id, npcId, x, y, z, heading, castleId FROM fort_spawnlist WHERE fortId = ? AND spawnType = ? ORDER BY id")) { + ps.setInt(1, getResidenceId()); + ps.setInt(2, 3); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + final int castleId = rs.getInt("castleId"); + final L2Spawn spawnDat = new L2Spawn(rs.getInt("npcId")); + spawnDat.setAmount(1); + spawnDat.setX(rs.getInt("x")); + spawnDat.setY(rs.getInt("y")); + spawnDat.setZ(rs.getInt("z")); + spawnDat.setHeading(rs.getInt("heading")); + spawnDat.setRespawnDelay(60); + _specialEnvoys.add(spawnDat); + _envoyCastles.put(spawnDat.getId(), castleId); + _availableCastles.add(castleId); + } + } + } catch (Exception e) { + // problem with initializing spawn, go to next one + LOG.warn("Fort {} initSpecialEnvoys: Spawn could not be initialized: {}", getResidenceId(), e.getMessage(), e); + } + } + + @Override + protected void initResidenceZone() { + for (L2FortZone zone : ZoneManager.getInstance().getAllZones(L2FortZone.class)) { + if (zone.getResidenceId() == getResidenceId()) { + setResidenceZone(zone); + break; + } + } + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/FortSiege.java b/src/main/java/com/l2jserver/gameserver/model/entity/FortSiege.java new file mode 100644 index 00000000..2f79ca1e --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/FortSiege.java @@ -0,0 +1,1018 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.character; +import static com.l2jserver.gameserver.config.Configuration.fortSiege; +import static java.util.concurrent.TimeUnit.MINUTES; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.Calendar; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ScheduledFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.ThreadPoolManager; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.enums.FortTeleportWhoType; +import com.l2jserver.gameserver.instancemanager.FortManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeGuardManager; +import com.l2jserver.gameserver.instancemanager.FortSiegeManager; +import com.l2jserver.gameserver.instancemanager.TerritoryWarManager; +import com.l2jserver.gameserver.model.CombatFlag; +import com.l2jserver.gameserver.model.FortSiegeSpawn; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2Object; +import com.l2jserver.gameserver.model.L2SiegeClan; +import com.l2jserver.gameserver.model.L2SiegeClan.SiegeClanType; +import com.l2jserver.gameserver.model.L2Spawn; +import com.l2jserver.gameserver.model.PcCondOverride; +import com.l2jserver.gameserver.model.TeleportWhereType; +import com.l2jserver.gameserver.model.actor.L2Npc; +import com.l2jserver.gameserver.model.actor.instance.L2DoorInstance; +import com.l2jserver.gameserver.model.actor.instance.L2FortCommanderInstance; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.events.EventDispatcher; +import com.l2jserver.gameserver.model.events.impl.sieges.fort.FortSiegeFinish; +import com.l2jserver.gameserver.model.events.impl.sieges.fort.FortSiegeStart; +import com.l2jserver.gameserver.network.NpcStringId; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.clientpackets.Say2; +import com.l2jserver.gameserver.network.serverpackets.NpcSay; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; + +public class FortSiege implements Siegable { + private static final Logger LOG = LoggerFactory.getLogger(FortSiege.class); + + // SQL + private static final String DELETE_FORT_SIEGECLANS_BY_CLAN_ID = "DELETE FROM fortsiege_clans WHERE fort_id = ? AND clan_id = ?"; + + private static final String DELETE_FORT_SIEGECLANS = "DELETE FROM fortsiege_clans WHERE fort_id = ?"; + + private class ScheduleEndSiegeTask implements Runnable { + @Override + public void run() { + if (!isInProgress()) { + return; + } + + try { + _siegeEnd = null; + endSiege(); + } catch (Exception e) { + LOG.warn("Exception: ScheduleEndSiegeTask() for Fort: {} {}", _fort.getName(), e.getMessage(), e); + } + } + } + + public class ScheduleStartSiegeTask implements Runnable { + private final Fort _fortInst; + private final int _time; + + public ScheduleStartSiegeTask(int time) { + _fortInst = _fort; + _time = time; + } + + @Override + public void run() { + if (isInProgress()) { + return; + } + + try { + final SystemMessage sm; + if (_time == 3600) // 1hr remains + { + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(600), 3000000); // Prepare task for 10 minutes left. + } else if (_time == 600) // 10min remains + { + getFort().despawnSuspiciousMerchant(); + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_MINUTES_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(10); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(300), 300000); // Prepare task for 5 minutes left. + } else if (_time == 300) // 5min remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_MINUTES_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(5); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(60), 240000); // Prepare task for 1 minute left. + } else if (_time == 60) // 1min remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_MINUTES_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(1); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(30), 30000); // Prepare task for 30 seconds left. + } else if (_time == 30) // 30seconds remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SECONDS_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(30); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(10), 20000); // Prepare task for 10 seconds left. + } else if (_time == 10) // 10seconds remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SECONDS_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(10); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(5), 5000); // Prepare task for 5 seconds left. + } else if (_time == 5) // 5seconds remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SECONDS_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(5); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(1), 4000); // Prepare task for 1 seconds left. + } else if (_time == 1) // 1seconds remains + { + sm = SystemMessage.getSystemMessage(SystemMessageId.S1_SECONDS_UNTIL_THE_FORTRESS_BATTLE_STARTS); + sm.addInt(1); + announceToPlayer(sm); + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleStartSiegeTask(0), 1000); // Prepare task start siege. + } else if (_time == 0)// start siege + { + _fortInst.getSiege().startSiege(); + } else { + LOG.warn("Exception: ScheduleStartSiegeTask(): unknown siege time: {}", _time); + } + } catch (Exception e) { + LOG.warn("Exception: ScheduleStartSiegeTask() for Fort: {} {}", _fortInst.getName(), e.getMessage(), e); + } + } + } + + private class ScheduleSuspiciousMerchantSpawn implements Runnable { + @Override + public void run() { + if (isInProgress()) { + return; + } + + try { + _fort.spawnSuspiciousMerchant(); + } catch (Exception e) { + LOG.warn("Exception: ScheduleSuspicoiusMerchantSpawn() for Fort: {} {}", _fort.getName(), e.getMessage(), e); + } + } + } + + public class ScheduleSiegeRestore implements Runnable { + @Override + public void run() { + if (!isInProgress()) { + return; + } + + try { + _siegeRestore = null; + resetSiege(); + announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.BARRACKS_FUNCTION_RESTORED)); + } catch (Exception e) { + LOG.warn("Exception: ScheduleSiegeRestore() for Fort: {} {}", _fort.getName(), e.getMessage(), e); + } + } + } + + private final List _attackerClans = new CopyOnWriteArrayList<>(); + + // Fort setting + private final List _commanders = new CopyOnWriteArrayList<>(); + private final Fort _fort; + private boolean _isInProgress = false; + private FortSiegeGuardManager _siegeGuardManager; + private ScheduledFuture _siegeEnd = null; + private ScheduledFuture _siegeRestore = null; + private ScheduledFuture _siegeStartTask = null; + + public FortSiege(Fort fort) { + _fort = fort; + + checkAutoTask(); + FortSiegeManager.getInstance().addSiege(this); + } + + /** + * When siege ends. + */ + @Override + public void endSiege() { + if (isInProgress()) { + _isInProgress = false; // Flag so that siege instance can be started + removeFlags(); // Removes all flags. Note: Remove flag before teleporting players + unSpawnFlags(); + + updatePlayerSiegeStateFlags(true); + + int ownerId = -1; + if (getFort().getOwnerClan() != null) { + ownerId = getFort().getOwnerClan().getId(); + } + getFort().getZone().banishForeigners(ownerId); + getFort().getZone().setIsActive(false); + getFort().getZone().updateZoneStatusForCharactersInside(); + getFort().getZone().setSiegeInstance(null); + + saveFortSiege(); // Save fort specific data + clearSiegeClan(); // Clear siege clan from db + removeCommanders(); // Remove commander from this fort + + getFort().spawnNpcCommanders(); // Spawn NPC commanders + getSiegeGuardManager().unspawnSiegeGuard(); // Remove all spawned siege guard from this fort + getFort().resetDoors(); // Respawn door to fort + + // Prepare 3hr task for suspicious merchant respawn + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleSuspiciousMerchantSpawn(), MINUTES.toMillis(fortSiege().getSuspiciousMerchantRespawnDelay())); + setSiegeDateTime(true); // store suspicious merchant spawn in DB + + if (_siegeEnd != null) { + _siegeEnd.cancel(true); + _siegeEnd = null; + } + if (_siegeRestore != null) { + _siegeRestore.cancel(true); + _siegeRestore = null; + } + + if ((getFort().getOwnerClan() != null) && (getFort().getFlagPole().getMeshIndex() == 0)) { + getFort().setVisibleFlag(true); + } + + LOG.info("Siege of {} fort finished.", getFort().getName()); + + // Notify to scripts. + EventDispatcher.getInstance().notifyEventAsync(new FortSiegeFinish(this), getFort()); + } + } + + /** + * When siege starts + */ + @Override + public void startSiege() { + if (!isInProgress()) { + if (_siegeStartTask != null) // used admin command "admin_startfortsiege" + { + _siegeStartTask.cancel(true); + getFort().despawnSuspiciousMerchant(); + } + _siegeStartTask = null; + + if (getAttackerClans().isEmpty()) { + return; + } + + _isInProgress = true; // Flag so that same siege instance cannot be started again + + loadSiegeClan(); // Load siege clan from db + updatePlayerSiegeStateFlags(false); + teleportPlayer(FortTeleportWhoType.Attacker, TeleportWhereType.TOWN); // Teleport to the closest town + + getFort().despawnNpcCommanders(); // Despawn NPC commanders + spawnCommanders(); // Spawn commanders + getFort().resetDoors(); // Spawn door + spawnSiegeGuard(); // Spawn siege guard + getFort().setVisibleFlag(false); + getFort().getZone().setSiegeInstance(this); + getFort().getZone().setIsActive(true); + getFort().getZone().updateZoneStatusForCharactersInside(); + + // Schedule a task to prepare auto siege end + _siegeEnd = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleEndSiegeTask(), fortSiege().getSiegeLength()); + + final SystemMessage sm = SystemMessage.getSystemMessage(SystemMessageId.THE_FORTRESS_BATTLE_S1_HAS_BEGUN); + sm.addCastleId(getFort().getResidenceId()); + announceToPlayer(sm); + saveFortSiege(); + + LOG.info("Siege of {} fort started.", getFort().getName()); + + // Notify to scripts. + EventDispatcher.getInstance().notifyEventAsync(new FortSiegeStart(this), getFort()); + } + } + + /** + * Announce to player. + * @param sm the system message to send to player + */ + public void announceToPlayer(SystemMessage sm) { + // announce messages only for participants + L2Clan clan; + for (L2SiegeClan siegeclan : getAttackerClans()) { + clan = ClanTable.getInstance().getClan(siegeclan.getClanId()); + for (L2PcInstance member : clan.getOnlineMembers(0)) { + member.sendPacket(sm); + } + } + if (getFort().getOwnerClan() != null) { + clan = ClanTable.getInstance().getClan(getFort().getOwnerClan().getId()); + for (L2PcInstance member : clan.getOnlineMembers(0)) { + if (member != null) { + member.sendPacket(sm); + } + } + } + } + + public void announceToPlayer(SystemMessage sm, String s) { + sm.addString(s); + announceToPlayer(sm); + } + + public void updatePlayerSiegeStateFlags(boolean clear) { + L2Clan clan; + for (L2SiegeClan siegeclan : getAttackerClans()) { + clan = ClanTable.getInstance().getClan(siegeclan.getClanId()); + for (L2PcInstance member : clan.getOnlineMembers(0)) { + if (clear) { + member.setSiegeState((byte) 0); + member.setSiegeSide(0); + member.setIsInSiege(false); + member.stopFameTask(); + } else { + member.setSiegeState((byte) 1); + member.setSiegeSide(getFort().getResidenceId()); + if (checkIfInZone(member)) { + member.setIsInSiege(true); + member.startFameTask(character().getFortressZoneFameTaskFrequency(), character().getFortressZoneFameAcquirePoints()); + } + } + member.broadcastUserInfo(); + } + } + if (getFort().getOwnerClan() != null) { + clan = ClanTable.getInstance().getClan(getFort().getOwnerClan().getId()); + for (L2PcInstance member : clan.getOnlineMembers(0)) { + if (member == null) { + continue; + } + + if (clear) { + member.setSiegeState((byte) 0); + member.setSiegeSide(0); + member.setIsInSiege(false); + member.stopFameTask(); + } else { + member.setSiegeState((byte) 2); + member.setSiegeSide(getFort().getResidenceId()); + if (checkIfInZone(member)) { + member.setIsInSiege(true); + member.startFameTask(character().getFortressZoneFameTaskFrequency(), character().getFortressZoneFameAcquirePoints()); + } + } + member.broadcastUserInfo(); + } + } + } + + /** + * @return true if object is inside the zone + */ + public boolean checkIfInZone(L2Object object) { + return checkIfInZone(object.getX(), object.getY(), object.getZ()); + } + + /** + * @return true if object is inside the zone + */ + public boolean checkIfInZone(int x, int y, int z) { + return (isInProgress() && (getFort().checkIfInZone(x, y, z))); // Fort zone during siege + } + + /** + * @param clan The L2Clan of the player + * @return true if clan is attacker + */ + @Override + public boolean checkIsAttacker(L2Clan clan) { + return (getAttackerClan(clan) != null); + } + + /** + * @param clan The L2Clan of the player + * @return true if clan is defender + */ + @Override + public boolean checkIsDefender(L2Clan clan) { + return (clan != null) && (getFort().getOwnerClan() == clan); + } + + /** Clear all registered siege clans from database for fort */ + public void clearSiegeClan() { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("DELETE FROM fortsiege_clans WHERE fort_id=?")) { + ps.setInt(1, getFort().getResidenceId()); + ps.execute(); + + if (getFort().getOwnerClan() != null) { + try (PreparedStatement delete = con.prepareStatement("DELETE FROM fortsiege_clans WHERE clan_id=?")) { + delete.setInt(1, getFort().getOwnerClan().getId()); + delete.execute(); + } + } + + getAttackerClans().clear(); + + // if siege is in progress, end siege + if (isInProgress()) { + endSiege(); + } + + // if siege isn't in progress (1hr waiting time till siege starts), cancel waiting time + if (_siegeStartTask != null) { + _siegeStartTask.cancel(true); + _siegeStartTask = null; + } + } catch (Exception e) { + LOG.warn("Exception: clearSiegeClan(): {}", e.getMessage(), e); + } + } + + /** Set the date for the next siege. */ + private void clearSiegeDate() { + getFort().getSiegeDate().setTimeInMillis(0); + } + + /** + * @return list of L2PcInstance registered as attacker in the zone. + */ + @Override + public List getAttackersInZone() { + final List players = new LinkedList<>(); + for (L2SiegeClan siegeclan : getAttackerClans()) { + L2Clan clan = ClanTable.getInstance().getClan(siegeclan.getClanId()); + for (L2PcInstance player : clan.getOnlineMembers(0)) { + if (player.isInSiege()) { + players.add(player); + } + } + } + return players; + } + + /** + * @return list of L2PcInstance in the zone. + */ + public List getPlayersInZone() { + return getFort().getZone().getPlayersInside(); + } + + /** + * @return list of L2PcInstance owning the fort in the zone. + */ + public List getOwnersInZone() { + final List players = new LinkedList<>(); + if (getFort().getOwnerClan() != null) { + L2Clan clan = ClanTable.getInstance().getClan(getFort().getOwnerClan().getId()); + if (clan != getFort().getOwnerClan()) { + return null; + } + + for (L2PcInstance player : clan.getOnlineMembers(0)) { + if (player.isInSiege()) { + players.add(player); + } + } + } + return players; + } + + /** + * TODO: To DP AI
          + * Commander was killed + */ + public void killedCommander(L2FortCommanderInstance instance) { + if (!_commanders.isEmpty() && (getFort() != null)) { + L2Spawn spawn = instance.getSpawn(); + if (spawn != null) { + List commanders = FortSiegeManager.getInstance().getCommanderSpawnList(getFort().getResidenceId()); + for (FortSiegeSpawn spawn2 : commanders) { + if (spawn2.getId() == spawn.getId()) { + NpcStringId npcString = switch (spawn2.getMessageId()) { + case 1 -> NpcStringId.YOU_MAY_HAVE_BROKEN_OUR_ARROWS_BUT_YOU_WILL_NEVER_BREAK_OUR_WILL_ARCHERS_RETREAT; + case 2 -> NpcStringId.AIIEEEE_COMMAND_CENTER_THIS_IS_GUARD_UNIT_WE_NEED_BACKUP_RIGHT_AWAY; + case 3 -> NpcStringId.AT_LAST_THE_MAGIC_FIELD_THAT_PROTECTS_THE_FORTRESS_HAS_WEAKENED_VOLUNTEERS_STAND_BACK; + case 4 -> NpcStringId.I_FEEL_SO_MUCH_GRIEF_THAT_I_CANT_EVEN_TAKE_CARE_OF_MYSELF_THERE_ISNT_ANY_REASON_FOR_ME_TO_STAY_HERE_ANY_LONGER; + default -> null; + }; + if (npcString != null) { + instance.broadcastPacket(new NpcSay(instance.getObjectId(), Say2.NPC_SHOUT, instance.getId(), npcString)); + } + } + } + _commanders.remove(spawn); + if (_commanders.isEmpty()) { + // spawn fort flags + spawnFlag(getFort().getResidenceId()); + // cancel door/commanders respawn + if (_siegeRestore != null) { + _siegeRestore.cancel(true); + } + // open doors in main building + for (L2DoorInstance door : getFort().getDoors()) { + if (door.getIsShowHp()) { + continue; + } + + // TODO this also opens control room door at big fort + door.openMe(); + } + getFort().getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.ALL_BARRACKS_OCCUPIED)); + } + // schedule restoring doors/commanders respawn + else if (_siegeRestore == null) { + getFort().getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.SEIZED_BARRACKS)); + _siegeRestore = ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleSiegeRestore(), MINUTES.toMillis(fortSiege().getCountDownLength())); + } else { + getFort().getSiege().announceToPlayer(SystemMessage.getSystemMessage(SystemMessageId.SEIZED_BARRACKS)); + } + } else { + LOG.warn("FortSiege.killedCommander(): killed commander, but commander not registered for fortress. NpcId: {} FortId: {}", instance.getId(), getFort().getResidenceId()); + } + } + } + + /** + * Remove the flag that was killed + */ + public void killedFlag(L2Npc flag) { + if (flag == null) { + return; + } + + for (L2SiegeClan clan : getAttackerClans()) { + if (clan.removeFlag(flag)) { + return; + } + } + } + + /** + * Register clan as attacker.
          + * @param player The L2PcInstance of the player trying to register. + * @param checkConditions True if should be checked conditions, false otherwise + * @return Number that defines what happened.
          + * 0 - Player don't have clan.
          + * 1 - Player don't have enough adena to register.
          + * 2 - Is not right time to register Fortress now.
          + * 3 - Players clan is already registered to siege.
          + * 4 - Players clan is successfully registered to siege. + */ + public int addAttacker(L2PcInstance player, boolean checkConditions) { + if (player.getClan() == null) { + return 0; // Player dont have clan + } + + if (checkConditions) { + if (getFort().getSiege().getAttackerClans().isEmpty() && (player.getInventory().getAdena() < 250000)) { + return 1; // Player dont havee enough adena to register + } + + else if ((System.currentTimeMillis() < TerritoryWarManager.getInstance().getTWStartTimeInMillis()) && TerritoryWarManager.getInstance().getIsRegistrationOver()) { + return 2; // Is not right time to register Fortress now + } + + if ((System.currentTimeMillis() > TerritoryWarManager.getInstance().getTWStartTimeInMillis()) && TerritoryWarManager.getInstance().isTWChannelOpen()) { + return 2; // Is not right time to register Fortress now + } + + for (Fort fort : FortManager.getInstance().getForts()) { + if (fort.getSiege().getAttackerClan(player.getClanId()) != null) { + return 3; // Players clan is already registred to siege + } + + if ((fort.getOwnerClan() == player.getClan()) && (fort.getSiege().isInProgress() || (fort.getSiege()._siegeStartTask != null))) { + return 3; // Players clan is already registred to siege + } + } + } + + saveSiegeClan(player.getClan()); + if (getAttackerClans().size() == 1) { + if (checkConditions) { + player.reduceAdena("FortressSiege", 250000, null, true); + } + startAutoTask(true); + } + return 4; // Players clan is successfully registred to siege + } + + /** + * Remove clan from siege + * @param clan The clan being removed + */ + public void removeAttacker(L2Clan clan) { + if ((clan == null) || (clan.getFortId() == getFort().getResidenceId()) || !FortSiegeManager.getInstance().checkIsRegistered(clan, getFort().getResidenceId())) { + return; + } + removeSiegeClan(clan.getId()); + } + + /** + * This function does not do any checks and should not be called from bypass ! + */ + private void removeSiegeClan(int clanId) { + final String query = (clanId != 0) ? DELETE_FORT_SIEGECLANS_BY_CLAN_ID : DELETE_FORT_SIEGECLANS; + try (Connection con = ConnectionFactory.getInstance().getConnection(); + PreparedStatement ps = con.prepareStatement(query)) { + ps.setInt(1, getFort().getResidenceId()); + if (clanId != 0) { + ps.setInt(2, clanId); + } + ps.execute(); + + loadSiegeClan(); + if (getAttackerClans().isEmpty()) { + if (isInProgress()) { + endSiege(); + } else { + saveFortSiege(); // Clear siege time in DB + } + + if (_siegeStartTask != null) { + _siegeStartTask.cancel(true); + _siegeStartTask = null; + } + } + } catch (Exception e) { + LOG.warn("Exception on removeSiegeClan: {}", e.getMessage(), e); + } + } + + /** + * Start the auto tasks + */ + private void checkAutoTask() { + if (_siegeStartTask != null) { + return; + } + + final long delay = getFort().getSiegeDate().getTimeInMillis() - Calendar.getInstance().getTimeInMillis(); + + if (delay < 0) { + // siege time in past + saveFortSiege(); + clearSiegeClan(); // remove all clans + // spawn suspicious merchant immediately + ThreadPoolManager.getInstance().executeGeneral(new ScheduleSuspiciousMerchantSpawn()); + } else { + loadSiegeClan(); + if (getAttackerClans().isEmpty()) { + // no attackers - waiting for suspicious merchant spawn + ThreadPoolManager.getInstance().scheduleGeneral(new ScheduleSuspiciousMerchantSpawn(), delay); + } else { + // preparing start siege task + if (delay > 3600000) // more than hour, how this can happens ? spawn suspicious merchant + { + ThreadPoolManager.getInstance().executeGeneral(new ScheduleSuspiciousMerchantSpawn()); + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(3600), delay - 3600000); + } + if (delay > 600000) // more than 10 min, spawn suspicious merchant + { + ThreadPoolManager.getInstance().executeGeneral(new ScheduleSuspiciousMerchantSpawn()); + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(600), delay - 600000); + } else if (delay > 300000) { + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(300), delay - 300000); + } else if (delay > 60000) { + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(60), delay - 60000); + } else { + // lower than 1 min, set to 1 min + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(60), 0); + } + + LOG.info("Siege of {} fort: {}", getFort().getName(), getFort().getSiegeDate().getTime()); + } + } + } + + /** + * Start the auto task + */ + private void startAutoTask(boolean setTime) { + if (_siegeStartTask != null) { + return; + } + + if (setTime) { + setSiegeDateTime(false); + } + + if (getFort().getOwnerClan() != null) { + getFort().getOwnerClan().broadcastToOnlineMembers(SystemMessage.getSystemMessage(SystemMessageId.A_FORTRESS_IS_UNDER_ATTACK)); + } + + // Execute siege auto start + _siegeStartTask = ThreadPoolManager.getInstance().scheduleGeneral(new FortSiege.ScheduleStartSiegeTask(3600), 0); + } + + /** + * Teleport players + * @param teleportWho + * @param teleportWhere + */ + public void teleportPlayer(FortTeleportWhoType teleportWho, TeleportWhereType teleportWhere) { + List players = switch (teleportWho) { + case Owner -> getOwnersInZone(); + case Attacker -> getAttackersInZone(); + default -> getPlayersInZone(); + }; + + for (L2PcInstance player : players) { + if (player.canOverrideCond(PcCondOverride.FORTRESS_CONDITIONS) || player.isJailed()) { + continue; + } + + player.teleToLocation(teleportWhere); + } + } + + /** + * Add clan as attacker + */ + private void addAttacker(int clanId) { + getAttackerClans().add(new L2SiegeClan(clanId, SiegeClanType.ATTACKER)); // Add registered attacker to attacker list + } + + /** + * @return {@code true} if the clan has already registered to a siege for the same day, {@code false} otherwise. + */ + public boolean checkIfAlreadyRegisteredForSameDay(L2Clan clan) { + for (FortSiege siege : FortSiegeManager.getInstance().getSieges()) { + if (siege == this) { + continue; + } + + if (siege.getSiegeDate().get(Calendar.DAY_OF_WEEK) == getSiegeDate().get(Calendar.DAY_OF_WEEK)) { + if (siege.checkIsAttacker(clan)) { + return true; + } + if (siege.checkIsDefender(clan)) { + return true; + } + } + } + + return false; + } + + private void setSiegeDateTime(boolean merchant) { + Calendar newDate = Calendar.getInstance(); + if (merchant) { + newDate.add(Calendar.MINUTE, fortSiege().getSuspiciousMerchantRespawnDelay()); + } else { + newDate.add(Calendar.MINUTE, 60); + } + getFort().setSiegeDate(newDate); + saveSiegeDate(); + } + + /** Load siege clans. */ + private void loadSiegeClan() { + getAttackerClans().clear(); + try (Connection con = ConnectionFactory.getInstance().getConnection(); + PreparedStatement ps = con.prepareStatement("SELECT clan_id FROM fortsiege_clans WHERE fort_id=?")) { + ps.setInt(1, getFort().getResidenceId()); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + addAttacker(rs.getInt("clan_id")); + } + } + } catch (Exception e) { + LOG.warn("Exception: loadSiegeClan(): {}", e.getMessage(), e); + } + } + + /** Remove commanders. */ + private void removeCommanders() { + if (!_commanders.isEmpty()) { + // Remove all instance of commanders for this fort + for (L2Spawn spawn : _commanders) { + if (spawn != null) { + spawn.stopRespawn(); + if (spawn.getLastSpawn() != null) { + spawn.getLastSpawn().deleteMe(); + } + } + } + _commanders.clear(); + } + } + + /** Remove all flags. */ + private void removeFlags() { + for (L2SiegeClan sc : getAttackerClans()) { + if (sc != null) { + sc.removeFlags(); + } + } + } + + /** Save fort siege related to database. */ + private void saveFortSiege() { + clearSiegeDate(); // clear siege date + saveSiegeDate(); // Save the new date + } + + /** Save siege date to database. */ + private void saveSiegeDate() { + try (Connection con = ConnectionFactory.getInstance().getConnection(); + PreparedStatement ps = con.prepareStatement("UPDATE fort SET siegeDate = ? WHERE id = ?")) { + ps.setLong(1, getSiegeDate().getTimeInMillis()); + ps.setInt(2, getFort().getResidenceId()); + ps.execute(); + } catch (Exception e) { + LOG.warn("Exception: saveSiegeDate(): {}", e.getMessage(), e); + } + } + + /** + * Save registration to database. + */ + private void saveSiegeClan(L2Clan clan) { + if (getAttackerClans().size() >= fortSiege().getAttackerMaxClans()) { + return; + } + + try (Connection con = ConnectionFactory.getInstance().getConnection(); + PreparedStatement ps = con.prepareStatement("INSERT INTO fortsiege_clans (clan_id,fort_id) values (?,?)")) { + ps.setInt(1, clan.getId()); + ps.setInt(2, getFort().getResidenceId()); + ps.execute(); + + addAttacker(clan.getId()); + } catch (Exception e) { + LOG.warn("Exception: saveSiegeClan(L2Clan clan): {}", e.getMessage(), e); + } + } + + /** Spawn commanders. */ + private void spawnCommanders() { + // Set commanders array size if one does not exist + try { + _commanders.clear(); + for (FortSiegeSpawn _sp : FortSiegeManager.getInstance().getCommanderSpawnList(getFort().getResidenceId())) { + final L2Spawn spawnDat = new L2Spawn(_sp.getId()); + spawnDat.setAmount(1); + spawnDat.setX(_sp.getLocation().getX()); + spawnDat.setY(_sp.getLocation().getY()); + spawnDat.setZ(_sp.getLocation().getZ()); + spawnDat.setHeading(_sp.getLocation().getHeading()); + spawnDat.setRespawnDelay(60); + spawnDat.doSpawn(); + spawnDat.stopRespawn(); + _commanders.add(spawnDat); + } + } catch (Exception e) { + // problem with initializing spawn, go to next one + LOG.warn("FortSiege.spawnCommander: Spawn could not be initialized: {}", e.getMessage(), e); + } + } + + private void spawnFlag(int Id) { + for (CombatFlag cf : FortSiegeManager.getInstance().getFlagList(Id)) { + cf.spawnMe(); + } + } + + private void unSpawnFlags() { + if (FortSiegeManager.getInstance().getFlagList(getFort().getResidenceId()) == null) { + return; + } + + for (CombatFlag cf : FortSiegeManager.getInstance().getFlagList(getFort().getResidenceId())) { + cf.unSpawnMe(); + } + } + + /** + * Spawn siege guard. + */ + private void spawnSiegeGuard() { + getSiegeGuardManager().spawnSiegeGuard(); + } + + @Override + public final L2SiegeClan getAttackerClan(L2Clan clan) { + if (clan == null) { + return null; + } + + return getAttackerClan(clan.getId()); + } + + @Override + public final L2SiegeClan getAttackerClan(int clanId) { + for (L2SiegeClan sc : getAttackerClans()) { + if ((sc != null) && (sc.getClanId() == clanId)) { + return sc; + } + } + + return null; + } + + @Override + public final List getAttackerClans() { + return _attackerClans; + } + + public final Fort getFort() { + return _fort; + } + + public final boolean isInProgress() { + return _isInProgress; + } + + @Override + public final Calendar getSiegeDate() { + return getFort().getSiegeDate(); + } + + @Override + public List getFlag(L2Clan clan) { + if (clan != null) { + L2SiegeClan sc = getAttackerClan(clan); + if (sc != null) { + return sc.getFlag(); + } + } + + return null; + } + + public final FortSiegeGuardManager getSiegeGuardManager() { + if (_siegeGuardManager == null) { + _siegeGuardManager = new FortSiegeGuardManager(getFort()); + } + + return _siegeGuardManager; + } + + public void resetSiege() { + // reload commanders and repair doors + removeCommanders(); + spawnCommanders(); + getFort().resetDoors(); + } + + public List getCommanders() { + return _commanders; + } + + @Override + public L2SiegeClan getDefenderClan(int clanId) { + return null; + } + + @Override + public L2SiegeClan getDefenderClan(L2Clan clan) { + return null; + } + + @Override + public List getDefenderClans() { + return null; + } + + @Override + public boolean giveFame() { + return true; + } + + @Override + public int getFameFrequency() { + return (int) character().getFortressZoneFameTaskFrequency(); + } + + @Override + public int getFameAmount() { + return character().getFortressZoneFameAcquirePoints(); + } + + @Override + public void updateSiege() { + } +} diff --git a/src/main/java/com/l2jserver/gameserver/model/entity/Hero.java b/src/main/java/com/l2jserver/gameserver/model/entity/Hero.java new file mode 100644 index 00000000..c50e0d5d --- /dev/null +++ b/src/main/java/com/l2jserver/gameserver/model/entity/Hero.java @@ -0,0 +1,814 @@ +/* + * Copyright © 2004-2026 L2J Server + * + * This file is part of L2J Server. + * + * L2J Server is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * L2J Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.l2jserver.gameserver.model.entity; + +import static com.l2jserver.gameserver.config.Configuration.clan; + +import java.sql.PreparedStatement; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.l2jserver.commons.database.ConnectionFactory; +import com.l2jserver.gameserver.cache.HtmCache; +import com.l2jserver.gameserver.data.sql.impl.CharNameTable; +import com.l2jserver.gameserver.data.sql.impl.ClanTable; +import com.l2jserver.gameserver.data.xml.impl.ClassListData; +import com.l2jserver.gameserver.data.xml.impl.NpcData; +import com.l2jserver.gameserver.instancemanager.CastleManager; +import com.l2jserver.gameserver.model.L2Clan; +import com.l2jserver.gameserver.model.L2World; +import com.l2jserver.gameserver.model.StatsSet; +import com.l2jserver.gameserver.model.actor.instance.L2PcInstance; +import com.l2jserver.gameserver.model.actor.templates.L2NpcTemplate; +import com.l2jserver.gameserver.model.itemcontainer.Inventory; +import com.l2jserver.gameserver.model.items.instance.L2ItemInstance; +import com.l2jserver.gameserver.model.olympiad.Olympiad; +import com.l2jserver.gameserver.network.SystemMessageId; +import com.l2jserver.gameserver.network.serverpackets.ExBrExtraUserInfo; +import com.l2jserver.gameserver.network.serverpackets.InventoryUpdate; +import com.l2jserver.gameserver.network.serverpackets.NpcHtmlMessage; +import com.l2jserver.gameserver.network.serverpackets.SocialAction; +import com.l2jserver.gameserver.network.serverpackets.SystemMessage; +import com.l2jserver.gameserver.network.serverpackets.UserInfo; +import com.l2jserver.gameserver.util.StringUtil; + +/** + * Hero entity. + * @author godson + */ +public class Hero { + + private static final Logger LOG = LoggerFactory.getLogger(Hero.class); + + private static final String GET_HEROES = "SELECT heroes.charId, characters.char_name, heroes.class_id, heroes.count, heroes.played, heroes.claimed FROM heroes, characters WHERE characters.charId = heroes.charId AND heroes.played = 1"; + private static final String GET_ALL_HEROES = "SELECT heroes.charId, characters.char_name, heroes.class_id, heroes.count, heroes.played, heroes.claimed FROM heroes, characters WHERE characters.charId = heroes.charId"; + private static final String UPDATE_ALL = "UPDATE heroes SET played = 0"; + private static final String INSERT_HERO = "INSERT INTO heroes (charId, class_id, count, played, claimed) VALUES (?,?,?,?,?)"; + private static final String UPDATE_HERO = "UPDATE heroes SET count = ?, played = ?, claimed = ? WHERE charId = ?"; + private static final String GET_CLAN_ALLY = "SELECT characters.clanid AS clanid, coalesce(clan_data.ally_Id, 0) AS allyId FROM characters LEFT JOIN clan_data ON clan_data.clan_id = characters.clanid WHERE characters.charId = ?"; + // delete hero items + private static final String DELETE_ITEMS = "DELETE FROM items WHERE item_id IN (6842, 6611, 6612, 6613, 6614, 6615, 6616, 6617, 6618, 6619, 6620, 6621, 9388, 9389, 9390) AND owner_id NOT IN (SELECT charId FROM characters WHERE accesslevel > 0)"; + + private static final Map HEROES = new ConcurrentHashMap<>(); + private static final Map COMPLETE_HEROES = new ConcurrentHashMap<>(); + + private static final Map HERO_COUNTS = new ConcurrentHashMap<>(); + private static final Map> HERO_FIGHTS = new ConcurrentHashMap<>(); + + private static final Map> HERO_DIARY = new ConcurrentHashMap<>(); + private static final Map HERO_MESSAGE = new ConcurrentHashMap<>(); + + public static final String COUNT = "count"; + public static final String PLAYED = "played"; + public static final String CLAIMED = "claimed"; + public static final String CLAN_NAME = "clan_name"; + public static final String CLAN_CREST = "clan_crest"; + public static final String ALLY_NAME = "ally_name"; + public static final String ALLY_CREST = "ally_crest"; + + public static final int ACTION_RAID_KILLED = 1; + public static final int ACTION_HERO_GAINED = 2; + public static final int ACTION_CASTLE_TAKEN = 3; + + protected Hero() { + init(); + } + + private void init() { + HEROES.clear(); + COMPLETE_HEROES.clear(); + HERO_COUNTS.clear(); + HERO_FIGHTS.clear(); + HERO_DIARY.clear(); + HERO_MESSAGE.clear(); + + try (var con = ConnectionFactory.getInstance().getConnection(); + var s1 = con.createStatement(); + var rs1 = s1.executeQuery(GET_HEROES); + var ps = con.prepareStatement(GET_CLAN_ALLY); + var s2 = con.createStatement(); + var rs2 = s2.executeQuery(GET_ALL_HEROES)) { + while (rs1.next()) { + StatsSet hero = new StatsSet(); + int charId = rs1.getInt(Olympiad.CHAR_ID); + hero.set(Olympiad.CHAR_NAME, rs1.getString(Olympiad.CHAR_NAME)); + hero.set(Olympiad.CLASS_ID, rs1.getInt(Olympiad.CLASS_ID)); + hero.set(COUNT, rs1.getInt(COUNT)); + hero.set(PLAYED, rs1.getInt(PLAYED)); + hero.set(CLAIMED, rs1.getBoolean(CLAIMED)); + + loadFights(charId); + loadDiary(charId); + loadMessage(charId); + + processHeroes(ps, charId, hero); + + HEROES.put(charId, hero); + } + + while (rs2.next()) { + StatsSet hero = new StatsSet(); + int charId = rs2.getInt(Olympiad.CHAR_ID); + hero.set(Olympiad.CHAR_NAME, rs2.getString(Olympiad.CHAR_NAME)); + hero.set(Olympiad.CLASS_ID, rs2.getInt(Olympiad.CLASS_ID)); + hero.set(COUNT, rs2.getInt(COUNT)); + hero.set(PLAYED, rs2.getInt(PLAYED)); + hero.set(CLAIMED, rs2.getBoolean(CLAIMED)); + + processHeroes(ps, charId, hero); + + COMPLETE_HEROES.put(charId, hero); + } + } catch (Exception ex) { + LOG.warn("Couldn't load Heroes!", ex); + } + + LOG.info("Loaded {} Heroes.", HEROES.size()); + LOG.info("Loaded {} all time Heroes.", COMPLETE_HEROES.size()); + } + + private void processHeroes(PreparedStatement ps, int charId, StatsSet hero) throws Exception { + ps.setInt(1, charId); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + int clanId = rs.getInt("clanid"); + int allyId = rs.getInt("allyId"); + String clanName = ""; + String allyName = ""; + int clanCrest = 0; + int allyCrest = 0; + if (clanId > 0) { + clanName = ClanTable.getInstance().getClan(clanId).getName(); + clanCrest = ClanTable.getInstance().getClan(clanId).getCrestId(); + if (allyId > 0) { + allyName = ClanTable.getInstance().getClan(clanId).getAllyName(); + allyCrest = ClanTable.getInstance().getClan(clanId).getAllyCrestId(); + } + } + hero.set(CLAN_CREST, clanCrest); + hero.set(CLAN_NAME, clanName); + hero.set(ALLY_CREST, allyCrest); + hero.set(ALLY_NAME, allyName); + } + ps.clearParameters(); + } + } + + private String calcFightTime(long FightTime) { + String format = String.format("%%0%dd", 2); + FightTime = FightTime / 1000; + String seconds = String.format(format, FightTime % 60); + String minutes = String.format(format, (FightTime % 3600) / 60); + return minutes + ":" + seconds; + } + + /** + * Restore hero message from Db. + * @param charId + */ + public void loadMessage(int charId) { + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT message FROM heroes WHERE charId=?")) { + ps.setInt(1, charId); + try (var rs = ps.executeQuery()) { + if (rs.next()) { + HERO_MESSAGE.put(charId, rs.getString("message")); + } + } + } catch (Exception ex) { + LOG.warn("Could not load Hero message for player Id {}!", charId, ex); + } + } + + public void loadDiary(int charId) { + final List diary = new ArrayList<>(); + int diaryEntries = 0; + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM heroes_diary WHERE charId=? ORDER BY time")) { + ps.setInt(1, charId); + try (var rs = ps.executeQuery()) { + while (rs.next()) { + StatsSet _diaryentry = new StatsSet(); + + long time = rs.getLong("time"); + int action = rs.getInt("action"); + int param = rs.getInt("param"); + + String date = (new SimpleDateFormat("yyyy-MM-dd HH")).format(new Date(time)); + _diaryentry.set("date", date); + + if (action == ACTION_RAID_KILLED) { + L2NpcTemplate template = NpcData.getInstance().getTemplate(param); + if (template != null) { + _diaryentry.set("action", template.getName() + " was defeated"); + } + } else if (action == ACTION_HERO_GAINED) { + _diaryentry.set("action", "Gained Hero status"); + } else if (action == ACTION_CASTLE_TAKEN) { + Castle castle = CastleManager.getInstance().getCastleById(param); + if (castle != null) { + _diaryentry.set("action", castle.getName() + " Castle was successfully taken"); + } + } + diary.add(_diaryentry); + diaryEntries++; + } + } + HERO_DIARY.put(charId, diary); + + LOG.info("Loaded {} diary entries for Hero {}.", diaryEntries, CharNameTable.getInstance().getNameById(charId)); + } catch (Exception ex) { + LOG.warn("Could not load Hero Diary for player Id {}!", charId, ex); + } + } + + public void loadFights(int charId) { + final List fights = new ArrayList<>(); + StatsSet heroCountData = new StatsSet(); + Calendar data = Calendar.getInstance(); + data.set(Calendar.DAY_OF_MONTH, 1); + data.set(Calendar.HOUR_OF_DAY, 0); + data.set(Calendar.MINUTE, 0); + data.set(Calendar.MILLISECOND, 0); + + long from = data.getTimeInMillis(); + int numberOfFights = 0; + int _victorys = 0; + int _losses = 0; + int _draws = 0; + + try (var con = ConnectionFactory.getInstance().getConnection(); + var ps = con.prepareStatement("SELECT * FROM olympiad_fights WHERE (charOneId=? OR charTwoId=?) AND startvictory"); + _victorys++; + } else if (winner == 2) { + fight.set("result", "loss"); + _losses++; + } else if (winner == 0) { + fight.set("result", "draw"); + _draws++; + } + + fights.add(fight); + + numberOfFights++; + } + } else if (charId == charTwoId) { + String name = CharNameTable.getInstance().getNameById(charOneId); + String cls = ClassListData.getInstance().getClass(charOneClass).getClientCode(); + if (name != null) { + StatsSet fight = new StatsSet(); + fight.set("oponent", name); + fight.set("oponentclass", cls); + + fight.set("time", calcFightTime(time)); + String date = (new SimpleDateFormat("yyyy-MM-dd HH:mm")).format(new Date(start)); + fight.set("start", date); + + fight.set("classed", classed); + if (winner == 1) { + fight.set("result", "loss"); + _losses++; + } else if (winner == 2) { + fight.set("result", "victory"); + _victorys++; + } else if (winner == 0) { + fight.set("result", "draw"); + _draws++; + } + + fights.add(fight); + + numberOfFights++; + } + } + } + } + + heroCountData.set("victory", _victorys); + heroCountData.set("draw", _draws); + heroCountData.set("loss", _losses); + + HERO_COUNTS.put(charId, heroCountData); + HERO_FIGHTS.put(charId, fights); + + LOG.info("Loaded {} fights for Hero {}.", numberOfFights, CharNameTable.getInstance().getNameById(charId)); + } catch (Exception ex) { + LOG.warn("Could not load Hero fights history for player Id {}!", charId, ex); + } + } + + public Map getHeroes() { + return HEROES; + } + + public int getHeroByClass(int classId) { + for (Entry e : HEROES.entrySet()) { + if (e.getValue().getInt(Olympiad.CLASS_ID) == classId) { + return e.getKey(); + } + } + return 0; + } + + public void resetData() { + HERO_DIARY.clear(); + HERO_FIGHTS.clear(); + HERO_COUNTS.clear(); + HERO_MESSAGE.clear(); + } + + public void showHeroDiary(L2PcInstance activeChar, int heroclass, int charid, int page) { + final int perpage = 10; + + final List mainList = HERO_DIARY.get(charid); + if (mainList != null) { + final NpcHtmlMessage diaryReply = new NpcHtmlMessage(); + final String htmContent = HtmCache.getInstance().getHtm(activeChar.getHtmlPrefix(), "data/html/olympiad/herodiary.htm"); + final String heroMessage = HERO_MESSAGE.get(charid); + if ((htmContent != null) && (heroMessage != null)) { + diaryReply.setHtml(htmContent); + diaryReply.replace("%heroname%", CharNameTable.getInstance().getNameById(charid)); + diaryReply.replace("%message%", heroMessage); + diaryReply.disableValidation(); + + if (!mainList.isEmpty()) { + final List list = new ArrayList<>(mainList); + Collections.reverse(list); + + boolean color = true; + final StringBuilder fList = new StringBuilder(500); + int counter = 0; + int breakat = 0; + for (int i = ((page - 1) * perpage); i < list.size(); i++) { + breakat = i; + StatsSet diaryEntry = list.get(i); + StringUtil.append(fList, ""); + if (color) { + StringUtil.append(fList, ""); + } else { + StringUtil.append(fList, "
          "); + } + StringUtil.append(fList, ""); + StringUtil.append(fList, ""); + StringUtil.append(fList, "
          " + diaryEntry.getString("date") + ":xx
          " + diaryEntry.getString("action") + "
           
          "); + StringUtil.append(fList, ""); + color = !color; + counter++; + if (counter >= perpage) { + break; + } + } + + if (breakat < (list.size() - 1)) { + diaryReply.replace("%buttprev%", "