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("&$413; ", topic
+ .getName(), "
");
+ 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("&$413; : ", topic
+ .getName(), " &$417; : ", topic.getOwnerName()
+ + " &$418; : ", dateFormat.format(posts.get(0)
+ .getDate()), "
");
+ 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(" ");
+ 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, "");
+ 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, " ");
+ }
+ }
+ }
+
+ 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, " ", String.valueOf(i), " ");
+ } else {
+ StringUtil.append(html, " ", String.valueOf(i), " ");
+ }
+ }
+ if (index == nbp) {
+ html.append(" ");
+ } else {
+ StringUtil.append(html, " ");
+ }
+
+ StringUtil.append(html, "
");
+ 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:
+ *
+ * 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.
+ *
+ *
+ * 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
+ *
+ *
+ * Apply taxes: Uses the "taxIngredient" entry in order to add a certain amount of adena to the ingredients.
+ *
+ * Additional product and ingredient multipliers.
+ *
+ * @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