From bff9251737b29c86ae04f5ff49b6abe010b4ddb5 Mon Sep 17 00:00:00 2001 From: HoutarouOreki Date: Wed, 11 Mar 2026 01:46:34 +0100 Subject: [PATCH] reinit branch --- .editorconfig | 265 ++++ .gitignore | 675 ++++++++ .timetracker | 1355 +++++++++++++++++ .vscode/settings.json | 3 + LICENSE.txt | 88 ++ OrekiWoofsBeehives.slnx | 18 + .../Behaviors/BlockBehaviorBeehiveAffected.cs | 244 +++ ...ockEntityBehaviorBeehiveYieldMultiplier.cs | 140 ++ ...ockEntityBehaviorVanillaSkepSwarmTarget.cs | 60 + .../BlockEntities/BeehiveStats.cs | 193 +++ .../BlockEntities/BlockEntityBeeSwarm.cs | 652 ++++++++ .../BlockEntityReusableBeehive.cs | 1062 +++++++++++++ OrekiWoofsBeehives/Blocks/BlockBeeSwarm.cs | 55 + .../Blocks/BlockBeehiveFrame.cs | 51 + .../Blocks/BlockReusableBeehive.cs | 678 +++++++++ OrekiWoofsBeehives/BoostCalculationType.cs | 8 + OrekiWoofsBeehives/ChatCommands.cs | 147 ++ OrekiWoofsBeehives/Config.cs | 234 +++ OrekiWoofsBeehives/CropBonusType.cs | 12 + .../Helpers/BeehiveInfoStringBuilder.cs | 504 ++++++ .../InformationVerbosityServerSideType.cs | 8 + OrekiWoofsBeehives/MetaConfig.cs | 7 + OrekiWoofsBeehives/ModConfigSetup.cs | 179 +++ .../MultipleBeehivesBoostCombinationType.cs | 7 + OrekiWoofsBeehives/OrekiWoofsBeehives.csproj | 86 ++ .../OrekiWoofsBeehivesModSystem.cs | 132 ++ .../Patches/BEFarmland_GetGrowthRate_Patch.cs | 57 + .../Patches/BlockCrop_GetDrops_Patch.cs | 68 + .../Properties/launchSettings.json | 16 + OrekiWoofsBeehives/Utilities/ApiExtensions.cs | 11 + .../Utilities/BeehiveRegistry.cs | 20 + .../Utilities/VanillaSkepRegistry.cs | 24 + .../blocktypes/beehive.json | 46 + .../blocktypes/beehiveframe.json | 96 ++ .../blocktypes/beehiveroof.json | 41 + .../blocktypes/beehivestand.json | 43 + .../blocktypes/beeswarm.json | 41 + .../config/configlib-patches.json | 430 ++++++ .../assets/orekiwoofsbeehives/lang/cs.json | 52 + .../assets/orekiwoofsbeehives/lang/en.json | 137 ++ .../assets/orekiwoofsbeehives/lang/pl.json | 52 + .../assets/orekiwoofsbeehives/lang/ru.json | 137 ++ .../assets/orekiwoofsbeehives/lang/uk.json | 52 + .../orekiwoofsbeehives/patches/farmland.json | 24 + .../orekiwoofsbeehives/patches/flowers.json | 13 + .../patches/plantcontainers.json | 46 + .../orekiwoofsbeehives/patches/skep.json | 13 + .../recipes/grid/beehive.json | 11 + .../recipes/grid/beehiveframe.json | 68 + .../recipes/grid/beehiveroof.json | 10 + .../recipes/grid/beehivestand.json | 10 + .../recipes/grid/honeycomb.json | 25 + .../shapes/block/beehive-closed.json | 155 ++ .../shapes/block/beehive-open.json | 144 ++ .../shapes/block/beehiveframepile.json | 926 +++++++++++ .../shapes/block/beehiveroof.json | 82 + .../shapes/block/beehivestand.json | 77 + .../shapes/block/beeswarm.json | 64 + .../shapes/item/beehiveframe.json | 70 + OrekiWoofsBeehives/modicon.png | Bin 0 -> 33503 bytes OrekiWoofsBeehives/modinfo.json | 14 + OrekiWoofsBees.Common/Configs/ChatCommands.cs | 99 ++ .../Configs/ConfigCommandAttribute.cs | 23 + .../Configs/ParserExtensions.cs | 16 + .../IPlantPositionRegistry.cs | 40 + .../OrekiWoofsBees.Common.csproj | 62 + OrekiWoofsBees.Common/Overlaps.cs | 32 + .../PlantPositionRegistryModSystem2.cs | 445 ++++++ .../PlantRecognitionUtilities.cs | 45 + OrekiWoofsBees.Common/StructVec3i.cs | 8 + OrekiWoofsBees.Common/SwarmState.cs | 8 + .../VectorConversionUtils.cs | 10 + OrekiWoofsBees.Full/.gitignore | 1 + .../OrekiWoofsBees.Full.csproj | 38 + .../Properties/launchSettings.json | 16 + RoamingBees/.gitignore | 675 ++++++++ .../Behaviors/BlockBehaviorBeeSwarm.cs | 24 + .../Behaviors/BlockBehaviorRoamingBees.cs | 26 + .../Behaviors/BlockEntityBehaviorBeeSwarm.cs | 615 ++++++++ .../BlockEntityBehaviorRoamingBees.cs | 433 ++++++ RoamingBees/RoamingBees/ChatCommands.cs | 83 + RoamingBees/RoamingBees/Config.cs | 113 ++ RoamingBees/RoamingBees/ModConfigSetup.cs | 100 ++ .../Particles/BeePathGeneration.cs | 432 ++++++ .../Particles/BeePlannedPathPoint.cs | 5 + .../Particles/BeePlannedPathPointContract.cs | 37 + RoamingBees/RoamingBees/Particles/BeeRole.cs | 9 + .../RoamingBees/Particles/BeeSpawnPacket.cs | 34 + .../Particles/BeeSpawnPacketDistributor.cs | 85 ++ .../Particles/BeeVisualParticleRenderer.cs | 235 +++ .../Catchup/BeeSpawnCatchupPacket.cs | 15 + .../Particles/Catchup/CatchupRequestPacket.cs | 11 + .../Particles/Catchup/IParticleCatchup.cs | 8 + .../Particles/Catchup/ParticleCatchup.cs | 17 + .../Particles/DebugPathBoxRenderer.cs | 92 ++ .../Particles/DebugPathRenderManager.cs | 65 + .../RoamingBees/Particles/IBeeSpawnHandler.cs | 6 + .../Particles/InternalBeeParticle.cs | 283 ++++ .../Particles/SwarmBeePathGeneration.cs | 272 ++++ .../Properties/launchSettings.json | 16 + RoamingBees/RoamingBees/RoamingBees.csproj | 83 + .../RoamingBees/RoamingBeesModSystem.cs | 96 ++ .../RoamingBees/Utilities/ApiExtensions.cs | 10 + .../RoamingBees/Utilities/VectorParsing.cs | 41 + .../roamingbees/config/configlib-patches.json | 248 +++ .../assets/roamingbees/lang/en.json | 29 + .../assets/roamingbees/lang/ru.json | 29 + .../assets/roamingbees/patches/beeswarm.json | 18 + .../roamingbees/patches/ceramicbroodpot.json | 145 ++ .../roamingbees/patches/langstrothstack.json | 49 + .../patches/orekiwoofsbeehives.json | 49 + .../assets/roamingbees/patches/skep.json | 39 + RoamingBees/RoamingBees/modicon.png | Bin 0 -> 7544 bytes RoamingBees/RoamingBees/modinfo.json | 14 + RoamingBees/ZZCakeBuild/Program.cs | 122 ++ .../ZZCakeBuild/RoamingBeesCakeBuild.csproj | 20 + RoamingBees/build.ps1 | 2 + RoamingBees/build.sh | 1 + TestScenarios/ConfigScenarios.md | 54 + TestScenarios/CropBoostScenarios.md | 223 +++ TestScenarios/SwarmScenarios.md | 190 +++ TestScenarios/readme.md | 1 + .../OrekiWoofsBeehivesCakeBuild.csproj | 20 + ZZCakeBuild/Program.cs | 122 ++ build.ps1 | 14 + build.sh | 1 + printFormattedCommits.ps1 | 17 + printLatestCommits.ps1 | 3 + readme.md | 3 + 129 files changed, 16115 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .timetracker create mode 100644 .vscode/settings.json create mode 100644 LICENSE.txt create mode 100644 OrekiWoofsBeehives.slnx create mode 100644 OrekiWoofsBeehives/Behaviors/BlockBehaviorBeehiveAffected.cs create mode 100644 OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorBeehiveYieldMultiplier.cs create mode 100644 OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorVanillaSkepSwarmTarget.cs create mode 100644 OrekiWoofsBeehives/BlockEntities/BeehiveStats.cs create mode 100644 OrekiWoofsBeehives/BlockEntities/BlockEntityBeeSwarm.cs create mode 100644 OrekiWoofsBeehives/BlockEntities/BlockEntityReusableBeehive.cs create mode 100644 OrekiWoofsBeehives/Blocks/BlockBeeSwarm.cs create mode 100644 OrekiWoofsBeehives/Blocks/BlockBeehiveFrame.cs create mode 100644 OrekiWoofsBeehives/Blocks/BlockReusableBeehive.cs create mode 100644 OrekiWoofsBeehives/BoostCalculationType.cs create mode 100644 OrekiWoofsBeehives/ChatCommands.cs create mode 100644 OrekiWoofsBeehives/Config.cs create mode 100644 OrekiWoofsBeehives/CropBonusType.cs create mode 100644 OrekiWoofsBeehives/Helpers/BeehiveInfoStringBuilder.cs create mode 100644 OrekiWoofsBeehives/InformationVerbosityServerSideType.cs create mode 100644 OrekiWoofsBeehives/MetaConfig.cs create mode 100644 OrekiWoofsBeehives/ModConfigSetup.cs create mode 100644 OrekiWoofsBeehives/MultipleBeehivesBoostCombinationType.cs create mode 100644 OrekiWoofsBeehives/OrekiWoofsBeehives.csproj create mode 100644 OrekiWoofsBeehives/OrekiWoofsBeehivesModSystem.cs create mode 100644 OrekiWoofsBeehives/Patches/BEFarmland_GetGrowthRate_Patch.cs create mode 100644 OrekiWoofsBeehives/Patches/BlockCrop_GetDrops_Patch.cs create mode 100644 OrekiWoofsBeehives/Properties/launchSettings.json create mode 100644 OrekiWoofsBeehives/Utilities/ApiExtensions.cs create mode 100644 OrekiWoofsBeehives/Utilities/BeehiveRegistry.cs create mode 100644 OrekiWoofsBeehives/Utilities/VanillaSkepRegistry.cs create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehive.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveframe.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveroof.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehivestand.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beeswarm.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/config/configlib-patches.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/cs.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/en.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/pl.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/ru.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/uk.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/farmland.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/flowers.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/plantcontainers.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/skep.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehive.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveframe.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveroof.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehivestand.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/honeycomb.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-closed.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-open.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveframepile.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveroof.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehivestand.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beeswarm.json create mode 100644 OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/item/beehiveframe.json create mode 100644 OrekiWoofsBeehives/modicon.png create mode 100644 OrekiWoofsBeehives/modinfo.json create mode 100644 OrekiWoofsBees.Common/Configs/ChatCommands.cs create mode 100644 OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs create mode 100644 OrekiWoofsBees.Common/Configs/ParserExtensions.cs create mode 100644 OrekiWoofsBees.Common/IPlantPositionRegistry.cs create mode 100644 OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj create mode 100644 OrekiWoofsBees.Common/Overlaps.cs create mode 100644 OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs create mode 100644 OrekiWoofsBees.Common/PlantRecognitionUtilities.cs create mode 100644 OrekiWoofsBees.Common/StructVec3i.cs create mode 100644 OrekiWoofsBees.Common/SwarmState.cs create mode 100644 OrekiWoofsBees.Common/VectorConversionUtils.cs create mode 100644 OrekiWoofsBees.Full/.gitignore create mode 100644 OrekiWoofsBees.Full/OrekiWoofsBees.Full.csproj create mode 100644 OrekiWoofsBees.Full/Properties/launchSettings.json create mode 100644 RoamingBees/.gitignore create mode 100644 RoamingBees/RoamingBees/Behaviors/BlockBehaviorBeeSwarm.cs create mode 100644 RoamingBees/RoamingBees/Behaviors/BlockBehaviorRoamingBees.cs create mode 100644 RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorBeeSwarm.cs create mode 100644 RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorRoamingBees.cs create mode 100644 RoamingBees/RoamingBees/ChatCommands.cs create mode 100644 RoamingBees/RoamingBees/Config.cs create mode 100644 RoamingBees/RoamingBees/ModConfigSetup.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeePathGeneration.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeePlannedPathPoint.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeePlannedPathPointContract.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeeRole.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeeSpawnPacket.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeeSpawnPacketDistributor.cs create mode 100644 RoamingBees/RoamingBees/Particles/BeeVisualParticleRenderer.cs create mode 100644 RoamingBees/RoamingBees/Particles/Catchup/BeeSpawnCatchupPacket.cs create mode 100644 RoamingBees/RoamingBees/Particles/Catchup/CatchupRequestPacket.cs create mode 100644 RoamingBees/RoamingBees/Particles/Catchup/IParticleCatchup.cs create mode 100644 RoamingBees/RoamingBees/Particles/Catchup/ParticleCatchup.cs create mode 100644 RoamingBees/RoamingBees/Particles/DebugPathBoxRenderer.cs create mode 100644 RoamingBees/RoamingBees/Particles/DebugPathRenderManager.cs create mode 100644 RoamingBees/RoamingBees/Particles/IBeeSpawnHandler.cs create mode 100644 RoamingBees/RoamingBees/Particles/InternalBeeParticle.cs create mode 100644 RoamingBees/RoamingBees/Particles/SwarmBeePathGeneration.cs create mode 100644 RoamingBees/RoamingBees/Properties/launchSettings.json create mode 100644 RoamingBees/RoamingBees/RoamingBees.csproj create mode 100644 RoamingBees/RoamingBees/RoamingBeesModSystem.cs create mode 100644 RoamingBees/RoamingBees/Utilities/ApiExtensions.cs create mode 100644 RoamingBees/RoamingBees/Utilities/VectorParsing.cs create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/config/configlib-patches.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/lang/en.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/lang/ru.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/patches/beeswarm.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/patches/ceramicbroodpot.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/patches/langstrothstack.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/patches/orekiwoofsbeehives.json create mode 100644 RoamingBees/RoamingBees/assets/roamingbees/patches/skep.json create mode 100644 RoamingBees/RoamingBees/modicon.png create mode 100644 RoamingBees/RoamingBees/modinfo.json create mode 100644 RoamingBees/ZZCakeBuild/Program.cs create mode 100644 RoamingBees/ZZCakeBuild/RoamingBeesCakeBuild.csproj create mode 100644 RoamingBees/build.ps1 create mode 100644 RoamingBees/build.sh create mode 100644 TestScenarios/ConfigScenarios.md create mode 100644 TestScenarios/CropBoostScenarios.md create mode 100644 TestScenarios/SwarmScenarios.md create mode 100644 TestScenarios/readme.md create mode 100644 ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj create mode 100644 ZZCakeBuild/Program.cs create mode 100644 build.ps1 create mode 100644 build.sh create mode 100644 printFormattedCommits.ps1 create mode 100644 printLatestCommits.ps1 create mode 100644 readme.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..760e393 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,265 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Code Actions #### + +# Type members +dotnet_hide_advanced_members = false +dotnet_member_insertion_location = with_other_members_of_the_same_kind +dotnet_property_generation_behavior = prefer_throwing_properties + +# Symbol search +dotnet_search_reference_assemblies = true + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_prefer_system_hash_code = true +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = false +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true:warning +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = false:suggestion +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = true +csharp_style_var_when_type_is_apparent = true + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_local_functions = true:suggestion +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = false:warning +csharp_prefer_simple_using_statement = true +csharp_prefer_system_threading_lock = true +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_simple_property_accessors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_implicitly_typed_lambda_expression = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_unbound_generic_type_in_nameof = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:suggestion + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.private_consts_should_be_lowercase.severity = suggestion +dotnet_naming_rule.private_consts_should_be_lowercase.symbols = private_consts +dotnet_naming_rule.private_consts_should_be_lowercase.style = underscore_lower + +dotnet_naming_rule.public_consts_should_be_uppercase.severity = suggestion +dotnet_naming_rule.public_consts_should_be_uppercase.symbols = public_consts +dotnet_naming_rule.public_consts_should_be_uppercase.style = underscore_upper + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.private_consts.applicable_kinds = field +dotnet_naming_symbols.private_consts.applicable_accessibilities = private, protected_internal, private_protected +dotnet_naming_symbols.private_consts.required_modifiers = const + +dotnet_naming_symbols.public_consts.applicable_kinds = field +dotnet_naming_symbols.public_consts.applicable_accessibilities = public, internal, protected +dotnet_naming_symbols.public_consts.required_modifiers = const + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.underscore_lower.required_prefix = +dotnet_naming_style.underscore_lower.required_suffix = +dotnet_naming_style.underscore_lower.word_separator = _ +dotnet_naming_style.underscore_lower.capitalization = all_lower + +dotnet_naming_style.underscore_upper.required_prefix = +dotnet_naming_style.underscore_upper.required_suffix = +dotnet_naming_style.underscore_upper.word_separator = _ +dotnet_naming_style.underscore_upper.capitalization = all_upper diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e4f89c --- /dev/null +++ b/.gitignore @@ -0,0 +1,675 @@ +# Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode,rider +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio,visualstudiocode,rider + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### VisualStudioCode ### +!.vscode/*.code-snippets + +# Local History for Visual Studio Code + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### VisualStudio ### + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files + +# Build results + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files + +# MSTest test Results + +# NUnit + +# Build Results of an ATL Project + +# Benchmark Results + +# .NET Core + +# ASP.NET Scaffolding + +# StyleCop + +# Files built by Visual Studio + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool + +# Coverlet is a free, cross platform Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# NuGet Symbol Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio + +# Telerik's JustMock configuration file + +# BizTalk build output + +# OpenCover UI analysis results + +# Azure Stream Analytics local run output + +# MSBuild Binary and Structured Log + +# NVidia Nsight GPU debugger configuration file + +# MFractors (Xamarin productivity tool) working folder + +# Local History for Visual Studio + +# Visual Studio History (VSHistory) files + +# BeatPulse healthcheck temp database + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 + +# Ionide (cross platform F# VS Code tools) working folder + +# Fody - auto-generated XML schema + +# VS Code files for those working on multiple tools + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs + +# JetBrains Rider + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode,rider \ No newline at end of file diff --git a/.timetracker b/.timetracker new file mode 100644 index 0000000..5ac1149 --- /dev/null +++ b/.timetracker @@ -0,0 +1,1355 @@ +{ + "total": 591213, + "sessions": [ + { + "begin": "2026-01-09T17:26:02+01:00", + "end": "2026-01-09T17:26:06+01:00", + "duration": 4 + }, + { + "begin": "2026-01-09T17:40:28+01:00", + "end": "2026-01-09T18:17:36+01:00", + "duration": 2228 + }, + { + "begin": "2026-01-09T18:17:36+01:00", + "end": "2026-01-09T19:15:30+01:00", + "duration": 3474 + }, + { + "begin": "2026-01-09T19:15:30+01:00", + "end": "2026-01-09T19:33:00+01:00", + "duration": 1050 + }, + { + "begin": "2026-01-09T19:33:00+01:00", + "end": "2026-01-09T20:54:43+01:00", + "duration": 4903 + }, + { + "begin": "2026-01-09T20:54:43+01:00", + "end": "2026-01-09T21:41:35+01:00", + "duration": 2812 + }, + { + "begin": "2026-01-11T14:48:58+01:00", + "end": "2026-01-11T16:49:08+01:00", + "duration": 7210 + }, + { + "begin": "2026-01-11T16:56:29+01:00", + "end": "2026-01-11T17:13:52+01:00", + "duration": 1043 + }, + { + "begin": "2026-01-11T17:37:51+01:00", + "end": "2026-01-11T17:41:11+01:00", + "duration": 200 + }, + { + "begin": "2026-01-11T17:41:12+01:00", + "end": "2026-01-11T17:54:43+01:00", + "duration": 811 + }, + { + "begin": "2026-01-11T18:00:22+01:00", + "end": "2026-01-11T18:39:43+01:00", + "duration": 2361 + }, + { + "begin": "2026-01-11T18:39:43+01:00", + "end": "2026-01-11T19:14:54+01:00", + "duration": 2111 + }, + { + "begin": "2026-01-11T19:16:57+01:00", + "end": "2026-01-11T19:37:08+01:00", + "duration": 1211 + }, + { + "begin": "2026-01-11T20:16:13+01:00", + "end": "2026-01-11T20:32:37+01:00", + "duration": 984 + }, + { + "begin": "2026-01-11T20:32:37+01:00", + "end": "2026-01-11T21:17:29+01:00", + "duration": 2692 + }, + { + "begin": "2026-01-11T21:23:07+01:00", + "end": "2026-01-11T21:42:45+01:00", + "duration": 1178 + }, + { + "begin": "2026-01-11T21:44:33+01:00", + "end": "2026-01-11T22:25:55+01:00", + "duration": 2482 + }, + { + "begin": "2026-01-11T22:28:43+01:00", + "end": "2026-01-11T22:37:10+01:00", + "duration": 507 + }, + { + "begin": "2026-01-11T22:37:25+01:00", + "end": "2026-01-11T23:08:32+01:00", + "duration": 1867 + }, + { + "begin": "2026-01-11T23:09:48+01:00", + "end": "2026-01-11T23:34:27+01:00", + "duration": 1479 + }, + { + "begin": "2026-01-12T17:15:10+01:00", + "end": "2026-01-12T17:27:37+01:00", + "duration": 747 + }, + { + "begin": "2026-01-12T17:27:37+01:00", + "end": "2026-01-12T17:39:36+01:00", + "duration": 719 + }, + { + "begin": "2026-01-12T17:46:07+01:00", + "end": "2026-01-12T17:57:01+01:00", + "duration": 654 + }, + { + "begin": "2026-01-12T17:59:55+01:00", + "end": "2026-01-12T18:00:06+01:00", + "duration": 11 + }, + { + "begin": "2026-01-12T18:00:17+01:00", + "end": "2026-01-12T18:19:02+01:00", + "duration": 1125 + }, + { + "begin": "2026-01-12T18:20:43+01:00", + "end": "2026-01-12T18:51:08+01:00", + "duration": 1825 + }, + { + "begin": "2026-01-12T18:52:07+01:00", + "end": "2026-01-12T19:14:12+01:00", + "duration": 1325 + }, + { + "begin": "2026-01-12T19:14:12+01:00", + "end": "2026-01-12T23:14:13+01:00", + "duration": 14401 + }, + { + "begin": "2026-01-12T23:14:13+01:00", + "end": "2026-01-12T23:48:16+01:00", + "duration": 2043 + }, + { + "begin": "2026-01-12T23:48:16+01:00", + "end": "2026-01-13T00:11:00+01:00", + "duration": 1364 + }, + { + "begin": "2026-01-13T00:11:00+01:00", + "end": "2026-01-13T01:11:34+01:00", + "duration": 3634 + }, + { + "begin": "2026-01-13T01:11:34+01:00", + "end": "2026-01-13T01:30:38+01:00", + "duration": 1144 + }, + { + "begin": "2026-01-13T01:30:38+01:00", + "end": "2026-01-13T01:40:51+01:00", + "duration": 613 + }, + { + "begin": "2026-01-13T01:40:51+01:00", + "end": "2026-01-13T01:52:08+01:00", + "duration": 677 + }, + { + "begin": "2026-01-13T01:52:08+01:00", + "end": "2026-01-13T02:03:54+01:00", + "duration": 706 + }, + { + "begin": "2026-01-13T02:03:54+01:00", + "end": "2026-01-13T02:06:36+01:00", + "duration": 162 + }, + { + "begin": "2026-01-13T02:06:36+01:00", + "end": "2026-01-13T02:32:04+01:00", + "duration": 1528 + }, + { + "begin": "2026-01-13T17:03:56+01:00", + "end": "2026-01-13T17:31:50+01:00", + "duration": 1674 + }, + { + "begin": "2026-01-13T17:31:50+01:00", + "end": "2026-01-13T17:50:44+01:00", + "duration": 1134 + }, + { + "begin": "2026-01-13T17:50:44+01:00", + "end": "2026-01-13T18:05:54+01:00", + "duration": 910 + }, + { + "begin": "2026-01-13T18:25:28+01:00", + "end": "2026-01-13T19:39:57+01:00", + "duration": 4469 + }, + { + "begin": "2026-01-13T19:52:37+01:00", + "end": "2026-01-13T20:04:04+01:00", + "duration": 687 + }, + { + "begin": "2026-01-13T20:04:05+01:00", + "end": "2026-01-13T21:09:31+01:00", + "duration": 3926 + }, + { + "begin": "2026-01-13T21:28:06+01:00", + "end": "2026-01-13T21:38:35+01:00", + "duration": 629 + }, + { + "begin": "2026-01-13T21:38:35+01:00", + "end": "2026-01-13T21:59:55+01:00", + "duration": 1280 + }, + { + "begin": "2026-01-13T22:10:09+01:00", + "end": "2026-01-13T22:38:26+01:00", + "duration": 1697 + }, + { + "begin": "2026-01-13T22:54:23+01:00", + "end": "2026-01-13T23:33:23+01:00", + "duration": 2340 + }, + { + "begin": "2026-01-13T23:38:49+01:00", + "end": "2026-01-14T00:30:46+01:00", + "duration": 3117 + }, + { + "begin": "2026-01-14T00:37:49+01:00", + "end": "2026-01-14T01:22:59+01:00", + "duration": 2710 + }, + { + "begin": "2026-01-14T01:29:18+01:00", + "end": "2026-01-14T01:48:53+01:00", + "duration": 1175 + }, + { + "begin": "2026-01-14T01:55:37+01:00", + "end": "2026-01-14T02:28:08+01:00", + "duration": 1951 + }, + { + "begin": "2026-01-14T02:33:22+01:00", + "end": "2026-01-14T02:44:26+01:00", + "duration": 664 + }, + { + "begin": "2026-01-14T03:01:28+01:00", + "end": "2026-01-14T03:11:36+01:00", + "duration": 608 + }, + { + "begin": "2026-01-14T03:15:22+01:00", + "end": "2026-01-14T05:01:56+01:00", + "duration": 6394 + }, + { + "begin": "2026-01-14T05:01:56+01:00", + "end": "2026-01-14T05:38:02+01:00", + "duration": 2166 + }, + { + "begin": "2026-01-14T05:38:02+01:00", + "end": "2026-01-14T06:48:58+01:00", + "duration": 4256 + }, + { + "begin": "2026-01-14T06:48:58+01:00", + "end": "2026-01-14T07:41:31+01:00", + "duration": 3153 + }, + { + "begin": "2026-01-14T17:07:34+01:00", + "end": "2026-01-14T17:25:16+01:00", + "duration": 1062 + }, + { + "begin": "2026-01-14T17:29:39+01:00", + "end": "2026-01-14T17:40:01+01:00", + "duration": 622 + }, + { + "begin": "2026-01-14T17:50:46+01:00", + "end": "2026-01-14T18:52:18+01:00", + "duration": 3692 + }, + { + "begin": "2026-01-14T18:52:20+01:00", + "end": "2026-01-14T19:05:17+01:00", + "duration": 777 + }, + { + "begin": "2026-01-14T19:05:17+01:00", + "end": "2026-01-14T19:15:23+01:00", + "duration": 606 + }, + { + "begin": "2026-01-14T20:01:14+01:00", + "end": "2026-01-14T20:11:22+01:00", + "duration": 608 + }, + { + "begin": "2026-01-14T20:27:04+01:00", + "end": "2026-01-14T20:38:05+01:00", + "duration": 661 + }, + { + "begin": "2026-01-14T21:01:59+01:00", + "end": "2026-01-14T21:16:07+01:00", + "duration": 848 + }, + { + "begin": "2026-01-14T21:35:32+01:00", + "end": "2026-01-14T21:55:58+01:00", + "duration": 1226 + }, + { + "begin": "2026-01-14T21:59:12+01:00", + "end": "2026-01-14T22:16:57+01:00", + "duration": 1065 + }, + { + "begin": "2026-01-14T22:16:58+01:00", + "end": "2026-01-14T23:01:48+01:00", + "duration": 2690 + }, + { + "begin": "2026-01-14T23:01:48+01:00", + "end": "2026-01-14T23:11:54+01:00", + "duration": 606 + }, + { + "begin": "2026-01-14T23:20:53+01:00", + "end": "2026-01-14T23:37:53+01:00", + "duration": 1020 + }, + { + "begin": "2026-01-14T23:37:53+01:00", + "end": "2026-01-14T23:43:26+01:00", + "duration": 333 + }, + { + "begin": "2026-01-14T23:43:26+01:00", + "end": "2026-01-15T03:40:29+01:00", + "duration": 14223 + }, + { + "begin": "2026-01-15T03:49:53+01:00", + "end": "2026-01-15T04:47:05+01:00", + "duration": 3432 + }, + { + "begin": "2026-01-15T18:42:39+01:00", + "end": "2026-01-15T20:28:35+01:00", + "duration": 6356 + }, + { + "begin": "2026-01-15T20:29:28+01:00", + "end": "2026-01-15T21:14:34+01:00", + "duration": 2706 + }, + { + "begin": "2026-01-15T21:30:40+01:00", + "end": "2026-01-15T23:13:58+01:00", + "duration": 6198 + }, + { + "begin": "2026-01-15T23:16:39+01:00", + "end": "2026-01-16T00:27:42+01:00", + "duration": 4263 + }, + { + "begin": "2026-01-16T00:48:24+01:00", + "end": "2026-01-16T02:09:39+01:00", + "duration": 4875 + }, + { + "begin": "2026-01-16T02:09:39+01:00", + "end": "2026-01-16T03:27:05+01:00", + "duration": 4646 + }, + { + "begin": "2026-01-16T17:07:37+01:00", + "end": "2026-01-16T17:19:01+01:00", + "duration": 684 + }, + { + "begin": "2026-01-16T17:20:47+01:00", + "end": "2026-01-16T17:30:55+01:00", + "duration": 608 + }, + { + "begin": "2026-01-16T17:42:01+01:00", + "end": "2026-01-16T18:03:36+01:00", + "duration": 1295 + }, + { + "begin": "2026-01-16T18:10:54+01:00", + "end": "2026-01-16T19:10:54+01:00", + "duration": 3600 + }, + { + "begin": "2026-01-16T19:20:28+01:00", + "end": "2026-01-16T19:41:07+01:00", + "duration": 1239 + }, + { + "begin": "2026-01-16T19:41:07+01:00", + "end": "2026-01-16T20:13:28+01:00", + "duration": 1941 + }, + { + "begin": "2026-01-16T20:13:28+01:00", + "end": "2026-01-16T21:13:11+01:00", + "duration": 3583 + }, + { + "begin": "2026-01-16T21:13:11+01:00", + "end": "2026-01-16T22:03:21+01:00", + "duration": 3010 + }, + { + "begin": "2026-01-16T22:03:21+01:00", + "end": "2026-01-16T22:15:51+01:00", + "duration": 750 + }, + { + "begin": "2026-01-16T22:15:51+01:00", + "end": "2026-01-17T00:44:06+01:00", + "duration": 8895 + }, + { + "begin": "2026-01-17T14:13:22+01:00", + "end": "2026-01-17T14:20:02+01:00", + "duration": 400 + }, + { + "begin": "2026-01-17T14:20:02+01:00", + "end": "2026-01-17T14:46:29+01:00", + "duration": 1587 + }, + { + "begin": "2026-01-17T14:46:29+01:00", + "end": "2026-01-17T16:03:56+01:00", + "duration": 4647 + }, + { + "begin": "2026-01-17T21:07:40+01:00", + "end": "2026-01-17T23:18:42+01:00", + "duration": 7862 + }, + { + "begin": "2026-01-17T23:18:44+01:00", + "end": "2026-01-17T23:40:41+01:00", + "duration": 1317 + }, + { + "begin": "2026-01-18T03:10:07+01:00", + "end": "2026-01-18T03:20:19+01:00", + "duration": 612 + }, + { + "begin": "2026-01-19T00:32:51+01:00", + "end": "2026-01-19T00:59:30+01:00", + "duration": 1599 + }, + { + "begin": "2026-01-19T00:59:31+01:00", + "end": "2026-01-19T01:21:24+01:00", + "duration": 1313 + }, + { + "begin": "2026-01-19T01:35:36+01:00", + "end": "2026-01-19T02:18:04+01:00", + "duration": 2548 + }, + { + "begin": "2026-01-19T02:18:04+01:00", + "end": "2026-01-19T02:29:05+01:00", + "duration": 661 + }, + { + "begin": "2026-01-19T02:39:54+01:00", + "end": "2026-01-19T02:47:51+01:00", + "duration": 477 + }, + { + "begin": "2026-01-19T02:47:52+01:00", + "end": "2026-01-19T03:15:58+01:00", + "duration": 1686 + }, + { + "begin": "2026-01-19T17:04:25+01:00", + "end": "2026-01-19T17:33:46+01:00", + "duration": 1761 + }, + { + "begin": "2026-01-19T17:33:46+01:00", + "end": "2026-01-19T17:44:39+01:00", + "duration": 653 + }, + { + "begin": "2026-01-19T17:44:39+01:00", + "end": "2026-01-19T18:00:34+01:00", + "duration": 955 + }, + { + "begin": "2026-01-19T18:00:34+01:00", + "end": "2026-01-19T18:12:43+01:00", + "duration": 729 + }, + { + "begin": "2026-01-19T18:59:28+01:00", + "end": "2026-01-19T19:09:35+01:00", + "duration": 607 + }, + { + "begin": "2026-01-19T19:24:05+01:00", + "end": "2026-01-19T19:46:32+01:00", + "duration": 1347 + }, + { + "begin": "2026-01-19T20:14:44+01:00", + "end": "2026-01-19T20:25:44+01:00", + "duration": 660 + }, + { + "begin": "2026-01-19T20:40:14+01:00", + "end": "2026-01-19T20:59:58+01:00", + "duration": 1184 + }, + { + "begin": "2026-01-19T20:59:58+01:00", + "end": "2026-01-19T21:18:37+01:00", + "duration": 1119 + }, + { + "begin": "2026-01-19T21:27:55+01:00", + "end": "2026-01-19T22:30:27+01:00", + "duration": 3752 + }, + { + "begin": "2026-01-19T22:31:26+01:00", + "end": "2026-01-19T22:38:32+01:00", + "duration": 426 + }, + { + "begin": "2026-01-19T22:38:33+01:00", + "end": "2026-01-19T23:27:13+01:00", + "duration": 2920 + }, + { + "begin": "2026-01-19T23:41:57+01:00", + "end": "2026-01-19T23:56:18+01:00", + "duration": 861 + }, + { + "begin": "2026-01-19T23:56:19+01:00", + "end": "2026-01-20T01:05:38+01:00", + "duration": 4159 + }, + { + "begin": "2026-01-20T20:21:29+01:00", + "end": "2026-01-20T20:41:51+01:00", + "duration": 1222 + }, + { + "begin": "2026-01-20T20:50:01+01:00", + "end": "2026-01-20T20:57:57+01:00", + "duration": 476 + }, + { + "begin": "2026-01-20T20:57:59+01:00", + "end": "2026-01-20T21:08:06+01:00", + "duration": 607 + }, + { + "begin": "2026-01-20T21:48:23+01:00", + "end": "2026-01-20T22:01:51+01:00", + "duration": 808 + }, + { + "begin": "2026-01-20T22:46:44+01:00", + "end": "2026-01-20T22:59:57+01:00", + "duration": 793 + }, + { + "begin": "2026-01-20T23:06:40+01:00", + "end": "2026-01-20T23:16:52+01:00", + "duration": 612 + }, + { + "begin": "2026-01-20T23:17:00+01:00", + "end": "2026-01-20T23:36:32+01:00", + "duration": 1172 + }, + { + "begin": "2026-01-20T23:53:31+01:00", + "end": "2026-01-21T00:03:38+01:00", + "duration": 607 + }, + { + "begin": "2026-01-21T00:06:57+01:00", + "end": "2026-01-21T00:18:45+01:00", + "duration": 708 + }, + { + "begin": "2026-01-21T00:21:13+01:00", + "end": "2026-01-21T00:31:27+01:00", + "duration": 614 + }, + { + "begin": "2026-01-21T00:53:25+01:00", + "end": "2026-01-21T01:08:20+01:00", + "duration": 895 + }, + { + "begin": "2026-01-21T01:10:12+01:00", + "end": "2026-01-21T01:20:19+01:00", + "duration": 607 + }, + { + "begin": "2026-01-21T01:29:38+01:00", + "end": "2026-01-21T01:50:08+01:00", + "duration": 1230 + }, + { + "begin": "2026-01-21T01:50:59+01:00", + "end": "2026-01-21T03:28:47+01:00", + "duration": 5868 + }, + { + "begin": "2026-01-21T03:47:46+01:00", + "end": "2026-01-21T04:10:58+01:00", + "duration": 1392 + }, + { + "begin": "2026-01-21T07:14:40+01:00", + "end": "2026-01-21T07:33:59+01:00", + "duration": 1159 + }, + { + "begin": "2026-01-21T08:19:56+01:00", + "end": "2026-01-21T08:30:17+01:00", + "duration": 621 + }, + { + "begin": "2026-01-23T17:01:04+01:00", + "end": "2026-01-23T17:20:19+01:00", + "duration": 1155 + }, + { + "begin": "2026-01-23T20:22:33+01:00", + "end": "2026-01-23T20:41:11+01:00", + "duration": 1118 + }, + { + "begin": "2026-01-23T21:36:43+01:00", + "end": "2026-01-23T21:46:50+01:00", + "duration": 607 + }, + { + "begin": "2026-01-24T02:21:40+01:00", + "end": "2026-01-24T02:31:46+01:00", + "duration": 606 + }, + { + "begin": "2026-01-25T02:20:23+01:00", + "end": "2026-01-25T02:45:57+01:00", + "duration": 1534 + }, + { + "begin": "2026-01-25T02:45:58+01:00", + "end": "2026-01-25T02:53:04+01:00", + "duration": 426 + }, + { + "begin": "2026-01-25T02:53:05+01:00", + "end": "2026-01-25T03:02:18+01:00", + "duration": 553 + }, + { + "begin": "2026-01-25T03:02:18+01:00", + "end": "2026-01-25T03:50:19+01:00", + "duration": 2881 + }, + { + "begin": "2026-01-25T22:21:50+01:00", + "end": "2026-01-25T23:02:21+01:00", + "duration": 2431 + }, + { + "begin": "2026-01-25T23:08:15+01:00", + "end": "2026-01-25T23:12:26+01:00", + "duration": 251 + }, + { + "begin": "2026-01-25T23:12:27+01:00", + "end": "2026-01-25T23:32:21+01:00", + "duration": 1194 + }, + { + "begin": "2026-01-26T00:57:30+01:00", + "end": "2026-01-26T01:07:38+01:00", + "duration": 608 + }, + { + "begin": "2026-01-26T17:55:19+01:00", + "end": "2026-01-26T18:42:49+01:00", + "duration": 2850 + }, + { + "begin": "2026-01-26T22:23:07+01:00", + "end": "2026-01-26T22:45:22+01:00", + "duration": 1335 + }, + { + "begin": "2026-01-27T17:47:29+01:00", + "end": "2026-01-27T17:56:42+01:00", + "duration": 553 + }, + { + "begin": "2026-01-27T21:59:53+01:00", + "end": "2026-01-27T22:10:19+01:00", + "duration": 626 + }, + { + "begin": "2026-01-27T22:41:39+01:00", + "end": "2026-01-27T23:14:03+01:00", + "duration": 1944 + }, + { + "begin": "2026-01-28T17:09:10+01:00", + "end": "2026-01-28T17:19:17+01:00", + "duration": 607 + }, + { + "begin": "2026-01-28T19:02:31+01:00", + "end": "2026-01-28T19:15:17+01:00", + "duration": 766 + }, + { + "begin": "2026-01-28T19:49:42+01:00", + "end": "2026-01-28T19:59:48+01:00", + "duration": 606 + }, + { + "begin": "2026-01-28T20:07:54+01:00", + "end": "2026-01-28T20:18:03+01:00", + "duration": 609 + }, + { + "begin": "2026-02-04T08:05:05+01:00", + "end": "2026-02-04T08:15:11+01:00", + "duration": 606 + }, + { + "begin": "2026-02-04T08:15:28+01:00", + "end": "2026-02-04T08:25:34+01:00", + "duration": 606 + }, + { + "begin": "2026-02-04T17:11:15+01:00", + "end": "2026-02-04T17:29:57+01:00", + "duration": 1122 + }, + { + "begin": "2026-02-04T17:29:57+01:00", + "end": "2026-02-04T17:34:08+01:00", + "duration": 251 + }, + { + "begin": "2026-02-04T17:34:08+01:00", + "end": "2026-02-04T17:34:36+01:00", + "duration": 28 + }, + { + "begin": "2026-02-04T17:34:36+01:00", + "end": "2026-02-04T17:34:40+01:00", + "duration": 4 + }, + { + "begin": "2026-02-04T17:41:42+01:00", + "end": "2026-02-04T17:58:48+01:00", + "duration": 1026 + }, + { + "begin": "2026-02-04T17:58:48+01:00", + "end": "2026-02-04T18:10:30+01:00", + "duration": 702 + }, + { + "begin": "2026-02-04T18:10:30+01:00", + "end": "2026-02-04T18:54:05+01:00", + "duration": 2615 + }, + { + "begin": "2026-02-04T18:54:05+01:00", + "end": "2026-02-04T19:53:14+01:00", + "duration": 3549 + }, + { + "begin": "2026-02-04T19:53:14+01:00", + "end": "2026-02-04T20:15:04+01:00", + "duration": 1310 + }, + { + "begin": "2026-02-04T20:15:04+01:00", + "end": "2026-02-04T20:25:38+01:00", + "duration": 634 + }, + { + "begin": "2026-02-04T20:25:38+01:00", + "end": "2026-02-04T20:35:45+01:00", + "duration": 607 + }, + { + "begin": "2026-02-04T20:35:45+01:00", + "end": "2026-02-04T20:35:50+01:00", + "duration": 5 + }, + { + "begin": "2026-02-04T20:35:50+01:00", + "end": "2026-02-04T20:35:50+01:00", + "duration": 0 + }, + { + "begin": "2026-02-04T22:34:27+01:00", + "end": "2026-02-04T22:40:44+01:00", + "duration": 377 + }, + { + "begin": "2026-02-05T08:15:27+01:00", + "end": "2026-02-05T08:26:15+01:00", + "duration": 648 + }, + { + "begin": "2026-02-05T18:12:08+01:00", + "end": "2026-02-05T19:40:40+01:00", + "duration": 5312 + }, + { + "begin": "2026-02-05T20:44:35+01:00", + "end": "2026-02-05T20:55:54+01:00", + "duration": 679 + }, + { + "begin": "2026-02-05T20:59:41+01:00", + "end": "2026-02-05T21:10:07+01:00", + "duration": 626 + }, + { + "begin": "2026-02-05T21:10:07+01:00", + "end": "2026-02-05T23:24:41+01:00", + "duration": 8074 + }, + { + "begin": "2026-02-05T23:24:41+01:00", + "end": "2026-02-06T01:30:18+01:00", + "duration": 7537 + }, + { + "begin": "2026-02-06T17:32:21+01:00", + "end": "2026-02-06T17:43:24+01:00", + "duration": 663 + }, + { + "begin": "2026-02-06T19:30:02+01:00", + "end": "2026-02-06T20:38:16+01:00", + "duration": 4094 + }, + { + "begin": "2026-02-06T20:59:05+01:00", + "end": "2026-02-06T21:33:42+01:00", + "duration": 2077 + }, + { + "begin": "2026-02-06T21:34:43+01:00", + "end": "2026-02-06T21:44:52+01:00", + "duration": 609 + }, + { + "begin": "2026-02-06T22:22:15+01:00", + "end": "2026-02-06T22:32:43+01:00", + "duration": 628 + }, + { + "begin": "2026-02-07T01:09:17+01:00", + "end": "2026-02-07T01:19:23+01:00", + "duration": 606 + }, + { + "begin": "2026-02-09T17:28:08+01:00", + "end": "2026-02-09T17:32:10+01:00", + "duration": 242 + }, + { + "begin": "2026-02-09T17:58:13+01:00", + "end": "2026-02-09T18:59:45+01:00", + "duration": 3692 + }, + { + "begin": "2026-02-15T17:47:09+01:00", + "end": "2026-02-15T18:26:00+01:00", + "duration": 2331 + }, + { + "begin": "2026-02-15T19:06:57+01:00", + "end": "2026-02-15T19:31:19+01:00", + "duration": 1462 + }, + { + "begin": "2026-02-15T19:31:20+01:00", + "end": "2026-02-15T21:57:26+01:00", + "duration": 8766 + }, + { + "begin": "2026-02-15T21:57:27+01:00", + "end": "2026-02-15T22:53:02+01:00", + "duration": 3335 + }, + { + "begin": "2026-02-15T22:53:03+01:00", + "end": "2026-02-15T23:08:58+01:00", + "duration": 955 + }, + { + "begin": "2026-02-15T23:08:59+01:00", + "end": "2026-02-15T23:22:35+01:00", + "duration": 816 + }, + { + "begin": "2026-02-15T23:29:44+01:00", + "end": "2026-02-15T23:46:03+01:00", + "duration": 979 + }, + { + "begin": "2026-02-15T23:49:24+01:00", + "end": "2026-02-16T00:00:37+01:00", + "duration": 673 + }, + { + "begin": "2026-02-16T00:00:41+01:00", + "end": "2026-02-16T00:20:35+01:00", + "duration": 1194 + }, + { + "begin": "2026-02-16T17:04:34+01:00", + "end": "2026-02-16T17:14:39+01:00", + "duration": 605 + }, + { + "begin": "2026-02-16T17:29:29+01:00", + "end": "2026-02-16T17:42:23+01:00", + "duration": 774 + }, + { + "begin": "2026-02-16T17:43:35+01:00", + "end": "2026-02-16T17:53:44+01:00", + "duration": 609 + }, + { + "begin": "2026-02-16T17:53:53+01:00", + "end": "2026-02-16T18:37:54+01:00", + "duration": 2641 + }, + { + "begin": "2026-02-17T17:58:07+01:00", + "end": "2026-02-17T18:10:31+01:00", + "duration": 744 + }, + { + "begin": "2026-02-17T20:34:51+01:00", + "end": "2026-02-17T20:44:56+01:00", + "duration": 605 + }, + { + "begin": "2026-02-17T20:48:03+01:00", + "end": "2026-02-17T20:58:08+01:00", + "duration": 605 + }, + { + "begin": "2026-02-17T21:13:50+01:00", + "end": "2026-02-17T21:42:13+01:00", + "duration": 1703 + }, + { + "begin": "2026-02-17T22:09:26+01:00", + "end": "2026-02-17T22:26:46+01:00", + "duration": 1040 + }, + { + "begin": "2026-02-18T00:04:31+01:00", + "end": "2026-02-18T00:15:38+01:00", + "duration": 667 + }, + { + "begin": "2026-02-18T00:31:46+01:00", + "end": "2026-02-18T01:02:33+01:00", + "duration": 1847 + }, + { + "begin": "2026-02-18T01:02:34+01:00", + "end": "2026-02-18T01:17:01+01:00", + "duration": 867 + }, + { + "begin": "2026-02-18T02:56:36+01:00", + "end": "2026-02-18T03:08:36+01:00", + "duration": 720 + }, + { + "begin": "2026-02-18T04:16:54+01:00", + "end": "2026-02-18T04:26:58+01:00", + "duration": 604 + }, + { + "begin": "2026-02-18T07:30:23+01:00", + "end": "2026-02-18T07:46:26+01:00", + "duration": 963 + }, + { + "begin": "2026-02-18T07:46:34+01:00", + "end": "2026-02-18T07:57:01+01:00", + "duration": 627 + }, + { + "begin": "2026-02-18T17:23:05+01:00", + "end": "2026-02-18T17:33:10+01:00", + "duration": 605 + }, + { + "begin": "2026-02-18T17:39:17+01:00", + "end": "2026-02-18T18:29:54+01:00", + "duration": 3037 + }, + { + "begin": "2026-02-20T19:18:47+01:00", + "end": "2026-02-20T20:41:11+01:00", + "duration": 4944 + }, + { + "begin": "2026-02-21T15:56:25+01:00", + "end": "2026-02-21T17:56:39+01:00", + "duration": 7214 + }, + { + "begin": "2026-02-21T17:59:20+01:00", + "end": "2026-02-21T18:41:48+01:00", + "duration": 2548 + }, + { + "begin": "2026-02-21T21:03:49+01:00", + "end": "2026-02-21T21:15:59+01:00", + "duration": 730 + }, + { + "begin": "2026-02-25T01:29:42+01:00", + "end": "2026-02-25T01:50:23+01:00", + "duration": 1241 + }, + { + "begin": "2026-02-25T02:04:27+01:00", + "end": "2026-02-25T02:10:31+01:00", + "duration": 364 + }, + { + "begin": "2026-02-25T17:37:05+01:00", + "end": "2026-02-25T21:33:21+01:00", + "duration": 14176 + }, + { + "begin": "2026-02-26T17:05:18+01:00", + "end": "2026-02-26T17:36:30+01:00", + "duration": 1872 + }, + { + "begin": "2026-02-26T17:52:27+01:00", + "end": "2026-02-26T20:36:01+01:00", + "duration": 9814 + }, + { + "begin": "2026-02-26T20:36:01+01:00", + "end": "2026-02-26T21:33:07+01:00", + "duration": 3426 + }, + { + "begin": "2026-02-26T21:33:07+01:00", + "end": "2026-02-26T23:50:09+01:00", + "duration": 8222 + }, + { + "begin": "2026-02-26T23:50:09+01:00", + "end": "2026-02-27T01:09:08+01:00", + "duration": 4739 + }, + { + "begin": "2026-02-27T01:09:08+01:00", + "end": "2026-02-27T02:14:34+01:00", + "duration": 3926 + }, + { + "begin": "2026-02-27T03:51:34+01:00", + "end": "2026-02-27T05:23:55+01:00", + "duration": 5541 + }, + { + "begin": "2026-02-27T05:24:07+01:00", + "end": "2026-02-27T05:56:32+01:00", + "duration": 1945 + }, + { + "begin": "2026-02-27T05:56:33+01:00", + "end": "2026-02-27T07:13:05+01:00", + "duration": 4592 + }, + { + "begin": "2026-02-27T07:15:22+01:00", + "end": "2026-02-27T08:19:05+01:00", + "duration": 3823 + }, + { + "begin": "2026-02-27T17:19:09+01:00", + "end": "2026-02-27T18:41:27+01:00", + "duration": 4938 + }, + { + "begin": "2026-02-27T18:41:27+01:00", + "end": "2026-02-27T20:12:19+01:00", + "duration": 5452 + }, + { + "begin": "2026-02-28T01:59:14+01:00", + "end": "2026-02-28T03:09:12+01:00", + "duration": 4198 + }, + { + "begin": "2026-02-28T03:38:50+01:00", + "end": "2026-02-28T08:36:17+01:00", + "duration": 17847 + }, + { + "begin": "2026-02-28T14:46:57+01:00", + "end": "2026-02-28T15:37:13+01:00", + "duration": 3016 + }, + { + "begin": "2026-03-01T01:21:06+01:00", + "end": "2026-03-01T01:31:35+01:00", + "duration": 629 + }, + { + "begin": "2026-03-01T01:31:36+01:00", + "end": "2026-03-01T01:58:13+01:00", + "duration": 1597 + }, + { + "begin": "2026-03-01T02:40:52+01:00", + "end": "2026-03-01T03:05:34+01:00", + "duration": 1482 + }, + { + "begin": "2026-03-01T15:07:31+01:00", + "end": "2026-03-01T15:27:45+01:00", + "duration": 1214 + }, + { + "begin": "2026-03-01T18:09:13+01:00", + "end": "2026-03-01T18:29:28+01:00", + "duration": 1215 + }, + { + "begin": "2026-03-01T18:47:57+01:00", + "end": "2026-03-01T19:09:37+01:00", + "duration": 1300 + }, + { + "begin": "2026-03-02T17:47:32+01:00", + "end": "2026-03-02T18:07:57+01:00", + "duration": 1225 + }, + { + "begin": "2026-03-03T17:57:58+01:00", + "end": "2026-03-03T18:18:13+01:00", + "duration": 1215 + }, + { + "begin": "2026-03-03T23:11:14+01:00", + "end": "2026-03-03T23:42:49+01:00", + "duration": 1895 + }, + { + "begin": "2026-03-04T00:20:10+01:00", + "end": "2026-03-04T00:40:28+01:00", + "duration": 1218 + }, + { + "begin": "2026-03-04T00:43:15+01:00", + "end": "2026-03-04T01:38:57+01:00", + "duration": 3342 + }, + { + "begin": "2026-03-05T00:38:00+01:00", + "end": "2026-03-05T01:18:40+01:00", + "duration": 2440 + }, + { + "begin": "2026-03-05T20:39:02+01:00", + "end": "2026-03-05T21:15:35+01:00", + "duration": 2193 + }, + { + "begin": "2026-03-05T21:41:27+01:00", + "end": "2026-03-05T22:02:30+01:00", + "duration": 1263 + }, + { + "begin": "2026-03-05T22:03:03+01:00", + "end": "2026-03-05T23:10:41+01:00", + "duration": 4058 + }, + { + "begin": "2026-03-05T23:10:43+01:00", + "end": "2026-03-05T23:30:51+01:00", + "duration": 1208 + }, + { + "begin": "2026-03-06T17:29:04+01:00", + "end": "2026-03-06T22:36:18+01:00", + "duration": 18434 + }, + { + "begin": "2026-03-06T22:36:18+01:00", + "end": "2026-03-06T23:05:17+01:00", + "duration": 1739 + }, + { + "begin": "2026-03-06T23:05:17+01:00", + "end": "2026-03-07T00:34:53+01:00", + "duration": 5376 + }, + { + "begin": "2026-03-07T00:34:53+01:00", + "end": "2026-03-07T01:10:11+01:00", + "duration": 2118 + }, + { + "begin": "2026-03-07T01:10:11+01:00", + "end": "2026-03-07T01:30:47+01:00", + "duration": 1236 + }, + { + "begin": "2026-03-07T01:52:21+01:00", + "end": "2026-03-07T02:30:36+01:00", + "duration": 2295 + }, + { + "begin": "2026-03-07T02:41:14+01:00", + "end": "2026-03-07T04:26:07+01:00", + "duration": 6293 + }, + { + "begin": "2026-03-07T04:36:56+01:00", + "end": "2026-03-07T04:59:38+01:00", + "duration": 1362 + }, + { + "begin": "2026-03-07T05:05:18+01:00", + "end": "2026-03-07T05:49:28+01:00", + "duration": 2650 + }, + { + "begin": "2026-03-07T05:49:29+01:00", + "end": "2026-03-07T05:51:24+01:00", + "duration": 115 + }, + { + "begin": "2026-03-07T05:51:25+01:00", + "end": "2026-03-07T06:09:20+01:00", + "duration": 1075 + }, + { + "begin": "2026-03-07T06:09:21+01:00", + "end": "2026-03-07T07:18:31+01:00", + "duration": 4150 + }, + { + "begin": "2026-03-07T13:31:26+01:00", + "end": "2026-03-07T14:24:08+01:00", + "duration": 3162 + }, + { + "begin": "2026-03-07T14:24:54+01:00", + "end": "2026-03-07T15:16:49+01:00", + "duration": 3115 + }, + { + "begin": "2026-03-07T16:32:50+01:00", + "end": "2026-03-07T16:54:48+01:00", + "duration": 1318 + }, + { + "begin": "2026-03-07T17:13:31+01:00", + "end": "2026-03-07T17:33:43+01:00", + "duration": 1212 + }, + { + "begin": "2026-03-07T17:41:12+01:00", + "end": "2026-03-07T17:45:10+01:00", + "duration": 238 + }, + { + "begin": "2026-03-09T01:03:45+01:00", + "end": "2026-03-09T01:39:38+01:00", + "duration": 2153 + }, + { + "begin": "2026-03-09T01:45:11+01:00", + "end": "2026-03-09T02:14:47+01:00", + "duration": 1776 + }, + { + "begin": "2026-03-09T02:15:31+01:00", + "end": "2026-03-09T02:52:44+01:00", + "duration": 2233 + }, + { + "begin": "2026-03-09T02:52:44+01:00", + "end": "2026-03-09T03:15:51+01:00", + "duration": 1387 + }, + { + "begin": "2026-03-09T03:21:11+01:00", + "end": "2026-03-09T03:57:24+01:00", + "duration": 2173 + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7c2b384 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "OrekiWoofsBeehives.slnx" +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..065c44b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,88 @@ +OrekiWoofs Beehives Mod License (Custom) +Version 1.0 - February 4, 2026 + +Copyright (c) 2026 OrekiWoof (HoutarouOreki on github.com, OrekiWoof on mods.vintagestory.at, thezjarek@gmail.com) +All rights reserved. + +This license applies only to the Source Code. + +1) Definitions +"Source Code" means the OrekiWoofs Beehives mod source code and associated +documentation contained in this repository or released as part of a mod package. + +"Mod Author" means OrekiWoof (HoutarouOreki on github.com, OrekiWoof on mods.vintagestory.at, email: thezjarek@gmail.com). + +"Game" means Vintage Story. + +"Mod Release" means a .zip file uploaded to https://mods.vintagestory.at as a mod release. + +"Major or Breaking Game Version" means a new Game version that either (a) increments +the major version number, or (b) causes the Mod to fail to function as intended. + +"Expressed Intent" means a public statement by the Mod Author indicating they plan to +update the Mod for the new Major or Breaking Game Version. This may appear in the Mod +description, release notes, or an official support thread linked from the Mod page. + +"Abandonment Trigger" occurs when a Major or Breaking Game Version is released and, +within 14 days of that release, the Mod Author neither publishes an update compatible +with that version nor provides Expressed Intent to do so. + +"Code Analysis Service" means any service that uploads, indexes, scans, or otherwise +analyzes code, including but not limited to public code hosting platforms, automated +code review services, and AI-assisted code analysis services. + +2) Ownership +The Mod Author retains all right, title, and interest in the Source Code. This license +grants only the permissions explicitly stated below. + +3) General Permission (Standard Use) +You may redistribute the Source Code only inside a Mod Release, and only in .zip format +uploaded to https://mods.vintagestory.at. + +4) Source Code Distribution Restrictions +You may not distribute the Source Code in any way except inside a Mod Release (.zip) +uploaded to https://mods.vintagestory.at. + +You may not upload the Source Code or any portion of it to any Code Analysis +Service, including services that host or analyze code such as GitHub, GitLab, Bitbucket, +or similar platforms. + +5) AI Training and Public Availability Restrictions +You may not use the Source Code (including documentation) to train, fine-tune, or +create datasets for any AI or machine learning system. + +You may not upload, mirror, or otherwise make the Source Code publicly available in +any location or service that enables AI training or allows the Source Code to be +publicly accessed or indexed. + +6) Conditional Continuation License (After Abandonment Trigger) +If the Abandonment Trigger occurs, any person may: + a) create fixed or updated versions of the Source Code, and + b) distribute those versions only as Mod Releases (.zip) on + https://mods.vintagestory.at. + +This conditional permission is subject to all restrictions in Sections 4 and 5 and +the following conditions: + - You must provide clear attribution to the Mod Author. + - You must state that your version is an unofficial continuation. + - You must include this license text in your Mod Release. + - You must not imply endorsement by the Mod Author. + +If the Mod Author resumes updates or provides Expressed Intent, this conditional +permission ends for future releases. Mod Releases already published under this +permission may remain available. + +7) No Additional Rights +No rights are granted for commercial use, sublicensing, or distribution outside the +permissions stated in this license. + +8) Termination +Any violation of this license automatically terminates all permissions granted herein. + +9) Game Terms Precedence +This license is subject to the Game's terms of service and EULA. In case of conflict, +the Game's terms control. + +10) Disclaimer +The Source Code is provided "as is" without warranty of any kind, express or implied. +The Mod Author is not liable for any damages arising from the use of the Source Code. \ No newline at end of file diff --git a/OrekiWoofsBeehives.slnx b/OrekiWoofsBeehives.slnx new file mode 100644 index 0000000..b77d6d2 --- /dev/null +++ b/OrekiWoofsBeehives.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/OrekiWoofsBeehives/Behaviors/BlockBehaviorBeehiveAffected.cs b/OrekiWoofsBeehives/Behaviors/BlockBehaviorBeehiveAffected.cs new file mode 100644 index 0000000..129b985 --- /dev/null +++ b/OrekiWoofsBeehives/Behaviors/BlockBehaviorBeehiveAffected.cs @@ -0,0 +1,244 @@ +using OrekiWoofsBeehives.BlockEntities; +using OrekiWoofsBeehives.Utilities; +using OrekiWoofsBees.Common; +using System; +using System.Collections.Generic; +using System.Globalization; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBeehives.Behaviors; + +public class BlockBehaviorBeehiveAffected(Block block) : BlockBehavior(block) +{ + private const float population_curve_k = 10f; + + public override string GetPlacedBlockInfo(IWorldAccessor world, BlockPos pos, IPlayer forPlayer) + { + var isCrop = PlantRecognitionUtilities.IsCrop(block); + var isFlower = PlantRecognitionUtilities.IsFlower(block, world.BlockAccessor, pos); + var isFarmland = block is BlockFarmland; + + if (!isFarmland && !isCrop && !isFlower) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var cfg = Config.Instance; + var registry = world.Api.GetOrekiWoofsBeehives()?.BeehiveRegistry; + + if (registry is null) + return ""; + + int beehiveCount = CountNearbyBeehives(world, pos); + + if (beehiveCount <= 0) + { + if (!isFarmland) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var baseText = base.GetPlacedBlockInfo(world, pos, forPlayer); + var pollinationText = GetPollinatedOnlyText(world, pos); + return string.IsNullOrEmpty(pollinationText) + ? baseText + : baseText + pollinationText; + } + + float totalEffectiveness = GetBeehiveBoostEffectiveness(world.Api, registry.BeehivePositions, pos); + + var growthOrPlantText = isFarmland + ? GetLocalizedFarmlandBonusText(world, pos, cfg, totalEffectiveness) + : " " + Lang.Get("orekiwoofsbeehives:blockinfo-as-a-plant", GetLocalizedPlantTypeText(isFlower, isCrop)); + + var inRangeText = Lang.Get("orekiwoofsbeehives:blockinfo-in-range-beehive", beehiveCount); + return inRangeText + growthOrPlantText; + } + + private static string GetLocalizedPlantTypeText(bool isFlower, bool isCrop) + { + var plantTypes = new List(2); + + if (isFlower) + plantTypes.Add(Lang.Get("orekiwoofsbeehives:blockinfo-flower")); + + if (isCrop) + plantTypes.Add(Lang.Get("orekiwoofsbeehives:blockinfo-crop")); + + return string.Join(" ", plantTypes); + } + + private static string GetLocalizedFarmlandBonusText(IWorldAccessor world, BlockPos farmlandPos, Config cfg, float totalEffectiveness) + { + var parts = new List(); + + if (cfg.SpeedBoost) + { + var speedPercent = (cfg.GrowthSpeedBonus * totalEffectiveness * 100f).ToString("N0", CultureInfo.InvariantCulture); + parts.Add(Lang.Get("orekiwoofsbeehives:blockinfo-growth-speed-bonus", speedPercent)); + } + + if (cfg.YieldBoost) + { + var yieldBonusFactor = Math.Max(0f, cfg.YieldMultiplier - 1f); + var yieldPercent = (yieldBonusFactor * totalEffectiveness * 100f).ToString("N0", CultureInfo.InvariantCulture); + parts.Add(Lang.Get("orekiwoofsbeehives:blockinfo-yield-bonus", yieldPercent)); + } + + var bonusText = parts.Count > 0 + ? $" (+{string.Join(", +", parts)})" + : string.Empty; + + var pollinationText = GetPollinationText(world, farmlandPos); + return bonusText + pollinationText; + } + + private static string GetPollinationText(IWorldAccessor world, BlockPos farmlandPos) + { + var cropPos = farmlandPos.UpCopy(); + if (world.BlockAccessor.GetBlock(cropPos) is not BlockCrop crop) + return string.Empty; + + var tracker = world.BlockAccessor.GetBlock(farmlandPos) + .GetBEBehavior(farmlandPos); + if (tracker is null) + return string.Empty; + + var currentStage = crop.CurrentCropStage; + var remainingStages = tracker.GetRemainingPollinationStages(currentStage); + + if (remainingStages <= 0) + return '\n' + Lang.Get("orekiwoofsbeehives:blockinfo-pollinated"); + + return '\n' + Lang.Get("orekiwoofsbeehives:blockinfo-will-be-pollinated", remainingStages); + } + + private static string GetPollinatedOnlyText(IWorldAccessor world, BlockPos farmlandPos) + { + var cropPos = farmlandPos.UpCopy(); + if (world.BlockAccessor.GetBlock(cropPos) is not BlockCrop crop) + return string.Empty; + + var tracker = world.BlockAccessor.GetBlock(farmlandPos) + .GetBEBehavior(farmlandPos); + if (tracker is null) + return string.Empty; + + return tracker.IsPollinated(crop.CurrentCropStage) + ? '\n' + Lang.Get("orekiwoofsbeehives:blockinfo-pollinated") + : string.Empty; + } + + public override void OnBlockPlaced(IWorldAccessor world, BlockPos pos, ref EnumHandling handling) + { + if (world.Side.IsServer() && PlantRecognitionUtilities.IsPlant(block, world.BlockAccessor, pos)) + world.Api.GetPlantPositionRegistry()?.AddPlantPosition(pos, block); + + base.OnBlockPlaced(world, pos, ref handling); + } + + public override void OnBlockRemoved(IWorldAccessor world, BlockPos pos, ref EnumHandling handling) + { + if (PlantRecognitionUtilities.IsPlant(block, world.BlockAccessor, pos)) + world.Api.GetPlantPositionRegistry()?.RemovePlantPosition(pos, block); + + base.OnBlockRemoved(world, pos, ref handling); + } + + public override void OnNeighbourBlockChange(IWorldAccessor world, BlockPos pos, BlockPos neibpos, ref EnumHandling handling) + { + if (world.Side.IsServer() && block is BlockFarmland && neibpos.X == pos.X && neibpos.Y == pos.Y + 1 && neibpos.Z == pos.Z) + { + var tracker = block.GetBEBehavior(pos); + tracker?.OnUpperNeighborChanged(world); + } + + base.OnNeighbourBlockChange(world, pos, neibpos, ref handling); + } + + public static int CountNearbyBeehives(IWorldAccessor world, BlockPos pos) + { + var count = world.Api.GetPlantPositionRegistry()?.CountBeehivesInRadius(pos, Config.Instance.BeehiveRadius); + + return count ?? 0; + } + + public static float GetBeehiveBoostEffectiveness(ICoreAPI api, IEnumerable beehivePositions, BlockPos plantPos) + { + if (api is null) + return 1f; + + var cfg = Config.Instance; + int radius = cfg.BeehiveRadius; + int clampedFullBoostDistance = Math.Clamp(cfg.PlantsFullBoostDistanceToHive, 1, radius); + int radiusDelta = Math.Max(1, radius - clampedFullBoostDistance); + var combinationType = cfg.GetMultipleBeehivesBoostCombinationType(); + float totalEffectiveness = 0f; + float maxEffectiveness = 0f; + + foreach (var beehivePos in beehivePositions) + { + if (!Overlaps.IsWithinSphericalRadius(plantPos, beehivePos, radius)) + continue; + + var blockPos = new BlockPos(beehivePos.X, beehivePos.Y, beehivePos.Z, plantPos.dimension); + if (api.World.BlockAccessor.GetBlockEntity(blockPos) is not BlockEntityReusableBeehive beehive) + continue; + if (beehive.BeePopulation < cfg.MinBeesForPlantBoost) + continue; + + float populationScale = GetPopulationBoostScale(beehive.BeePopulation, cfg); + if (populationScale <= 0f) + continue; + + float distance = blockPos.DistanceTo(plantPos); + float distanceEffectiveness = distance <= clampedFullBoostDistance + ? 1f + : Math.Clamp(1f - ((distance - clampedFullBoostDistance) / radiusDelta), 0f, 1f); + float effectiveness = distanceEffectiveness * populationScale; + + if (combinationType == MultipleBeehivesBoostCombinationType.Max) + { + maxEffectiveness = Math.Max(maxEffectiveness, effectiveness); + if (maxEffectiveness >= 1f) + return 1f; + continue; + } + + totalEffectiveness += effectiveness; + if (totalEffectiveness >= 1f) + return 1f; + } + + if (combinationType == MultipleBeehivesBoostCombinationType.Max) + return Math.Min(maxEffectiveness, 1f); + + return Math.Min(totalEffectiveness, 1f); + } + + public static float GetPopulationBoostScale(double beePopulation, Config cfg) + { + if (beePopulation < cfg.MinBeesForPlantBoost) + return 0f; + + if (cfg.GetBoostCalculationType() == BoostCalculationType.Full) + return 1f; + + float minPopulation = cfg.MinBeesForPlantBoost; + float maxPopulation = cfg.MaxBeePopulation * (cfg.PopulationPercentForMaxBoost / 100f); + + if (maxPopulation <= minPopulation) + return 1f; + + float x = (float)Math.Clamp((beePopulation - minPopulation) / (maxPopulation - minPopulation), 0d, 1d); + + if (cfg.GetBoostCalculationType() == BoostCalculationType.LinearPopulation) + return x; + + float numerator = MathF.Log(1f + (population_curve_k * x)); + float denominator = MathF.Log(1f + population_curve_k); + if (denominator <= 0f) + return x; + + return Math.Clamp(numerator / denominator, 0f, 1f); + } +} diff --git a/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorBeehiveYieldMultiplier.cs b/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorBeehiveYieldMultiplier.cs new file mode 100644 index 0000000..97b6281 --- /dev/null +++ b/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorBeehiveYieldMultiplier.cs @@ -0,0 +1,140 @@ +using OrekiWoofsBeehives.Utilities; +using System.Collections.Generic; +using System.Linq; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBeehives.Behaviors; + +public class BlockEntityBehaviorBeehiveYieldMultiplier(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity) +{ + private const string stages_attribute = "orekiwoofsbeehives_stagesWithBeehivePresent"; + public const int REQUIRED_POLLINATION_STAGES = 2; + private readonly HashSet stagesWithBeehivePresent = []; + + public IReadOnlyCollection StagesWithBeehivePresent => stagesWithBeehivePresent; + + public override void Initialize(ICoreAPI api, JsonObject properties) + { + base.Initialize(api, properties); + +#if DEBUG + var interval = 1000; +#else + var interval = 10000; +#endif + if (api.Side == EnumAppSide.Server) + Blockentity.RegisterGameTickListener(OnServerTick, interval); + } + + public override void ToTreeAttributes(ITreeAttribute tree) + { + base.ToTreeAttributes(tree); + + if (stagesWithBeehivePresent.Count == 0) + return; + + string serialized = string.Join(",", stagesWithBeehivePresent.OrderBy(x => x)); + tree.SetString(stages_attribute, serialized); + } + + public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve) + { + base.FromTreeAttributes(tree, worldAccessForResolve); + + stagesWithBeehivePresent.Clear(); + + var serialized = tree.GetString(stages_attribute); + if (string.IsNullOrWhiteSpace(serialized)) + return; + + foreach (string stageValue in serialized.Split(',')) + { + if (int.TryParse(stageValue, out int stage)) + stagesWithBeehivePresent.Add(stage); + } + } + + private void OnServerTick(float dt) + { + if (Blockentity.Block is not BlockFarmland) + return; + + BlockPos cropPos = Blockentity.Pos.UpCopy(); + if (Api.World.BlockAccessor.GetBlock(cropPos) is not BlockCrop crop) + { + ClearTracking(); + return; + } + + int stage = crop.CurrentCropStage; + if (stage < 0) + return; + + if (stagesWithBeehivePresent.Count > 0 && stage < stagesWithBeehivePresent.Max()) + stagesWithBeehivePresent.Clear(); + + var modSystem = Api.GetOrekiWoofsBeehives(); + if (modSystem?.BeehiveRegistry is null) + return; + + float effectiveness = BlockBehaviorBeehiveAffected.GetBeehiveBoostEffectiveness(Api, modSystem.BeehiveRegistry.BeehivePositions, cropPos); + if (effectiveness <= 0f) + return; + + stagesWithBeehivePresent.Add(stage); + } + + public void OnUpperNeighborChanged(IWorldAccessor world) + { + if (Blockentity.Block is not BlockFarmland) + return; + + BlockPos cropPos = Blockentity.Pos.UpCopy(); + if (world.BlockAccessor.GetBlock(cropPos) is not BlockCrop crop) + { + ClearTracking(); + return; + } + + int stage = crop.CurrentCropStage; + if (stage >= 0 && stagesWithBeehivePresent.Count > 0 && stage < stagesWithBeehivePresent.Max()) + ClearTracking(); + } + + private void ClearTracking() + { + if (stagesWithBeehivePresent.Count == 0) + return; + + stagesWithBeehivePresent.Clear(); + } + + public int GetCoveredStagesCount(int currentStage) + { + if (currentStage < 0) + return 0; + + var coveredStages = 0; + foreach (var stage in stagesWithBeehivePresent) + { + if (stage >= 0 && stage <= currentStage) + coveredStages++; + } + + return coveredStages; + } + + public int GetRemainingPollinationStages(int currentStage) + { + var coveredStages = GetCoveredStagesCount(currentStage); + return System.Math.Max(0, REQUIRED_POLLINATION_STAGES - coveredStages); + } + + public bool IsPollinated(int currentStage) + { + return GetRemainingPollinationStages(currentStage) == 0; + } +} diff --git a/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorVanillaSkepSwarmTarget.cs b/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorVanillaSkepSwarmTarget.cs new file mode 100644 index 0000000..12882e1 --- /dev/null +++ b/OrekiWoofsBeehives/Behaviors/BlockEntityBehaviorVanillaSkepSwarmTarget.cs @@ -0,0 +1,60 @@ +using OrekiWoofsBeehives.Utilities; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; + +namespace OrekiWoofsBeehives.Behaviors; + +public class BlockEntityBehaviorVanillaSkepSwarmTarget(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity) +{ + public override void Initialize(ICoreAPI api, JsonObject properties) + { + base.Initialize(api, properties); + + if (api.Side.IsServer()) + TryRegister(api.World, Blockentity.Pos); + } + + public override void OnBlockRemoved() + { + base.OnBlockRemoved(); + + if (Api.Side.IsServer()) + Api.GetOrekiWoofsBeehives()?.VanillaSkepRegistry.UnregisterVanillaSkep(Blockentity.Pos); + } + + public override void OnBlockUnloaded() + { + base.OnBlockUnloaded(); + + if (Api.Side.IsServer()) + Api.GetOrekiWoofsBeehives()?.VanillaSkepRegistry.UnregisterVanillaSkep(Blockentity.Pos); + } + + private static void TryRegister(IWorldAccessor world, Vintagestory.API.MathTools.BlockPos pos) + { + var currentBlock = world.BlockAccessor.GetBlock(pos); + var path = currentBlock?.Code?.Path; + if (path == null) + return; + + var populatedCode = GetPopulatedSkepCode(path); + if (populatedCode == null) + { + world.Api.GetOrekiWoofsBeehives()?.VanillaSkepRegistry.UnregisterVanillaSkep(pos); + return; + } + + world.Api.GetOrekiWoofsBeehives()?.VanillaSkepRegistry.RegisterVanillaSkep(pos, populatedCode); + } + + private static string? GetPopulatedSkepCode(string path) + { + if (path.StartsWith("skep-reed-empty-")) + return $"game:{path.Replace("-empty-", "-populated-")}"; + + if (path.StartsWith("skep-papyrus-empty-")) + return $"game:{path.Replace("-empty-", "-populated-")}"; + + return null; + } +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/BlockEntities/BeehiveStats.cs b/OrekiWoofsBeehives/BlockEntities/BeehiveStats.cs new file mode 100644 index 0000000..452bb54 --- /dev/null +++ b/OrekiWoofsBeehives/BlockEntities/BeehiveStats.cs @@ -0,0 +1,193 @@ +using System; +using Vintagestory.API.Common; + +namespace OrekiWoofsBeehives.BlockEntities; + +public readonly record struct BeehiveStats +{ + public required int DailyNetPopulationChange { get; init; } + public required double FramesPerDay { get; init; } + public required double FeedConsumedPerDay { get; init; } + public required double FlowerProductionMultiplier { get; init; } + public required double BeeProductionMultiplier { get; init; } + public required BeehiveStatsComponents Components { get; init; } + + public static BeehiveStats Create(BlockEntityReusableBeehive beehive, bool isGreenhouse, double? totalDaysDate = null) + { + ClimateCondition climate = totalDaysDate.HasValue + ? beehive.Api.World.BlockAccessor.GetClimateAt(beehive.Pos, EnumGetClimateMode.ForSuppliedDateValues, totalDaysDate.Value) + : beehive.Api.World.BlockAccessor.GetClimateAt(beehive.Pos, EnumGetClimateMode.NowValues); + var cfg = Config.Instance; + float currentTemperature = GetTemperature(climate, isGreenhouse && cfg.GreenhouseAffectsBeehive); + int dailyDeaths = CalculateDailyDeaths(beehive, currentTemperature); + double dailyGrowthRate = CalculateDailyGrowthRate(beehive); + int filledFramesCount = beehive.CountFilledFrames(); + double emptyFrameMultiplier = GetEmptyFrameMultiplier(beehive.CountEmptyFrames()); + int dailyFilledFrameBonus = (int)(filledFramesCount * cfg.BonusGrowthPerFilledFrame * emptyFrameMultiplier); + int dailyPercentageGrowth = (int)(beehive.BeePopulation * dailyGrowthRate); + double temperatureMultiplier = GetTemperatureMultiplier(currentTemperature); + int dailyGrowth = (int)((dailyPercentageGrowth + dailyFilledFrameBonus) * temperatureMultiplier); + double framesPerDay = CalculateFramesPerDay(beehive, temperatureMultiplier); + double feedConsumedPerDay = CalculateFeedConsumptionPerDay(beehive, currentTemperature, temperatureMultiplier); + int emptyFrames = beehive.CountEmptyFrames(); + int totalFrames = beehive.CountTotalFrames(); + double beeProductionMultiplier = beehive.BeePopulation / cfg.ReferenceBees; + double effectiveFlowers = GetEffectiveFlowers(beehive); + double flowerProductionMultiplier = effectiveFlowers / cfg.ReferenceFlowers; + + int dailyNetPopulationChange = dailyGrowth - dailyDeaths; + return new BeehiveStats + { + DailyNetPopulationChange = dailyNetPopulationChange, + BeeProductionMultiplier = beeProductionMultiplier, + FlowerProductionMultiplier = flowerProductionMultiplier, + FramesPerDay = framesPerDay, + FeedConsumedPerDay = feedConsumedPerDay, + Components = new( + dailyDeaths, + dailyGrowthRate, + dailyPercentageGrowth, + filledFramesCount, + emptyFrames, + totalFrames, + dailyFilledFrameBonus, + dailyGrowth, + temperatureMultiplier, + effectiveFlowers, + isGreenhouse, + currentTemperature + ) + }; + } + + private static int CalculateDailyDeaths(BlockEntityReusableBeehive beehive, float currentTemperature) + { + var cfg = Config.Instance; + var missingFlowers = Math.Max(0, cfg.FlowerThreshold - GetEffectiveFlowers(beehive)); + var winterReverseRamp = GetWinterReverseRamp(currentTemperature); + var winterRamp = 1 - winterReverseRamp; + int baseDeaths = (int)(cfg.BaseDeathsPerDay * winterRamp); + int flowerDeaths = (int)(missingFlowers * cfg.DeathPerMissingFlower * winterRamp); + int dailyDeaths = baseDeaths + flowerDeaths; + + bool hasFilledOrFeedFrames = beehive.CountFilledFrames() > 0; + if (cfg.WinterHardMode && !hasFilledOrFeedFrames && winterReverseRamp > 0) + dailyDeaths += (int)(cfg.WinterDailyBeeDeathsWithoutFood * winterReverseRamp); + + return dailyDeaths; + } + + private static double CalculateDailyGrowthRate(BlockEntityReusableBeehive beehive) + { + var cfg = Config.Instance; + double baseGrowthRate = Math.Pow(2, 1.0 / cfg.DoublingTimeDays) - 1; + + double emptyFrameMultiplier = GetEmptyFrameMultiplier(beehive.CountEmptyFrames()); + double growthModifier = emptyFrameMultiplier; + + return baseGrowthRate * growthModifier; + } + + private static double GetEmptyFrameMultiplier(int emptyFrames) + { + var hasEmptyFrames = emptyFrames > 0; + return hasEmptyFrames ? 0.5 : 1.0; + } + + private static float GetTemperature(ClimateCondition climate, bool isGreenhouse) + { + float currentTemperature = climate?.Temperature ?? 20f; + if (isGreenhouse) + currentTemperature += 5; + return currentTemperature; + } + + private static double GetTemperatureMultiplier(float currentTemperature) + { + var cfg = Config.Instance; + + if (currentTemperature >= cfg.MaxTemperatureGrowth) + return 1.0; + if (currentTemperature <= cfg.MinTemperatureGrowth) + return 0.0; + + float temperatureRange = cfg.MaxTemperatureGrowth - cfg.MinTemperatureGrowth; + return (currentTemperature - cfg.MinTemperatureGrowth) / temperatureRange; + } + + private static double CalculateFramesPerDay(BlockEntityReusableBeehive beehive, double temperatureMultiplier) + { + if (beehive.BeePopulation <= 0) + return 0; + + double effectiveFlowers = GetEffectiveFlowers(beehive); + if (effectiveFlowers <= 0) + return 0; + + var cfg = Config.Instance; + double flowerFactor = effectiveFlowers / cfg.ReferenceFlowers; + double beeFactor = beehive.BeePopulation / cfg.ReferenceBees; + + return flowerFactor * beeFactor * temperatureMultiplier; + } + + private static double CalculateFeedConsumptionPerDay(BlockEntityReusableBeehive beehive, float currentTemperature, double temperatureMultiplier) + { + if (beehive.BeePopulation <= 0) + return 0; + + var cfg = Config.Instance; + double beeFactor = beehive.BeePopulation / cfg.ReferenceBees; + double feed_consumption_speed = cfg.MaxFlowersForHoneyProduction / cfg.ReferenceFlowers * 1.5; + + double feedTemperatureMultiplier = temperatureMultiplier; + if (cfg.WinterHardMode) + { + double winterReverseRamp = GetWinterReverseRamp(currentTemperature); + double winterFeedFloor = winterReverseRamp * cfg.WinterFoodConsumptionMultiplier; + feedTemperatureMultiplier = Math.Max(feedTemperatureMultiplier, winterFeedFloor); + } + + return beeFactor * feed_consumption_speed * feedTemperatureMultiplier; + } + + private static double GetWinterReverseRamp(float currentTemperature) + { + var cfg = Config.Instance; + if (currentTemperature >= cfg.MaxTemperatureGrowth) + return 0; + if (currentTemperature <= cfg.MinTemperatureGrowth) + return 1; + + float temperatureRange = cfg.MaxTemperatureGrowth - cfg.MinTemperatureGrowth; + if (temperatureRange <= 0) + return 1; + + return 1.0 - ((currentTemperature - cfg.MinTemperatureGrowth) / temperatureRange); + } + + private static double GetEffectiveFlowers(BlockEntityReusableBeehive beehive) + { + const double cropMultiplier = 0.25; + var cfg = Config.Instance; + if (beehive.FlowersAround is null || beehive.CropsAround is null) + return cfg.ReferenceFlowers; + double effectiveFlowers = beehive.FlowersAround.Value + (beehive.CropsAround.Value * cropMultiplier); + return Math.Min(effectiveFlowers, cfg.MaxFlowersForHoneyProduction); + } +} + +public readonly record struct BeehiveStatsComponents( + int DailyDeaths, + double DailyGrowthRate, + int DailyPercentageGrowth, + int FilledFramesCount, + int EmptyFrames, + int TotalFrames, + int DailyFilledFrameBonus, + int DailyGrowth, + double TemperatureMultiplier, + double EffectiveFlowers, + bool IsGreenhouse, + float Temperature +); diff --git a/OrekiWoofsBeehives/BlockEntities/BlockEntityBeeSwarm.cs b/OrekiWoofsBeehives/BlockEntities/BlockEntityBeeSwarm.cs new file mode 100644 index 0000000..58c50c9 --- /dev/null +++ b/OrekiWoofsBeehives/BlockEntities/BlockEntityBeeSwarm.cs @@ -0,0 +1,652 @@ +using OrekiWoofsBeehives.Utilities; +using OrekiWoofsBees.Common; +using System; +using System.Collections.Generic; +using System.Text; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.API.Config; + +namespace OrekiWoofsBeehives.BlockEntities; + +public class BlockEntityBeeSwarm : BlockEntity +{ + public double MigrationPhaseDurationHours { get; protected set; } = 5.0; + + public BlockPos OriginHivePos { get; private set; } = new BlockPos(0); + public double Population { get; private set; } + public double SpawnDateTime { get; private set; } + public SwarmState SwarmState { get; private set; } = SwarmState.BuildingSwarm; + + private double plannedPopulation; + private double stateStartTotalHours; + private double buildingDurationHours = 3.0; + private double hangingDurationHours = 5.0; + private double migrationStartPopulation; + private double transferredDuringMigration; + private int retryDay = -1; + private BlockPos? targetPos; + private bool targetIsVanillaSkep; + private string? targetPopulatedSkepCode; + private int eligibleTargetsAtLastSelection; + private List? targetHiveCandidates; + private long? candidateRefreshListenerId; + + public override void Initialize(ICoreAPI api) + { + base.Initialize(api); +#if DEBUG + RegisterGameTickListener(OnGameTick, 500); +#else + RegisterGameTickListener(OnGameTick, 5_000); +#endif + + if (api.Side != EnumAppSide.Server) + return; + + if (SwarmState == SwarmState.HangingOut) + StartCandidateRefresh(); + + FastForwardTo(api.World.Calendar.TotalHours, debugAsCatchUp: true); + } + + public void InitializeFromOrigin( + BlockPos originHivePos, + double plannedPopulation, + double spawnTotalHours, + double buildingDurationHours, + double hangingDurationHours) + { + OriginHivePos = originHivePos.Copy(); + this.plannedPopulation = Math.Max(0, plannedPopulation); + SpawnDateTime = spawnTotalHours; + stateStartTotalHours = spawnTotalHours; + this.buildingDurationHours = Math.Max(0.1, buildingDurationHours); + this.hangingDurationHours = Math.Max(0.1, hangingDurationHours); + SwarmState = SwarmState.BuildingSwarm; + Population = 0; + retryDay = -1; + targetPos = null; + targetIsVanillaSkep = false; + targetPopulatedSkepCode = null; + eligibleTargetsAtLastSelection = 0; + targetHiveCandidates = null; + migrationStartPopulation = 0; + transferredDuringMigration = 0; + MarkDirty(true); + } + + private void OnGameTick(float dt) + { + if (Api.Side != EnumAppSide.Server) + return; + + FastForwardTo(Api.World.Calendar.TotalHours, debugAsCatchUp: false); + } + + private void OnCandidateRefreshTick(float dt) + { + if (Api.Side != EnumAppSide.Server || SwarmState != SwarmState.HangingOut) + return; + + targetHiveCandidates = FindEligibleTargets(); + eligibleTargetsAtLastSelection = targetHiveCandidates.Count; + MarkDirty(false); + } + + private void StartCandidateRefresh() + { + StopCandidateRefresh(); + candidateRefreshListenerId = RegisterGameTickListener(OnCandidateRefreshTick, 5_000); + } + + private void StopCandidateRefresh() + { + if (candidateRefreshListenerId.HasValue) + { + UnregisterGameTickListener(candidateRefreshListenerId.Value); + candidateRefreshListenerId = null; + } + } + + public void FastForwardTo(double nowHours) + { + FastForwardTo(nowHours, debugAsCatchUp: false); + } + + public void FastForwardTo(double nowHours, bool debugAsCatchUp) + { + if (Api?.Side != EnumAppSide.Server) + return; + + if (nowHours <= SpawnDateTime) + return; + + var debugEnabled = debugAsCatchUp && Api.GetOrekiWoofsBeehives()?.DebugUnloadEnabled == true; + var initialState = SwarmState; + var initialPopulation = Population; + var initialTargetPos = targetPos?.Copy(); + var initialTransferred = transferredDuringMigration; + + if (debugEnabled) + BroadcastUnloadDebug($"swarm {Pos}: catch-up start from state={initialState}, population={initialPopulation:F2}, spawnHour={SpawnDateTime:F2}, targetHour={nowHours:F2}"); + + for (int i = 0; i < 12; i++) + { + if (Api.World.BlockAccessor.GetBlockEntity(Pos) is not BlockEntityBeeSwarm) + { + if (debugEnabled) + BroadcastUnloadDebug($"swarm {Pos}: removed during catch-up"); + + return; + } + + var previousState = SwarmState; + var previousPopulation = Population; + var previousStateStartTotalHours = stateStartTotalHours; + var previousRetryDay = retryDay; + var previousTransferredDuringMigration = transferredDuringMigration; + var previousTargetPos = targetPos?.Copy(); + + switch (SwarmState) + { + case SwarmState.BuildingSwarm: + UpdateBuildingSwarm(nowHours); + break; + case SwarmState.HangingOut: + UpdateHangingOut(nowHours); + break; + case SwarmState.MigratingToNewHive: + UpdateMigratingToNewHive(nowHours); + break; + } + + if (Api.World.BlockAccessor.GetBlockEntity(Pos) is not BlockEntityBeeSwarm) + { + if (debugEnabled) + BroadcastUnloadDebug($"swarm {Pos}: finished and removed during catch-up"); + + return; + } + + var changed = + previousState != SwarmState || + Math.Abs(previousPopulation - Population) > 0.0001 || + previousStateStartTotalHours != stateStartTotalHours || + previousRetryDay != retryDay || + Math.Abs(previousTransferredDuringMigration - transferredDuringMigration) > 0.0001 || + !Equals(previousTargetPos, targetPos); + + if (debugEnabled) + { + if (previousState != SwarmState) + BroadcastUnloadDebug($"swarm {Pos}: state {previousState} -> {SwarmState} at hour={nowHours:F2}"); + + var movedNow = transferredDuringMigration - previousTransferredDuringMigration; + if (movedNow > 0.0001) + BroadcastUnloadDebug($"swarm {Pos}: moved {movedNow:F2} bees this catch-up step (remaining={Population:F2})"); + } + + if (!changed) + { + if (debugEnabled) + BroadcastUnloadDebug($"swarm {Pos}: catch-up reached stable state={SwarmState}, population={Population:F2}, transferred={transferredDuringMigration:F2}"); + + return; + } + } + + if (debugEnabled) + { + BroadcastUnloadDebug( + $"swarm {Pos}: catch-up loop limit reached; state {initialState}->{SwarmState}, population {initialPopulation:F2}->{Population:F2}, transferred {initialTransferred:F2}->{transferredDuringMigration:F2}, target {initialTargetPos}->{targetPos}"); + } + } + + private void UpdateBuildingSwarm(double nowHours) + { + var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours); + var progress = Math.Clamp(elapsedHours / buildingDurationHours, 0, 1); + var desiredPopulationInSwarm = plannedPopulation * progress; + var toMoveNow = desiredPopulationInSwarm - Population; + + if (toMoveNow > 0) + { + var sourceHive = GetOriginHive(); + if (sourceHive != null) + { + var moved = sourceHive.TakeBeePopulationForSwarm(toMoveNow); + Population += moved; + MarkDirty(false); + } + } + + if (progress < 1) + return; + + SwarmState = SwarmState.HangingOut; + stateStartTotalHours = nowHours; + StartCandidateRefresh(); + MarkDirty(false); + } + + private void UpdateHangingOut(double nowHours) + { + if (retryDay >= 0) + { + if (CanRetryInCurrentWindow(nowHours)) + TryStartMigration(nowHours); + return; + } + + var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours); + if (elapsedHours < hangingDurationHours) + return; + + TryStartMigration(nowHours); + } + + private bool CanRetryInCurrentWindow(double nowHours) + { + var currentDay = GetCurrentDayIndex(nowHours); + if (currentDay < retryDay) + return false; + + var hourOfDay = Api.World.Calendar.HourOfDay; + return hourOfDay is >= 8 and <= 12; + } + + private void TryStartMigration(double nowHours) + { + targetHiveCandidates = FindEligibleTargets(); + eligibleTargetsAtLastSelection = targetHiveCandidates.Count; + + if (targetHiveCandidates.Count == 0) + { + var currentDay = GetCurrentDayIndex(nowHours); + if (retryDay < 0) + { + retryDay = currentDay + 1; + MarkDirty(false); + return; + } + + Disperse(); + return; + } + + foreach (var candidate in targetHiveCandidates) + { + if (candidate.IsVanillaSkep) + { + if (TryConvertSkepToPopulated(candidate.Pos, candidate.PopulatedSkepCode!)) + { + StartMigrating(nowHours, candidate.Pos, true, candidate.PopulatedSkepCode); + return; + } + + continue; + } + + StartMigrating(nowHours, candidate.Pos, false, null); + return; + } + + Disperse(); + } + + private void StartMigrating(double nowHours, BlockPos targetPos, bool targetIsVanillaSkep, string? targetPopulatedSkepCode) + { + StopCandidateRefresh(); + SwarmState = SwarmState.MigratingToNewHive; + stateStartTotalHours = nowHours; + migrationStartPopulation = Population; + transferredDuringMigration = 0; + this.targetPos = targetPos.Copy(); + this.targetIsVanillaSkep = targetIsVanillaSkep; + this.targetPopulatedSkepCode = targetPopulatedSkepCode; + retryDay = -1; + + if (!targetIsVanillaSkep && Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive targetHive) + targetHive.SetIncomingSwarm(Pos); + + MarkDirty(false); + } + + private void UpdateMigratingToNewHive(double nowHours) + { + if (targetPos == null) + { + Disperse(); + return; + } + + var elapsedHours = Math.Max(0, nowHours - stateStartTotalHours); + var progress = Math.Clamp(elapsedHours / MigrationPhaseDurationHours, 0, 1); + var desiredTransferred = migrationStartPopulation * progress; + var toTransferNow = desiredTransferred - transferredDuringMigration; + + if (toTransferNow > 0) + { + var moved = targetIsVanillaSkep + ? Math.Min(Population, Math.Max(0, toTransferNow)) + : MoveToTargetHive(toTransferNow); + + transferredDuringMigration += moved; + Population = Math.Max(0, Population - moved); + MarkDirty(false); + } + + if (progress < 1) + return; + + RemoveSwarmBlock(); + } + + private double MoveToTargetHive(double amount) + { + if (targetPos == null) + return 0; + + if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is not BlockEntityReusableBeehive targetHive) + return 0; + + return targetHive.AddBeePopulationFromSwarm(amount); + } + + private List FindEligibleTargets() + { + var candidates = new List(); + var cfg = Config.Instance; + var radius = Math.Max(1, cfg.BeehiveRadius); + + var modSystem = Api.GetOrekiWoofsBeehives(); + if (modSystem != null) + { + foreach (var pos in modSystem.BeehiveRegistry.BeehivePositions) + { + var targetPos = new BlockPos(pos.X, pos.Y, pos.Z); + if (targetPos.Equals(OriginHivePos) || targetPos.Equals(Pos)) + continue; + + if (!Overlaps.IsWithinSphericalRadius(Pos, pos, radius)) + continue; + + if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is not BlockEntityReusableBeehive hive) + continue; + + if (!hive.OpenForIncomingSwarms) + continue; + + var score = CalculateBeehiveTargetScore(hive, targetPos, radius); + candidates.Add(new SwarmTargetCandidate(targetPos, false, score, null)); + } + } + + var skepRegistry = modSystem?.VanillaSkepRegistry; + if (skepRegistry != null) + { + foreach (var entry in skepRegistry.Entries) + { + var targetPos = new BlockPos(entry.Key.X, entry.Key.Y, entry.Key.Z); + if (!Overlaps.IsWithinSphericalRadius(Pos, entry.Key, radius)) + continue; + + if (targetPos.Equals(OriginHivePos) || targetPos.Equals(Pos)) + continue; + + var block = Api.World.BlockAccessor.GetBlock(targetPos); + if (block?.Code?.Path?.Contains("empty") != true) + continue; + + var score = CalculateSkepTargetScore(targetPos, radius); + candidates.Add(new SwarmTargetCandidate(targetPos, true, score, entry.Value)); + } + } + + candidates.Sort((a, b) => b.Score.CompareTo(a.Score)); + return candidates; + } + + private bool TryConvertSkepToPopulated(BlockPos pos, string populatedCode) + { + var block = Api.World.GetBlock(new AssetLocation(populatedCode)); + if (block == null || block.Id == 0) + return false; + + Api.World.BlockAccessor.ExchangeBlock(block.BlockId, pos); + return true; + } + + private double CalculateBeehiveTargetScore(BlockEntityReusableBeehive hive, BlockPos targetPos, int radius) + { + var distanceFactor = GetDistanceFactor(targetPos, radius); + var flowerFactor = GetBeehiveFlowerFactor(hive); + var frameFactor = hive.CountFilledFrames() > 0 ? 2.0 : 1.0; + return distanceFactor * flowerFactor * frameFactor + 10; + } + + private static double GetBeehiveFlowerFactor(BlockEntityReusableBeehive hive) + { + var cfg = Config.Instance; + var maxFlowers = Math.Max(1, cfg.MaxFlowersForHoneyProduction); + var flowers = hive.FlowersAround.GetValueOrDefault(0); + var crops = hive.CropsAround.GetValueOrDefault(0); + var effectiveFlowers = Math.Min(maxFlowers, flowers + crops * 0.25); + var normalized = effectiveFlowers / maxFlowers; + return Math.Max(0.05, normalized); + } + + private double CalculateSkepTargetScore(BlockPos targetPos, int radius) + { + var distanceFactor = GetDistanceFactor(targetPos, radius); + var flowerFactor = GetSkepFlowerFactor(targetPos); + return distanceFactor * flowerFactor; + } + + private double GetSkepFlowerFactor(BlockPos targetPos) + { + var registry = Api.GetPlantPositionRegistry(); + if (registry == null) + return 0.05; + + var (flowers, crops, _, _) = registry.GetPlantCountsNearPosition(targetPos, Config.Instance.BeehiveRadius); + var maxFlowers = Math.Max(1, Config.Instance.MaxFlowersForHoneyProduction); + var effectiveFlowers = Math.Min(maxFlowers, flowers + crops * 0.25); + var normalized = effectiveFlowers / maxFlowers; + return Math.Max(0.05, normalized); + } + + private double GetDistanceFactor(BlockPos targetPos, int radius) + { + var distance = targetPos.DistanceTo(Pos); + var normalized = Math.Clamp(1.0 - (distance / radius), 0.01, 1.0); + return normalized; + } + + public override void GetBlockInfo(IPlayer forPlayer, StringBuilder dsc) + { + dsc.AppendLine(GetPhaseStatusLine()); + dsc.AppendLine(Lang.Get("orekiwoofsbeehives:beeswarm-info-population", Population.ToString("N0"))); + + var eligibleTargets = targetHiveCandidates?.Count ?? eligibleTargetsAtLastSelection; + dsc.AppendLine(Lang.Get("orekiwoofsbeehives:beeswarm-info-eligible-targets", eligibleTargets)); + } + + private string GetPhaseStatusLine() + { + var hoursLeft = GetHoursLeftInCurrentPhase(); + var hoursLeftText = hoursLeft < 1 + ? Lang.Get("orekiwoofsbeehives:beeswarm-less-than-hour-left") + : Lang.Get("orekiwoofsbeehives:beeswarm-hours-left", Math.Ceiling(hoursLeft).ToString("F0")); + + return SwarmState switch + { + SwarmState.BuildingSwarm => Lang.Get("orekiwoofsbeehives:beeswarm-phase-forming", hoursLeftText), + SwarmState.HangingOut => Lang.Get("orekiwoofsbeehives:beeswarm-phase-scouting", hoursLeftText), + SwarmState.MigratingToNewHive => Lang.Get("orekiwoofsbeehives:beeswarm-phase-moving", hoursLeftText), + _ => Lang.Get("orekiwoofsbeehives:beeswarm-phase-scouting", hoursLeftText), + }; + } + + private double GetHoursLeftInCurrentPhase() + { + var now = Api.World.Calendar.TotalHours; + + if (SwarmState == SwarmState.BuildingSwarm) + return Math.Max(0, buildingDurationHours - (now - stateStartTotalHours)); + + if (SwarmState == SwarmState.HangingOut) + { + if (retryDay >= 0) + { + var nextPickHour = retryDay * Api.World.Calendar.HoursPerDay + 8; + return Math.Max(0, nextPickHour - now); + } + + return Math.Max(0, hangingDurationHours - (now - stateStartTotalHours)); + } + + if (SwarmState == SwarmState.MigratingToNewHive) + return Math.Max(0, MigrationPhaseDurationHours - (now - stateStartTotalHours)); + + return 0; + } + + private int GetCurrentDayIndex(double nowHours) + { + var hpd = Math.Max(1, Api.World.Calendar.HoursPerDay); + return (int)Math.Floor(nowHours / hpd); + } + + private BlockEntityReusableBeehive? GetOriginHive() + { + return Api.World.BlockAccessor.GetBlockEntity(OriginHivePos) as BlockEntityReusableBeehive; + } + + private void Disperse() + { + StopCandidateRefresh(); + var originHive = GetOriginHive(); + if (originHive != null && Population > 0) + { + var returnPercent = Math.Clamp(Config.Instance.SwarmReturnToOriginOnFailedMigrationPercent, 0, 100); + if (returnPercent > 0) + { + var returnAmount = Population * returnPercent / 100.0; + originHive.AddBeePopulationFromSwarm(returnAmount); + } + } + + RemoveSwarmBlock(); + } + + private void RemoveSwarmBlock() + { + NotifyTargetHiveSwarmFinished(); + NotifyOriginHiveSwarmFinished(); + Api.World.BlockAccessor.SetBlock(0, Pos); + } + + private void NotifyOriginHiveSwarmFinished() + { + var originHive = GetOriginHive(); + originHive?.ClearActiveSwarm(Pos); + } + + private void NotifyTargetHiveSwarmFinished() + { + if (targetIsVanillaSkep || targetPos == null) + return; + + if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive targetHive) + targetHive.ClearIncomingSwarm(Pos); + } + + public override void OnBlockRemoved() + { + NotifyTargetHiveSwarmFinished(); + NotifyOriginHiveSwarmFinished(); + base.OnBlockRemoved(); + } + + public override void ToTreeAttributes(ITreeAttribute tree) + { + base.ToTreeAttributes(tree); + + tree.SetBlockPos("originPos", OriginHivePos); + tree.SetDouble("population", Population); + tree.SetDouble("plannedPopulation", plannedPopulation); + tree.SetDouble("spawnDateTime", SpawnDateTime); + tree.SetDouble("stateStartTotalHours", stateStartTotalHours); + tree.SetDouble(nameof(MigrationPhaseDurationHours), MigrationPhaseDurationHours); + tree.SetDouble("buildingDurationHours", buildingDurationHours); + tree.SetDouble("hangingDurationHours", hangingDurationHours); + tree.SetInt("swarmState", (int)SwarmState); + tree.SetDouble("migrationStartPopulation", migrationStartPopulation); + tree.SetDouble("transferredDuringMigration", transferredDuringMigration); + tree.SetInt("retryDay", retryDay); + tree.SetBool("targetIsVanillaSkep", targetIsVanillaSkep); + tree.SetString("targetPopulatedSkepCode", targetPopulatedSkepCode ?? string.Empty); + tree.SetInt("eligibleTargetsAtLastSelection", eligibleTargetsAtLastSelection); + + // for roamingbees + tree.SetInt("roamingbees_swarm_state", (int)SwarmState); + tree.SetBlockPos("roamingbees_swarm_originHivePos", OriginHivePos); + if (targetPos is not null) + tree.SetBlockPos("roamingbees_swarm_targetHivePos", targetPos); + + tree["roamingbees_swarm_candidateHives"] = BuildCandidateHivesTreeArray(); + + if (targetPos != null) + tree.SetBlockPos("targetPos", targetPos); + } + + private TreeArrayAttribute BuildCandidateHivesTreeArray() + { + if (targetHiveCandidates == null) + return new TreeArrayAttribute([]); + + var count = Math.Min(targetHiveCandidates.Count, 10); + var entries = new TreeAttribute[count]; + for (int i = 0; i < count; i++) + { + var entry = new TreeAttribute(); + entry.SetBlockPos("pos", targetHiveCandidates[i].Pos); + entries[i] = entry; + } + return new TreeArrayAttribute(entries); + } + + public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve) + { + base.FromTreeAttributes(tree, worldAccessForResolve); + + OriginHivePos = tree.GetBlockPos("originPos"); + Population = tree.GetDouble("population"); + plannedPopulation = tree.GetDouble("plannedPopulation"); + SpawnDateTime = tree.GetDouble("spawnDateTime"); + stateStartTotalHours = tree.GetDouble("stateStartTotalHours"); + if (tree.HasAttribute(nameof(MigrationPhaseDurationHours))) + MigrationPhaseDurationHours = tree.GetDouble(nameof(MigrationPhaseDurationHours)); + buildingDurationHours = tree.GetDouble("buildingDurationHours"); + hangingDurationHours = tree.GetDouble("hangingDurationHours"); + SwarmState = (SwarmState)tree.GetInt("swarmState"); + migrationStartPopulation = tree.GetDouble("migrationStartPopulation"); + transferredDuringMigration = tree.GetDouble("transferredDuringMigration"); + retryDay = tree.GetInt("retryDay"); + targetIsVanillaSkep = tree.GetBool("targetIsVanillaSkep"); + targetPopulatedSkepCode = tree.GetString("targetPopulatedSkepCode"); + eligibleTargetsAtLastSelection = tree.GetInt("eligibleTargetsAtLastSelection"); + + targetPos = tree.GetBlockPos("targetPos"); + } + + private void BroadcastUnloadDebug(string message) + { + Api.GetOrekiWoofsBeehives()?.BroadcastUnloadDebug(message); + } + + private readonly record struct SwarmTargetCandidate(BlockPos Pos, bool IsVanillaSkep, double Score, string? PopulatedSkepCode); +} diff --git a/OrekiWoofsBeehives/BlockEntities/BlockEntityReusableBeehive.cs b/OrekiWoofsBeehives/BlockEntities/BlockEntityReusableBeehive.cs new file mode 100644 index 0000000..c8d13cc --- /dev/null +++ b/OrekiWoofsBeehives/BlockEntities/BlockEntityReusableBeehive.cs @@ -0,0 +1,1062 @@ +using OrekiWoofsBeehives.Helpers; +using OrekiWoofsBeehives.Utilities; +using OrekiWoofsBees.Common; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBeehives.BlockEntities; + +public class BlockEntityReusableBeehive : BlockEntityContainer +{ + private const string feed_remaining_attribute = "feedRemaining"; + private const double feed_full_amount = 1.0; + private const double feed_empty_threshold = 0.0001; + private const double swarm_building_hours = 3.0; + private const double swarm_hanging_hours = 5.0; + + private readonly StringBuilder infoStringBuilder = new(); + private readonly InventoryGeneric inventory; + private bool isOpen; + private double lastUpdateTotalHours = 0; + private bool wasFullyScanned; + private float? scanningProgress; + private float? rescanningProgress; + private RoomRegistry? roomRegistry; + private readonly List positionsToCheckGreenhouse = []; + private bool isInGreenhouse; + private BlockPos? activeSwarmPos; + private BlockPos? incomingSwarmPos; + + public override InventoryBase Inventory => inventory; + public override string InventoryClassName => "beehive"; + + public int? FlowersAround { get; internal set; } + public int? CropsAround { get; internal set; } + + public double BeePopulation { get; internal set; } + public double HoneyProgress { get; internal set; } + public double NextSwarmAllowedTotalDays { get; internal set; } + public bool IsReceivingIncomingSwarm => incomingSwarmPos != null; + + public double PreSwarmProgress { get; internal set; } + public bool SwarmsDisabled { get; internal set; } + + public float TimeSinceLastSpawn { get; private set; } + public bool WasFullyScanned => wasFullyScanned; + public bool OpenForIncomingSwarms => BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation && !IsReceivingIncomingSwarm; + + public BlockEntityReusableBeehive() + { + inventory = new InventoryGeneric(8, null, null); + } + + public bool IsOpen + { + get => isOpen; + set + { + if (isOpen == value) + return; + + isOpen = value; + if (Api?.World != null) + Api.World.BlockAccessor.MarkBlockDirty(Pos); + } + } + + public override void Initialize(ICoreAPI api) + { + base.Initialize(api); + + roomRegistry = Api.ModLoader.GetModSystem(); + positionsToCheckGreenhouse.Clear(); + positionsToCheckGreenhouse.AddRange([Pos.UpCopy(), Pos.DownCopy(), Pos.NorthCopy(), Pos.EastCopy(), Pos.WestCopy(), Pos.SouthCopy()]); + + if (lastUpdateTotalHours == 0) + lastUpdateTotalHours = api.World.Calendar.TotalHours; + + + var plantPositionRegistry = Api.GetPlantPositionRegistry(); + + if (plantPositionRegistry is null) + { + api.Logger.Warning($"{nameof(plantPositionRegistry)} still not loaded!"); + return; + } + + plantPositionRegistry.RegisterBeehive(Pos, Config.Instance.BeehiveRadius); + + var beehivesModSystem = Api.GetOrekiWoofsBeehives(); + if (beehivesModSystem is null) + { + api.Logger.Warning($"{nameof(OrekiWoofsBeehivesModSystem)} still not loaded!"); + return; + } + beehivesModSystem.BeehiveRegistry.Register(this); + +#if DEBUG + RegisterGameTickListener(OnGameTick, 500); +#else + RegisterGameTickListener(OnGameTick, 5_000); +#endif + + if (wasFullyScanned && api.Side.IsServer()) + { + // we trust our loaded numbers, don't take the scanning of PlantPositionRegistry yet. + // However, we do need to update when plants are placed or removed. + // Should unsub when the new scan is done. + plantPositionRegistry.CropEvent += OnCropEvent; + plantPositionRegistry.FlowerEvent += OnFlowerEvent; + } + } + + private void OnCropEvent(BlockPos pos, int delta) + { + var radius = Math.Max(1, Config.Instance.BeehiveRadius); + if (Overlaps.IsWithinSphericalRadius(Pos, StructVec3i.FromBlockPos(pos), radius)) + { + CropsAround += delta; + base.MarkDirty(true); + } + } + + private void OnFlowerEvent(BlockPos pos, int delta) + { + var radius = Math.Max(1, Config.Instance.BeehiveRadius); + if (Overlaps.IsWithinSphericalRadius(Pos, StructVec3i.FromBlockPos(pos), radius)) + { + FlowersAround += delta; + base.MarkDirty(true); + } + } + + public override void GetBlockInfo(IPlayer forPlayer, StringBuilder dsc) + { + dsc.Append(infoStringBuilder); + foreach (BlockEntityBehavior behavior in Behaviors) + behavior.GetBlockInfo(forPlayer, dsc); + } + + public float GetScanningProgress() => scanningProgress ?? 0f; + public float GetRescanningProgress() => rescanningProgress ?? 0f; + + private void OnGameTick(float dt) + { + var stopwatch = Stopwatch.StartNew(); + + UpdateGreenhouseStatus(); + UpdateStatsText(); + + if (Api.Side == EnumAppSide.Client) + return; + UpdateFlowersAndCrops(); + UpdateBeePopulationAndHoney(); + + stopwatch.Stop(); + if (stopwatch.Elapsed.TotalSeconds > 0.2) + { + var modSystem = Api.GetOrekiWoofsBeehives(); + modSystem?.Mod?.Logger.Warning($"{nameof(BlockEntityReusableBeehive)} {nameof(OnGameTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s"); + } + } + + private void ClearStaleIncomingSwarmReservation() + { + if (incomingSwarmPos == null) + return; + + if (Api.World.BlockAccessor.GetBlockEntity(incomingSwarmPos) is BlockEntityBeeSwarm) + return; + + incomingSwarmPos = null; + MarkDirty(false); + } + + private void UpdateStatsText() + { + var stats = CalculateStats(); + infoStringBuilder.Clear(); + BeehiveInfoStringBuilder.BuildBeehiveInfo(infoStringBuilder, stats, this); + } + + + private void UpdateFlowersAndCrops() + { + int radius = Config.Instance.BeehiveRadius; + IPlantPositionRegistry? modSystem = Api.GetPlantPositionRegistry(); + if (modSystem is null) + return; + + var (flowerCount, cropCount, initialProgress, rescanProgress) = modSystem.GetPlantCountsNearPosition(Pos, radius); + + if (Api.Side.IsServer()) + { + scanningProgress = initialProgress; + rescanningProgress = rescanProgress; + } + + if (!wasFullyScanned || !(wasFullyScanned && initialProgress < 1f)) + { + FlowersAround = flowerCount; + CropsAround = cropCount; + } + if (initialProgress == 1) + { + wasFullyScanned = true; + Api.GetPlantPositionRegistry()!.CropEvent -= OnCropEvent; + Api.GetPlantPositionRegistry()!.FlowerEvent -= OnFlowerEvent; + } + } + + private void UpdateBeePopulationAndHoney() + { + double currentTotalHours = Api.World.Calendar.TotalHours; + double hoursElapsed = currentTotalHours - lastUpdateTotalHours; + + if (hoursElapsed <= 0) + return; + + double hoursPerDay = Api.World.Calendar.HoursPerDay; + double daysElapsed = hoursElapsed / hoursPerDay; + + var maxDaysSkip = 0.3; + var maxHoursSkip = maxDaysSkip * hoursPerDay; + var isCatchUp = hoursElapsed > maxHoursSkip; + var startingPopulation = BeePopulation; + var startingHoneyProgress = HoneyProgress; + + if (isCatchUp) + BroadcastUnloadDebug($"hive {Pos}: catch-up start elapsed={hoursElapsed:F2}h population={startingPopulation:F0} honeyProgress={startingHoneyProgress:F3}"); + + var framesChangedVisually = false; + while (lastUpdateTotalHours < currentTotalHours) + { + var populationBeforeStep = BeePopulation; + + if (lastUpdateTotalHours + maxHoursSkip < currentTotalHours) + { + lastUpdateTotalHours += maxHoursSkip; + daysElapsed = maxDaysSkip; + } + else + { + daysElapsed = (currentTotalHours - lastUpdateTotalHours) / hoursPerDay; + lastUpdateTotalHours = currentTotalHours; + } + + var lastUpdateTotalDays = lastUpdateTotalHours / Api.World.Calendar.HoursPerDay; + var stats = CalculateStats(lastUpdateTotalDays); + + UpdateBeePopulation(daysElapsed, stats); + framesChangedVisually |= UpdateHoneyProduction(daysElapsed, stats); + framesChangedVisually |= UpdateFeedConsumption(daysElapsed, stats); + + ClearStaleIncomingSwarmReservation(); + UpdateSwarming(lastUpdateTotalHours, daysElapsed * hoursPerDay, currentTotalHours); + + if (isCatchUp) + { + var populationDelta = BeePopulation - populationBeforeStep; + if (Math.Abs(populationDelta) > 0.01) + BroadcastUnloadDebug($"hive {Pos}: step @hour={lastUpdateTotalHours:F2} deltaPopulation={populationDelta:F2} population={BeePopulation:F2}"); + } + } + + if (isCatchUp) + BroadcastUnloadDebug($"hive {Pos}: catch-up end population={BeePopulation:F2} (delta={(BeePopulation - startingPopulation):F2}) honeyProgress={HoneyProgress:F3}"); + + MarkDirty(framesChangedVisually); + } + + private void UpdateSwarming(double simulationTotalHours, double hoursElapsed, double? catchUpTargetTotalHours) + { + if (activeSwarmPos != null) + { + if (Api.World.BlockAccessor.GetBlockEntity(activeSwarmPos) is BlockEntityBeeSwarm) + return; + + if (catchUpTargetTotalHours.HasValue) + BroadcastUnloadDebug($"hive {Pos}: active swarm reference {activeSwarmPos} no longer exists during catch-up"); + + activeSwarmPos = null; + } + + var hoursPerDay = Math.Max(1, Api.World.Calendar.HoursPerDay); + var simulationTotalDays = simulationTotalHours / hoursPerDay; + + if (!CanBeginPreSwarm(simulationTotalDays)) + { + if (PreSwarmProgress > 0) + { + PreSwarmProgress = 0; + MarkDirty(false); + } + return; + } + + var stats = CalculateStats(simulationTotalDays); + var cfg = Config.Instance; + var ratePerHour = 1.0 / Math.Max(1, cfg.PreSwarmDurationHours); + + if (stats.Components.Temperature >= cfg.MaxTemperatureGrowth) + PreSwarmProgress += ratePerHour * hoursElapsed; + else + PreSwarmProgress -= ratePerHour * hoursElapsed; + + PreSwarmProgress = Math.Clamp(PreSwarmProgress, 0, 1); + MarkDirty(false); + + if (!IsReadyToStartSwarm(simulationTotalHours)) + return; + + var swarmPopulationPct = HasEligibleSwarmTargets() ? cfg.SwarmPopulationPercentage : cfg.SwarmPopulationPercentageWhenNoBeehivesAvailable; + + if (swarmPopulationPct == 0) + { + var hoursPerDayForCooldown = Math.Max(1, Api.World.Calendar.HoursPerDay); + NextSwarmAllowedTotalDays = (simulationTotalHours / hoursPerDayForCooldown) + cfg.SwarmCooldownDays; + MarkDirty(false); + return; + } + + var plannedSwarmPopulation = BeePopulation * (swarmPopulationPct / 100.0); + if (plannedSwarmPopulation <= 0) + return; + + if (!TryFindSwarmSpawnPosition(out var swarmPos, out var attachmentSide)) + return; + + var swarmBlock = Api.World.GetBlock(new AssetLocation($"orekiwoofsbeehives:beeswarm-{attachmentSide}")); + if (swarmBlock == null) + return; + + Api.World.BlockAccessor.SetBlock(swarmBlock.BlockId, swarmPos); + + if (Api.World.BlockAccessor.GetBlockEntity(swarmPos) is not BlockEntityBeeSwarm swarmBe) + return; + + var isCatchUpSpawn = catchUpTargetTotalHours.HasValue && catchUpTargetTotalHours.Value > simulationTotalHours; + if (isCatchUpSpawn) + BroadcastUnloadDebug($"hive {Pos}: spawned swarm {swarmPos} at simulatedHour={simulationTotalHours:F2}, plannedPopulation={plannedSwarmPopulation:F2}, attachment={attachmentSide}"); + + swarmBe.InitializeFromOrigin( + originHivePos: Pos, + plannedPopulation: plannedSwarmPopulation, + spawnTotalHours: simulationTotalHours, + buildingDurationHours: swarm_building_hours, + hangingDurationHours: swarm_hanging_hours + ); + + NextSwarmAllowedTotalDays = (simulationTotalHours / hoursPerDay) + cfg.SwarmCooldownDays; + activeSwarmPos = swarmPos; + PreSwarmProgress = 0; + + if (catchUpTargetTotalHours.HasValue && catchUpTargetTotalHours.Value > simulationTotalHours) + { + swarmBe.FastForwardTo(catchUpTargetTotalHours.Value, debugAsCatchUp: true); + + if (Api.World.BlockAccessor.GetBlockEntity(swarmPos) is BlockEntityBeeSwarm) + BroadcastUnloadDebug($"hive {Pos}: swarm {swarmPos} catch-up ended still active"); + else + BroadcastUnloadDebug($"hive {Pos}: swarm {swarmPos} catch-up ended and swarm finished"); + } + + MarkDirty(false); + } + + public bool IsReadyToStartSwarm(bool ignoreDayTime = false) + { + if (Api?.World == null) + return false; + + return IsReadyToStartSwarm(Api.World.Calendar.TotalHours, ignoreDayTime); + } + + private bool IsReadyToStartSwarm(double simulationTotalHours, bool ignoreDayTime = false) + { + if (Api?.World == null) + return false; + + if (activeSwarmPos != null) + return false; + + if (!Config.Instance.EnableSwarms || SwarmsDisabled) + return false; + + var hoursPerDay = Math.Max(1, Api.World.Calendar.HoursPerDay); + var simulationTotalDays = simulationTotalHours / hoursPerDay; + if (simulationTotalDays < NextSwarmAllowedTotalDays) + return false; + + var cfg = Config.Instance; + var requiredPopulation = cfg.MaxBeePopulation * (cfg.PopulationPercentRequirementForSwarm / 100.0); + if (BeePopulation <= requiredPopulation) + return false; + + if (PreSwarmProgress < 1.0) + return false; + + if (ignoreDayTime) + return true; + + var dayHour = simulationTotalHours % hoursPerDay; + if (dayHour < 0) + dayHour += hoursPerDay; + + if (dayHour is < 8 or > 12) + return false; + + return true; + } + + private bool CanBeginPreSwarm(double simulationTotalDays) + { + if (activeSwarmPos != null) + return false; + + if (!Config.Instance.EnableSwarms || SwarmsDisabled) + return false; + + if (simulationTotalDays < NextSwarmAllowedTotalDays) + return false; + + var cfg = Config.Instance; + var requiredPopulation = cfg.MaxBeePopulation * (cfg.PopulationPercentRequirementForSwarm / 100.0); + if (BeePopulation <= requiredPopulation) + return false; + + return true; + } + + public bool IsSwarmBuildingNearby() + { + if (Api?.World == null || activeSwarmPos == null) + return false; + + if (Api.World.BlockAccessor.GetBlockEntity(activeSwarmPos) is not BlockEntityBeeSwarm swarm) + return false; + + return swarm.SwarmState == SwarmState.BuildingSwarm; + } + + public double TakeBeePopulationForSwarm(double requestedAmount) + { + var clampedAmount = Math.Max(0, requestedAmount); + if (clampedAmount <= 0 || BeePopulation <= 0) + return 0; + + var taken = Math.Min(BeePopulation, clampedAmount); + BeePopulation -= taken; + MarkDirty(false); + return taken; + } + + public double AddBeePopulationFromSwarm(double requestedAmount) + { + var clampedAmount = Math.Max(0, requestedAmount); + if (clampedAmount <= 0) + return 0; + + var cfg = Config.Instance; + var room = Math.Max(0, cfg.MaxBeePopulation - BeePopulation); + var added = Math.Min(room, clampedAmount); + BeePopulation += added; + MarkDirty(false); + return added; + } + + public void ClearActiveSwarm(BlockPos swarmPos) + { + if (activeSwarmPos == null) + return; + + if (!activeSwarmPos.Equals(swarmPos)) + return; + + activeSwarmPos = null; + MarkDirty(false); + } + + public void SetIncomingSwarm(BlockPos swarmPos) + { + incomingSwarmPos = swarmPos.Copy(); + MarkDirty(false); + } + + public void ClearIncomingSwarm(BlockPos swarmPos) + { + if (incomingSwarmPos == null) + return; + + if (!incomingSwarmPos.Equals(swarmPos)) + return; + + incomingSwarmPos = null; + MarkDirty(false); + } + + private bool HasEligibleSwarmTargets() + { + var cfg = Config.Instance; + var radius = Math.Max(1, cfg.BeehiveRadius); + var modSystem = Api.GetOrekiWoofsBeehives(); + if (modSystem == null) + return false; + + foreach (var pos in modSystem.BeehiveRegistry.BeehivePositions) + { + var targetPos = new BlockPos(pos.X, pos.Y, pos.Z); + if (targetPos.Equals(Pos)) + continue; + + if (!Overlaps.IsWithinSphericalRadius(Pos, pos, radius)) + continue; + + if (Api.World.BlockAccessor.GetBlockEntity(targetPos) is BlockEntityReusableBeehive hive && hive.OpenForIncomingSwarms) + return true; + } + + foreach (var entry in modSystem.VanillaSkepRegistry.Entries) + { + if (!Overlaps.IsWithinSphericalRadius(Pos, entry.Key, radius)) + continue; + + var targetPos = new BlockPos(entry.Key.X, entry.Key.Y, entry.Key.Z); + var block = Api.World.BlockAccessor.GetBlock(targetPos); + if (block?.Code?.Path?.Contains("empty") == true) + return true; + } + + return false; + } + + private bool TryFindSwarmSpawnPosition(out BlockPos swarmPos, out string attachmentSide) + { + swarmPos = Pos.Copy(); + attachmentSide = "down"; + + var sideCandidatesWood = new List(); + var sideCandidatesNonWood = new List(); + var floorCandidates = new List(); + var ceilingCandidates = new List(); + + const int horizontalSearchRadius = 10; + const int verticalSearchHalfHeight = 5; + var horizontalSearchRadiusSquared = horizontalSearchRadius * horizontalSearchRadius; + var ba = Api.World.BlockAccessor; + for (int dx = -horizontalSearchRadius; dx <= horizontalSearchRadius; dx++) + { + for (int dy = -verticalSearchHalfHeight; dy <= verticalSearchHalfHeight; dy++) + { + for (int dz = -horizontalSearchRadius; dz <= horizontalSearchRadius; dz++) + { + if ((dx * dx) + (dz * dz) > horizontalSearchRadiusSquared) + continue; + + var basePos = Pos.AddCopy(dx, dy, dz); + var supportBlock = ba.GetBlock(basePos); + if (supportBlock == null || supportBlock.Id == 0) + continue; + var supportBias = GetSupportBias(supportBlock, basePos); + + foreach (var facing in BlockFacing.HORIZONTALS) + { + if (!supportBlock.SideSolid[facing.Index]) + continue; + + var candidateSwarmPos = basePos.AddCopy(facing); + if (!IsEmptyForSwarm(candidateSwarmPos)) + continue; + + var candidateAttachmentSide = GetAttachmentSideCode(basePos, candidateSwarmPos); + var candidateWeight = CalculateCandidateWeight(candidateSwarmPos, candidateAttachmentSide, supportBias); + + if (supportBlock.BlockMaterial == EnumBlockMaterial.Wood) + sideCandidatesWood.Add(new SwarmSpawnCandidate(candidateSwarmPos, candidateAttachmentSide, candidateWeight)); + else + sideCandidatesNonWood.Add(new SwarmSpawnCandidate(candidateSwarmPos, candidateAttachmentSide, candidateWeight)); + } + + if (supportBlock.SideSolid[BlockFacing.UP.Index]) + { + var floorCandidate = basePos.UpCopy(); + if (IsEmptyForSwarm(floorCandidate)) + floorCandidates.Add(new SwarmSpawnCandidate(floorCandidate, "down", CalculateCandidateWeight(floorCandidate, "down", supportBias))); + } + + if (!supportBlock.SideSolid[BlockFacing.DOWN.Index]) + continue; + + var ceilingCandidate = basePos.DownCopy(); + if (!IsEmptyForSwarm(ceilingCandidate)) + continue; + + ceilingCandidates.Add(new SwarmSpawnCandidate(ceilingCandidate, "up", CalculateCandidateWeight(ceilingCandidate, "up", supportBias))); + } + } + } + + var selected = ((SelectBiasedCandidate(sideCandidatesWood) + ?? SelectBiasedCandidate(sideCandidatesNonWood)) + ?? SelectBiasedCandidate(floorCandidates)) + ?? SelectBiasedCandidate(ceilingCandidates); + if (selected == null) + return false; + + swarmPos = selected.Value.Pos; + attachmentSide = selected.Value.AttachmentSide; + return true; + } + + private static string GetAttachmentSideCode(BlockPos supportPos, BlockPos swarmPos) + { + var dx = supportPos.X - swarmPos.X; + var dy = supportPos.Y - swarmPos.Y; + var dz = supportPos.Z - swarmPos.Z; + + if (dx < 0) + return "west"; + if (dx > 0) + return "east"; + if (dy < 0) + return "down"; + if (dy > 0) + return "up"; + if (dz < 0) + return "north"; + return "south"; + } + + private bool IsEmptyForSwarm(BlockPos pos) + { + var block = Api.World.BlockAccessor.GetBlock(pos); + return block != null && (block.Id == 0 || block.Replaceable >= 6000); + } + + private double GetSupportBias(Block supportBlock, BlockPos supportPos) + { + var bias = 1d; + var codePath = supportBlock.Code?.Path ?? string.Empty; + + if (codePath.Contains("beehive", StringComparison.OrdinalIgnoreCase)) + bias *= 0.15d; + + if (codePath.Contains("log", StringComparison.OrdinalIgnoreCase)) + { + var leavesNearby = CountNearbyLeaves(supportPos, 4); + if (leavesNearby > 0) + bias *= 8d + leavesNearby; + } + + return bias; + } + + private int CountNearbyLeaves(BlockPos center, int radius) + { + int count = 0; + var radiusSq = radius * radius; + var ba = Api.World.BlockAccessor; + + for (int dx = -radius; dx <= radius; dx++) + { + for (int dy = -radius; dy <= radius; dy++) + { + for (int dz = -radius; dz <= radius; dz++) + { + if ((dx * dx) + (dy * dy) + (dz * dz) > radiusSq) + continue; + + var block = ba.GetBlock(center.AddCopy(dx, dy, dz)); + var path = block?.Code?.Path; + if (path?.Contains("leaves", StringComparison.OrdinalIgnoreCase) == true) + count++; + } + } + } + + return count; + } + + private double CalculateCandidateWeight(BlockPos candidatePos, string attachmentSide, double supportBias) + { + var distance = candidatePos.DistanceTo(Pos); + var distanceWeight = 1d / (distance + 1d); + var attachmentWeight = attachmentSide switch + { + "down" => 0.8d, + "up" => 0.4d, + _ => 1d + }; + + return supportBias * distanceWeight * attachmentWeight; + } + + private SwarmSpawnCandidate? SelectBiasedCandidate(List candidates) + { + if (candidates.Count == 0) + return null; + + var totalWeight = 0d; + for (int i = 0; i < candidates.Count; i++) + totalWeight += Math.Max(0, candidates[i].Weight); + + if (totalWeight <= 0) + return candidates[0]; + + var roll = Api.World.Rand.NextDouble() * totalWeight; + var running = 0d; + for (int i = 0; i < candidates.Count; i++) + { + var candidate = candidates[i]; + running += Math.Max(0, candidate.Weight); + if (roll < running) + return candidate; + } + + return candidates[^1]; + } + + private readonly record struct SwarmSpawnCandidate(BlockPos Pos, string AttachmentSide, double Weight); + + private void UpdateBeePopulation(double daysElapsed, BeehiveStats stats) + { + if (BeePopulation <= 0) + { + BeePopulation = 0; + return; + } + + var cfg = Config.Instance; + double change = stats.DailyNetPopulationChange * daysElapsed; + + double newPopulation = BeePopulation + change; + BeePopulation = Math.Clamp(newPopulation, 0, cfg.MaxBeePopulation); + } + + private BeehiveStats CalculateStats(double? totalDaysDate = null) + { + return BeehiveStats.Create(this, isInGreenhouse, totalDaysDate); + } + + private void UpdateGreenhouseStatus() + { + if (roomRegistry != null) + isInGreenhouse = GetGreenhouseStatus(positionsToCheckGreenhouse, roomRegistry); + } + + private static bool GetGreenhouseStatus(IEnumerable positionsToCheckGreenhouse, RoomRegistry roomRegistry) + { + bool isGreenhouse = false; + foreach (var posToCheck in positionsToCheckGreenhouse) + { + var room = roomRegistry?.GetRoomForPosition(posToCheck); + isGreenhouse = room != null && (room.SkylightCount > room.NonSkylightCount && room.ExitCount == 0); + if (isGreenhouse) + break; + } + return isGreenhouse; + } + + private bool UpdateHoneyProduction(double daysElapsed, BeehiveStats stats) + { + int emptyFrameSlot = GetFirstEmptyFrameSlot(); + var hasNoEmptyFrames = emptyFrameSlot < 0; + if (hasNoEmptyFrames) + return false; + + if (stats.FramesPerDay <= 0) + return false; + + HoneyProgress += stats.FramesPerDay * daysElapsed; + + var anyFilled = false; + var shouldFillFrame = HoneyProgress >= 1.0 && emptyFrameSlot >= 0; + while (shouldFillFrame) + { + FillFrame(emptyFrameSlot); + anyFilled = true; + HoneyProgress -= 1.0; + emptyFrameSlot = GetFirstEmptyFrameSlot(); + shouldFillFrame = HoneyProgress >= 1.0 && emptyFrameSlot >= 0; + } + + hasNoEmptyFrames = emptyFrameSlot < 0; + if (hasNoEmptyFrames) + HoneyProgress = 0; + + return anyFilled; + } + + private bool UpdateFeedConsumption(double daysElapsed, BeehiveStats stats) + { + if (stats.FeedConsumedPerDay <= 0 || BeePopulation <= 0) + return false; + + var feedToConsume = stats.FeedConsumedPerDay * daysElapsed; + if (feedToConsume <= 0) + return false; + + var anyReplaced = false; + for (int i = 0; i < inventory.Count && feedToConsume > 0; i++) + { + if (inventory[i].Empty) + continue; + + var stack = inventory[i].Itemstack; + if (!IsFilledFeedFrame(stack)) + continue; + + var remaining = GetFeedRemaining(stack); + if (remaining <= feed_empty_threshold) + { + ReplaceWithEmptyFrame(i); + anyReplaced = true; + continue; + } + + var consumed = Math.Min(remaining, feedToConsume); + remaining -= consumed; + feedToConsume -= consumed; + + if (remaining <= feed_empty_threshold) + { + ReplaceWithEmptyFrame(i); + anyReplaced = true; + } + else + SetFeedRemaining(stack, remaining); + + inventory[i].MarkDirty(); + } + + return anyReplaced; + } + + private static bool IsFilledFeedFrame(ItemStack? stack) + { + return stack?.Block?.Code?.Path == "beehiveframe-filled-feed"; + } + + private static double GetFeedRemaining(ItemStack? stack) + { + if (stack?.Attributes == null) + return feed_full_amount; + + if (!stack.Attributes.HasAttribute(feed_remaining_attribute)) + return feed_full_amount; + + return Math.Clamp(stack.Attributes.GetDouble(feed_remaining_attribute), 0, feed_full_amount); + } + + public bool TryGetCurrentFeedStatus(out double remaining) + { + remaining = 0; + + for (int i = 0; i < inventory.Count; i++) + { + if (inventory[i].Empty) + continue; + + var stack = inventory[i].Itemstack; + if (!IsFilledFeedFrame(stack)) + continue; + + remaining = GetFeedRemaining(stack!); + return true; + } + + return false; + } + + private static void SetFeedRemaining(ItemStack? stack, double remaining) + { + stack?.Attributes.SetDouble(feed_remaining_attribute, Math.Clamp(remaining, 0, feed_full_amount)); + } + + private void ReplaceWithEmptyFrame(int slotIndex) + { + var emptyFrameBlock = Api.World.GetBlock(new AssetLocation("orekiwoofsbeehives:beehiveframe-empty")); + if (emptyFrameBlock == null) + return; + + inventory[slotIndex].Itemstack = new ItemStack(emptyFrameBlock, 1); + inventory[slotIndex].MarkDirty(); + } + + public int GetFirstEmptyFrameSlot() + { + for (int i = 0; i < inventory.Count; i++) + { + if (!inventory[i].Empty) + { + var itemStack = inventory[i].Itemstack; + if (itemStack?.Block?.Code?.Path == "beehiveframe-empty") + return i; + } + } + return -1; + } + + private void FillFrame(int slotIndex) + { + if (slotIndex < 0 || slotIndex >= inventory.Count) + return; + + var emptyFrame = inventory[slotIndex].Itemstack; + if (emptyFrame?.Block?.Code?.Path != "beehiveframe-empty") + return; + + Block? filledFrameBlock = Api.World.GetBlock(new AssetLocation("orekiwoofsbeehives:beehiveframe-filled")); + if (filledFrameBlock == null) + return; + + var filledFrame = new ItemStack(filledFrameBlock, 1); + inventory[slotIndex].Itemstack = filledFrame; + inventory[slotIndex].MarkDirty(); + } + + public int CountFilledFrames() + { + int count = 0; + for (int i = 0; i < inventory.Count; i++) + { + if (!inventory[i].Empty) + { + var itemStack = inventory[i].Itemstack; + if (itemStack?.Block?.Code?.Path?.StartsWith("beehiveframe-filled") == true) + count++; + } + } + return count; + } + + public int CountEmptyFrames() + { + int count = 0; + for (int i = 0; i < inventory.Count; i++) + { + if (!inventory[i].Empty) + { + var itemStack = inventory[i].Itemstack; + if (itemStack?.Block?.Code?.Path == "beehiveframe-empty") + count++; + } + } + return count; + } + + public int CountTotalFrames() + { + int count = 0; + for (int i = 0; i < inventory.Count; i++) + { + if (!inventory[i].Empty) + { + var itemStack = inventory[i].Itemstack; + if (itemStack?.Block?.Code?.Path?.StartsWith("beehiveframe") == true) + count++; + } + } + return count; + } + + public override void ToTreeAttributes(ITreeAttribute tree) + { + base.ToTreeAttributes(tree); + tree.SetBool("isOpen", IsOpen); + tree.SetDouble("beePopulation", BeePopulation); + tree.SetInt("roamingbees_targetBeeParticleCount", GetTargetBeeParticleCount()); + tree.SetInt("roamingbees_radius", Config.Instance.BeehiveRadius); + tree.SetDouble("honeyProgress", HoneyProgress); + tree.SetDouble("lastUpdateTotalHours", lastUpdateTotalHours); + tree.SetBool(nameof(wasFullyScanned), wasFullyScanned); + + if (FlowersAround.HasValue) + tree.SetInt("flowersAround", FlowersAround.Value); + if (CropsAround.HasValue) + tree.SetInt("cropsAround", CropsAround.Value); + + if (scanningProgress.HasValue) + tree.SetFloat(nameof(scanningProgress), scanningProgress.Value); + if (rescanningProgress.HasValue) + tree.SetFloat(nameof(rescanningProgress), rescanningProgress.Value); + + if (activeSwarmPos != null) + tree.SetBlockPos("activeSwarmPos", activeSwarmPos); + + if (incomingSwarmPos != null) + tree.SetBlockPos("incomingSwarmPos", incomingSwarmPos); + + tree.SetDouble(nameof(NextSwarmAllowedTotalDays), NextSwarmAllowedTotalDays); + tree.SetDouble(nameof(PreSwarmProgress), PreSwarmProgress); + tree.SetBool(nameof(SwarmsDisabled), SwarmsDisabled); + } + + public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve) + { + base.FromTreeAttributes(tree, worldAccessForResolve); + IsOpen = tree.GetBool("isOpen"); + BeePopulation = tree.GetDouble("beePopulation"); + HoneyProgress = tree.GetDouble("honeyProgress"); + lastUpdateTotalHours = tree.GetDouble("lastUpdateTotalHours"); + wasFullyScanned = tree.GetBool(nameof(wasFullyScanned)); + + if (tree.HasAttribute("flowersAround")) + FlowersAround = tree.GetInt("flowersAround"); + if (tree.HasAttribute("cropsAround")) + CropsAround = tree.GetInt("cropsAround"); + + if (Api != null && Api.Side.IsClient()) + { + TimeSinceLastSpawn = tree.GetFloat(nameof(TimeSinceLastSpawn)); + if (tree.HasAttribute(nameof(scanningProgress))) + scanningProgress = tree.GetFloat(nameof(scanningProgress)); + if (tree.HasAttribute(nameof(rescanningProgress))) + rescanningProgress = tree.GetFloat(nameof(rescanningProgress)); + } + + activeSwarmPos = tree.GetBlockPos("activeSwarmPos"); + incomingSwarmPos = tree.GetBlockPos("incomingSwarmPos"); + + NextSwarmAllowedTotalDays = tree.GetDouble(nameof(NextSwarmAllowedTotalDays)); + PreSwarmProgress = tree.GetDouble(nameof(PreSwarmProgress)); + SwarmsDisabled = tree.GetBool(nameof(SwarmsDisabled)); + } + + private int GetTargetBeeParticleCount() + { + var cfg = Config.Instance; + if (cfg.BeehiveAlwaysSpawnNumberOfBees > 0) + return cfg.BeehiveAlwaysSpawnNumberOfBees; + + if (cfg.BeesPerParticle <= 0) + return 0; + + return (int)Math.Max(0, BeePopulation / cfg.BeesPerParticle); + } + + public override void OnBlockRemoved() + { + base.OnBlockRemoved(); + Api.GetPlantPositionRegistry()?.UnregisterBeehive(Pos); + Api.GetOrekiWoofsBeehives()?.BeehiveRegistry.Unregister(Pos); + } + + public override void OnBlockUnloaded() + { + base.OnBlockUnloaded(); + Api.GetPlantPositionRegistry()?.UnregisterBeehive(Pos); + Api.GetOrekiWoofsBeehives()?.BeehiveRegistry.Unregister(Pos); + } + + private void BroadcastUnloadDebug(string message) + { + Api.GetOrekiWoofsBeehives()?.BroadcastUnloadDebug(message); + } +} diff --git a/OrekiWoofsBeehives/Blocks/BlockBeeSwarm.cs b/OrekiWoofsBeehives/Blocks/BlockBeeSwarm.cs new file mode 100644 index 0000000..935a314 --- /dev/null +++ b/OrekiWoofsBeehives/Blocks/BlockBeeSwarm.cs @@ -0,0 +1,55 @@ +using OrekiWoofsBeehives.BlockEntities; +using System; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBeehives.Blocks; + +public class BlockBeeSwarm : Block +{ + private static readonly Cuboidf northBox = new(0.25f, 0.25f, 0f, 0.75f, 0.75f, 0.2f); + private static readonly Cuboidf eastBox = new(0.8f, 0.25f, 0.25f, 1f, 0.75f, 0.75f); + private static readonly Cuboidf southBox = new(0.25f, 0.25f, 0.8f, 0.75f, 0.75f, 1f); + private static readonly Cuboidf westBox = new(0f, 0.25f, 0.25f, 0.2f, 0.75f, 0.75f); + private static readonly Cuboidf downBox = new(0.25f, 0f, 0.25f, 0.75f, 0.2f, 0.75f); + private static readonly Cuboidf upBox = new(0.25f, 0.8f, 0.25f, 0.75f, 1f, 0.75f); + + public override float GetAmbientSoundStrength(IWorldAccessor world, BlockPos pos) + { + if (world.BlockAccessor.GetBlockEntity(pos) is not BlockEntityBeeSwarm swarm) + return 0; + + if (swarm.Population < 100) + return 0; + + if (swarm.Population < Config.Instance.BeehiveConsideredEmptyBelowPopulation) + return Math.Clamp(Config.Instance.BeehiveAmbientVolume * 0.2f, 0f, 1f); + + return Math.Clamp(Config.Instance.BeehiveAmbientVolume, 0f, 1f); + } + + public override Cuboidf[] GetSelectionBoxes(IBlockAccessor blockAccessor, BlockPos pos) + { + return [GetCurrentVariantBox().Clone()]; + } + + public override Cuboidf[] GetCollisionBoxes(IBlockAccessor blockAccessor, BlockPos pos) + { + return [GetCurrentVariantBox().Clone()]; + } + + private Cuboidf GetCurrentVariantBox() + { + var side = Variant?["side"] ?? "north"; + return side switch + { + "north" => northBox, + "east" => eastBox, + "south" => southBox, + "west" => westBox, + "down" => downBox, + "up" => upBox, + _ => northBox, + }; + } +} diff --git a/OrekiWoofsBeehives/Blocks/BlockBeehiveFrame.cs b/OrekiWoofsBeehives/Blocks/BlockBeehiveFrame.cs new file mode 100644 index 0000000..972014a --- /dev/null +++ b/OrekiWoofsBeehives/Blocks/BlockBeehiveFrame.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Vintagestory.API.Common; +using Vintagestory.API.Config; + +namespace OrekiWoofsBeehives.Blocks; + +public class BlockBeehiveFrame : Block +{ + private const string feed_remaining_attribute = "feedRemaining"; + private const double feed_full_amount = 1.0; + + public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) + { + base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); + + var stack = inSlot.Itemstack; + if (stack?.Block?.Code?.Path != "beehiveframe-filled-feed") + return; + + var remaining = GetFeedRemaining(stack); + var remainingPercent = Math.Clamp(remaining * 100.0, 0, 100); + if (remainingPercent >= 100) + return; + + dsc.AppendLine(FormatLang("orekiwoofsbeehives:beehiveframe-feed-percent", new Dictionary + { + ["percent"] = $"{remainingPercent:F0}" + })); + } + + private static double GetFeedRemaining(ItemStack stack) + { + if (stack.Attributes == null) + return feed_full_amount; + + if (!stack.Attributes.HasAttribute(feed_remaining_attribute)) + return feed_full_amount; + + return Math.Clamp(stack.Attributes.GetDouble(feed_remaining_attribute), 0, feed_full_amount); + } + + private static string FormatLang(string langKey, Dictionary values) + { + var result = Lang.Get(langKey); + foreach (var kvp in values) + result = result.Replace($">>>{kvp.Key}<<<", kvp.Value); + return result; + } +} diff --git a/OrekiWoofsBeehives/Blocks/BlockReusableBeehive.cs b/OrekiWoofsBeehives/Blocks/BlockReusableBeehive.cs new file mode 100644 index 0000000..5c93c5b --- /dev/null +++ b/OrekiWoofsBeehives/Blocks/BlockReusableBeehive.cs @@ -0,0 +1,678 @@ +using OrekiWoofsBeehives.BlockEntities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.API.Util; + +namespace OrekiWoofsBeehives.Blocks; + +public class BlockReusableBeehive : Block +{ + private readonly WorldInteraction openCloseInteraction = new() { ActionLangCode = "blockhelp-door-openclose", MouseButton = EnumMouseButton.Right }; + private readonly AssetLocation beehiveFrameEmptyAsset = new("orekiwoofsbeehives:beehiveframe-empty"); + private readonly AssetLocation beehiveFrameFilledAsset = new("orekiwoofsbeehives:beehiveframe-filled"); + private readonly AssetLocation beehiveFrameFilledFeedAsset = new("orekiwoofsbeehives:beehiveframe-filled-feed"); + + private readonly Cuboidf[] slotBoxes = new Cuboidf[8]; + private readonly Cuboidf[] closedSelectionBoxes = []; + private readonly Cuboidf[] openSelectionBoxes = []; + + private MeshData? closedMesh; + private MeshData? openMesh; + + public BlockReusableBeehive() + { + for (int i = 0; i < 8; i++) + { + float x1 = 2 + i * 1.5f; + float x2 = x1 + 1.5f; + slotBoxes[i] = new Cuboidf( + x1 / 16f, 6 / 16f, 2 / 16f, + x2 / 16f, 14 / 16f, 14 / 16f + ); + } + + closedSelectionBoxes = + [ + new Cuboidf(0, 0, 0, 1, 0.375f, 1), + new Cuboidf(0, 0.875f, 0, 1, 1, 1), + new Cuboidf(0, 0.375f, 0.875f, 1, 0.875f, 1), + new Cuboidf(0, 0.375f, 0, 0.125f, 0.875f, 0.875f), + new Cuboidf(0.875f, 0.375f, 0, 1, 0.875f, 0.875f), + new Cuboidf(0.125f, 0.375f, 0, 0.875f, 0.875f, 0.125f) + ]; + + openSelectionBoxes = + [ + new Cuboidf(0, 0, 0, 1, 0.375f, 1), + new Cuboidf(0, 0.875f, 0, 1, 1, 1), + new Cuboidf(0, 0.375f, 0.875f, 1, 0.875f, 1), + new Cuboidf(0, 0.375f, 0, 0.125f, 0.875f, 0.875f), + new Cuboidf(0.875f, 0.375f, 0, 1, 0.875f, 0.875f) + ]; + } + + public override float GetAmbientSoundStrength(IWorldAccessor world, BlockPos pos) + { + var entity = GetBlockEntity(pos); + if (entity == null) + return 0; + + if (entity.BeePopulation < 100) + return 0; + + if (entity.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation) + return Math.Clamp(Config.Instance.BeehiveAmbientVolume * 0.2f, 0f, 1f); + + return Math.Clamp(Config.Instance.BeehiveAmbientVolume, 0f, 1f); + } + + public override void OnLoaded(ICoreAPI api) + { + base.OnLoaded(api); + + var frontFacing = GetFrontFacing(); + if (frontFacing != null) + SideSolid[frontFacing.Index] = false; + + if (api.Side != EnumAppSide.Client) + return; + + ICoreClientAPI capi = (api as ICoreClientAPI)!; + + var closedShape = api.Assets.Get(new AssetLocation("orekiwoofsbeehives:shapes/block/beehive-closed.json")).ToObject(); + var openShape = api.Assets.Get(new AssetLocation("orekiwoofsbeehives:shapes/block/beehive-open.json")).ToObject(); + + capi.Tesselator.TesselateShape(this, closedShape, out closedMesh); + capi.Tesselator.TesselateShape(this, openShape, out openMesh); + + var rotateY = -GetRotationY(); + + closedMesh?.Rotate(new Vec3f(0.5f, 0.5f, 0.5f), 0, rotateY, 0); + openMesh?.Rotate(new Vec3f(0.5f, 0.5f, 0.5f), 0, rotateY, 0); + } + + public override void OnBlockBroken(IWorldAccessor world, BlockPos pos, IPlayer? byPlayer, float dropQuantityMultiplier = 1f) + { + if (world.BlockAccessor.GetBlockEntity(pos) is not BlockEntityReusableBeehive be) + { + base.OnBlockBroken(world, pos, byPlayer, dropQuantityMultiplier); + return; + } + + if (be.BeePopulation <= 0 && be.HoneyProgress <= 0 && !be.Inventory.Any(slot => !slot.Empty)) + { + base.OnBlockBroken(world, pos, byPlayer, dropQuantityMultiplier); + return; + } + + var stack = new ItemStack(this); + var beehiveData = new TreeAttribute(); + + beehiveData.SetDouble("beePopulation", be.BeePopulation); + beehiveData.SetDouble("honeyProgress", be.HoneyProgress); + beehiveData.SetDouble("nextSwarmAllowedTotalDays", be.NextSwarmAllowedTotalDays); + beehiveData.SetDouble("preSwarmProgress", be.PreSwarmProgress); + beehiveData.SetBool("swarmsDisabled", be.SwarmsDisabled); + + var inventoryTree = new TreeAttribute(); + be.Inventory.ToTreeAttributes(inventoryTree); + beehiveData["inventory"] = inventoryTree; + + stack.Attributes["beehiveData"] = beehiveData; + + if (byPlayer == null) + be.OnBlockRemoved(); + + world.SpawnItemEntity(stack, pos.ToVec3d().Add(0.5, 0.5, 0.5)); + world.BlockAccessor.SetBlock(0, pos); + } + + public override void OnBlockPlaced(IWorldAccessor world, BlockPos blockPos, ItemStack? byItemStack = null) + { + base.OnBlockPlaced(world, blockPos, byItemStack); + + if (byItemStack?.Attributes?.HasAttribute("beehiveData") != true) + { + if (world.BlockAccessor.GetBlockEntity(blockPos) is BlockEntityReusableBeehive freshBe) + { + freshBe.SwarmsDisabled = !Config.Instance.SwarmSettingAfterPlacing; + freshBe.MarkDirty(true); + } + return; + } + + if (world.BlockAccessor.GetBlockEntity(blockPos) is not BlockEntityReusableBeehive be) + return; + + if (byItemStack.Attributes["beehiveData"] is not ITreeAttribute beehiveData) + return; + + be.BeePopulation = beehiveData.GetDouble("beePopulation"); + be.HoneyProgress = beehiveData.GetDouble("honeyProgress"); + be.NextSwarmAllowedTotalDays = beehiveData.GetDouble("nextSwarmAllowedTotalDays"); + be.PreSwarmProgress = beehiveData.GetDouble("preSwarmProgress"); + be.SwarmsDisabled = beehiveData.GetBool("swarmsDisabled"); + + if (beehiveData["inventory"] is ITreeAttribute inventoryTree) + be.Inventory.FromTreeAttributes(inventoryTree); + + be.MarkDirty(true); + } + + public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) + { + base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); + + if (inSlot.Itemstack?.Attributes?.HasAttribute("beehiveData") != true) + return; + + if (inSlot.Itemstack.Attributes["beehiveData"] is not ITreeAttribute beehiveData) + return; + + double beePopulation = beehiveData.GetDouble("beePopulation"); + double honeyProgress = beehiveData.GetDouble("honeyProgress"); + + if (beePopulation <= 0 && honeyProgress <= 0) + return; + + dsc.AppendLine(); + dsc.AppendLine("Contains:"); + + if (beePopulation > 0) + dsc.AppendLine($" Bee Population: {beePopulation:F0}"); + + if (honeyProgress > 0) + dsc.AppendLine($" Honey Progress: {honeyProgress * 100:F1}%"); + + if (beehiveData["inventory"] is not ITreeAttribute inventoryTree) + return; + + int frameCount = 0; + int filledFrameCount = 0; + + for (int i = 0; i < 8; i++) + { + if (!inventoryTree.HasAttribute($"slot{i}")) continue; + if (inventoryTree[$"slot{i}"] is not ITreeAttribute slotTree) continue; + if (!slotTree.HasAttribute("itemstack")) continue; + + frameCount++; + var itemCode = slotTree.GetString("itemstack"); + if (itemCode?.Contains("filled") == true) + filledFrameCount++; + } + + if (frameCount > 0) + dsc.AppendLine($" Frames: {frameCount} ({filledFrameCount} filled)"); + } + + public override int GetMergableQuantity(ItemStack sinkStack, ItemStack sourceStack, EnumMergePriority priority) + { + if (sinkStack?.Attributes?.HasAttribute("beehiveData") == true || + sourceStack?.Attributes?.HasAttribute("beehiveData") == true) + return 0; + + return base.GetMergableQuantity(sinkStack, sourceStack, priority); + } + + public override WorldInteraction[] GetPlacedBlockInteractionHelp(IWorldAccessor world, BlockSelection selection, IPlayer forPlayer) + { + if (world.BlockAccessor.GetBlockEntity(selection.Position) is not BlockEntityReusableBeehive be) + return base.GetPlacedBlockInteractionHelp(world, selection, forPlayer); + + var clickedSlot = GetClickedSlot(selection.HitPosition, be); + if (clickedSlot is -1) + { + var interactions = new List { openCloseInteraction }; + + if (be.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation) + { + string[] existingValidSkeps = ["game:skep-reed-populated-east", "game:skep-papyrus-populated-east"]; + var skepStacks = new List(); + + foreach (var skepCode in existingValidSkeps) + { + var skepBlock = world.GetBlock(new AssetLocation(skepCode)); + if (skepBlock != null) + skepStacks.Add(new ItemStack(skepBlock)); + } + + if (skepStacks.Count > 0) + { + interactions.Add(new WorldInteraction + { + ActionLangCode = "blockhelp-groundstorage-add", + MouseButton = EnumMouseButton.Right, + HotKeyCode = "ctrl", + Itemstacks = [.. skepStacks] + }); + } + } + + if (Config.Instance.EnableSwarms) + { + interactions.Add(new WorldInteraction + { + ActionLangCode = be.SwarmsDisabled + ? "orekiwoofsbeehives:blockhelp-beehive-enable-swarm" + : "orekiwoofsbeehives:blockhelp-beehive-disable-swarm", + MouseButton = EnumMouseButton.Right, + Itemstacks = GetWrenchStacks(world) + }); + } + + interactions.AddRange(base.GetPlacedBlockInteractionHelp(world, selection, forPlayer)); + return [.. interactions]; + } + + var frameEmptyItem = new ItemStack(world.GetBlock(beehiveFrameEmptyAsset)); + var frameFilledItem = new ItemStack(world.GetBlock(beehiveFrameFilledAsset)); + var frameFilledFeedItem = new ItemStack(world.GetBlock(beehiveFrameFilledFeedAsset)); + + var amountOfFilled = be.Inventory.Count(x => IsFilledFrame(x.Itemstack)); + var amountOfEmpty = be.Inventory.Count(x => x?.Itemstack?.Block?.Code.Equals(beehiveFrameEmptyAsset) == true); + + var list = new List(); + + AssetLocation? code = be.Inventory[clickedSlot]?.Itemstack?.Block?.Code; + var looksAtEmptySlot = be.Inventory[clickedSlot].Empty; + if (looksAtEmptySlot) + list.Add(new WorldInteraction { ActionLangCode = "blockhelp-groundstorage-add", MouseButton = EnumMouseButton.Right, Itemstacks = [frameEmptyItem, frameFilledItem, frameFilledFeedItem] }); + else if (code == beehiveFrameEmptyAsset || IsFilledFrame(code)) + list.Add(new WorldInteraction { ActionLangCode = "blockhelp-toolrack-take", MouseButton = EnumMouseButton.Right }); + + if (amountOfFilled > 0 || amountOfEmpty > 0) + list.Add(new WorldInteraction { ActionLangCode = "blockhelp-groundstorage-removebulk", HotKeyCode = "ctrl", MouseButton = EnumMouseButton.Right }); + + list.Add(new WorldInteraction { ActionLangCode = "heldhelp-fill", MouseButton = EnumMouseButton.Right, HotKeyCodes = ["ctrl", "shift"] }); + list.AddRange(base.GetPlacedBlockInteractionHelp(world, selection, forPlayer)); + return [.. list]; + } + + public override void OnJsonTesselation(ref MeshData sourceMesh, ref int[] lightRgbsByCorner, BlockPos pos, Block[] chunkExtBlocks, int extIndex3d) + { + if (api?.World == null || closedMesh == null || openMesh == null) return; + if (api.World.BlockAccessor.GetBlockEntity(pos) is not BlockEntityReusableBeehive be) + return; + + sourceMesh = be.IsOpen ? openMesh.Clone() : closedMesh.Clone(); + + if (!be.IsOpen) + return; + + var rotateY = GetRotationY(); + + for (int i = 0; i < 8; i++) + { + if (be.Inventory[i].Empty) continue; + + Block? frameBlock = be.Inventory[i].Itemstack?.Block; + if (frameBlock == null) continue; + + ICoreClientAPI capi = (api as ICoreClientAPI)!; + capi.Tesselator.TesselateBlock(frameBlock, out MeshData frameMesh); + + float slotCenterX = (2f + i * 1.5f + 0.75f) / 16f; + + frameMesh.Rotate(new Vec3f(0.5f, 0.5f, 0.5f), 0, 0, GameMath.PIHALF); + frameMesh.Translate(slotCenterX - 1f + 0.03125f, 0.125f, 0); + + if (rotateY != 0f) + frameMesh.Rotate(new Vec3f(0.5f, 0.5f, 0.5f), 0, rotateY, 0); + + sourceMesh.AddMeshData(frameMesh); + } + } + + public override Cuboidf[] GetSelectionBoxes(IBlockAccessor blockAccessor, BlockPos pos) + { + var rotateY = GetRotationY(); + + if (blockAccessor.GetBlockEntity(pos) is BlockEntityReusableBeehive be && be.IsOpen) + { + var boxes = new Cuboidf[openSelectionBoxes.Length + slotBoxes.Length]; + + for (int i = 0; i < openSelectionBoxes.Length; i++) + boxes[i] = RotateBox(openSelectionBoxes[i], rotateY); + + for (int i = 0; i < slotBoxes.Length; i++) + boxes[openSelectionBoxes.Length + i] = RotateBox(slotBoxes[i], rotateY); + + return boxes; + } + + var closedBoxes = new Cuboidf[closedSelectionBoxes.Length]; + for (int i = 0; i < closedSelectionBoxes.Length; i++) + closedBoxes[i] = RotateBox(closedSelectionBoxes[i], rotateY); + return closedBoxes; + } + + private float GetRotationY() + { + var facing = Variant?["side"] ?? "north"; + return GetRotationY(facing); + } + + private BlockFacing? GetFrontFacing() + { + var facing = Variant?["side"] ?? "north"; + return facing switch + { + "north" => BlockFacing.SOUTH, + "east" => BlockFacing.WEST, + "south" => BlockFacing.NORTH, + "west" => BlockFacing.EAST, + _ => null, + }; + } + + private static float GetRotationY(string facing) + { + return facing switch + { + "north" => GameMath.PI, + "east" => GameMath.PIHALF * 3, + "south" => 0f, + "west" => GameMath.PIHALF, + _ => 0f + }; + } + + private static Cuboidf RotateBox(Cuboidf box, float rotateY) + { + if (rotateY == 0f) + return box.Clone(); + + float centerX = 0.5f; + float centerZ = 0.5f; + + float x1 = box.X1 - centerX; + float z1 = box.Z1 - centerZ; + float x2 = box.X2 - centerX; + float z2 = box.Z2 - centerZ; + + float cos = GameMath.Cos(rotateY); + float sin = GameMath.Sin(rotateY); + + float nx1 = x1 * cos - z1 * sin + centerX; + float nz1 = x1 * sin + z1 * cos + centerZ; + float nx2 = x2 * cos - z2 * sin + centerX; + float nz2 = x2 * sin + z2 * cos + centerZ; + + return new Cuboidf( + Math.Min(nx1, nx2), box.Y1, Math.Min(nz1, nz2), + Math.Max(nx1, nx2), box.Y2, Math.Max(nz1, nz2) + ); + } + + public override bool OnBlockInteractStart(IWorldAccessor world, IPlayer byPlayer, BlockSelection blockSel) + { + if (world.BlockAccessor.GetBlockEntity(blockSel.Position) is not BlockEntityReusableBeehive be) + return base.OnBlockInteractStart(world, byPlayer, blockSel); + + bool shift = byPlayer.Entity.Controls.ShiftKey; + bool ctrl = byPlayer.Entity.Controls.CtrlKey; + ItemSlot activeSlot = byPlayer.InventoryManager.ActiveHotbarSlot; + + int clickedSlot = GetClickedSlot(blockSel.HitPosition, be); + + if (clickedSlot == -1 && ctrl && !activeSlot.Empty && + activeSlot.Itemstack?.Block?.Code?.Path?.Contains("skep") == true && + activeSlot.Itemstack?.Block?.Code?.Path?.Contains("populated") == true && + be.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation) + { + be.BeePopulation += Config.Instance.InitialBeePopulation; + be.MarkDirty(true); + + activeSlot.TakeOut(1); + activeSlot.MarkDirty(); + + return true; + } + + if (clickedSlot == -1 && !ctrl && !shift && + Config.Instance.EnableSwarms && + activeSlot.Itemstack?.Item?.Code?.Path?.StartsWith("wrench") == true) + { + if (world.Side == EnumAppSide.Server) + { + be.SwarmsDisabled = !be.SwarmsDisabled; + be.MarkDirty(true); + } + return true; + } + + if (!be.IsOpen) + { + be.IsOpen = true; + be.MarkDirty(true); + world.BlockAccessor.MarkBlockDirty(blockSel.Position); + world.PlaySoundAt(new AssetLocation("game:sounds/block/door"), blockSel.Position.X, blockSel.Position.Y, blockSel.Position.Z, byPlayer); + return true; + } + + if (ctrl && shift) + return PutInAllSlots(world, byPlayer, be); + else if (ctrl) + return TakeOutAllSlots(world, byPlayer, be); + else if (shift && clickedSlot >= 0) + return PutInSlot(world, byPlayer, be, clickedSlot); + else + { + if (clickedSlot >= 0) + { + if (be.Inventory[clickedSlot].Empty && !activeSlot.Empty && activeSlot.Itemstack != null && IsFrame(activeSlot.Itemstack)) + return PutInSlot(world, byPlayer, be, clickedSlot); + else if (!be.Inventory[clickedSlot].Empty) + return TakeOutSlot(world, byPlayer, be, clickedSlot); + } + else + { + be.IsOpen = false; + be.MarkDirty(true); + world.BlockAccessor.MarkBlockDirty(blockSel.Position); + world.PlaySoundAt(new AssetLocation("game:sounds/block/door"), blockSel.Position.X, blockSel.Position.Y, blockSel.Position.Z, byPlayer); + return true; + } + } + + return base.OnBlockInteractStart(world, byPlayer, blockSel); + } + + private int GetClickedSlot(Vec3d hitPosition, BlockEntityReusableBeehive be) + { + if (!be.IsOpen) + return -1; + + Vec3d hitPos = hitPosition; + + var rotateY = GetRotationY(); + for (int i = 0; i < 8; i++) + { + if (RotateBox(slotBoxes[i], -rotateY).Contains(hitPos.X, hitPos.Y, hitPos.Z)) + return i; + } + return -1; + } + +#if NET10_0_OR_GREATER + public override bool DoPartialSelection(IWorldAccessor world, BlockPos pos) => true; +#else + public override bool DoParticalSelection(IWorldAccessor world, BlockPos pos) => true; +#endif + + public override Vec4f GetSelectionColor(ICoreClientAPI capi, BlockPos pos) + { + var baseColor = base.GetSelectionColor(capi, pos); + BlockSelection? blockSel = capi.World.Player.CurrentBlockSelection; + if (blockSel == null || !blockSel.Position.Equals(pos)) + return baseColor; + + if (capi.World.BlockAccessor.GetBlockEntity(pos) is not BlockEntityReusableBeehive be || !be.IsOpen) + return baseColor; + + var slotIndex = GetClickedSlot(blockSel.HitPosition, be); + + if (slotIndex is < 0 or >= 8) + return baseColor; + + if (be.Inventory[slotIndex].Empty || !IsFilledFrame(be.Inventory[slotIndex].Itemstack)) + return baseColor; + + if (IsFilledFeedFrame(be.Inventory[slotIndex].Itemstack)) + return new Vec4f(0.8f, 0.2f, 0f, baseColor.A); + + return new Vec4f(0.6f, 0.3f, 0f, baseColor.A); + } + + private static bool IsFrame(ItemStack? stack) + { + return stack?.Block?.Code?.Path?.Contains("beehiveframe") == true; + } + + private static ItemStack[] GetWrenchStacks(IWorldAccessor world) + { + var wrenchWildcard = new AssetLocation("game:wrench-*"); + return world.Items + .Where(item => item?.Code != null && WildcardUtil.Match(wrenchWildcard, item.Code)) + .Select(item => new ItemStack(item)) + .ToArray(); + } + + private static bool IsFilledFrame(ItemStack? stack) + { + return stack?.Block?.Code?.Path is "beehiveframe-filled" or "beehiveframe-filled-feed"; + } + + private static bool IsFilledFeedFrame(ItemStack? stack) + { + return stack?.Block?.Code?.Path == "beehiveframe-filled-feed"; + } + + private static bool IsFilledFrame(AssetLocation? code) + { + return code?.Path is "beehiveframe-filled" or "beehiveframe-filled-feed"; + } + + private static bool PutInSlot(IWorldAccessor world, IPlayer byPlayer, BlockEntityReusableBeehive be, int slot) + { + ItemSlot activeSlot = byPlayer.InventoryManager.ActiveHotbarSlot; + if (activeSlot.Empty || !IsFrame(activeSlot.Itemstack)) + return false; + if (!be.Inventory[slot].Empty) + return false; + + var moved = activeSlot.TryPutInto(world, be.Inventory[slot], 1); + if (moved <= 0) + return false; + + be.MarkDirty(true); + world.PlaySoundAt(new AssetLocation("game:sounds/player/build"), be.Pos.X, be.Pos.Y, be.Pos.Z, byPlayer); + return true; + } + + private static bool TakeOutSlot(IWorldAccessor world, IPlayer byPlayer, BlockEntityReusableBeehive be, int slot) + { + if (be.Inventory[slot].Empty) + return false; + + ItemSlot activeSlot = byPlayer.InventoryManager.ActiveHotbarSlot; + var moved = be.Inventory[slot].TryPutInto(world, activeSlot, 1); + if (moved <= 0) + return false; + + be.MarkDirty(true); + world.PlaySoundAt(new AssetLocation("game:sounds/player/build"), be.Pos.X, be.Pos.Y, be.Pos.Z, byPlayer); + return true; + } + + private static bool PutInAllSlots(IWorldAccessor world, IPlayer byPlayer, BlockEntityReusableBeehive be) + { + var anyMoved = false; + + for (var i = 0; i < 8; i++) + { + if (!be.Inventory[i].Empty) + continue; + + var inventorySlots = byPlayer.InventoryManager.OpenedInventories.SelectMany(inv => inv); + var a = inventorySlots?.Where(x => x != null); + var b = a?.Where(x => !x.Empty); + var c = b?.Where(x => IsFrame(x.Itemstack)); + var d = c?.Where(x => x.Itemstack?.Block?.Code?.Path?.Contains("empty") == true); + var emptyFrameSlot = d?.FirstOrDefault(); + + if (emptyFrameSlot != null) + { + emptyFrameSlot.TryPutInto(world, be.Inventory[i], 1); + anyMoved = true; + } + } + + if (anyMoved) + { + be.MarkDirty(true); + world.PlaySoundAt(new AssetLocation("game:sounds/player/build"), be.Pos.X, be.Pos.Y, be.Pos.Z, byPlayer); + } + return anyMoved; + } + + private static bool TakeOutAllSlots(IWorldAccessor world, IPlayer byPlayer, BlockEntityReusableBeehive be) + { + var hasFilledFrames = false; + var hasFeedFrames = false; + for (var i = 0; i < 8; i++) + { + if (be.Inventory[i].Empty) + continue; + + var itemStack = be.Inventory[i].Itemstack; + if (itemStack == null) + continue; + + if (itemStack.Block?.Code?.Path == "beehiveframe-filled") + { + hasFilledFrames = true; + break; + } + + if (itemStack.Block?.Code?.Path == "beehiveframe-filled-feed") + hasFeedFrames = true; + } + + var anyMoved = false; + for (var i = 0; i < 8; i++) + { + if (be.Inventory[i].Empty) + continue; + + var isFilled = be.Inventory[i].Itemstack?.Block?.Code?.Path == "beehiveframe-filled"; + var isFeed = IsFilledFeedFrame(be.Inventory[i].Itemstack); + if (hasFilledFrames && !isFilled) + continue; + + if (!hasFilledFrames && hasFeedFrames && !isFeed) + continue; + + if (!byPlayer.InventoryManager.TryGiveItemstack(be.Inventory[i].Itemstack)) + world.SpawnItemEntity(be.Inventory[i].Itemstack, be.Pos.ToVec3d().Add(0.5, 0.5, 0.5)); + + be.Inventory[i].Itemstack = null; + anyMoved = true; + } + + if (anyMoved) + { + be.MarkDirty(true); + world.PlaySoundAt(new AssetLocation("game:sounds/player/build"), be.Pos.X, be.Pos.Y, be.Pos.Z, byPlayer); + } + return anyMoved; + } +} diff --git a/OrekiWoofsBeehives/BoostCalculationType.cs b/OrekiWoofsBeehives/BoostCalculationType.cs new file mode 100644 index 0000000..3231ecb --- /dev/null +++ b/OrekiWoofsBeehives/BoostCalculationType.cs @@ -0,0 +1,8 @@ +namespace OrekiWoofsBeehives; + +public enum BoostCalculationType +{ + Full, + LinearPopulation, + NormalizedLogk10Population, +} diff --git a/OrekiWoofsBeehives/ChatCommands.cs b/OrekiWoofsBeehives/ChatCommands.cs new file mode 100644 index 0000000..6b8448c --- /dev/null +++ b/OrekiWoofsBeehives/ChatCommands.cs @@ -0,0 +1,147 @@ +using OrekiWoofsBees.Common.Configs; +using OrekiWoofsBees.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Client; +using Vintagestory.API.Server; +using Vintagestory.API.Common; + +namespace OrekiWoofsBeehives; + +public partial class OrekiWoofsBeehivesModSystem +{ + private const string lang_domain = "orekiwoofsbeehives"; + public bool DebugUnloadEnabled { get; private set; } + + private const string meta_config_filename = "OrekiWoofsBeehivesMeta.json"; + + public void SetupClientCommands(ICoreClientAPI api) + { + var rootCommand = api.ChatCommands.Create("beehives"); + ConfigCommands.Register(rootCommand, api.ChatCommands, lang_domain, serverSide: false, () => Config.Instance, SaveClientConfig); + + rootCommand + .BeginSubCommand("tester") + .HandleWith(HandleTesterCommand) + .EndSubCommand() + + .BeginSubCommand("version") + .HandleWith(HandleVersionCommand) + .EndSubCommand(); + } + + public void SetupServerCommands(ICoreServerAPI api) + { + var rootCommand = api.ChatCommands.Create("beehives"); + ConfigCommands.Register(rootCommand, api.ChatCommands, lang_domain, serverSide: true, () => Config.Instance, SaveServerConfig); + + rootCommand + .BeginSubCommand("PlantReg") + .BeginSubCommand("BlocksPerTick") + .WithDescription(Lang.Get($"{lang_domain}:plantreg-blockpertick-desc")) + .WithArgs(api.ChatCommands.Parsers.OptionalIntRange("value", 0, 1000)) + .HandleWith(HandlePlantRegBlockPerTickCommand) + .EndSubCommand() + .EndSubCommand() + + .BeginSubCommand("setPopulation") + .WithDescription(Lang.Get($"{lang_domain}:setpopulation-desc")) + .WithArgs(api.ChatCommands.Parsers.IntRange("value", 0, Config.Instance.MaxBeePopulation)) + .HandleWith(HandleSetPopulationCommand) + .EndSubCommand() + + .BeginSubCommand("debugUnload") + .WithDescription(Lang.Get($"{lang_domain}:debugunload-desc")) + .WithArgs(api.ChatCommands.Parsers.Bool("value")) + .HandleWith(HandleDebugUnloadCommand) + .EndSubCommand(); + } + + public void BroadcastUnloadDebug(string message) + { + if (!DebugUnloadEnabled || serverApi == null) + return; + + var fullMessage = $"[Beehives CatchUp] {message}"; + serverApi.BroadcastMessageToAllGroups(fullMessage, EnumChatType.Notification); + } + + private void SaveClientConfig(bool _) + { + var temp = api?.LoadModConfig(config_filename) ?? new Config(); + temp.InformationVerbosity = Config.Instance.InformationVerbosity; + temp.DisableServerRecommended = Config.Instance.DisableServerRecommended; + temp.BeehiveAmbientVolume = Config.Instance.BeehiveAmbientVolume; + api?.StoreModConfig(temp, config_filename); + } + + private void SaveServerConfig(bool _) + { + api?.StoreModConfig(Config.Instance, config_filename); + serverApi?.Network.GetChannel(CONFIG_CHANNEL_NAME).BroadcastPacket(Config.Instance); + } + + private TextCommandResult HandlePlantRegBlockPerTickCommand(TextCommandCallingArgs args) + { + var registry = api?.ModLoader.GetModSystem(); + if (registry == null) + return TextCommandResult.Error(Lang.Get($"{lang_domain}:plantreg-unavailable")); + + if (args.Parsers[0].IsMissing) + return TextCommandResult.Success($"PlantReg BlocksPerTick={registry.BlocksPerTick}"); + + if (args.Parsers[0].GetValue() is not int value) + return TextCommandResult.Error(Lang.Get($"{lang_domain}:plantreg-blockpertick-parse-error")); + + registry.BlocksPerTick = value; + return TextCommandResult.Success($"PlantReg BlocksPerTick={registry.BlocksPerTick}"); + } + + private TextCommandResult HandleSetPopulationCommand(TextCommandCallingArgs args) + { + if (args.Caller.Player is not IServerPlayer serverPlayer) + return TextCommandResult.Error("This command can only be used by a player."); + + if (args.Parsers[0].GetValue() is not int value) + return TextCommandResult.Error("Couldn't parse population value."); + + var blockSel = serverPlayer.CurrentBlockSelection; + if (blockSel == null) + return TextCommandResult.Error("Look at a beehive block first."); + + var beehive = api?.World.BlockAccessor.GetBlockEntity(blockSel.Position) as BlockEntities.BlockEntityReusableBeehive; + if (beehive == null) + return TextCommandResult.Error("The targeted block is not a reusable beehive."); + + beehive.BeePopulation = value; + beehive.MarkDirty(true); + + return TextCommandResult.Success($"Bee population set to {value}."); + } + + private TextCommandResult HandleDebugUnloadCommand(TextCommandCallingArgs args) + { + if (args.Parsers[0].GetValue() is not bool value) + return TextCommandResult.Error("Couldn't parse. Use true or false."); + + DebugUnloadEnabled = value; + return TextCommandResult.Success($"debugUnload={DebugUnloadEnabled}"); + } + + private TextCommandResult HandleTesterCommand(TextCommandCallingArgs args) + { + metaConfig.IsTester = !metaConfig.IsTester; + api?.StoreModConfig(metaConfig, meta_config_filename); + return TextCommandResult.Success(metaConfig.IsTester + ? "Dev version warning disabled." + : "Dev version warning enabled."); + } + + private TextCommandResult HandleVersionCommand(TextCommandCallingArgs args) + { + var result = $"{Mod.Info.ModID}@{Mod.Info.Version}"; + var roamingBees = api?.ModLoader.GetMod("roamingbees"); + if (roamingBees != null) + result += $"\n{roamingBees.Info.ModID}@{roamingBees.Info.Version}"; + return TextCommandResult.Success(result); + } +} diff --git a/OrekiWoofsBeehives/Config.cs b/OrekiWoofsBeehives/Config.cs new file mode 100644 index 0000000..cdce90b --- /dev/null +++ b/OrekiWoofsBeehives/Config.cs @@ -0,0 +1,234 @@ +using OrekiWoofsBees.Common.Configs; +using ProtoBuf; + +namespace OrekiWoofsBeehives; + +[ProtoContract(SkipConstructor = true)] +public class Config +{ + public static Config Instance { get; internal set; } = new(); + + [ProtoMember(1)] + [ConfigCommand(serverSide: true, Min = 5, Max = 70)] + public int BeehiveRadius { get; set; } = 35; + + [ProtoMember(41)] + [ConfigCommand(serverSide: true)] + public bool YieldBoost { get; set; } = true; + + [ProtoMember(42)] + [ConfigCommand(serverSide: true)] + public bool SpeedBoost { get; set; } = false; + + [ProtoMember(2)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float GrowthSpeedBonus { get; set; } = 0.2f; + + [ProtoMember(37)] + [ConfigCommand(serverSide: true, Min = 0.1, Max = 3)] + public float YieldMultiplier { get; set; } = 1.2f; + + [ProtoMember(31)] + [ConfigCommand(serverSide: true, Min = 5, Max = 50)] + public int PlantsFullBoostDistanceToHive { get; set; } = 20; + + [ProtoMember(32)] + [ConfigCommand(serverSide: true, Min = 100, Max = 5000)] + public int MinBeesForPlantBoost { get; set; } = 1000; + + [ProtoMember(46)] + [ConfigCommand(serverSide: true, AllowedValues = new[] { "Full", "LinearPopulation", "NormalizedLogk10Population" })] + public string BoostCalculation { get; set; } = "NormalizedLogk10Population"; + + [ProtoMember(47)] + [ConfigCommand(serverSide: true, Min = 0, Max = 100)] + public float PopulationPercentForMaxBoost { get; set; } = 100f; + + [ProtoMember(48)] + [ConfigCommand(serverSide: true, AllowedValues = new[] { "Additive", "Max" })] + public string MultipleBeehivesBoostCombination { get; set; } = "Max"; + + // bee population + [ProtoMember(3)] + [ConfigCommand(serverSide: true, Min = 1000, Max = 200000)] + public int MaxBeePopulation { get; set; } = 50000; + + [ProtoMember(4)] + [ConfigCommand(serverSide: true, Min = 100, Max = 50000)] + public int InitialBeePopulation { get; set; } = 5000; + + [ProtoMember(49)] + [ConfigCommand(serverSide: true, Min = 0, Max = 200000)] + public int BeehiveConsideredEmptyBelowPopulation { get; set; } = 500; + + [ProtoMember(50)] + [ConfigCommand(serverSide: true, Min = 40, Max = 100)] + public int PopulationPercentRequirementForSwarm { get; set; } = 80; + + [ProtoMember(51)] + [ConfigCommand(serverSide: true, Min = 1, Max = 70)] + public int SwarmPopulationPercentage { get; set; } = 40; + + [ProtoMember(54)] + [ConfigCommand(serverSide: true, Min = 0, Max = 70)] + public int SwarmPopulationPercentageWhenNoBeehivesAvailable { get; set; } = 5; + + [ProtoMember(52)] + [ConfigCommand(serverSide: true, Min = 1, Max = 30)] + public int SwarmCooldownDays { get; set; } = 8; + + [ProtoMember(53)] + [ConfigCommand(serverSide: true, Min = 0, Max = 100)] + public int SwarmReturnToOriginOnFailedMigrationPercent { get; set; } = 20; + + [ProtoMember(56)] + [ConfigCommand(serverSide: true, Min = 1, Max = 1000)] + public int PreSwarmDurationHours { get; set; } = 40; + + [ProtoMember(55)] + [ConfigCommand(serverSide: true)] + public bool EnableSwarms { get; set; } = true; + + [ProtoMember(57)] + [ConfigCommand(serverSide: true)] + public bool SwarmSettingAfterPlacing { get; set; } = false; + + [ProtoMember(5)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1000)] + public int BaseDeathsPerDay { get; set; } = 100; + + [ProtoMember(6)] + [ConfigCommand(serverSide: true, Min = 0, Max = 50)] + public int FlowerThreshold { get; set; } = 10; + + [ProtoMember(7)] + [ConfigCommand(serverSide: true, Min = 0, Max = 500)] + public int DeathPerMissingFlower { get; set; } = 100; + + [ProtoMember(8)] + [ConfigCommand(serverSide: true, Min = 1, Max = 30)] + public float DoublingTimeDays { get; set; } = 9.0f; + + [ProtoMember(9)] + [ConfigCommand(serverSide: true, Min = 0, Max = 2000)] + public int BonusGrowthPerFilledFrame { get; set; } = 100; + + // honey production + [ProtoMember(10)] + [ConfigCommand(serverSide: true, Min = 1, Max = 50)] + public float ReferenceFlowers { get; set; } = 10.0f; + + [ProtoMember(11)] + [ConfigCommand(serverSide: true, Min = 1000, Max = 50000)] + public float ReferenceBees { get; set; } = 25000.0f; + + [ProtoMember(12)] + [ConfigCommand(serverSide: true, Min = 1, Max = 100)] + public int MaxFlowersForHoneyProduction { get; set; } = 25; + + // winter + [ProtoMember(38)] + [ConfigCommand(serverSide: true)] + public bool WinterHardMode { get; set; } = false; + + [ProtoMember(39)] + [ConfigCommand(serverSide: true, Min = 0, Max = 50000)] + public int WinterDailyBeeDeathsWithoutFood { get; set; } = 500; + + [ProtoMember(40)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float WinterFoodConsumptionMultiplier { get; set; } = 0.1f; + + // weather effects + [ProtoMember(15)] + [ConfigCommand(serverSide: true, Min = -20, Max = 20)] + public float MinTemperatureGrowth { get; set; } = 0f; + + [ProtoMember(16)] + [ConfigCommand(serverSide: true, Min = 0, Max = 40)] + public float MaxTemperatureGrowth { get; set; } = 10f; + + [ProtoMember(35)] + [ConfigCommand(serverSide: true)] + public bool GreenhouseAffectsBeehive { get; set; } = true; + + // particles + [ProtoMember(28)] + [ConfigCommand(serverSide: true, Min = 100, Max = 5000)] + public int BeesPerParticle { get; set; } = 1000; + + [ProtoMember(34)] + [ConfigCommand(serverSide: true, Min = 0, Max = 50)] + public int BeehiveAlwaysSpawnNumberOfBees { get; set; } = 0; + + // client-side + [ProtoMember(29)] + [ConfigCommand(serverSide: false, Min = 0, Max = 4)] + public int InformationVerbosity { get; set; } = 1; + + [ProtoMember(44)] + [ConfigCommand(serverSide: false)] + public bool DisableServerRecommended { get; set; } = false; + + [ProtoMember(33)] + [ConfigCommand(serverSide: false, Min = 0, Max = 1)] + public float BeehiveAmbientVolume { get; set; } = 0.5f; + + // server-side + [ProtoMember(43)] + [ConfigCommand(serverSide: true, AllowedValues = new[] { "ClientSide", "ServerRecommended", "ServerForced" })] + public string InformationVerbosityServerSideType { get; set; } = "ClientSide"; + + [ProtoMember(45)] + [ConfigCommand(serverSide: true, Min = 0, Max = 4)] + public int InformationVerbosityServer { get; set; } = 1; + + public InformationVerbosityServerSideType GetInformationVerbosityServerSideType() + { + return InformationVerbosityServerSideType switch + { + "ClientSide" => OrekiWoofsBeehives.InformationVerbosityServerSideType.ClientSide, + "ServerRecommended" => OrekiWoofsBeehives.InformationVerbosityServerSideType.ServerRecommended, + "ServerForced" => OrekiWoofsBeehives.InformationVerbosityServerSideType.ServerForced, + _ => OrekiWoofsBeehives.InformationVerbosityServerSideType.ClientSide, + }; + } + + public int GetEffectiveInformationVerbosity() + { + switch (GetInformationVerbosityServerSideType()) + { + case OrekiWoofsBeehives.InformationVerbosityServerSideType.ClientSide: + return InformationVerbosity; + case OrekiWoofsBeehives.InformationVerbosityServerSideType.ServerRecommended: + if (DisableServerRecommended) + return InformationVerbosity; + return InformationVerbosityServer; + case OrekiWoofsBeehives.InformationVerbosityServerSideType.ServerForced: + return InformationVerbosityServer; + default: + return InformationVerbosity; + } + } + + public BoostCalculationType GetBoostCalculationType() + { + return BoostCalculation switch + { + "Full" => BoostCalculationType.Full, + "LinearPopulation" => BoostCalculationType.LinearPopulation, + "NormalizedLogk10Population" => BoostCalculationType.NormalizedLogk10Population, + _ => BoostCalculationType.Full, + }; + } + + public MultipleBeehivesBoostCombinationType GetMultipleBeehivesBoostCombinationType() + { + return MultipleBeehivesBoostCombination switch + { + "Additive" => MultipleBeehivesBoostCombinationType.Additive, + "Max" => MultipleBeehivesBoostCombinationType.Max, + _ => MultipleBeehivesBoostCombinationType.Max, + }; + } +} diff --git a/OrekiWoofsBeehives/CropBonusType.cs b/OrekiWoofsBeehives/CropBonusType.cs new file mode 100644 index 0000000..6f785ab --- /dev/null +++ b/OrekiWoofsBeehives/CropBonusType.cs @@ -0,0 +1,12 @@ +using System; + +namespace OrekiWoofsBeehives; + +[Flags] +public enum CropBonusType +{ + None = 0, + Yield = 1, + Speed = 2, + YieldAndSpeed = Yield | Speed +} diff --git a/OrekiWoofsBeehives/Helpers/BeehiveInfoStringBuilder.cs b/OrekiWoofsBeehives/Helpers/BeehiveInfoStringBuilder.cs new file mode 100644 index 0000000..feb7942 --- /dev/null +++ b/OrekiWoofsBeehives/Helpers/BeehiveInfoStringBuilder.cs @@ -0,0 +1,504 @@ +using OrekiWoofsBeehives.BlockEntities; +using OrekiWoofsBeehives.Behaviors; +using System; +using System.Collections.Generic; +using System.Text; +using Vintagestory.API.Config; + +namespace OrekiWoofsBeehives.Helpers; + +public static class BeehiveInfoStringBuilder +{ + public static void BuildBeehiveInfo( + StringBuilder builder, + BeehiveStats stats, + BlockEntityReusableBeehive beehive) + { + var verbosity = Config.Instance.GetEffectiveInformationVerbosity(); + + if (verbosity == 0) + return; + + if (beehive.Api?.World != null && beehive.Api.World.Calendar.TotalDays < beehive.NextSwarmAllowedTotalDays) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-recently-swarmed", Config.Instance.SwarmCooldownDays)); + + if (beehive.IsSwarmBuildingNearby()) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-source-swarm-forming")}"); + else if (!beehive.IsReadyToStartSwarm() && beehive.IsReadyToStartSwarm(ignoreDayTime: true)) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-ready-waiting-morning")}"); + + if (!beehive.SwarmsDisabled && !beehive.IsSwarmBuildingNearby() + && beehive.PreSwarmProgress > 0 && !beehive.IsReadyToStartSwarm(ignoreDayTime: true)) + AppendPreSwarmProgress(builder, beehive, stats, verbosity); + + if (verbosity >= 4) + AppendScoutingProgress(builder, beehive); + + if (beehive.IsReceivingIncomingSwarm) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-incoming-swarm")}"); + + if (!beehive.IsReceivingIncomingSwarm && beehive.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation) + { + builder.AppendLine($"{Lang.GetWithFallback("orekiwoofsbeehives:beehive-info-no-bees-use-skep-or-wait-for-swarm", "orekiwoofsbeehives:beehive-info-no-bees")}"); + return; + } + + if (beehive.IsReadyToStartSwarm()) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-ready-to-swarm")}"); + + AppendPopulationInfo(builder, stats, beehive, verbosity); + + if (verbosity >= 1) + AppendFlowerInfo(builder, stats, beehive, verbosity); + + if (verbosity >= 3) + AppendFrameInfo(builder, stats); + + AppendHoneyProgress(builder, stats, beehive, verbosity); + + if (verbosity >= 3) + AppendFeedInfo(builder, beehive, stats); + + AppendFrameStatusMessages(builder, stats, beehive, verbosity); + + AppendPopulationChange(builder, stats, verbosity); + + AppendCropBoostEffectiveness(builder, beehive, verbosity); + + AppendTemperatureInfo(builder, stats, verbosity); + + if (verbosity >= 3) + AppendHoneyProduction(builder, stats); + + if (Config.Instance.EnableSwarms && beehive.SwarmsDisabled) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-swarms-disabled")); + } + + private static void AppendScoutingProgress(StringBuilder builder, BlockEntityReusableBeehive beehive) + { + var scoutingProgressString = Lang.Get("orekiwoofsbeehives:beehiveScoutingProgress"); + var scanningProgress = beehive.GetScanningProgress(); + var rescanningProgress = beehive.GetRescanningProgress(); + + builder.AppendLine($"{scoutingProgressString}: {scanningProgress * 100:F1}% (+ {rescanningProgress * 100:F1}%)"); + } + + private static void AppendPreSwarmProgress( + StringBuilder builder, + BlockEntityReusableBeehive beehive, + BeehiveStats stats, + int verbosity) + { + if (verbosity == 1) + { + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-building")}"); + return; + } + + var cfg = Config.Instance; + var isIncreasing = stats.Components.Temperature >= cfg.MaxTemperatureGrowth; + + string detail; + if (isIncreasing) + { + var hoursLeft = (1.0 - beehive.PreSwarmProgress) * cfg.PreSwarmDurationHours; + var hoursPerDay = beehive.Api?.World?.Calendar?.HoursPerDay ?? 24.0; + + if (hoursLeft < 1) + { + detail = Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-time-lessthanhourleft"); + } + else if (hoursLeft < hoursPerDay) + { + detail = Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-time-hours", $"{hoursLeft:F0}"); + } + else + { + var daysLeft = (int)Math.Round(hoursLeft / hoursPerDay); + detail = daysLeft == 1 + ? Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-time-1day") + : Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-time-days", daysLeft); + } + } + else + { + detail = Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-paused"); + } + + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-progress", detail)}"); + } + + private static void AppendPopulationInfo( + StringBuilder builder, + BeehiveStats stats, + BlockEntityReusableBeehive beehive, + int verbosity) + { + var cfg = Config.Instance; + + if (verbosity == 1) + { + var percentage = beehive.BeePopulation / cfg.MaxBeePopulation; + + string levelKey = percentage switch + { + (<= 0) => "orekiwoofsbeehives:beehive-info-population-none", + (<= 0.25) => "orekiwoofsbeehives:beehive-info-population-low", + (<= 0.60) => "orekiwoofsbeehives:beehive-info-population-medium", + (<= 0.80) => "orekiwoofsbeehives:beehive-info-population-high", + _ => "orekiwoofsbeehives:beehive-info-population-veryhigh", + }; + + builder.AppendLine(Lang.Get(levelKey)); + } + else if (verbosity == 2) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-population-v2", new Dictionary + { + ["beePopulation"] = $"{beehive.BeePopulation:N0}", + ["maxBeePopulation"] = $"{cfg.MaxBeePopulation:N0}", + ["beeProductionMultiplier"] = $"{stats.BeeProductionMultiplier:F1}" + })); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-population", new Dictionary + { + ["beePopulation"] = $"{beehive.BeePopulation:N0}", + ["maxBeePopulation"] = $"{cfg.MaxBeePopulation:N0}", + ["beeProductionMultiplier"] = $"{stats.BeeProductionMultiplier:F1}" + })); + } + } + + private static void AppendFlowerInfo( + StringBuilder builder, + BeehiveStats stats, + BlockEntityReusableBeehive beehive, + int verbosity) + { + var effectiveFlowers = stats.Components.EffectiveFlowers; + + if (verbosity < 3) + { + var scanningProgress = beehive.GetScanningProgress(); + var scoutingSuffix = (!beehive.WasFullyScanned && scanningProgress < 1f) + ? " (" + Lang.Get("orekiwoofsbeehives:beehive-info-flowers-scanning-suffix") + $" - {scanningProgress * 100:F0}%)" + : string.Empty; + + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-flowers-simple", new Dictionary + { + ["flowersAround"] = $"{beehive.FlowersAround}", + ["cropsAround"] = $"{beehive.CropsAround}" + }) + scoutingSuffix); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-flowers", new Dictionary + { + ["flowersAround"] = $"{beehive.FlowersAround}", + ["cropsAround"] = $"{beehive.CropsAround}", + ["effectiveFlowers"] = $"{effectiveFlowers:F1}", + ["flowerProductionMultiplier"] = $"{stats.FlowerProductionMultiplier:F1}" + })); + } + + if (beehive.WasFullyScanned && beehive.FlowersAround.HasValue) + { + var cfg = Config.Instance; + int flowersAround = beehive.FlowersAround.Value; + + if (flowersAround < cfg.FlowerThreshold) + { + int flowersNeeded = cfg.FlowerThreshold - flowersAround; + builder.AppendLine($"{FormatLang("orekiwoofsbeehives:beehive-info-flowers-warning-causing-deaths", new Dictionary + { + ["flowersNeeded"] = $"{flowersNeeded}" + })}"); + } + else if (effectiveFlowers < cfg.MaxFlowersForHoneyProduction) + { + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-flowers-warning-suboptimal")); + } + } + } + + private static void AppendFrameInfo( + StringBuilder builder, + BeehiveStats stats) + { + var cfg = Config.Instance; + var filledFrames = stats.Components.FilledFramesCount; + var emptyFrames = stats.Components.EmptyFrames; + var totalFrames = stats.Components.TotalFrames; + + if (filledFrames > 0 && emptyFrames > 0) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-mixed", new Dictionary + { + ["filledFrames"] = $"{filledFrames}", + ["filledFrameBonus"] = $"{filledFrames * cfg.BonusGrowthPerFilledFrame:N0}", + ["emptyFrames"] = $"{emptyFrames}" + })); + } + else if (filledFrames > 0) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-filled", new Dictionary + { + ["filledFrames"] = $"{filledFrames}", + ["filledFrameBonus"] = $"{filledFrames * cfg.BonusGrowthPerFilledFrame:N0}" + })); + } + else if (emptyFrames > 0) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-empty", new Dictionary + { + ["emptyFrames"] = $"{emptyFrames}" + })); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-total", new Dictionary + { + ["totalFrames"] = $"{totalFrames}" + })); + } + } + + private static void AppendHoneyProgress( + StringBuilder builder, + BeehiveStats stats, + BlockEntityReusableBeehive beehive, + int verbosity) + { + var emptyFrameSlot = beehive.GetFirstEmptyFrameSlot(); + var totalFrames = stats.Components.TotalFrames; + + if (verbosity == 1) + { + if (emptyFrameSlot >= 0) + { + if (stats.FramesPerDay <= 0) + { + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-progress-v1-noproduction")); + } + else + { + var daysToFill = (1.0 - beehive.HoneyProgress) / stats.FramesPerDay; + if (daysToFill < 1.0) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-progress-v1-lessthanday")); + else + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-progress-v1-days", new Dictionary + { + ["days"] = $"{Math.Ceiling(daysToFill):F0}" + })); + } + } + else if (totalFrames == 0) + { + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-no-frames")); + } + } + else + { + if (emptyFrameSlot >= 0) + { + if (stats.FramesPerDay > 0) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-progress-filling", new Dictionary + { + ["honeyProgress"] = $"{beehive.HoneyProgress * 100:F1}", + ["daysToFill"] = $"{(1.0 - beehive.HoneyProgress) / stats.FramesPerDay:F1}" + })); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-progress-noproduction", new Dictionary + { + ["honeyProgress"] = $"{beehive.HoneyProgress * 100:F1}" + })); + } + } + else if (totalFrames > 0 && verbosity < 3) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-all-filled")}"); + else if (totalFrames == 0 && verbosity < 3) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-no-frames")); + } + } + + private static void AppendTemperatureInfo( + StringBuilder builder, + BeehiveStats stats, + int verbosity) + { + var temperature = stats.Components.Temperature; + var temperatureMultiplier = stats.Components.TemperatureMultiplier; + var cfg = Config.Instance; + + if (verbosity is >= 1 and < 3) + { + if (temperature <= cfg.MinTemperatureGrowth) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-temperature-overwintering")); + else if (temperature < cfg.MaxTemperatureGrowth) + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-temperature-cold")); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-temperature", new Dictionary + { + ["temperature"] = $"{temperature:F1}", + ["multiplier"] = $"{temperatureMultiplier:F2}" + })); + } + + if (stats.Components.IsGreenhouse && Config.Instance.GreenhouseAffectsBeehive) + builder.AppendLine(Lang.Get("game:greenhousetempbonus")); + + if (cfg.WinterHardMode + && stats.Components.FilledFramesCount <= 0 + && temperature <= cfg.MinTemperatureGrowth) + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:beehive-info-winter-starving")}"); + } + + private static void AppendFrameStatusMessages( + StringBuilder builder, + BeehiveStats stats, + BlockEntityReusableBeehive beehive, + int verbosity) + { + if (verbosity >= 3) + return; + + var cfg = Config.Instance; + var percentage = beehive.BeePopulation / cfg.MaxBeePopulation; + var filledFrames = stats.Components.FilledFramesCount; + var emptyFrames = stats.Components.EmptyFrames; + + if (filledFrames > 0 && percentage > 0 && percentage <= 0.80) + { + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-frames-boosting")); + } + + if (emptyFrames > 0 && percentage > 0 && percentage <= 0.60) + { + builder.AppendLine(Lang.Get("orekiwoofsbeehives:beehive-info-frames-sacrificing")); + } + } + + private static void AppendPopulationChange(StringBuilder builder, BeehiveStats stats, int verbosity) + { + if (verbosity == 1) + { + var netChange = stats.DailyNetPopulationChange; + var statusKey = + netChange < 0 ? "orekiwoofsbeehives:beehive-info-population-status-decreasing" + : netChange <= 100 ? "orekiwoofsbeehives:beehive-info-population-status-stagnant" + : netChange <= 300 ? "orekiwoofsbeehives:beehive-info-population-status-slowgrowth" + : "orekiwoofsbeehives:beehive-info-population-status-growing"; + builder.AppendLine(Lang.Get(statusKey)); + } + else + { + var changeSign = stats.DailyNetPopulationChange >= 0 ? "+" : ""; + + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-population-change", new Dictionary + { + ["changeSign"] = changeSign, + ["netChange"] = $"{stats.DailyNetPopulationChange:N0}", + ["dailyGrowth"] = $"{stats.Components.DailyGrowth:N0}", + ["dailyDeaths"] = $"{stats.Components.DailyDeaths:N0}" + })); + } + } + + private static void AppendCropBoostEffectiveness(StringBuilder builder, BlockEntityReusableBeehive beehive, int verbosity) + { + var cfg = Config.Instance; + if (!cfg.YieldBoost && !cfg.SpeedBoost) + return; + + float populationScale = BlockBehaviorBeehiveAffected.GetPopulationBoostScale(beehive.BeePopulation, cfg); + if (populationScale <= 0f) + { + builder.AppendLine($"{Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-population-too-low")}"); + return; + } + + if (verbosity == 1) + { + var levelText = populationScale < 0.34f + ? Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-level-low") + : populationScale < 0.67f + ? Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-level-medium") + : Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-level-high"); + builder.AppendLine(Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-effectiveness", levelText)); + return; + } + + var resultingBoostParts = new List(); + + if (cfg.YieldBoost) + { + var yieldBonusFactor = Math.Max(0f, cfg.YieldMultiplier - 1f); + var yieldPercent = (yieldBonusFactor * populationScale * 100f).ToString("N0"); + resultingBoostParts.Add(Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-result-yield", yieldPercent)); + } + + if (cfg.SpeedBoost) + { + var speedPercent = (cfg.GrowthSpeedBonus * populationScale * 100f).ToString("N0"); + resultingBoostParts.Add(Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-result-speed", speedPercent)); + } + + var effectivenessPercent = (populationScale * 100f).ToString("N0"); + var cropBoostLine = Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-effectiveness", effectivenessPercent + "%"); + + if (resultingBoostParts.Count > 0) + cropBoostLine += " (" + string.Join(", ", resultingBoostParts) + ")"; + + builder.AppendLine(cropBoostLine); + } + + private static void AppendHoneyProduction(StringBuilder builder, BeehiveStats stats) + { + if (stats.FramesPerDay > 0) + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-honey-production", new Dictionary + { + ["framesPerDay"] = $"{stats.FramesPerDay:F2}", + ["daysPerFrame"] = $"{1.0 / stats.FramesPerDay:F1}" + })); + } + else + { + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-honey-production-simple", new Dictionary + { + ["framesPerDay"] = $"{stats.FramesPerDay:F2}" + })); + } + } + + private static void AppendFeedInfo(StringBuilder builder, BlockEntityReusableBeehive beehive, BeehiveStats stats) + { + if (!beehive.TryGetCurrentFeedStatus(out double remaining)) + return; + + var daysPerFrame = stats.FeedConsumedPerDay > 0 + ? 1.0 / stats.FeedConsumedPerDay + : 0.0; + + builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-feed-current", new Dictionary + { + ["feedRemainingPercent"] = $"{remaining * 100:F0}", + ["daysPerFrame"] = $"{daysPerFrame:F1}" + })); + } + + private static string FormatLang(string langKey, Dictionary values) + { + var result = Lang.Get(langKey); + foreach (var kvp in values) + result = result.Replace($">>>{kvp.Key}<<<", kvp.Value); + return result; + } +} diff --git a/OrekiWoofsBeehives/InformationVerbosityServerSideType.cs b/OrekiWoofsBeehives/InformationVerbosityServerSideType.cs new file mode 100644 index 0000000..ceb4659 --- /dev/null +++ b/OrekiWoofsBeehives/InformationVerbosityServerSideType.cs @@ -0,0 +1,8 @@ +namespace OrekiWoofsBeehives; + +public enum InformationVerbosityServerSideType +{ + ClientSide, + ServerRecommended, + ServerForced, +} diff --git a/OrekiWoofsBeehives/MetaConfig.cs b/OrekiWoofsBeehives/MetaConfig.cs new file mode 100644 index 0000000..4dfe3c7 --- /dev/null +++ b/OrekiWoofsBeehives/MetaConfig.cs @@ -0,0 +1,7 @@ +namespace OrekiWoofsBeehives; + +public class MetaConfig +{ + public bool IsTester { get; set; } + public string? ModVersion { get; set; } +} diff --git a/OrekiWoofsBeehives/ModConfigSetup.cs b/OrekiWoofsBeehives/ModConfigSetup.cs new file mode 100644 index 0000000..86e990e --- /dev/null +++ b/OrekiWoofsBeehives/ModConfigSetup.cs @@ -0,0 +1,179 @@ +using ConfigLib; +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace OrekiWoofsBeehives; + +public partial class OrekiWoofsBeehivesModSystem : ModSystem +{ + private ConfigLibModSystem? configLibSystem; + public const string CONFIG_CHANNEL_NAME = "orekiwoofsbeehives-config"; + private const string config_filename = "OrekiWoofsBeehives.json"; + + private void SubscribeToConfigChange(ICoreAPI api) + { + configLibSystem = api.ModLoader.GetModSystem(); + if (configLibSystem == null) + return; + + configLibSystem.SettingChanged += OnSettingChanged; + configLibSystem.ConfigsLoaded += OnConfigsLoaded; + } + + private void UnsubscribeFromConfigChange() + { + if (configLibSystem is null) + return; + configLibSystem.SettingChanged -= OnSettingChanged; + configLibSystem.ConfigsLoaded -= OnConfigsLoaded; + } + + private void OnConfigsLoaded() + { + configLibSystem?.GetConfig("orekiwoofsbeehives")?.AssignSettingsValues(Config.Instance); + } + + private void OnSettingChanged(string domain, IConfig config, ISetting setting) + { + if (domain != "orekiwoofsbeehives") + return; + + try + { + if (api != null) + Config.Instance = api.LoadModConfig(config_filename) ?? Config.Instance; + setting.AssignSettingValue(Config.Instance); + serverApi?.Network.GetChannel(CONFIG_CHANNEL_NAME).BroadcastPacket(Config.Instance); + if (api?.Side == EnumAppSide.Server) + api.StoreModConfig(Config.Instance, config_filename); + } + catch (Exception ex) + { + Mod.Logger.Error(ex); + } + } + + private void InitializeServerConfig(ICoreServerAPI api) + { + try + { + var oldCropBonus = GetBackCompatibilityCropBonus(api); + Config.Instance = api.LoadModConfig(config_filename); + Config.Instance ??= new Config(); + + ConvertOldCropBonus(oldCropBonus, Config.Instance); + + api.StoreModConfig(Config.Instance, config_filename); + + if (oldCropBonus is not null && api.ModLoader.IsModEnabled("configlib")) + ReloadConfigForConfigLib(api); + + api.Event.PlayerJoin += OnPlayerJoin; + } + catch (Exception e) + { + Mod.Logger.Error("Could not load config! Loading default settings instead."); + Mod.Logger.Error(e); + Config.Instance = new Config(); + } + } + + private void InitializeClientConfig(ICoreClientAPI api) + { + Config? clientSideConfig; + try + { + clientSideConfig = (Config?)api.LoadModConfig(config_filename); + } + catch (Exception e) + { + Mod.Logger.Error("Could not load client-side config! Loading default client-side settings instead."); + Mod.Logger.Error(e); + clientSideConfig = new Config(); + } + + if (clientSideConfig is null) + return; + + Config.Instance.BeehiveAmbientVolume = clientSideConfig.BeehiveAmbientVolume; + Config.Instance.InformationVerbosity = clientSideConfig.InformationVerbosity; + Config.Instance.DisableServerRecommended = clientSideConfig.DisableServerRecommended; + } + + private static void ReloadConfigForConfigLib(ICoreAPI api) + { + var configLibModSystem = api.ModLoader.GetModSystem(); + var configLibConfig = configLibModSystem.GetConfig("orekiwoofsbeehives"); + configLibConfig?.ReadFromFile(); + } + + private void OnPlayerJoin(IServerPlayer byPlayer) + { + serverApi!.Network.GetChannel(CONFIG_CHANNEL_NAME).SendPacket(Config.Instance, byPlayer); + } + + private void OnConfigReceivedFromServer(Config packet) + { + var currentConfig = Config.Instance; + Config.Instance = packet; + Config.Instance.InformationVerbosity = currentConfig.InformationVerbosity; + Config.Instance.DisableServerRecommended = currentConfig.DisableServerRecommended; + Config.Instance.BeehiveAmbientVolume = currentConfig.BeehiveAmbientVolume; + } + + private static CropBonusType? GetBackCompatibilityCropBonus(ICoreAPI api) + { + var cfgJson = api.LoadModConfig(config_filename); + if (cfgJson is null || !cfgJson.KeyExists("CropBonus")) + return null; + + var cropBonusObj = cfgJson["CropBonus"]; + if (cropBonusObj is null) + return null; + + var cropBonusInt = cropBonusObj.AsInt(-1); + if (cropBonusInt >= 0) + { + var cropBonus = (CropBonusType)cropBonusInt; + return cropBonus; + } + + var cropBonusStr = cropBonusObj.AsString(null); + return cropBonusStr switch + { + "Yield" => CropBonusType.Yield, + "Speed" => CropBonusType.Speed, + "YieldAndSpeed" => CropBonusType.YieldAndSpeed, + "None" => CropBonusType.None, + _ => null, + }; + } + + private void ConvertOldCropBonus(CropBonusType? cropBonus, Config config) + { + if (cropBonus == null) + return; + + config.SpeedBoost = false; + config.YieldBoost = false; + switch (cropBonus.Value) + { + case CropBonusType.Yield: + config.YieldBoost = true; + break; + case CropBonusType.Speed: + config.SpeedBoost = true; + break; + case CropBonusType.YieldAndSpeed: + config.YieldBoost = true; + config.SpeedBoost = true; + break; + default: + break; + } + + Mod.Logger.Event($"Converted CropBonus:{cropBonus} to YieldBoost:{config.YieldBoost}, SpeedBoost:{config.SpeedBoost}"); + } +} diff --git a/OrekiWoofsBeehives/MultipleBeehivesBoostCombinationType.cs b/OrekiWoofsBeehives/MultipleBeehivesBoostCombinationType.cs new file mode 100644 index 0000000..5bf21fd --- /dev/null +++ b/OrekiWoofsBeehives/MultipleBeehivesBoostCombinationType.cs @@ -0,0 +1,7 @@ +namespace OrekiWoofsBeehives; + +public enum MultipleBeehivesBoostCombinationType +{ + Additive, + Max, +} diff --git a/OrekiWoofsBeehives/OrekiWoofsBeehives.csproj b/OrekiWoofsBeehives/OrekiWoofsBeehives.csproj new file mode 100644 index 0000000..6bf0818 --- /dev/null +++ b/OrekiWoofsBeehives/OrekiWoofsBeehives.csproj @@ -0,0 +1,86 @@ + + + + net8.0 + false + bin\$(Configuration)\Mods\orekiwoofsbeehives + enable + Debug;Release;Debug22 + + + + + net10.0 + $(VINTAGE_STORY_22PRE2) + --dataPath "..\VintageStoryDataBeehives\Data22" + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + false + + + $(VINTAGE_STORY)/Mods/VSSurvivalMod.dll + False + + + $(VINTAGE_STORY)/Mods/VSEssentials.dll + False + + + $(VINTAGE_STORY)/Mods/VSCreativeMod.dll + False + + + $(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll + False + + + $(VINTAGE_STORY)/Lib/0Harmony.dll + False + + + $(VINTAGE_STORY)/VintagestoryLib.dll + false + + + $(VINTAGE_STORY)/Lib/protobuf-net.dll + False + + + $(VINTAGE_STORY)/Lib/cairo-sharp.dll + False + + + $(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll + False + + + + + + E:\Code\VintageStory\configlib_1.10.14\configlib.dll + False + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + Common\%(RecursiveDir)%(Filename)%(Extension) + + + + diff --git a/OrekiWoofsBeehives/OrekiWoofsBeehivesModSystem.cs b/OrekiWoofsBeehives/OrekiWoofsBeehivesModSystem.cs new file mode 100644 index 0000000..1c6eb1e --- /dev/null +++ b/OrekiWoofsBeehives/OrekiWoofsBeehivesModSystem.cs @@ -0,0 +1,132 @@ +using HarmonyLib; +using OrekiWoofsBeehives.Behaviors; +using OrekiWoofsBeehives.BlockEntities; +using OrekiWoofsBeehives.Blocks; +using OrekiWoofsBeehives.Utilities; +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace OrekiWoofsBeehives; + +public partial class OrekiWoofsBeehivesModSystem : ModSystem +{ + private Harmony? _harmony; + private ICoreServerAPI? serverApi; + private ICoreAPI? api; + private MetaConfig metaConfig = new(); + + public BeehiveRegistry BeehiveRegistry { get; } = new(); + public VanillaSkepRegistry VanillaSkepRegistry { get; } = new(); + + // Called on server and client + // Useful for registering block/entity classes on both sides + public override void Start(ICoreAPI api) + { + this.api = api; + api.RegisterBlockBehaviorClass(nameof(BlockBehaviorBeehiveAffected), typeof(BlockBehaviorBeehiveAffected)); + api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorBeehiveYieldMultiplier), typeof(BlockEntityBehaviorBeehiveYieldMultiplier)); + api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorVanillaSkepSwarmTarget), typeof(BlockEntityBehaviorVanillaSkepSwarmTarget)); + + api.RegisterBlockClass(nameof(BlockBeeSwarm), typeof(BlockBeeSwarm)); + api.RegisterBlockClass(nameof(BlockBeehiveFrame), typeof(BlockBeehiveFrame)); + + api.RegisterBlockClass(nameof(BlockReusableBeehive), typeof(BlockReusableBeehive)); + api.RegisterBlockEntityClass(nameof(BlockEntityReusableBeehive), typeof(BlockEntityReusableBeehive)); + api.RegisterBlockEntityClass(nameof(BlockEntityBeeSwarm), typeof(BlockEntityBeeSwarm)); + + api.Network.RegisterChannel(CONFIG_CHANNEL_NAME).RegisterMessageType(); + + _harmony = new Harmony("com.orekiwoof.beehives"); + _harmony.PatchAllUncategorized(); + if (api.ModLoader.GetMod("game").Info.Version.StartsWith("1.22")) + _harmony.PatchCategory("1.22"); + if (api.ModLoader.GetMod("game").Info.Version.StartsWith("1.21")) + _harmony.PatchCategory("1.21"); + } + + public override double ExecuteOrder() => 0.12; + + public override void Dispose() + { + _harmony?.UnpatchAll("com.orekiwoof.beehives"); + if (serverApi != null) + serverApi.Event.PlayerJoin -= OnPlayerJoin; + if (api?.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem") == true) + UnsubscribeFromConfigChange(); + base.Dispose(); + } + + public override void StartServerSide(ICoreServerAPI api) + { + serverApi = api; + InitializeServerConfig(api); + SetupServerCommands(api); + + try + { + if (api.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem")) + SubscribeToConfigChange(api); + } + catch (Exception) + { + Mod.Logger.VerboseDebug("Failed to subscribe to config change"); + } + } + + public override void StartClientSide(ICoreClientAPI api) + { + InitializeClientConfig(api); + SetupClientCommands(api); + api.Network.GetChannel(CONFIG_CHANNEL_NAME)?.SetMessageHandler(OnConfigReceivedFromServer); + + try + { + if (api.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem")) + SubscribeToConfigChange(api); + } + catch (Exception) + { + Mod.Logger.VerboseDebug("Failed to subscribe to config change"); + } + + LoadMetaConfig(api); + ShowDevVersionWarningIfNeeded(api); + } + + private void LoadMetaConfig(ICoreClientAPI api) + { + try + { + metaConfig = api.LoadModConfig(meta_config_filename) ?? new MetaConfig(); + } + catch (Exception e) + { + Mod.Logger.Warning("Could not load meta config: {0}", e.Message); + metaConfig = new MetaConfig(); + } + + if (metaConfig.ModVersion != Mod.Info.Version) + { + metaConfig.ModVersion = Mod.Info.Version; + api.StoreModConfig(metaConfig, meta_config_filename); + } + } + + private void ShowDevVersionWarningIfNeeded(ICoreClientAPI api) + { + if (!Mod.Info.Version.Contains("-dev")) + return; + + if (metaConfig.IsTester) + return; + + api.Event.LevelFinalize += () => + { + api.ShowChatMessage("This is a development version of OrekiWoof's Beehives. It can break and crash. When reporting issues, mention the exact version of this mod (and Roaming Bees mod if installed)."); + api.ShowChatMessage("To disable this warning, use \".beehives tester\""); + api.ShowChatMessage("To check mod versions, use \".beehives version\""); + }; + } +} diff --git a/OrekiWoofsBeehives/Patches/BEFarmland_GetGrowthRate_Patch.cs b/OrekiWoofsBeehives/Patches/BEFarmland_GetGrowthRate_Patch.cs new file mode 100644 index 0000000..dd18f4b --- /dev/null +++ b/OrekiWoofsBeehives/Patches/BEFarmland_GetGrowthRate_Patch.cs @@ -0,0 +1,57 @@ +using HarmonyLib; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using OrekiWoofsBeehives.Utilities; +using OrekiWoofsBeehives.Behaviors; +using Vintagestory.GameContent; + +namespace OrekiWoofsBeehives.Patches; + +[HarmonyPatch(typeof(BlockEntityFarmland), nameof(BlockEntityFarmland.GetGrowthRate), [typeof(EnumSoilNutrient)])] +[HarmonyPatchCategory("1.21")] +public static class BEFarmland_GetGrowthRate_Patch +{ + [HarmonyPostfix] + public static void Postfix(BlockEntityFarmland __instance, ref float __result) + { + GetGrowthRateCommon.CommonMethod(__instance, ref __result); + } +} + +#if DEBUG22 +[HarmonyPatch(typeof(BlockEntitySoilNutrition), nameof(BlockEntitySoilNutrition.GetGrowthRate), [typeof(EnumSoilNutrient)])] +[HarmonyPatchCategory("1.22")] +public static class BESoilNutrition_GetGrowthRate_Patch +{ + [HarmonyPostfix] + public static void Postfix(BlockEntitySoilNutrition __instance, ref float __result) + { + GetGrowthRateCommon.CommonMethod(__instance, ref __result); + } +} +#endif + +public static class GetGrowthRateCommon +{ + + public static void CommonMethod(BlockEntity __instance, ref float __result) + { + BlockPos? cropPos = __instance?.Pos?.UpCopy(); + var cfg = Config.Instance; + if (!cfg.SpeedBoost) + return; + + var registry = __instance?.Api?.GetOrekiWoofsBeehives()?.BeehiveRegistry; + + if (registry is null || cropPos is null || __instance is null) + return; + + float totalBeeEffectiveness = BlockBehaviorBeehiveAffected.GetBeehiveBoostEffectiveness(__instance.Api, registry.BeehivePositions, cropPos); + if (totalBeeEffectiveness > 0f) + { + float multiplier = 1.0f + (totalBeeEffectiveness * cfg.GrowthSpeedBonus); + __result *= multiplier; + } + } +} + diff --git a/OrekiWoofsBeehives/Patches/BlockCrop_GetDrops_Patch.cs b/OrekiWoofsBeehives/Patches/BlockCrop_GetDrops_Patch.cs new file mode 100644 index 0000000..2080595 --- /dev/null +++ b/OrekiWoofsBeehives/Patches/BlockCrop_GetDrops_Patch.cs @@ -0,0 +1,68 @@ +using HarmonyLib; +using OrekiWoofsBeehives.Behaviors; +using System; +using System.Collections.Generic; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBeehives.Patches; + +[HarmonyPatch(typeof(BlockCrop), nameof(BlockCrop.GetDrops), argumentTypes: [typeof(IWorldAccessor), typeof(BlockPos), typeof(IPlayer), typeof(float)])] +public static class BlockCrop_GetDrops_Patch +{ + [HarmonyPostfix] + public static void Postfix(BlockCrop __instance, IWorldAccessor world, BlockPos pos, ref ItemStack[] __result) + { + if (!ShouldApplyYieldBonus(__instance, world, pos, __result, out var yieldBonus)) + return; + + var additionalMultiplier = yieldBonus; + + var finalDrops = new List(__result.Length); + foreach (var drop in __result) + { + if (drop == null || drop.StackSize <= 0) + continue; + + var newDrop = drop.Clone(); + float additional = newDrop.StackSize * additionalMultiplier; + int guaranteed = (int)MathF.Floor(additional); + float chance = additional - guaranteed; + int bonus = guaranteed; + if (chance > 0f && world.Rand.NextDouble() < chance) + bonus += 1; + + newDrop.StackSize += bonus; + if (newDrop.StackSize > 0) + finalDrops.Add(newDrop); + } + + __result = [.. finalDrops]; + } + + private static bool ShouldApplyYieldBonus(BlockCrop crop, IWorldAccessor world, BlockPos pos, ItemStack[]? drops, out float yieldBonus) + { + yieldBonus = 0f; + + if (drops == null || drops.Length == 0) + return false; + + var cfg = Config.Instance; + if (!cfg.YieldBoost) + return false; + + yieldBonus = Math.Max(0f, cfg.YieldMultiplier - 1f); + if (yieldBonus <= 0f) + return false; + + if (world.BlockAccessor.GetBlockEntity(pos.DownCopy()) is not BlockEntityFarmland farmland) + return false; + + var tracker = farmland.Block.GetBEBehavior(farmland.Pos); + if (tracker is null) + return false; + + return tracker.IsPollinated(crop.CurrentCropStage); + } +} diff --git a/OrekiWoofsBeehives/Properties/launchSettings.json b/OrekiWoofsBeehives/Properties/launchSettings.json new file mode 100644 index 0000000..f1f3c56 --- /dev/null +++ b/OrekiWoofsBeehives/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Client": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + }, + "Server": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + } + } +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/Utilities/ApiExtensions.cs b/OrekiWoofsBeehives/Utilities/ApiExtensions.cs new file mode 100644 index 0000000..a4e12ec --- /dev/null +++ b/OrekiWoofsBeehives/Utilities/ApiExtensions.cs @@ -0,0 +1,11 @@ +using OrekiWoofsBees.Common; +using Vintagestory.API.Common; + +namespace OrekiWoofsBeehives.Utilities; + +internal static class ApiExtensions +{ + public static OrekiWoofsBeehivesModSystem? GetOrekiWoofsBeehives(this ICoreAPI api) => api.ModLoader.GetModSystem(); + + public static IPlantPositionRegistry? GetPlantPositionRegistry(this ICoreAPI api) => api.ModLoader.GetModSystem(); +} diff --git a/OrekiWoofsBeehives/Utilities/BeehiveRegistry.cs b/OrekiWoofsBeehives/Utilities/BeehiveRegistry.cs new file mode 100644 index 0000000..2a1a2c1 --- /dev/null +++ b/OrekiWoofsBeehives/Utilities/BeehiveRegistry.cs @@ -0,0 +1,20 @@ +using OrekiWoofsBeehives.BlockEntities; +using OrekiWoofsBees.Common; +using System.Collections.Generic; +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBeehives.Utilities; + +public class BeehiveRegistry +{ + private readonly List beehivePositions = []; + public IEnumerable BeehivePositions => beehivePositions; + + public void Register(BlockEntityReusableBeehive beehive) => beehivePositions.Add(new(beehive.Pos.X, beehive.Pos.Y, beehive.Pos.Z)); + + public void Unregister(BlockPos pos) + { + var position = new StructVec3i(pos.X, pos.Y, pos.Z); + beehivePositions.Remove(position); + } +} diff --git a/OrekiWoofsBeehives/Utilities/VanillaSkepRegistry.cs b/OrekiWoofsBeehives/Utilities/VanillaSkepRegistry.cs new file mode 100644 index 0000000..aa1aa11 --- /dev/null +++ b/OrekiWoofsBeehives/Utilities/VanillaSkepRegistry.cs @@ -0,0 +1,24 @@ +using OrekiWoofsBees.Common; +using System.Collections.Generic; +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBeehives.Utilities; + +public class VanillaSkepRegistry +{ + private readonly Dictionary emptySkepPositions = []; + + public IEnumerable> Entries => emptySkepPositions; + + public void RegisterVanillaSkep(BlockPos pos, string populatedSkepCode) + { + var key = new StructVec3i(pos.X, pos.Y, pos.Z); + emptySkepPositions[key] = populatedSkepCode; + } + + public void UnregisterVanillaSkep(BlockPos pos) + { + var key = new StructVec3i(pos.X, pos.Y, pos.Z); + emptySkepPositions.Remove(key); + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehive.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehive.json new file mode 100644 index 0000000..0967ed8 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehive.json @@ -0,0 +1,46 @@ +{ + "code": "beehive", + "class": "BlockReusableBeehive", + "entityClass": "BlockEntityReusableBeehive", + "variantgroups": [ + { "code": "side", "loadFromProperties": "abstract/horizontalorientation" } + ], + "behaviors": [{"name": "HorizontalOrientable", "properties": { "dropBlockFace": "north", "orientate": true }}], + "blockmaterial": "Wood", + "creativeinventory": { + "general": ["*-north"], + "decorative": ["*-north"] + }, + "lightAbsorption": 0, + "drop": { + "code": "orekiwoofsbeehives:beehive-north", + "type": "block" + }, + "resistance": 2.5, + "collisionbox": { "x1": 0, "y1": 0, "z1": 0, "x2": 1, "y2": 1, "z2": 1 }, + "attributes": { + "beeHive": true, + "handbook": { + "extraSections": [ + { "title": "orekiwoofsbeehives:handbook-beehive-title", "text": "orekiwoofsbeehives:handbook-beehive" } + ] + } + }, + "shapebytype": { + "*-north": { "base": "orekiwoofsbeehives:block/beehive-closed", "rotateY": 0 }, + "*-east": { "base": "orekiwoofsbeehives:block/beehive-closed", "rotateY": 270 }, + "*-south": { "base": "orekiwoofsbeehives:block/beehive-closed", "rotateY": 180 }, + "*-west": { "base": "orekiwoofsbeehives:block/beehive-closed", "rotateY": 90 } + }, + "textures": { + "all": { + "base": "game:block/wood/henbox/sides" + } + }, + "sounds": { + "place": "game:block/planks", + "break": "game:block/planks", + "hit": "game:block/planks", + "ambient": "game:creature/beehive" + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveframe.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveframe.json new file mode 100644 index 0000000..3d2ebf6 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveframe.json @@ -0,0 +1,96 @@ +{ + "code": "beehiveframe", + "class": "BlockBeehiveFrame", + "behaviors": [ + { + "name": "GroundStorable", + "properties": { + "layout": "Stacking", + "stackingCapacity": 16, + "cbScaleYByLayer": 1, + "upSolid": true, + "placeRemoveSound": "game:block/planks", + "stackingModel": "orekiwoofsbeehives:block/beehiveframepile", + "modelItemsToStackSizeRatio": 5, + "stackingTextures": { + "frame": "game:block/wood-generic", + "center": "game:block/linen" + }, + "stackingTexturesByType": { + "beehiveframe-filled": { + "center": "game:block/resin" + }, + "beehiveframe-filled-feed": { + "center": "game:block/food/grain/flax" + } + } + } + }, + { "name": "Unplaceable" }, + { "name": "RightClickPickup" } + ], + "variantgroups": [ + { "code": "state", "states": ["empty", "filled", "filled-feed"] } + ], + "maxstacksize": 64, + "creativeinventory": { + "general": ["*"], + "items": ["*"] + }, + "shapeByType": { + "*": { "base": "orekiwoofsbeehives:item/beehiveframe", "rotateY": 90 } + }, + "texturesByType": { + "beehiveframe-empty": { + "frame": { "base": "game:block/wood-generic" }, + "center": { "base": "game:block/linen" } + }, + "beehiveframe-filled": { + "frame": { "base": "game:block/wood-generic" }, + "center": { "base": "game:block/resin" } + }, + "beehiveframe-filled-feed": { + "frame": { "base": "game:block/wood-generic" }, + "center": { "base": "game:block/food/grain/flax" } + } + }, + "blockmaterial": "Wood", + "replaceable": 700, + "resistance": 1.5, + "lightAbsorption": 0, + "sounds": { + "walk": "walk/wood" + }, + "collisionbox": { "x1": 0.125, "y1": 0, "z1": 0.125, "x2": 0.875, "y2": 0.0625, "z2": 0.875 }, + "selectionbox": { "x1": 0.125, "y1": 0, "z1": 0.125, "x2": 0.875, "y2": 0.0625, "z2": 0.875 }, + "sideopaque": { + "all": false + }, + "sidesolid": { + "all": false + }, + "heldTpIdleAnimation": "holdbothhandslarge", + "heldTpUseAnimation": "twohandplaceblock", + "guiTransform": { + "translation": { "x": 0, "y": 3, "z": 0 }, + "origin": { "x": 0.5, "y": 0.0625, "z": 0.5 }, + "scale": 1.33 + }, + "tpHandTransform": { + "translation": { "x": -1, "y": -0.6, "z": -1.05 }, + "rotation": { "x": -87, "y": 9, "z": 4 }, + "origin": { "x": 0.5, "y": 0.125, "z": 0.5 }, + "scale": 0.5 + }, + "fpHandTransform": { + "translation": { "x": -0.3, "y": 0, "z": 0.5 }, + "rotation": { "x": 16, "y": -51, "z": 91 }, + "origin": { "x": 0.5, "y": 0.125, "z": 0.5 } + }, + "groundTransform": { + "translation": { "x": 0, "y": 0, "z": 0 }, + "rotation": { "x": 90, "y": -45, "z": 90 }, + "origin": { "x": 0.5, "y": 0, "z": 0.5 }, + "scale": 1 + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveroof.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveroof.json new file mode 100644 index 0000000..ba76320 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehiveroof.json @@ -0,0 +1,41 @@ +{ + "code": "beehiveroof", + "class": "Block", + "variantgroups": [ + { "code": "side", "loadFromProperties": "abstract/horizontalorientation" } + ], + "behaviors": [{"name": "HorizontalOrientable", "properties": { "dropBlockFace": "north", "orientate": true }}], + "blockmaterial": "Wood", + "creativeinventory": { + "general": ["*-north"], + "decorative": ["*-north"] + }, + "lightAbsorption": 0, + "drop": { + "code": "orekiwoofsbeehives:beehiveroof-north", + "type": "block" + }, + "resistance": 2.0, + "collisionbox": { "x1": 0, "y1": 0, "z1": 0, "x2": 1, "y2": 0.25, "z2": 1 }, + "selectionbox": { "x1": 0, "y1": 0, "z1": 0, "x2": 1, "y2": 0.25, "z2": 1 }, + "shapebytype": { + "*-north": { "base": "orekiwoofsbeehives:block/beehiveroof", "rotateY": 0 }, + "*-east": { "base": "orekiwoofsbeehives:block/beehiveroof", "rotateY": 270 }, + "*-south": { "base": "orekiwoofsbeehives:block/beehiveroof", "rotateY": 180 }, + "*-west": { "base": "orekiwoofsbeehives:block/beehiveroof", "rotateY": 90 } + }, + "textures": { + "roof-plank": { "base": "game:block/wood/planks/oak1" } + }, + "sideopaque": { + "all": false + }, + "sidesolid": { + "all": false + }, + "sounds": { + "place": "game:block/planks", + "break": "game:block/planks", + "hit": "game:block/planks" + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehivestand.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehivestand.json new file mode 100644 index 0000000..b66e136 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beehivestand.json @@ -0,0 +1,43 @@ +{ + "code": "beehivestand", + "class": "Block", + "variantgroups": [ + { "code": "side", "loadFromProperties": "abstract/horizontalorientation" } + ], + "behaviors": [{"name": "HorizontalOrientable", "properties": { "orientate": true }}], + "blockmaterial": "Wood", + "creativeinventory": { + "general": ["*-north"], + "decorative": ["*-north"] + }, + "lightAbsorption": 0, + "drop": { + "code": "orekiwoofsbeehives:beehivestand-north", + "type": "block" + }, + "resistance": 2.0, + "collisionbox": { "x1": 0, "y1": 0, "z1": 0, "x2": 1, "y2": 1, "z2": 1 }, + "selectionbox": { "x1": 0, "y1": 0, "z1": 0, "x2": 1, "y2": 1, "z2": 1 }, + "shapebytype": { + "*-north": { "base": "orekiwoofsbeehives:block/beehivestand", "rotateY": 0 }, + "*-east": { "base": "orekiwoofsbeehives:block/beehivestand", "rotateY": 270 }, + "*-south": { "base": "orekiwoofsbeehives:block/beehivestand", "rotateY": 180 }, + "*-west": { "base": "orekiwoofsbeehives:block/beehivestand", "rotateY": 90 } + }, + "textures": { + "all": { + "base": "game:block/wood/henbox/sides" + } + }, + "sideopaque": { + "all": false + }, + "sidesolid": { + "all": false + }, + "sounds": { + "place": "game:block/planks", + "break": "game:block/planks", + "hit": "game:block/planks" + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beeswarm.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beeswarm.json new file mode 100644 index 0000000..d3942fb --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/blocktypes/beeswarm.json @@ -0,0 +1,41 @@ +{ + "code": "beeswarm", + "class": "BlockBeeSwarm", + "entityClass": "BlockEntityBeeSwarm", + "variantgroups": [ + { "code": "side", "states": ["north", "east", "south", "west", "down", "up"] } + ], + "blockmaterial": "Leaves", + "creativeinventory": { + "general": [] + }, + "replaceable": 0, + "resistance": 0.1, + "lightAbsorption": 0, + "collisionbox": { "x1": 0.2, "y1": 0.2, "z1": 0.2, "x2": 0.8, "y2": 0.8, "z2": 0.8 }, + "selectionbox": { "x1": 0.2, "y1": 0.2, "z1": 0.2, "x2": 0.8, "y2": 0.8, "z2": 0.8 }, + "shapebytype": { + "*-north": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateY": 0 }, + "*-east": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateY": 270 }, + "*-south": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateY": 180 }, + "*-west": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateY": 90 }, + "*-down": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateX": 270, "rotateY": 0 }, + "*-up": { "base": "orekiwoofsbeehives:block/beeswarm", "rotateX": 90, "rotateY": 0 } + }, + "textures": { + "swarm": { + "base": "game:block/soil/fertcompost" + } + }, + "sideopaque": { + "all": false + }, + "sidesolid": { + "all": false + }, + "sounds": { + "break": "game:block/leaves", + "hit": "game:block/leaves", + "ambient": "game:creature/beehive" + } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/config/configlib-patches.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/config/configlib-patches.json new file mode 100644 index 0000000..9d640f6 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/config/configlib-patches.json @@ -0,0 +1,430 @@ +{ + "version": 5, + "file": "OrekiWoofsBeehives.json", + "patches": {}, + "settings": [ + { + "type": "separator", + "title": "Client-side" + }, + { + "code": "InformationVerbosity", + "comment": "config-desc-InformationVerbosity", + "type": "int", + "default": 1, + "range": { + "min": 0, + "max": 4 + }, + "clientSide": true + }, + { + "code": "DisableServerRecommended", + "comment": "config-desc-DisableServerRecommended", + "type": "boolean", + "default": false, + "clientSide": true + }, + { + "code": "BeehiveAmbientVolume", + "comment": "config-desc-BeehiveAmbientVolume", + "type": "float", + "default": 0.5, + "range": { + "min": 0.0, + "max": 1.0 + }, + "clientSide": true + }, + { + "type": "separator", + "title": "Server-side" + }, + { + "code": "InformationVerbosityServerSideType", + "comment": "config-desc-InformationVerbosityServerSideType", + "type": "string", + "default": "ClientSide", + "values": [ + "ClientSide", + "ServerRecommended", + "ServerForced" + ] + }, + { + "code": "InformationVerbosityServer", + "comment": "config-desc-InformationVerbosityServer", + "type": "int", + "default": 1, + "range": { + "min": 0, + "max": 4 + } + }, + { + "type": "separator", + "title": "Beehive Settings" + }, + { + "code": "BeehiveRadius", + "comment": "config-desc-BeehiveRadius", + "type": "int", + "default": 35, + "range": { + "min": 5, + "max": 70 + } + }, + { + "code": "YieldBoost", + "comment": "config-desc-YieldBoost", + "type": "boolean", + "default": true + }, + { + "code": "SpeedBoost", + "comment": "config-desc-SpeedBoost", + "type": "boolean", + "default": false + }, + { + "code": "GrowthSpeedBonus", + "comment": "config-desc-GrowthSpeedBonus", + "type": "float", + "default": 0.2, + "range": { + "min": 0.0, + "max": 1.0 + } + }, + { + "code": "YieldMultiplier", + "comment": "config-desc-YieldMultiplier", + "type": "float", + "default": 1.2, + "range": { + "min": 0.1, + "max": 3.0 + } + }, + { + "code": "PlantsFullBoostDistanceToHive", + "comment": "config-desc-PlantsFullBoostDistanceToHive", + "type": "int", + "default": 20, + "range": { + "min": 5, + "max": 50 + } + }, + { + "code": "MinBeesForPlantBoost", + "comment": "config-desc-MinBeesForPlantBoost", + "type": "int", + "default": 1000, + "range": { + "min": 100, + "max": 5000 + } + }, + { + "code": "BoostCalculation", + "comment": "config-desc-BoostCalculation", + "type": "string", + "default": "NormalizedLogk10Population", + "values": [ + "Full", + "LinearPopulation", + "NormalizedLogk10Population" + ] + }, + { + "code": "PopulationPercentForMaxBoost", + "comment": "config-desc-PopulationPercentForMaxBoost", + "type": "float", + "default": 100, + "range": { + "min": 0, + "max": 100 + } + }, + { + "code": "MultipleBeehivesBoostCombination", + "comment": "config-desc-MultipleBeehivesBoostCombination", + "type": "string", + "default": "Max", + "values": [ + "Additive", + "Max" + ] + }, + { + "type": "separator", + "title": "Bee Population" + }, + { + "code": "MaxBeePopulation", + "comment": "config-desc-MaxBeePopulation", + "type": "int", + "default": 50000, + "range": { + "min": 1000, + "max": 200000 + } + }, + { + "code": "InitialBeePopulation", + "comment": "config-desc-InitialBeePopulation", + "type": "int", + "default": 5000, + "range": { + "min": 100, + "max": 50000 + } + }, + { + "code": "BeehiveConsideredEmptyBelowPopulation", + "comment": "config-desc-BeehiveConsideredEmptyBelowPopulation", + "type": "int", + "default": 500, + "range": { + "min": 0, + "max": 200000 + } + }, + { + "code": "BaseDeathsPerDay", + "comment": "config-desc-BaseDeathsPerDay", + "type": "int", + "default": 100, + "range": { + "min": 0, + "max": 1000 + } + }, + { + "code": "FlowerThreshold", + "comment": "config-desc-FlowerThreshold", + "type": "int", + "default": 10, + "range": { + "min": 0, + "max": 50 + } + }, + { + "code": "DeathPerMissingFlower", + "comment": "config-desc-DeathPerMissingFlower", + "type": "int", + "default": 100, + "range": { + "min": 0, + "max": 500 + } + }, + { + "code": "DoublingTimeDays", + "comment": "config-desc-DoublingTimeDays", + "type": "float", + "default": 9.0, + "range": { + "min": 1.0, + "max": 30.0 + } + }, + { + "code": "BonusGrowthPerFilledFrame", + "comment": "config-desc-BonusGrowthPerFilledFrame", + "type": "int", + "default": 100, + "range": { + "min": 0, + "max": 2000 + } + }, + { + "type": "separator", + "title": "Swarming" + }, + { + "code": "EnableSwarms", + "comment": "config-desc-EnableSwarms", + "type": "boolean", + "default": true + }, + { + "code": "SwarmSettingAfterPlacing", + "comment": "config-desc-SwarmSettingAfterPlacing", + "type": "boolean", + "default": false + }, + { + "code": "PreSwarmDurationHours", + "comment": "config-desc-PreSwarmDurationHours", + "type": "int", + "default": 40, + "range": { + "min": 1, + "max": 240 + } + }, + { + "code": "PopulationPercentRequirementForSwarm", + "comment": "config-desc-PopulationPercentRequirementForSwarm", + "type": "int", + "default": 80, + "range": { + "min": 40, + "max": 100 + } + }, + { + "code": "SwarmPopulationPercentage", + "comment": "config-desc-SwarmPopulationPercentage", + "type": "int", + "default": 40, + "range": { + "min": 1, + "max": 70 + } + }, + { + "code": "SwarmCooldownDays", + "comment": "config-desc-SwarmCooldownDays", + "type": "int", + "default": 4, + "range": { + "min": 1, + "max": 30 + } + }, + { + "code": "SwarmReturnToOriginOnFailedMigrationPercent", + "comment": "config-desc-SwarmReturnToOriginOnFailedMigrationPercent", + "type": "int", + "default": 20, + "range": { + "min": 0, + "max": 100 + } + }, + { + "type": "separator", + "title": "Honey Production" + }, + { + "code": "ReferenceFlowers", + "comment": "config-desc-ReferenceFlowers", + "type": "float", + "default": 10.0, + "range": { + "min": 1.0, + "max": 50.0 + } + }, + { + "code": "ReferenceBees", + "comment": "config-desc-ReferenceBees", + "type": "float", + "default": 25000.0, + "range": { + "min": 1000.0, + "max": 50000.0 + } + }, + { + "code": "MaxFlowersForHoneyProduction", + "comment": "config-desc-MaxFlowersForHoneyProduction", + "type": "int", + "default": 25, + "range": { + "min": 1, + "max": 100 + } + }, + { + "type": "separator", + "title": "Winter" + }, + { + "code": "WinterHardMode", + "comment": "config-desc-WinterHardMode", + "type": "boolean", + "default": false + }, + { + "code": "WinterDailyBeeDeathsWithoutFood", + "comment": "config-desc-WinterDailyBeeDeathsWithoutFood", + "type": "int", + "default": 300, + "range": { + "min": 0, + "max": 50000 + } + }, + { + "code": "WinterFoodConsumptionMultiplier", + "comment": "config-desc-WinterFoodConsumptionMultiplier", + "type": "float", + "default": 0.2, + "range": { + "min": 0.0, + "max": 1.0 + } + }, + { + "type": "separator", + "title": "Weather Effects" + }, + { + "code": "MinTemperatureGrowth", + "comment": "config-desc-MinTemperatureGrowth", + "type": "float", + "default": 0.0, + "range": { + "min": -20.0, + "max": 20.0 + } + }, + { + "code": "MaxTemperatureGrowth", + "comment": "config-desc-MaxTemperatureGrowth", + "type": "float", + "default": 10.0, + "range": { + "min": 0.0, + "max": 40.0 + } + }, + { + "code": "GreenhouseAffectsBeehive", + "comment": "config-desc-GreenhouseAffectsBeehive", + "type": "boolean", + "default": true + }, + { + "type": "separator", + "title": "Particles" + }, + { + "code": "BeesPerParticle", + "comment": "config-desc-BeesPerParticle", + "type": "int", + "default": 1000, + "range": { + "min": 100, + "max": 5000 + } + }, + { + "code": "BeehiveAlwaysSpawnNumberOfBees", + "comment": "config-desc-BeehiveAlwaysSpawnNumberOfBees", + "type": "int", + "default": 0, + "range": { + "min": 0, + "max": 50 + } + } + ] +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/cs.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/cs.json new file mode 100644 index 0000000..4027107 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/cs.json @@ -0,0 +1,52 @@ +{ + "block-beehive-*": "Včelí úl", + "block-beehivestand-*": "Stojan na úl", + "block-beehiveroof-*": "Střecha úlu", + "block-beehiveframe-empty": "Prázdný rámek do úlu", + "block-beehiveframe-filled": "Naplněný rámek", + "block-beehiveframe-filled-feed": "Rámek s krmivem", + "beehiveframe-feed-percent": ">>>percent<<<%", + "beehive-info-no-bees": "Žádné včely. Použij osídlený koš (skep) k osídlení úlu.", + "beehive-info-frames-boosting": "Uložený med podporuje růst populace.", + "beehive-info-frames-sacrificing": "Včely obětují polovinu růstu populace pro výrobu medu.", + "beehive-info-population": "Populace včel: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>flowersAround<<<, Plodiny: >>>cropsAround<<<, Efektivně: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<>>flowersAround<<<, Plodiny: >>>cropsAround<<<", + "beehive-info-flowers-scanning-suffix": "(oblast se stále prozkoumává)", + "beehive-info-frames-mixed": "Rámky: >>>filledFrames<<< naplněných (+>>>filledFrameBonus<<< včel/den), >>>emptyFrames<<< prázdných (-0.5x včel/den)", + "beehive-info-frames-filled": "Rámky: >>>filledFrames<<< naplněných (+>>>filledFrameBonus<<< včel/den)", + "beehive-info-frames-empty": "Rámky: >>>emptyFrames<<< prázdných (-0.5x včel/den)", + "beehive-info-frames-total": "Rámky: >>>totalFrames<<<", + "beehive-info-progress-filling": "Postup plnění rámku: >>>honeyProgress<<<% (naplní se za >>>daysToFill<<< dní)", + "beehive-info-progress-noproduction": "Postup plnění rámku: >>>honeyProgress<<<% (žádná produkce – potřeba včely a květiny)", + "beehive-info-progress-v1-noproduction": "Žádná produkce medu", + "beehive-info-progress-v1-lessthanday": "Naplněný rámek bude vyroben za méně než den", + "beehive-info-progress-v1-days": "Naplněný rámek bude vyroben za >>>days<<< dní", + "beehive-info-all-filled": "Všechny rámky jsou naplněné! Přidej prázdné rámky pro pokračování produkce.", + "beehive-info-no-frames": "V úlu nejsou žádné rámky. Přidej prázdné rámky pro zahájení produkce.", + "beehive-info-temperature": "Teplota: >>>temperature<<<°C (>>>multiplier<<>>changeSign<<<>>>netChange<<>>dailyGrowth<<<, úbytek: ->>>dailyDeaths<<<)", + "beehive-info-population-status-decreasing": "Populace klesá", + "beehive-info-population-status-stagnant": "Populace stagnuje", + "beehive-info-population-status-slowgrowth": "Populace pomalu roste", + "beehive-info-population-status-growing": "Populace roste", + "beehive-info-honey-production": "Produkce medu: >>>framesPerDay<<>>daysPerFrame<<< dní na rámek)", + "beehive-info-honey-production-simple": "Produkce medu: >>>framesPerDay<<>>feedRemainingPercent<<<% (>>>daysPerFrame<<< dní na rámek)", + "blockinfo-in-range-beehive": "V dosahu {p0:# úlů|# úlu|# úlů}", + "blockinfo-growth-speed-bonus": "{0}% rychlost růstu", + "blockinfo-yield-bonus": "{0}% výnos", + "blockinfo-will-be-pollinated": "Bude opyleno za {p0:# další fáze růstu|# další fázi růstu|# další fáze růstu}", + "blockinfo-pollinated": "Opyleno", + "beehiveScoutingProgress": "Postup průzkumu", + "handbook-beehive": "- Střecha
- Stojan", + "handbook-beehive-title": "Dekorace" +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/en.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/en.json new file mode 100644 index 0000000..cb0fd0f --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/en.json @@ -0,0 +1,137 @@ +{ + "block-beehive-*": "Beehive", + "block-beehivestand-*": "Beehive stand", + "block-beehiveroof-*": "Beehive roof", + "block-beehiveframe-empty": "Empty beehive frame", + "block-beehiveframe-filled": "Filled beehive frame", + "block-beehiveframe-filled-feed": "Beehive frame with feed", + "block-beeswarm-*": "Bee swarm", + "beehiveframe-feed-percent": ">>>percent<<<%", + "beehive-info-no-bees": "No bees. Use a populated skep to populate the beehive.", + "beehive-info-no-bees-use-skep-or-wait-for-swarm": "No bees. Use a populated skep to populate the beehive, or wait for a swarm.", + "beehive-info-frames-boosting": "The stored honey is boosting population growth.", + "beehive-info-frames-sacrificing": "The bees are sacrificing half their population growth to make honey.", + "beehive-info-population": "Bee population: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>flowersAround<<<, Crops: >>>cropsAround<<<, Effectively: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<>>flowersAround<<<, Crops: >>>cropsAround<<<", + "beehive-info-flowers-scanning-suffix": "still scouting the area", + "beehive-info-flowers-warning-causing-deaths": "Lack of flowers is causing the population to drop. Plant >>>flowersNeeded<<< more flowers nearby.", + "beehive-info-flowers-warning-suboptimal": "The beehive would be more productive if there were more flowers nearby.", + "beehive-info-frames-mixed": "Frames: >>>filledFrames<<< filled (+>>>filledFrameBonus<<< bees/day), >>>emptyFrames<<< empty (-0.5x bees/day)", + "beehive-info-frames-filled": "Frames: >>>filledFrames<<< filled (+>>>filledFrameBonus<<< bees/day)", + "beehive-info-frames-empty": "Frames: >>>emptyFrames<<< empty (-0.5x bees/day)", + "beehive-info-frames-total": "Frames: >>>totalFrames<<<", + "beehive-info-progress-filling": "Frame progress: >>>honeyProgress<<<% (fills in >>>daysToFill<<< days)", + "beehive-info-progress-noproduction": "Frame progress: >>>honeyProgress<<<% (no production - need bees and flowers)", + "beehive-info-progress-v1-noproduction": "No honey production", + "beehive-info-progress-v1-lessthanday": "Will produce a filled frame in less than a day", + "beehive-info-progress-v1-days": "Will produce a filled frame in >>>days<<< days", + "beehive-info-all-filled": "All frames filled! Add empty frames to continue production.", + "beehive-info-no-frames": "No frames in hive. Add empty frames to start production.", + "beehive-info-temperature": "Temperature: >>>temperature<<<°C (>>>multiplier<<>>changeSign<<<>>>netChange<<>>dailyGrowth<<<, decrease: ->>>dailyDeaths<<<)", + "beehive-info-population-status-decreasing": "Decreasing population", + "beehive-info-population-status-stagnant": "Stagnant population", + "beehive-info-population-status-slowgrowth": "Slowly increasing population", + "beehive-info-population-status-growing": "Growing population", + "beehive-info-incoming-swarm": "A swarm is moving into this beehive.", + "beehive-info-source-swarm-forming": "Some of the bees are forming a swarm nearby.", + "beehive-info-recently-swarmed": "This beehive has swarmed in the last {p0:# days|# day|# days}.", + "beehive-info-ready-waiting-morning": "Ready to swarm, waiting for morning.", + "beehive-info-ready-to-swarm": "Ready to swarm.", + "beehive-info-honey-production": "Honey production: >>>framesPerDay<<>>daysPerFrame<<< days per frame)", + "beehive-info-honey-production-simple": "Honey production: >>>framesPerDay<<>>feedRemainingPercent<<<% (>>>daysPerFrame<<< days per frame)", + "blockinfo-in-range-beehive": "In range of {p0:# beehives|# beehive|# beehives}", + "blockinfo-as-a-plant": "as a {0}", + "blockinfo-flower": "flower", + "blockinfo-crop": "crop", + "blockinfo-growth-speed-bonus": "{0}% growth speed", + "blockinfo-yield-bonus": "{0}% yield", + "blockinfo-crop-boost-effectiveness": "Crop boost effectiveness: {0}", + "blockinfo-crop-boost-result-yield": "+{0}% yield", + "blockinfo-crop-boost-result-speed": "+{0}% speed", + "blockinfo-crop-boost-level-low": "Low", + "blockinfo-crop-boost-level-medium": "Medium", + "blockinfo-crop-boost-level-high": "High", + "blockinfo-crop-boost-population-too-low": "Population too low for a crop boost", + "blockinfo-will-be-pollinated": "Will be pollinated in {p0:# more growth stages|# more growth stage|# more growth stages}", + "blockinfo-pollinated": "Pollinated", + "beehiveScoutingProgress": "Scouting progress", + "beeswarm-info-population": "Swarm population: {0}", + "beeswarm-info-eligible-targets": "Eligible new homes in the area: {0}", + "beeswarm-phase-forming": "The swarm is forming ({0})", + "beeswarm-phase-scouting": "The swarm is scouting for a new home ({0})", + "beeswarm-phase-moving": "The swarm is moving to a new home ({0})", + "beeswarm-hours-left": "{0} hours left", + "beeswarm-less-than-hour-left": "less than 1 hour left", + + "handbook-beehive": "- Roof
- Stand", + "handbook-beehive-title": "Decorations", + + "config-desc-InformationVerbosity": "How much information should be shown when hovering over a beehive block.", + "config-desc-DisableServerRecommended": "If true, ignores InformationVerbosityServer when InformationVerbosityServerSideType == ServerRecommended. Has no effect when server mode is ServerForced.", + "config-desc-BeehiveAmbientVolume": "Ambient beehive volume. Note that this may take a while to apply, because it works from the next \"sound reload\".", + "config-desc-InformationVerbosityServerSideType": "Controls whether InformationVerbosity is client-side only, server-recommended, or server-forced.", + "config-desc-InformationVerbosityServer": "Server-side verbosity value used when InformationVerbosityServerSideType is ServerRecommended or ServerForced.", + "config-desc-BeehiveRadius": "The radius within which beehives affect crops, and within which the bees will visit flowers.", + "config-desc-YieldBoost": "Whether beehive proximity gives crops a yield bonus.", + "config-desc-SpeedBoost": "Whether beehive proximity gives crops a growth speed bonus.", + "config-desc-GrowthSpeedBonus": "Growth speed bonus when in beehive range (0.2 = up to 20% faster). Works only with SpeedBoost enabled.", + "config-desc-YieldMultiplier": "Crop yield multiplier when in beehive range (1.5 = 150%). Works only with YieldBoost enabled.", + "config-desc-PlantsFullBoostDistanceToHive": "Spherical range where crops receive the full growth boost. Beyond this, the boost falls off linearly until BeehiveRadius.", + "config-desc-MinBeesForPlantBoost": "Minimum bee population required for a beehive to boost nearby plants.", + "config-desc-BoostCalculation": "How bee population scales crop boost once MinBeesForPlantBoost is reached.", + "config-desc-PopulationPercentForMaxBoost": "Bee population percentage of MaxBeePopulation needed to reach full population-based crop boost.", + "config-desc-MultipleBeehivesBoostCombination": "How crop boost from multiple beehives is combined.", + "config-desc-MaxBeePopulation": "Maximum number of bees in a beehive.", + "config-desc-InitialBeePopulation": "Bee population added when a populated skep is added to the beehive.", + "config-desc-BeehiveConsideredEmptyBelowPopulation": "A beehive is eligible to receive incoming swarms while its population stays below this value.", + "config-desc-PopulationPercentRequirementForSwarm": "Population percent of MaxBeePopulation required before a beehive can start a swarm.", + "config-desc-SwarmPopulationPercentage": "Percent of a beehive's population that leaves to form a swarm.", + "config-desc-SwarmCooldownDays": "Days a beehive must wait after starting a swarm before it can start another one.", + "config-desc-SwarmReturnToOriginOnFailedMigrationPercent": "Percent of swarm bees that return to the origin hive when the swarm fails to find a new hive.", + "config-desc-BaseDeathsPerDay": "Number of bees that die per day regardless of anything.", + "config-desc-FlowerThreshold": "Minimum flowers needed to avoid extra bee deaths.", + "config-desc-DeathPerMissingFlower": "Extra bee deaths per day for each flower below threshold.", + "config-desc-DoublingTimeDays": "Number of days for bee population to double. Note that this is only used when calculating daily positive growth rate.", + "config-desc-BonusGrowthPerFilledFrame": "Extra bees per day per filled frame.", + "config-desc-ReferenceFlowers": "Reference flower count for honey production rate (10 flowers + 25000 bees = 1 frame/day).", + "config-desc-ReferenceBees": "Reference bee count for honey production rate (10 flowers + 25000 bees = 1 frame/day).", + "config-desc-MaxFlowersForHoneyProduction": "Maximum number of flowers counted for honey production calculations.", + "config-desc-WinterHardMode": "Enables winter hard mode for additional winter mechanics.", + "config-desc-WinterDailyBeeDeathsWithoutFood": "Extra daily bee deaths in winter when there are no filled/feed frames. Works only with WinterHardMode enabled.", + "config-desc-WinterFoodConsumptionMultiplier": "Winter food consumption multiplier (0-1). From 10C to 0C this effect ramps up in reverse relative to honey production; below 0C it stays at max. Works only with WinterHardMode enabled.", + "config-desc-MinTemperatureGrowth": "Temperature at which bee growth and honey production stops.", + "config-desc-MaxTemperatureGrowth": "Temperature at which bee growth and honey production reaches maximum.", + "config-desc-GreenhouseAffectsBeehive": "Whether greenhouses give 5C temperature boost to the beehive.", + "config-desc-BeesPerParticle": "Number of bees represented by each particle group. If this is 1000, there are 5000 bees in a beehive, then the beehive will spawn at most 5 particles.", + "config-desc-BeehiveAlwaysSpawnNumberOfBees": "Will spawn this amount of bees per beehive block, regardless of their population.", + "beehive-info-swarms-disabled": "Swarming is disabled on this beehive.", + "beehive-info-pre-swarm-building": "Bees are preparing to swarm.", + "beehive-info-pre-swarm-progress": "Preparing to swarm ({0})", + "beehive-info-pre-swarm-time-lessthanhourleft": "less than an hour left", + "beehive-info-pre-swarm-time-hours": "{0} hours left", + "beehive-info-pre-swarm-time-1day": "1 day left", + "beehive-info-pre-swarm-time-days": "{0} days left", + "beehive-info-pre-swarm-paused": "paused due to weather conditions", + "blockhelp-beehive-enable-swarm": "Enable swarming", + "blockhelp-beehive-disable-swarm": "Disable swarming", + "config-desc-EnableSwarms": "Whether beehives can produce swarms.", + "config-desc-PreSwarmDurationHours": "Duration in hours for bees to build up their swarm urge (0 to 100%). Progress only increases at MaxTemperatureGrowth or higher; otherwise it decreases.", + "config-desc-SwarmSettingAfterPlacing": "Whether a newly placed beehive has swarming enabled or disabled. Can be changed with a wrench.", + "setpopulation-desc": "Set bee population of the beehive block you are currently looking at.", + "debugunload-desc": "Enable or disable unload catch-up debug messages in server chat.", + "plantreg-blockpertick-desc": "Get or set plant registry scan BlocksPerTick (0-1000).", + "plantreg-blockpertick-parse-error": "Couldn't parse. Use an integer in range 0-1000.", + "plantreg-unavailable": "Plant registry mod system is not available." +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/pl.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/pl.json new file mode 100644 index 0000000..1427250 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/pl.json @@ -0,0 +1,52 @@ +{ + "block-beehive-*": "Ul", + "block-beehivestand-*": "Stojak na ul", + "block-beehiveroof-*": "Daszek ulu", + "block-beehiveframe-empty": "Pusta ramka ulu", + "block-beehiveframe-filled": "Wypełniona ramka ulu", + "block-beehiveframe-filled-feed": "Ramka ulu z jedzeniem dla pszczół", + "beehiveframe-feed-percent": ">>>percent<<<%", + "beehive-info-no-bees": "Brak pszczół. Użyj na ulu zamieszkanej kószki aby wprowadzić populację.", + "beehive-info-frames-boosting": "Przechowywany miód wzmacnia wzrost populacji.", + "beehive-info-frames-sacrificing": "Pszczoły poświęcają połowę wzrostu populacji, aby produkować miód.", + "beehive-info-population": "Populacja pszczół: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>flowersAround<<<, Uprawy: >>>cropsAround<<<, Efektywnie: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<>>flowersAround<<<, Uprawy: >>>cropsAround<<<", + "beehive-info-flowers-scanning-suffix": "(wciąż trwa sprawdzanie okolicy)", + "beehive-info-frames-mixed": "Wypełnione ramki: >>>filledFrames<<< (+>>>filledFrameBonus<<< pszczół/dzień), Puste: >>>emptyFrames<<< (-0.5x pszczół/dzień)", + "beehive-info-frames-filled": "Wypełnione ramki: >>>filledFrames<<< (+>>>filledFrameBonus<<< pszczół/dzień)", + "beehive-info-frames-empty": "Puste ramki: >>>emptyFrames<<< (-0.5x pszczół/dzień)", + "beehive-info-frames-total": "Ramki: >>>totalFrames<<<", + "beehive-info-progress-filling": "Postęp ramki: >>>honeyProgress<<<% (wypełni się za >>>daysToFill<<< dni)", + "beehive-info-progress-noproduction": "Postęp ramki: >>>honeyProgress<<<% (brak produkcji - potrzeba pszczół i kwiatów)", + "beehive-info-progress-v1-noproduction": "Brak produkcji miodu", + "beehive-info-progress-v1-lessthanday": "Wyprodukuje wypełnioną ramkę za mniej niż dzień", + "beehive-info-progress-v1-days": "Wyprodukuje wypełnioną ramkę za >>>days<<< dni", + "beehive-info-all-filled": "Wszystkie ramki wypełnione", + "beehive-info-no-frames": "Brak ramek w ulu", + "beehive-info-temperature": "Temperatura: >>>temperature<<<°C (>>>multiplier<<>>changeSign<<<>>>netChange<<>>dailyGrowth<<<, spadek: ->>>dailyDeaths<<<)", + "beehive-info-population-status-decreasing": "Malejąca populacja", + "beehive-info-population-status-stagnant": "Stagnująca populacja", + "beehive-info-population-status-slowgrowth": "Powoli rosnąca populacja", + "beehive-info-population-status-growing": "Rosnąca populacja", + "beehive-info-honey-production": "Produkcja miodu: >>>framesPerDay<<>>daysPerFrame<<< dni na ramkę)", + "beehive-info-honey-production-simple": "Produkcja miodu: >>>framesPerDay<<>>feedRemainingPercent<<<% (>>>daysPerFrame<<< dni na ramkę)", + "blockinfo-in-range-beehive": "W zasięgu {p0:# uli|# ula|# uli}", + "blockinfo-growth-speed-bonus": "{0}% szybkości wzrostu", + "blockinfo-yield-bonus": "{0}% plonów", + "blockinfo-will-be-pollinated": "Zostanie zapylone za {p0:# etapów wzrostu|# etap wzrostu|# etapy wzrostu}", + "blockinfo-pollinated": "Zapylone", + "beehiveScoutingProgress": "Rozpoznanie terenu", + "handbook-beehive": "- Daszek
- Stojak", + "handbook-beehive-title": "Urozmaicenia" +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/ru.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/ru.json new file mode 100644 index 0000000..9f3b73b --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/ru.json @@ -0,0 +1,137 @@ +{ + "block-beehive-*": "Улей", + "block-beehivestand-*": "Подставка для улья", + "block-beehiveroof-*": "Крыша улья", + "block-beehiveframe-empty": "Пустая рамка улья", + "block-beehiveframe-filled": "Заполненная рамка улья", + "block-beehiveframe-filled-feed": "Кормовая рамка улья", + "block-beeswarm-*": "Пчелиный рой", + "beehiveframe-feed-percent": ">>>percent<<<%", + "beehive-info-no-bees": "Пчёл нет. Используйте заселённую корзину, чтобы заселить улей.", + "beehive-info-no-bees-use-skep-or-wait-for-swarm": "Пчёл нет. Используйте заселённый плетённый улей или дождитесь роя.", + "beehive-info-frames-boosting": "Запасённый мёд ускоряет рост популяции.", + "beehive-info-frames-sacrificing": "Пчёлы жертвуют половиной прироста популяции ради производства мёда.", + "beehive-info-population": "Популяция пчёл: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>flowersAround<<<, посевы: >>>cropsAround<<<, эффективно: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<>>flowersAround<<<, посевы: >>>cropsAround<<<", + "beehive-info-flowers-scanning-suffix": "(область всё ещё исследуется)", + "beehive-info-flowers-warning-causing-deaths": "Нехватка цветов приводит к сокращению популяции. Посадите ещё >>>flowersNeeded<<< цветов поблизости.", + "beehive-info-flowers-warning-suboptimal": "Улей был бы продуктивнее, если бы поблизости было больше цветов.", + "beehive-info-frames-mixed": "Рамки: >>>filledFrames<<< заполнено (+>>>filledFrameBonus<<< пчёл/день), >>>emptyFrames<<< пусто (-0.5x пчёл/день)", + "beehive-info-frames-filled": "Рамки: >>>filledFrames<<< заполнено (+>>>filledFrameBonus<<< пчёл/день)", + "beehive-info-frames-empty": "Рамки: >>>emptyFrames<<< пусто (-0.5x пчёл/день)", + "beehive-info-frames-total": "Рамки: всего >>>totalFrames<<<", + "beehive-info-progress-filling": "Прогресс заполнения рамки: >>>honeyProgress<<<% (заполнится через >>>daysToFill<<< дн.)", + "beehive-info-progress-noproduction": "Прогресс заполнения рамки: >>>honeyProgress<<<% (производство отсутствует — нужны пчёлы и цветы)", + "beehive-info-progress-v1-noproduction": "Производство мёда отсутствует", + "beehive-info-progress-v1-lessthanday": "Заполненная рамка будет получена менее чем за день", + "beehive-info-progress-v1-days": "Заполненная рамка будет получена через >>>days<<< дн.", + "beehive-info-all-filled": "Все рамки заполнены! Добавьте пустые рамки, чтобы продолжить сбор мёда.", + "beehive-info-no-frames": "Нет рамок в улье. Добавьте пустые рамки, чтобы пчёлы начали сбор мёда.", + "beehive-info-temperature": "Температура: >>>temperature<<<°C (>>>multiplier<<>>changeSign<<<>>>netChange<<>>dailyGrowth<<<, спад: - >>>dailyDeaths<<<)", + "beehive-info-population-status-decreasing": "Сокращение популяции", + "beehive-info-population-status-stagnant": "Популяция стабильна", + "beehive-info-population-status-slowgrowth": "Популяция медленно увеличивается", + "beehive-info-population-status-growing": "Растущая популяция", + "beehive-info-incoming-swarm": "В этот улей вселяется рой.", + "beehive-info-source-swarm-forming": "Некоторые пчелы собираются в рой неподалеку.", + "beehive-info-recently-swarmed": "Этот улей роился в последние {p0:# дней|# день|# дней}.", + "beehive-info-ready-waiting-morning": "Готовые к роению, ожидают утра.", + "beehive-info-ready-to-swarm": "Готовы к роению.", + "beehive-info-honey-production": "Производство мёда: >>>framesPerDay<<>>daysPerFrame<<< дн. на рамку)", + "beehive-info-honey-production-simple": "Производство мёда: >>>framesPerDay<<>>feedRemainingPercent<<<% (>>>daysPerFrame<<< дн. на рамку)", + "blockinfo-in-range-beehive": "В радиусе действия {p0:# ульев|# улья|# ульев}", + "blockinfo-as-a-plant": "как {0}", + "blockinfo-flower": "цветок", + "blockinfo-crop": "урожай", + "blockinfo-growth-speed-bonus": "{0}% к скорости роста", + "blockinfo-yield-bonus": "{0}% к урожайности", + "blockinfo-crop-boost-effectiveness": "Повышение урожайности: {0}", + "blockinfo-crop-boost-result-yield": "+{0}% урожайности", + "blockinfo-crop-boost-result-speed": "+{0}% скорости роста", + "blockinfo-crop-boost-level-low": "Низкое", + "blockinfo-crop-boost-level-medium": "Среднее", + "blockinfo-crop-boost-level-high": "Высокое", + "blockinfo-crop-boost-population-too-low": "Популяция слишком мала для повышения урожая", + "blockinfo-will-be-pollinated": "Будет опылено через {p0:# стадии роста|# стадию роста|# стадии роста}", + "blockinfo-pollinated": "Опылено", + "beehiveScoutingProgress": "Прогресс разведки", + "beeswarm-info-population": "Популяция роя: {0}", + "beeswarm-info-eligible-targets": "Подходящие новые места в этом районе: {0}", + "beeswarm-phase-forming": "Формируется рой ({0})", + "beeswarm-phase-scouting": "Рой ищет новый дом ({0})", + "beeswarm-phase-moving": "Рой перелетает в новый дом ({0})", + "beeswarm-hours-left": "{0} осталось часов", + "beeswarm-less-than-hour-left": "осталось менее часа", + + "handbook-beehive": "- Крыша улья
- Подставка для улья", + "handbook-beehive-title": "Украшения", + + "config-desc-InformationVerbosity": "Какой объём информации должен отображаться при наведении курсора мыши на блок улья.", + "config-desc-DisableServerRecommended": "При значении true InformationVerbosityServer игнорируется, когда InformationVerbosityServerSideType = = ServerRecommended. Не действует, если выбран режим сервера ServerForced.", + "config-desc-BeehiveAmbientVolume": "Громкость окружающего звука улья. Обратите внимание, что для применения этого может потребоваться некоторое время, поскольку это работает после следующей «перезагрузки звука».", + "config-desc-InformationVerbosityServerSideType": "Определяет, является ли параметр InformationVerbosity только клиентским, рекомендуемым сервером или принудительно задаваемым сервером.", + "config-desc-InformationVerbosityServer": "Серверное значение подробности информации, используемое, когда InformationVerbosityServerSideType установлен в ServerRecommended или ServerForced.", + "config-desc-BeehiveRadius": "Радиус, в котором ульи влияют на посевы и в пределах которого пчёлы посещают цветы.", + "config-desc-YieldBoost": "Даёт ли близость улья прибавку к урожайности культур.", + "config-desc-SpeedBoost": "Даёт ли близость улья прибавку к скорости роста культур.", + "config-desc-GrowthSpeedBonus": "Бонус к скорости роста в радиусе улья (0.2 = до 20% быстрее). Работает только при включённом SpeedBoost.", + "config-desc-YieldMultiplier": "Множитель урожайности культур в радиусе улья (1.5 = 150%). Работает только при включённом YieldBoost.", + "config-desc-PlantsFullBoostDistanceToHive": "Сферический радиус, в котором культуры получают полный бонус роста. Далее бонус линейно уменьшается до расстояния BeehiveRadius.", + "config-desc-MinBeesForPlantBoost": "Минимальное количество пчёл в улье, необходимое для усиления роста ближайших растений.", + "config-desc-BoostCalculation": "Способ масштабирования бонуса роста культур в зависимости от численности пчёл после достижения MinBeesForPlantBoost.", + "config-desc-PopulationPercentForMaxBoost": "Процент от MaxBeePopulation, необходимый для получения максимального бонуса роста культур.", + "config-desc-MultipleBeehivesBoostCombination": "Способ объединения бонусов роста культур от нескольких ульев.", + "config-desc-MaxBeePopulation": "Максимальное количество пчёл в улье.", + "config-desc-InitialBeePopulation": "Численность пчёл, добавляемая в улей при помещении в него заселённого плетёного улья.", + "config-desc-BeehiveConsideredEmptyBelowPopulation": "Если численность пчёл ниже этого значения, улей считается достаточно пустым и может принять прилетающий рой.", + "config-desc-PopulationPercentRequirementForSwarm": "Процент от MaxBeePopulation, необходимый для того, чтобы улей мог выпустить рой.", + "config-desc-SwarmPopulationPercentage": "Процент пчёл, покидающих улей при образовании роя.", + "config-desc-SwarmCooldownDays": "Количество дней после выхода роя, в течение которых улей не может выпустить новый рой.", + "config-desc-SwarmReturnToOriginOnFailedMigrationPercent": "Процент пчёл роя, возвращающихся в исходный улей, если рой не смог заселиться в новый улей.", + "config-desc-BaseDeathsPerDay": "Количество пчёл, погибающих за день независимо от условий.", + "config-desc-FlowerThreshold": "Минимальное количество цветов, необходимое, чтобы избежать дополнительной гибели пчёл.", + "config-desc-DeathPerMissingFlower": "Дополнительная гибель пчёл в день за каждый недостающий цветок ниже порога.", + "config-desc-DoublingTimeDays": "Количество дней, за которое популяция пчёл удваивается. Используется только при расчёте положительного ежедневного прироста.", + "config-desc-BonusGrowthPerFilledFrame": "Дополнительное количество пчёл в день за каждую заполненную рамку.", + "config-desc-ReferenceFlowers": "Базовое количество цветов для расчёта скорости производства мёда (10 цветов + 25000 пчёл = 1 рамка в день).", + "config-desc-ReferenceBees": "Базовое количество пчёл для расчёта скорости производства мёда (10 цветов + 25000 пчёл = 1 рамка в день).", + "config-desc-MaxFlowersForHoneyProduction": "Максимальное количество цветов, учитываемых при расчёте производства мёда.", + "config-desc-WinterHardMode": "Включает усложнённый зимний режим с дополнительными зимними механиками.", + "config-desc-WinterDailyBeeDeathsWithoutFood": "Дополнительная ежедневная гибель пчёл зимой, когда нет заполненных/кормовых рамок. Работает только с включенным WinterHardMode.", + "config-desc-WinterFoodConsumptionMultiplier": "Коэффициент потребления зимних продуктов питания (0-1). При температуре от 10° C до 0° C этот эффект увеличивается в обратном направлении относительно производства мёда; при температуре ниже 0° C он остается максимальным. Работает только при включенном режиме WinterHardMode.", + "config-desc-MinTemperatureGrowth": "Температура, при которой прекращается рост пчёл и производство мёда.", + "config-desc-MaxTemperatureGrowth": "Температура, при которой рост пчёл и производство мёда достигают максимума.", + "config-desc-GreenhouseAffectsBeehive": "Дают ли теплицы повышение температуры в улье на 5°C.", + "config-desc-BeesPerParticle": "Количество пчёл, представленных каждой группой частиц. Если это 1000, а в улье 5000 пчёл, тогда улей породит не более 5 частиц.", + "config-desc-BeehiveAlwaysSpawnNumberOfBees": "Всегда создаёт указанное количество пчёл на блок улья независимо от их численности.", + "beehive-info-swarms-disabled": "Роение в этом улье отключено.", + "beehive-info-pre-swarm-building": "Пчёлы готовятся выпустить рой.", + "beehive-info-pre-swarm-progress": "Подготовка к выходу роя ({0})", + "beehive-info-pre-swarm-time-lessthanhourleft": "меньше часа", + "beehive-info-pre-swarm-time-hours": "осталось {0} ч", + "beehive-info-pre-swarm-time-1day": "остался 1 день", + "beehive-info-pre-swarm-time-days": "осталось {0} дн", + "beehive-info-pre-swarm-paused": "приостановлено из-за погодных условий", + "blockhelp-beehive-enable-swarm": "Включить роение", + "blockhelp-beehive-disable-swarm": "Отключить роение", + "config-desc-EnableSwarms": "Определяет, могут ли ульи выпускать рои.", + "config-desc-PreSwarmDurationHours": "Время в часах, за которое у пчёл накапливается готовность к роению (0–100%). Прогресс растёт только при MaxTemperatureGrowth или выше, иначе уменьшается.", + "config-desc-SwarmSettingAfterPlacing": "Будет ли в только что установленном улье включено или отключено роение. Можно изменить гаечным ключом.", + "setpopulation-desc": "Установить численность пчёл в улье, на который вы сейчас смотрите.", + "debugunload-desc": "Включить или отключить отладочные сообщения догоняющей обработки после выгрузки в чате сервера.", + "plantreg-blockpertick-desc": "Показать или задать параметр BlocksPerTick (0–1000) для сканирования реестра растений.", + "plantreg-blockpertick-parse-error": "Не удалось распознать значение. Используйте целое число от 0 до 1000.", + "plantreg-unavailable": "Система реестра растений недоступна." +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/uk.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/uk.json new file mode 100644 index 0000000..7d4a4ae --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/lang/uk.json @@ -0,0 +1,52 @@ +{ + "block-beehive-*": "Вулик", + "block-beehivestand-*": "Підставка для вулика", + "block-beehiveroof-*": "Дах вулика", + "block-beehiveframe-empty": "Порожня рамка вулика", + "block-beehiveframe-filled": "Наповнена рамка вулика", + "block-beehiveframe-filled-feed": "Рамка вулика з поживкою", + "beehiveframe-feed-percent": ">>>percent<<<%", + "beehive-info-no-bees": "Бджіл немає. Використовуйте заселену сапетку, щоб заселити вулик.", + "beehive-info-frames-boosting": "Запаси меду сприяють зростанню чисельності населення.", + "beehive-info-frames-sacrificing": "Бджоли жертвують половиною свого приросту популяції, щоб виробляти мед.", + "beehive-info-population": "Популяція бджіл: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<>>flowersAround<<<, Врожай: >>>cropsAround<<<, Effectively: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<>>flowersAround<<<, Врожай: >>>cropsAround<<<", + "beehive-info-flowers-scanning-suffix": "(все ще розвідує територію)", + "beehive-info-frames-mixed": "Рамки: >>>filledFrames<<< наповнена (+>>>filledFrameBonus<<< бджіл/день), >>>emptyFrames<<< порожня (-0.5x бджіл/день)", + "beehive-info-frames-filled": "Рамки: >>>filledFrames<<< наповнена (+>>>filledFrameBonus<<< бджіл/день)", + "beehive-info-frames-empty": "Рамки: >>>emptyFrames<<< порожня (-0.5x бджіл/день)", + "beehive-info-frames-total": "Рамки: >>>totalFrames<<<", + "beehive-info-progress-filling": "Поступ рамки: >>>honeyProgress<<<% (fills in >>>daysToFill<<< днів)", + "beehive-info-progress-noproduction": "Поступ рамки: >>>honeyProgress<<<% (немає виробництва - потрібні бджоли і квіти)", + "beehive-info-progress-v1-noproduction": "Виробництво меду відсутнє", + "beehive-info-progress-v1-lessthanday": "Рамка заповниться менш ніж за день", + "beehive-info-progress-v1-days": "Заповнить рамку за >>>days<<< днів", + "beehive-info-all-filled": "Всі рамки заповнені! Додайте порожні рамки, щоб продовжити виробництво.", + "beehive-info-no-frames": "У вулику немає рамок. Додайте порожні рамки, щоб розпочати виробництво.", + "beehive-info-temperature": "Температура: >>>temperature<<<°C (>>>multiplier<<>>changeSign<<<>>>netChange<<>>dailyGrowth<<<, decrease: ->>>dailyDeaths<<<)", + "beehive-info-population-status-decreasing": "Зменшення популяції", + "beehive-info-population-status-stagnant": "Стагнація популяції", + "beehive-info-population-status-slowgrowth": "Повільне зростання популяції", + "beehive-info-population-status-growing": "Зростання популяції", + "beehive-info-honey-production": "Виробництво меду: >>>framesPerDay<<>>daysPerFrame<<< днів на рамку)", + "beehive-info-honey-production-simple": "Виробництво меду: >>>framesPerDay<<>>feedRemainingPercent<<<% (>>>daysPerFrame<<< днів на рамку)", + "blockinfo-in-range-beehive": "У діапазоні {p0:# вуликів|# вулик|# вуликів}", + "blockinfo-growth-speed-bonus": "{0}% швидкість росту", + "blockinfo-yield-bonus": "{0}% урожаю", + "blockinfo-will-be-pollinated": "Буде запилюватися {p0:# на пізніших стадіях росту|# на пізнішій стадії росту|# на пізніших стадіях росту}", + "blockinfo-pollinated": "Запилений", + "beehiveScoutingProgress": "Прогрес розвідки", + "handbook-beehive": "- Дах
- підставка", + "handbook-beehive-title": "Прикраси" +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/farmland.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/farmland.json new file mode 100644 index 0000000..1a425d0 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/farmland.json @@ -0,0 +1,24 @@ +[ + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/soil/farmland", + "side": "Server" + }, + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorBeehiveYieldMultiplier" + } + ], + "file": "game:blocktypes/soil/farmland", + "side": "Server" + } +] diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/flowers.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/flowers.json new file mode 100644 index 0000000..94c1412 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/flowers.json @@ -0,0 +1,13 @@ +[ + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/plant/**", + "side": "Server" + } +] diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/plantcontainers.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/plantcontainers.json new file mode 100644 index 0000000..dcc321d --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/plantcontainers.json @@ -0,0 +1,46 @@ +[ + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/clay/flowerpot-fancy", + "side": "Server" + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/clay/fired/flowerpot", + "side": "Server" + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/clay/planter-fancy", + "side": "Server" + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorBeehiveAffected" + } + ], + "file": "game:blocktypes/clay/fired/planter", + "side": "Server" + } +] diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/skep.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/skep.json new file mode 100644 index 0000000..4dfeb92 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/patches/skep.json @@ -0,0 +1,13 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorVanillaSkepSwarmTarget" + } + ], + "file": "game:blocktypes/reed/skep", + "side": "Server" + } +] diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehive.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehive.json new file mode 100644 index 0000000..a7cc630 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehive.json @@ -0,0 +1,11 @@ +{ + "ingredientPattern": "PNP,PSP,P P", + "ingredients": { + "N": { "type": "item", "code": "game:metalnailsandstrips-*", "quantity": 2 }, + "P": { "type": "item", "code": "game:plank-*", "quantity": 2 }, + "S": { "type": "item", "code": "game:beeswax" } + }, + "width": 3, + "height": 3, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehive-north" } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveframe.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveframe.json new file mode 100644 index 0000000..ff8cbfd --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveframe.json @@ -0,0 +1,68 @@ +[ + { + "ingredientPattern": "PPP,PLP,PPP", + "ingredients": { + "P": { "type": "item", "code": "game:plank-*" }, + "L": { "type": "block", "code": "game:linen-*" } + }, + "width": 3, + "height": 3, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-empty", "quantity": 1 } + }, + { + "ingredientPattern": "PL", + "ingredients": { + "P": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-empty", "quantity": 1 }, + "L": { "type": "item", "code": "game:honeycomb", "quantity": 1 } + }, + "width": 2, + "height": 1, + "shapeless": true, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-filled", "quantity": 1 } + }, + { + "ingredientPattern": "PL", + "ingredients": { + "P": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-empty", "quantity": 1 }, + "L": { "type": "item", "code": "game:beeswax", "quantity": 2 } + }, + "width": 2, + "height": 1, + "shapeless": true, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-filled-feed", "quantity": 1 } + }, + { + "ingredientPattern": "PL", + "ingredients": { + "P": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-empty", "quantity": 1 }, + "L": { "type": "block", "code": "game:bowl-*-fired" } + }, + "attributes": { + "liquidContainerProps": { + "requiresContent": { "type": "item", "code": "honeyportion" }, + "requiresLitres": 0.4 + } + }, + "width": 2, + "height": 1, + "shapeless": true, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-filled-feed", "quantity": 1 } + }, + { + "ingredientPattern": "PL", + "ingredients": { + "P": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-empty", "quantity": 1 }, + "L": { "type": "block", "code": "game:woodbucket" } + }, + "attributes": { + "liquidContainerProps": { + "requiresContent": { "type": "item", "code": "honeyportion" }, + "requiresLitres": 0.4 + } + }, + "width": 2, + "height": 1, + "shapeless": true, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveframe-filled-feed", "quantity": 1 } + } +] diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveroof.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveroof.json new file mode 100644 index 0000000..7924b56 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehiveroof.json @@ -0,0 +1,10 @@ +{ + "ingredientPattern": "_B_,___,P_P", + "ingredients": { + "B": { "type": "item", "code": "game:plank-*", "quantity": 2 }, + "P": { "type": "item", "code": "game:plank-*" } + }, + "width": 3, + "height": 3, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehiveroof-north", "quantity": 1 } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehivestand.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehivestand.json new file mode 100644 index 0000000..ed82cd7 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/beehivestand.json @@ -0,0 +1,10 @@ +{ + "ingredientPattern": "B_B,P_P", + "ingredients": { + "B": { "type": "item", "code": "game:plank-*", "quantity": 2 }, + "P": { "type": "item", "code": "game:plank-*" } + }, + "width": 3, + "height": 2, + "output": { "type": "block", "code": "orekiwoofsbeehives:beehivestand-north", "quantity": 1 } +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/honeycomb.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/honeycomb.json new file mode 100644 index 0000000..6b3e0a9 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/recipes/grid/honeycomb.json @@ -0,0 +1,25 @@ +{ + "ingredientPattern": "K_,F_", + "ingredients": { + "K": { + "type": "item", + "code": "game:knife-*", + "isTool": true + }, + "F": { + "type": "block", + "code": "orekiwoofsbeehives:beehiveframe-filled", + "returnedStack": { + "type": "block", + "code": "orekiwoofsbeehives:beehiveframe-empty" + } + } + }, + "width": 2, + "height": 2, + "output": { + "type": "item", + "code": "game:honeycomb", + "quantity": 1 + } +} \ No newline at end of file diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-closed.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-closed.json new file mode 100644 index 0000000..6877c36 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-closed.json @@ -0,0 +1,155 @@ +{ + "editor": { + "allAngles": true + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "wood": "game:block/wood/henbox/sides" + }, + "elements": [ + { + "name": "BottomLeft", + "from": [ 0, 0, 0 ], + "to": [ 7, 6, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 9, 10, 16, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 0, 10, 7, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 0, 0, 7, 16 ] }, + "down": { "texture": "#wood", "uv": [ 0, 0, 7, 16 ] } + } + }, + { + "name": "BottomRight", + "from": [ 9, 0, 0 ], + "to": [ 16, 6, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 10, 7, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 9, 10, 16, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 9, 0, 16, 16 ] }, + "down": { "texture": "#wood", "uv": [ 9, 0, 16, 16 ] } + } + }, + { + "name": "BottomCenterLower", + "from": [ 7, 0, 1 ], + "to": [ 9, 2.5, 16 ], + "faces": { + "east": { "texture": "#wood", "uv": [ 1, 13.5, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 7, 13.5, 9, 16 ] }, + "west": { "texture": "#wood", "uv": [ 1, 13.5, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] }, + "down": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] } + } + }, + { + "name": "BottomCenterUpper", + "from": [ 7, 4.5, 1 ], + "to": [ 9, 6, 16 ], + "faces": { + "east": { "texture": "#wood", "uv": [ 1, 10, 16, 11.5 ] }, + "south": { "texture": "#wood", "uv": [ 7, 10, 9, 11.5 ] }, + "west": { "texture": "#wood", "uv": [ 1, 10, 16, 11.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] } + } + }, + { + "name": "BottomFrontLower", + "from": [ 7, 0, 0 ], + "to": [ 9, 2.5, 1.5 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 13.5, 9, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 13.5, 1.5, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 13.5, 1.5, 16 ] }, + "up": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] }, + "down": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] } + } + }, + { + "name": "BottomFrontUpper", + "from": [ 7, 4.5, 0 ], + "to": [ 9, 6, 1.5 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 10, 9, 11.5 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 1.5, 11.5 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 1.5, 11.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] }, + "down": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] } + } + }, + { + "name": "BottomIndentBack", + "from": [ 7, 2.5, 1.5 ], + "to": [ 9, 4.5, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 11.5, 9, 13.5 ] }, + "east": { "texture": "#wood", "uv": [ 1.5, 11.5, 16, 13.5 ] }, + "south": { "texture": "#wood", "uv": [ 7, 11.5, 9, 13.5 ] }, + "west": { "texture": "#wood", "uv": [ 1.5, 11.5, 16, 13.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1.5, 9, 16 ] }, + "down": { "texture": "#wood", "uv": [ 7, 1.5, 9, 16 ] } + } + }, + { + "name": "Top", + "from": [ 0, 14, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "east": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "south": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "west": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "up": { "texture": "#wood", "uv": [ 0, 0, 16, 16 ] }, + "down": { "texture": "#wood", "uv": [ 0, 0, 16, 16 ] } + } + }, + { + "name": "Back", + "from": [ 0, 6, 14 ], + "to": [ 16, 14, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 2, 16, 10 ] }, + "east": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "south": { "texture": "#wood", "uv": [ 0, 2, 16, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] } + } + }, + { + "name": "Right", + "from": [ 0, 6, 0 ], + "to": [ 2, 14, 14 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] }, + "east": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] }, + "south": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] } + } + }, + { + "name": "Left", + "from": [ 14, 6, 0 ], + "to": [ 16, 14, 14 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "east": { "texture": "#wood", "uv": [ 2, 2, 16, 10 ] }, + "south": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] } + } + }, + { + "name": "Front", + "from": [ 2, 6, 1 ], + "to": [ 14, 14, 2 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 2, 2, 14, 10 ] }, + "east": { "texture": "#wood", "uv": [ 0.5, 2, 2, 10 ] }, + "south": { "texture": "#wood", "uv": [ 2, 2, 14, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0.5, 2, 2, 10 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-open.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-open.json new file mode 100644 index 0000000..4096d21 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehive-open.json @@ -0,0 +1,144 @@ +{ + "editor": { + "allAngles": true + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "wood": "game:block/wood/henbox/sides" + }, + "elements": [ + { + "name": "BottomLeft", + "from": [ 0, 0, 0 ], + "to": [ 7, 6, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 9, 10, 16, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 0, 10, 7, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 0, 0, 7, 16 ] }, + "down": { "texture": "#wood", "uv": [ 0, 0, 7, 16 ] } + } + }, + { + "name": "BottomRight", + "from": [ 9, 0, 0 ], + "to": [ 16, 6, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 10, 7, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 9, 10, 16, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 9, 0, 16, 16 ] }, + "down": { "texture": "#wood", "uv": [ 9, 0, 16, 16 ] } + } + }, + { + "name": "BottomCenterLower", + "from": [ 7, 0, 1 ], + "to": [ 9, 2.5, 16 ], + "faces": { + "east": { "texture": "#wood", "uv": [ 1, 13.5, 16, 16 ] }, + "south": { "texture": "#wood", "uv": [ 7, 13.5, 9, 16 ] }, + "west": { "texture": "#wood", "uv": [ 1, 13.5, 16, 16 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] }, + "down": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] } + } + }, + { + "name": "BottomCenterUpper", + "from": [ 7, 4.5, 1 ], + "to": [ 9, 6, 16 ], + "faces": { + "east": { "texture": "#wood", "uv": [ 1, 10, 16, 11.5 ] }, + "south": { "texture": "#wood", "uv": [ 7, 10, 9, 11.5 ] }, + "west": { "texture": "#wood", "uv": [ 1, 10, 16, 11.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1, 9, 16 ] } + } + }, + { + "name": "BottomFrontLower", + "from": [ 7, 0, 0 ], + "to": [ 9, 2.5, 1.5 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 13.5, 9, 16 ] }, + "east": { "texture": "#wood", "uv": [ 0, 13.5, 1.5, 16 ] }, + "west": { "texture": "#wood", "uv": [ 0, 13.5, 1.5, 16 ] }, + "up": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] }, + "down": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] } + } + }, + { + "name": "BottomFrontUpper", + "from": [ 7, 4.5, 0 ], + "to": [ 9, 6, 1.5 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 10, 9, 11.5 ] }, + "east": { "texture": "#wood", "uv": [ 0, 10, 1.5, 11.5 ] }, + "west": { "texture": "#wood", "uv": [ 0, 10, 1.5, 11.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] }, + "down": { "texture": "#wood", "uv": [ 7, 0, 9, 1.5 ] } + } + }, + { + "name": "BottomIndentBack", + "from": [ 7, 2.5, 1.5 ], + "to": [ 9, 4.5, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 7, 11.5, 9, 13.5 ] }, + "east": { "texture": "#wood", "uv": [ 1.5, 11.5, 16, 13.5 ] }, + "south": { "texture": "#wood", "uv": [ 7, 11.5, 9, 13.5 ] }, + "west": { "texture": "#wood", "uv": [ 1.5, 11.5, 16, 13.5 ] }, + "up": { "texture": "#wood", "uv": [ 7, 1.5, 9, 16 ] }, + "down": { "texture": "#wood", "uv": [ 7, 1.5, 9, 16 ] } + } + }, + { + "name": "Top", + "from": [ 0, 14, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "east": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "south": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "west": { "texture": "#wood", "uv": [ 0, 0, 16, 2 ] }, + "up": { "texture": "#wood", "uv": [ 0, 0, 16, 16 ] }, + "down": { "texture": "#wood", "uv": [ 0, 0, 16, 16 ] } + } + }, + { + "name": "Back", + "from": [ 0, 6, 14 ], + "to": [ 16, 14, 16 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 2, 16, 10 ] }, + "east": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "south": { "texture": "#wood", "uv": [ 0, 2, 16, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] } + } + }, + { + "name": "Right", + "from": [ 0, 6, 0 ], + "to": [ 2, 14, 14 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] }, + "east": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] }, + "south": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] } + } + }, + { + "name": "Left", + "from": [ 14, 6, 0 ], + "to": [ 16, 14, 14 ], + "faces": { + "north": { "texture": "#wood", "uv": [ 14, 2, 16, 10 ] }, + "east": { "texture": "#wood", "uv": [ 2, 2, 16, 10 ] }, + "south": { "texture": "#wood", "uv": [ 0, 2, 2, 10 ] }, + "west": { "texture": "#wood", "uv": [ 0, 2, 14, 10 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveframepile.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveframepile.json new file mode 100644 index 0000000..c005c5f --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveframepile.json @@ -0,0 +1,926 @@ +{ + "editor": { + "allAngles": false, + "entityTextureMode": false + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "frame": "game:block/wood-generic", + "center": "game:block/linen" + }, + "elements": [ + { + "name": "Frame-0", + "from": [ 3, 0.45, 5 ], + "to": [ 13, 0.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-0", + "from": [ 2, 0, 11 ], + "to": [ 14, 1, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-0", + "from": [ 2, 0, 4 ], + "to": [ 14, 1, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-0", + "from": [ 2, 0, 5 ], + "to": [ 3, 1, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-0", + "from": [ 13, 0, 5 ], + "to": [ 14, 1, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-1", + "from": [ 3, 1.45, 5 ], + "to": [ 13, 1.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-1", + "from": [ 2, 1, 11 ], + "to": [ 14, 2, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-1", + "from": [ 2, 1, 4 ], + "to": [ 14, 2, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-1", + "from": [ 2, 1, 5 ], + "to": [ 3, 2, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-1", + "from": [ 13, 1, 5 ], + "to": [ 14, 2, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-2", + "from": [ 3, 2.45, 5 ], + "to": [ 13, 2.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-2", + "from": [ 2, 2, 11 ], + "to": [ 14, 3, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-2", + "from": [ 2, 2, 4 ], + "to": [ 14, 3, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-2", + "from": [ 2, 2, 5 ], + "to": [ 3, 3, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-2", + "from": [ 13, 2, 5 ], + "to": [ 14, 3, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-3", + "from": [ 3, 3.45, 5 ], + "to": [ 13, 3.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-3", + "from": [ 2, 3, 11 ], + "to": [ 14, 4, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-3", + "from": [ 2, 3, 4 ], + "to": [ 14, 4, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-3", + "from": [ 2, 3, 5 ], + "to": [ 3, 4, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-3", + "from": [ 13, 3, 5 ], + "to": [ 14, 4, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-4", + "from": [ 3, 4.45, 5 ], + "to": [ 13, 4.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-4", + "from": [ 2, 4, 11 ], + "to": [ 14, 5, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-4", + "from": [ 2, 4, 4 ], + "to": [ 14, 5, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-4", + "from": [ 2, 4, 5 ], + "to": [ 3, 5, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-4", + "from": [ 13, 4, 5 ], + "to": [ 14, 5, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-5", + "from": [ 3, 5.45, 5 ], + "to": [ 13, 5.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-5", + "from": [ 2, 5, 11 ], + "to": [ 14, 6, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-5", + "from": [ 2, 5, 4 ], + "to": [ 14, 6, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-5", + "from": [ 2, 5, 5 ], + "to": [ 3, 6, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-5", + "from": [ 13, 5, 5 ], + "to": [ 14, 6, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-6", + "from": [ 3, 6.45, 5 ], + "to": [ 13, 6.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-6", + "from": [ 2, 6, 11 ], + "to": [ 14, 7, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-6", + "from": [ 2, 6, 4 ], + "to": [ 14, 7, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-6", + "from": [ 2, 6, 5 ], + "to": [ 3, 7, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-6", + "from": [ 13, 6, 5 ], + "to": [ 14, 7, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-7", + "from": [ 3, 7.45, 5 ], + "to": [ 13, 7.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-7", + "from": [ 2, 7, 11 ], + "to": [ 14, 8, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-7", + "from": [ 2, 7, 4 ], + "to": [ 14, 8, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-7", + "from": [ 2, 7, 5 ], + "to": [ 3, 8, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-7", + "from": [ 13, 7, 5 ], + "to": [ 14, 8, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-8", + "from": [ 3, 8.45, 5 ], + "to": [ 13, 8.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-8", + "from": [ 2, 8, 11 ], + "to": [ 14, 9, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-8", + "from": [ 2, 8, 4 ], + "to": [ 14, 9, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-8", + "from": [ 2, 8, 5 ], + "to": [ 3, 9, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-8", + "from": [ 13, 8, 5 ], + "to": [ 14, 9, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-9", + "from": [ 3, 9.45, 5 ], + "to": [ 13, 9.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-9", + "from": [ 2, 9, 11 ], + "to": [ 14, 10, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-9", + "from": [ 2, 9, 4 ], + "to": [ 14, 10, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-9", + "from": [ 2, 9, 5 ], + "to": [ 3, 10, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-9", + "from": [ 13, 9, 5 ], + "to": [ 14, 10, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-10", + "from": [ 3, 10.45, 5 ], + "to": [ 13, 10.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-10", + "from": [ 2, 10, 11 ], + "to": [ 14, 11, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-10", + "from": [ 2, 10, 4 ], + "to": [ 14, 11, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-10", + "from": [ 2, 10, 5 ], + "to": [ 3, 11, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-10", + "from": [ 13, 10, 5 ], + "to": [ 14, 11, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-11", + "from": [ 3, 11.45, 5 ], + "to": [ 13, 11.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-11", + "from": [ 2, 11, 11 ], + "to": [ 14, 12, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-11", + "from": [ 2, 11, 4 ], + "to": [ 14, 12, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-11", + "from": [ 2, 11, 5 ], + "to": [ 3, 12, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-11", + "from": [ 13, 11, 5 ], + "to": [ 14, 12, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-12", + "from": [ 3, 12.45, 5 ], + "to": [ 13, 12.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-12", + "from": [ 2, 12, 11 ], + "to": [ 14, 13, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-12", + "from": [ 2, 12, 4 ], + "to": [ 14, 13, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-12", + "from": [ 2, 12, 5 ], + "to": [ 3, 13, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-12", + "from": [ 13, 12, 5 ], + "to": [ 14, 13, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-13", + "from": [ 3, 13.45, 5 ], + "to": [ 13, 13.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-13", + "from": [ 2, 13, 11 ], + "to": [ 14, 14, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-13", + "from": [ 2, 13, 4 ], + "to": [ 14, 14, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-13", + "from": [ 2, 13, 5 ], + "to": [ 3, 14, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-13", + "from": [ 13, 13, 5 ], + "to": [ 14, 14, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-14", + "from": [ 3, 14.45, 5 ], + "to": [ 13, 14.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-14", + "from": [ 2, 14, 11 ], + "to": [ 14, 15, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-14", + "from": [ 2, 14, 4 ], + "to": [ 14, 15, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-14", + "from": [ 2, 14, 5 ], + "to": [ 3, 15, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-14", + "from": [ 13, 14, 5 ], + "to": [ 14, 15, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + }, + { + "name": "Frame-15", + "from": [ 3, 15.45, 5 ], + "to": [ 13, 15.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder-15", + "from": [ 2, 15, 11 ], + "to": [ 14, 16, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder-15", + "from": [ 2, 15, 4 ], + "to": [ 14, 16, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder-15", + "from": [ 2, 15, 5 ], + "to": [ 3, 16, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder-15", + "from": [ 13, 15, 5 ], + "to": [ 14, 16, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveroof.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveroof.json new file mode 100644 index 0000000..20d8453 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehiveroof.json @@ -0,0 +1,82 @@ +{ + "editor": { + "allAngles": false + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "roof-plank": "game:block/wood/planks/oak1" + }, + "elements": [ + { + "name": "origin", + "from": [ 0.0, 0.0, 0.0 ], + "to": [ 0.0, 0.0, 0.0 ], + "rotationOrigin": [ 8.0, 0.0, 8.0 ], + "faces": {}, + "children": [ + { + "name": "base left", + "from": [ 0.0, 0.0, 0.0 ], + "to": [ 4.5, 2.0, 16.0 ], + "rotationOrigin": [ 0.0, 0.0, 0.1 ], + "faces": { + "north": { "texture": "#roof-plank", "uv": [ 0.0, 14.0, 4.5, 16.0 ] }, + "south": { "texture": "#roof-plank", "uv": [ 0.0, 14.0, 4.5, 16.0 ] }, + "down": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 4.5, 16.0 ] } + } + }, + { + "name": "base center", + "from": [ 4.5, 0.0, 0.0 ], + "to": [ 11.5, 3.0, 16.0 ], + "rotationOrigin": [ 0.0, 0.0, 0.1 ], + "faces": { + "north": { "texture": "#roof-plank", "uv": [ 4.5, 13.0, 11.5, 16.0 ] }, + "south": { "texture": "#roof-plank", "uv": [ 4.5, 13.0, 11.5, 16.0 ] }, + "down": { "texture": "#roof-plank", "uv": [ 4.5, 0.0, 11.5, 16.0 ] } + } + }, + { + "name": "base right", + "from": [ 11.5, 0.0, 0.0 ], + "to": [ 16.0, 2.0, 16.0 ], + "rotationOrigin": [ 0.0, 0.0, 0.1 ], + "faces": { + "north": { "texture": "#roof-plank", "uv": [ 11.5, 14.0, 16.0, 16.0 ] }, + "south": { "texture": "#roof-plank", "uv": [ 11.5, 14.0, 16.0, 16.0 ] }, + "down": { "texture": "#roof-plank", "uv": [ 11.5, 0.0, 16.0, 16.0 ] } + } + }, + { + "name": "slanted left", + "from": [ -5.0, 3.0, -1.001 ], + "to": [ 8.5, 5.25, 17.009 ], + "rotationOrigin": [ 8.0, 4.0, 8.0 ], + "rotationZ": 22.5, + "faces": { + "north": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 13.5, 2.25 ] }, + "south": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 13.5, 2.25 ] }, + "west": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 2.25 ] }, + "up": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 13.5 ], "rotation": 90 }, + "down": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 13.5 ], "rotation": 90 } + } + }, + { + "name": "slanted right", + "from": [ -5.0, 3.0, -1.001 ], + "to": [ 8.5, 5.25, 17.009 ], + "rotationOrigin": [ 8.0, 4.0, 8.0 ], + "rotationY": 180.0, + "rotationZ": 22.5, + "faces": { + "north": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 13.5, 2.25 ] }, + "south": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 13.5, 2.25 ] }, + "west": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 2.25 ] }, + "up": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 13.5 ], "rotation": 90 }, + "down": { "texture": "#roof-plank", "uv": [ 0.0, 0.0, 18.0, 13.5 ], "rotation": 90 } + } + }] + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehivestand.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehivestand.json new file mode 100644 index 0000000..071fd83 --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beehivestand.json @@ -0,0 +1,77 @@ +{ + "editor": { + "allAngles": false + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "all": "game:block/wood/henbox/sides" + }, + "elements": [ + { + "name": "TopPlate", + "from": [ 0, 14, 0 ], + "to": [ 16, 16, 16 ], + "faces": { + "north": { "texture": "#all", "uv": [ 0, 14, 16, 16 ] }, + "east": { "texture": "#all", "uv": [ 0, 14, 16, 16 ] }, + "south": { "texture": "#all", "uv": [ 0, 14, 16, 16 ] }, + "west": { "texture": "#all", "uv": [ 0, 14, 16, 16 ] }, + "up": { "texture": "#all", "uv": [ 0, 0, 16, 16 ] }, + "down": { "texture": "#all", "uv": [ 0, 0, 16, 16 ] } + } + }, + { + "name": "LegFrontLeft", + "from": [ 0, 0, 0 ], + "to": [ 2, 14, 2 ], + "faces": { + "north": { "texture": "#all", "uv": [ 2, 2, 0, 10 ] }, + "east": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "south": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "west": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "up": { "texture": "#all", "uv": [ 0, 0, 2, 2 ] }, + "down": { "texture": "#all", "uv": [ 0, 14, 2, 16 ] } + } + }, + { + "name": "LegFrontRight", + "from": [ 14, 0, 0 ], + "to": [ 16, 14, 2 ], + "faces": { + "north": { "texture": "#all", "uv": [ 16, 2, 14, 10 ] }, + "east": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "south": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "west": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "up": { "texture": "#all", "uv": [ 14, 0, 16, 2 ] }, + "down": { "texture": "#all", "uv": [ 14, 14, 16, 16 ] } + } + }, + { + "name": "LegBackLeft", + "from": [ 0, 0, 14 ], + "to": [ 2, 14, 16 ], + "faces": { + "north": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "east": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "south": { "texture": "#all", "uv": [ 0, 2, 2, 10 ] }, + "west": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "up": { "texture": "#all", "uv": [ 0, 14, 2, 16 ] }, + "down": { "texture": "#all", "uv": [ 0, 0, 2, 2 ] } + } + }, + { + "name": "LegBackRight", + "from": [ 14, 0, 14 ], + "to": [ 16, 14, 16 ], + "faces": { + "north": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "east": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "south": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "west": { "texture": "#all", "uv": [ 14, 2, 16, 10 ] }, + "up": { "texture": "#all", "uv": [ 14, 14, 16, 16 ] }, + "down": { "texture": "#all", "uv": [ 14, 0, 16, 2 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beeswarm.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beeswarm.json new file mode 100644 index 0000000..64d6dec --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/block/beeswarm.json @@ -0,0 +1,64 @@ +{ + "editor": { + "allAngles": false + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "swarm": "game:block/soil/fertcompost" + }, + "elements": [ + { + "name": "layer-1", + "from": [ 4, 4, 0 ], + "to": [ 12, 12, 0.8 ], + "faces": { + "north": { "texture": "#swarm", "uv": [ 4, 4, 12, 12 ] }, + "east": { "texture": "#swarm", "uv": [ 0, 4, 0.8, 12 ] }, + "south": { "texture": "#swarm", "uv": [ 4, 4, 12, 12 ] }, + "west": { "texture": "#swarm", "uv": [ 0, 4, 0.8, 12 ] }, + "up": { "texture": "#swarm", "uv": [ 4, 0, 12, 0.8 ] }, + "down": { "texture": "#swarm", "uv": [ 4, 0, 12, 0.8 ] } + } + }, + { + "name": "layer-2", + "from": [ 5, 5, 0.8 ], + "to": [ 11, 11, 1.6 ], + "faces": { + "north": { "texture": "#swarm", "uv": [ 5, 5, 11, 11 ] }, + "east": { "texture": "#swarm", "uv": [ 0.8, 5, 1.6, 11 ] }, + "south": { "texture": "#swarm", "uv": [ 5, 5, 11, 11 ] }, + "west": { "texture": "#swarm", "uv": [ 0.8, 5, 1.6, 11 ] }, + "up": { "texture": "#swarm", "uv": [ 5, 0.8, 11, 1.6 ] }, + "down": { "texture": "#swarm", "uv": [ 5, 0.8, 11, 1.6 ] } + } + }, + { + "name": "layer-3", + "from": [ 6, 6, 1.6 ], + "to": [ 10, 10, 2.4 ], + "faces": { + "north": { "texture": "#swarm", "uv": [ 6, 6, 10, 10 ] }, + "east": { "texture": "#swarm", "uv": [ 1.6, 6, 2.4, 10 ] }, + "south": { "texture": "#swarm", "uv": [ 6, 6, 10, 10 ] }, + "west": { "texture": "#swarm", "uv": [ 1.6, 6, 2.4, 10 ] }, + "up": { "texture": "#swarm", "uv": [ 6, 1.6, 10, 2.4 ] }, + "down": { "texture": "#swarm", "uv": [ 6, 1.6, 10, 2.4 ] } + } + }, + { + "name": "layer-4", + "from": [ 7, 7, 2.4 ], + "to": [ 9, 9, 3.2 ], + "faces": { + "north": { "texture": "#swarm", "uv": [ 7, 7, 9, 9 ] }, + "east": { "texture": "#swarm", "uv": [ 2.4, 7, 3.2, 9 ] }, + "south": { "texture": "#swarm", "uv": [ 7, 7, 9, 9 ] }, + "west": { "texture": "#swarm", "uv": [ 2.4, 7, 3.2, 9 ] }, + "up": { "texture": "#swarm", "uv": [ 7, 2.4, 9, 3.2 ] }, + "down": { "texture": "#swarm", "uv": [ 7, 2.4, 9, 3.2 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/item/beehiveframe.json b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/item/beehiveframe.json new file mode 100644 index 0000000..f644efd --- /dev/null +++ b/OrekiWoofsBeehives/assets/orekiwoofsbeehives/shapes/item/beehiveframe.json @@ -0,0 +1,70 @@ +{ + "editor": { + "allAngles": true + }, + "textureWidth": 16, + "textureHeight": 16, + "textures": { + "frame": "game:block/wood-generic", + "center": "game:block/linen" + }, + "elements": [ + { + "name": "Frame", + "from": [ 3, 0.45, 5 ], + "to": [ 13, 0.55, 11 ], + "faces": { + "up": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] }, + "down": { "texture": "#center", "uv": [ 3, 5, 13, 11 ] } + } + }, + { + "name": "TopBorder", + "from": [ 2, 0, 11 ], + "to": [ 14, 1, 12 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "west": { "texture": "#frame", "uv": [ 11, 0, 12, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] }, + "down": { "texture": "#frame", "uv": [ 2, 11, 14, 12 ] } + } + }, + { + "name": "BottomBorder", + "from": [ 2, 0, 4 ], + "to": [ 14, 1, 5 ], + "faces": { + "north": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "south": { "texture": "#frame", "uv": [ 2, 0, 14, 1 ] }, + "east": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "west": { "texture": "#frame", "uv": [ 4, 0, 5, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] }, + "down": { "texture": "#frame", "uv": [ 2, 4, 14, 5 ] } + } + }, + { + "name": "LeftBorder", + "from": [ 2, 0, 5 ], + "to": [ 3, 1, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] }, + "down": { "texture": "#frame", "uv": [ 2, 5, 3, 11 ] } + } + }, + { + "name": "RightBorder", + "from": [ 13, 0, 5 ], + "to": [ 14, 1, 11 ], + "faces": { + "east": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "west": { "texture": "#frame", "uv": [ 5, 0, 11, 1 ] }, + "up": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] }, + "down": { "texture": "#frame", "uv": [ 13, 5, 14, 11 ] } + } + } + ] +} diff --git a/OrekiWoofsBeehives/modicon.png b/OrekiWoofsBeehives/modicon.png new file mode 100644 index 0000000000000000000000000000000000000000..db2bbd294cbc38f6785c7274a0a6a6f31ac22af7 GIT binary patch literal 33503 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

lIaveCx{C@ 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 literal 0 HcmV?d00001 diff --git a/OrekiWoofsBeehives/modinfo.json b/OrekiWoofsBeehives/modinfo.json new file mode 100644 index 0000000..09eefc8 --- /dev/null +++ b/OrekiWoofsBeehives/modinfo.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://moddbcdn.vintagestory.at/schema/modinfo.latest.json", + "type": "Code", + "modid": "orekiwoofsbeehives", + "name": "OrekiWoof's Simple Immersive Beehive", + "authors": [ + "OrekiWoof" + ], + "description": "Beehive with 8 slots for honey frames. Boosts your plants.", + "version": "2.0.0-dev.6", + "dependencies": { + "game": "1.21.0" + } +} \ No newline at end of file diff --git a/OrekiWoofsBees.Common/Configs/ChatCommands.cs b/OrekiWoofsBees.Common/Configs/ChatCommands.cs new file mode 100644 index 0000000..05f0242 --- /dev/null +++ b/OrekiWoofsBees.Common/Configs/ChatCommands.cs @@ -0,0 +1,99 @@ +using System; +using System.Reflection; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Server; + +namespace OrekiWoofsBees.Common.Configs; + +public static class ConfigCommands +{ + public static void Register( + IChatCommandApi chatApi, + string commandName, + string langDomain, + bool serverSide, + Func getInstance, + Action saveConfig) + { + Register(chatApi.Create(commandName), chatApi, langDomain, serverSide, getInstance, saveConfig); + } + + public static void Register( + IChatCommand builder, + IChatCommandApi chatApi, + string langDomain, + bool serverSide, + Func getInstance, + Action saveConfig) + { + if (serverSide) + builder = builder.RequiresPrivilege(Privilege.controlserver); + + var p = chatApi.Parsers; + foreach (var prop in typeof(T).GetProperties()) + { + var attr = prop.GetCustomAttribute(); + if (attr == null || attr.ServerSide != serverSide) continue; + + var name = prop.Name; + var descKey = $"{langDomain}:config-desc-{name}"; + + OnCommandDelegate handler; + ICommandArgumentParser parser; + + if (prop.PropertyType == typeof(int)) + { + parser = p.OptionalIntRange("value", (int)attr.Min, (int)attr.Max); + handler = args => HandleOptional(args, name, descKey, + () => $"{prop.GetValue(getInstance())}", + () => { prop.SetValue(getInstance(), (int)args.Parsers[0].GetValue()!); saveConfig(serverSide); }, + () => args.Parsers[0].GetValue() is int); + } + else if (prop.PropertyType == typeof(float)) + { + parser = ParserExtensions.OptionalFloatRange("value", (float)attr.Min, (float)attr.Max); + handler = args => HandleOptional(args, name, descKey, + () => $"{(float)prop.GetValue(getInstance())!:G}", + () => { prop.SetValue(getInstance(), (float)args.Parsers[0].GetValue()!); saveConfig(serverSide); }, + () => args.Parsers[0].GetValue() is float); + } + else if (prop.PropertyType == typeof(bool)) + { + parser = p.OptionalBool("value"); + handler = args => HandleOptional(args, name, descKey, + () => $"{prop.GetValue(getInstance())}", + () => { prop.SetValue(getInstance(), (bool)args.Parsers[0].GetValue()!); saveConfig(serverSide); }, + () => args.Parsers[0].GetValue() is bool); + } + else if (prop.PropertyType == typeof(string)) + { + parser = p.OptionalWordRange("value", attr.AllowedValues); + handler = args => HandleOptional(args, name, descKey, + () => $"{prop.GetValue(getInstance())}", + () => { prop.SetValue(getInstance(), (string)args.Parsers[0].GetValue()!); saveConfig(serverSide); }, + () => args.Parsers[0].GetValue() is string); + } + else continue; + + builder + .BeginSubCommand(name) + .WithDescription(Lang.Get(descKey)) + .WithArgs(parser) + .HandleWith(handler) + .EndSubCommand(); + } + } + + private static TextCommandResult HandleOptional( + TextCommandCallingArgs args, string name, string descKey, + Func formatValue, Action applyAndSave, Func canParse) + { + if (args.Parsers[0].IsMissing) + return TextCommandResult.Success($"{Lang.Get(descKey)}\n{name}={formatValue()}"); + if (!canParse()) + return TextCommandResult.Error("Couldn't parse."); + applyAndSave(); + return TextCommandResult.Success($"{name}={formatValue()}"); + } +} diff --git a/OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs b/OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs new file mode 100644 index 0000000..8463a52 --- /dev/null +++ b/OrekiWoofsBees.Common/Configs/ConfigCommandAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace OrekiWoofsBees.Common.Configs; + +///

+/// Marks a Config property as having a /beehives or .beehives chat command. +/// The registration loop in ChatCommands.cs picks these up. +/// +[AttributeUsage(AttributeTargets.Property)] +public class ConfigCommandAttribute(bool serverSide) : Attribute +{ + /// True = registered as a server command and broadcasts on change. + public bool ServerSide { get; } = serverSide; + + /// Inclusive + public double Min { get; set; } + + /// Inclusive + public double Max { get; set; } + + /// Allowed values for string enum properties (uses OptionalWordRange). + public string[] AllowedValues { get; set; } = []; +} diff --git a/OrekiWoofsBees.Common/Configs/ParserExtensions.cs b/OrekiWoofsBees.Common/Configs/ParserExtensions.cs new file mode 100644 index 0000000..4202b64 --- /dev/null +++ b/OrekiWoofsBees.Common/Configs/ParserExtensions.cs @@ -0,0 +1,16 @@ +using Vintagestory.API.Common; + +namespace OrekiWoofsBees.Common.Configs; + +public static class ParserExtensions +{ + public static FloatArgParser OptionalFloatRange(string argName, float min, float max) + { + return new FloatArgParser(argName, min, max, isMandatoryArg: false); + } + + public static DoubleArgParser OptionalDoubleRange(string argName, double min, double max) + { + return new DoubleArgParser(argName, min, max, isMandatoryArg: false); + } +} diff --git a/OrekiWoofsBees.Common/IPlantPositionRegistry.cs b/OrekiWoofsBees.Common/IPlantPositionRegistry.cs new file mode 100644 index 0000000..3fff757 --- /dev/null +++ b/OrekiWoofsBees.Common/IPlantPositionRegistry.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBees.Common; + +public interface IPlantPositionRegistry +{ + ICoreAPI? Api { get; } + + event Action? CropEvent; + event Action? FlowerEvent; + + void AddPlantPosition(BlockPos pos, Block block); + + int CountBeehivesInRadius(BlockPos pos, int radius); + + ( + int FlowerCount, + int CropCount, + float InitialScanProgress, + float RescanProgress + ) + GetPlantCountsNearPosition(BlockPos hivePos, int radius); + + ( + IEnumerable Flowers, + IEnumerable Crops, + float InitialScanProgress, + float RescanProgress + ) + GetPlantsNearPosition(BlockPos hivePos, int radius); + + void RegisterBeehive(BlockPos pos, int radius); + + void RemovePlantPosition(BlockPos pos, Block block); + + void UnregisterBeehive(BlockPos pos); +} \ No newline at end of file diff --git a/OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj b/OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj new file mode 100644 index 0000000..199a8e6 --- /dev/null +++ b/OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj @@ -0,0 +1,62 @@ + + + + net8.0 + false + enable + Debug;Release;Debug22 + + + + net10.0 + $(VINTAGE_STORY_22PRE2) + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + false + + + $(VINTAGE_STORY)/Mods/VSSurvivalMod.dll + False + + + $(VINTAGE_STORY)/Mods/VSEssentials.dll + False + + + $(VINTAGE_STORY)/Mods/VSCreativeMod.dll + False + + + $(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll + False + + + $(VINTAGE_STORY)/Lib/0Harmony.dll + False + + + $(VINTAGE_STORY)/VintagestoryLib.dll + false + + + $(VINTAGE_STORY)/Lib/protobuf-net.dll + False + + + $(VINTAGE_STORY)/Lib/cairo-sharp.dll + False + + + $(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll + False + + + E:\Code\VintageStory\configlib_1.10.14\configlib.dll + False + + + + diff --git a/OrekiWoofsBees.Common/Overlaps.cs b/OrekiWoofsBees.Common/Overlaps.cs new file mode 100644 index 0000000..ece00f7 --- /dev/null +++ b/OrekiWoofsBees.Common/Overlaps.cs @@ -0,0 +1,32 @@ +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBees.Common; + +public static class Overlaps +{ + public static bool IsWithinSphericalRadius(BlockPos center, StructVec3i pos, int radius) + { + return IsWithinSphericalRadiusSq(center, pos, radius * radius); + } + + public static bool IsWithinSphericalRadiusSq(BlockPos center, StructVec3i pos, int radiusSq) + { + int dx = pos.X - center.X; + int dy = pos.Y - center.Y; + int dz = pos.Z - center.Z; + return dx * dx + dy * dy + dz * dz <= radiusSq; + } + + public static bool IsWithinSphericalRadius(StructVec3i center, StructVec3i pos, int radius) + { + return IsWithinSphericalRadiusSq(center, pos, radius * radius); + } + + public static bool IsWithinSphericalRadiusSq(StructVec3i center, StructVec3i pos, int radiusSq) + { + int dx = pos.X - center.X; + int dy = pos.Y - center.Y; + int dz = pos.Z - center.Z; + return dx * dx + dy * dy + dz * dz <= radiusSq; + } +} diff --git a/OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs b/OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs new file mode 100644 index 0000000..6a4d53c --- /dev/null +++ b/OrekiWoofsBees.Common/PlantPositionRegistryModSystem2.cs @@ -0,0 +1,445 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBees.Common; + +/// +/// tracks plant positions for all registered beehives. +/// Instead of each beehive scanning its radius every tick, this registry +/// incrementally scans blocks across all beehives to keep performance consistent +/// +public class PlantPositionRegistryModSystem2 : ModSystem, IPlantPositionRegistry +{ + private const int default_blocks_per_tick = 20; + private const int tick_interval_ms = 20; + + private static readonly Dictionary offsetTables = []; + + private long? tickListenerId; + private readonly Dictionary beehives = []; + private readonly HashSet flowerPositions = []; + private readonly HashSet cropPositions = []; + + // blocks below this are skipped + private readonly Dictionary<(int X, int Z), int> soilFloorCache = []; + + private int lastScannedBeehiveIndex = 0; + private int blocksPerTick = default_blocks_per_tick; + + public ICoreAPI? Api { get; private set; } + + public int BlocksPerTick + { + get => blocksPerTick; + set => blocksPerTick = Math.Clamp(value, 0, 1000); + } + + public event Action? FlowerEvent; + public event Action? CropEvent; + + public override double ExecuteOrder() => 0.10; + + public override void Start(ICoreAPI api) + { + Api = api; + if (api.Side.IsServer()) + tickListenerId = api.Event.RegisterGameTickListener(OnTick, tick_interval_ms); + } + + public override void Dispose() + { + if (Api != null && tickListenerId.HasValue) + Api.Event.UnregisterGameTickListener(tickListenerId.Value); + beehives.Clear(); + flowerPositions.Clear(); + cropPositions.Clear(); + FlowerEvent = null; + CropEvent = null; + } + + public void RegisterBeehive(BlockPos pos, int radius) + { + var key = StructVec3i.FromBlockPos(pos); + if (beehives.ContainsKey(key)) + return; + + if (!offsetTables.ContainsKey(radius)) + offsetTables[radius] = new ScanOffsetTable(radius); + + beehives[key] = new BeehiveScanCursor(pos, radius); + } + + public void UnregisterBeehive(BlockPos pos) + { + var key = StructVec3i.FromBlockPos(pos); + beehives.Remove(key); + } + + public ( + IEnumerable Flowers, + IEnumerable Crops, + float InitialScanProgress, + float RescanProgress + ) GetPlantsNearPosition(BlockPos hivePos, int radius) + { + var key = StructVec3i.FromBlockPos(hivePos); + + var flowers = flowerPositions + .Where(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius)) + .Select(p => new BlockPos(p.X, p.Y, p.Z, hivePos.dimension)); + + var crops = cropPositions + .Where(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius)) + .Select(p => new BlockPos(p.X, p.Y, p.Z, hivePos.dimension)); + + float initialProgress = 1.0f; + float rescanProgress = 0.0f; + if (!beehives.TryGetValue(key, out var cursor)) + return (flowers, crops, initialProgress, rescanProgress); + + var table = offsetTables[cursor.Radius]; + if (table.Count > 0) + { + initialProgress = Math.Min(1.0f, (float)cursor.BlocksCheckedCount / table.Count); + rescanProgress = cursor.GetRescanProgress(offsetTables); + } + + return (flowers, crops, initialProgress, rescanProgress); + } + + public (int FlowerCount, int CropCount, float InitialScanProgress, float RescanProgress) GetPlantCountsNearPosition(BlockPos hivePos, int radius) + { + int flowers = flowerPositions.Count(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius)); + int crops = cropPositions.Count(p => Overlaps.IsWithinSphericalRadius(hivePos, p, radius)); + + float initialProgress = 0.0f; + float rescanProgress = 0.0f; + var key = StructVec3i.FromBlockPos(hivePos); + if (!beehives.TryGetValue(key, out var cursor)) + return (flowers, crops, initialProgress, rescanProgress); + + var table = offsetTables[cursor.Radius]; + if (table.Count > 0) + { + initialProgress = Math.Min(1.0f, (float)cursor.BlocksCheckedCount / table.Count); + rescanProgress = cursor.GetRescanProgress(offsetTables); + } + + return (flowers, crops, initialProgress, rescanProgress); + } + + public void AddPlantPosition(BlockPos pos, Block block) + { + if (Api is null) + return; + + var structPos = new StructVec3i(pos.X, pos.Y, pos.Z); + + if (PlantRecognitionUtilities.IsCrop(block)) + { + cropPositions.Add(structPos); + flowerPositions.Remove(structPos); + CropEvent?.Invoke(pos, 1); + } + else if (PlantRecognitionUtilities.IsFlower(block, Api.World.BlockAccessor, pos)) + { + flowerPositions.Add(structPos); + cropPositions.Remove(structPos); + FlowerEvent?.Invoke(pos, 1); + } + } + + public void RemovePlantPosition(BlockPos pos, Block block) + { + if (Api is null) + return; + + var structPos = new StructVec3i(pos.X, pos.Y, pos.Z); + flowerPositions.Remove(structPos); + cropPositions.Remove(structPos); + + if (PlantRecognitionUtilities.IsCrop(block)) + CropEvent?.Invoke(pos, -1); + else if (PlantRecognitionUtilities.IsFlower(block, Api.World.BlockAccessor, pos)) + FlowerEvent?.Invoke(pos, -1); + } + + public int CountBeehivesInRadius(BlockPos pos, int radius) + { + var plantPos = StructVec3i.FromBlockPos(pos); + + int count = 0; + foreach (var (beehivePos, _) in beehives) + { + if (Overlaps.IsWithinSphericalRadius(beehivePos, plantPos, radius)) + count++; + } + + return count; + } + + private void OnTick(float dt) + { + if (Api is null) + return; + + var stopwatch = Stopwatch.StartNew(); + if (beehives.Count == 0) + return; + + var accessor = Api.World.BlockAccessor; + + for (int i = 0; i < BlocksPerTick; i++) + { + var nextBlock = GetNextBlockToCheck(); + if (nextBlock == null) + break; + + var (cursor, blockPos) = nextBlock.Value; + + // check if this position is below the soil floor for this X/Z + var xz = (blockPos.X, blockPos.Z); + if (soilFloorCache.TryGetValue(xz, out int soilFloorY) && blockPos.Y < soilFloorY) + { + // it's below the soil floor, skip + cursor.Advance(); + continue; + } + + cursor.Advance(); + + var block = accessor.GetBlock(blockPos); + var structPos = new StructVec3i(blockPos.X, blockPos.Y, blockPos.Z); + + if (block == null || block.BlockId == 0) + { + // block is air or unloaded - remove from caches if present + flowerPositions.Remove(structPos); + cropPositions.Remove(structPos); + continue; + } + + // check if this is soil with soil below - mark as soil floor + if (block is BlockSoil) + { + var blockBelow = accessor.GetBlock(blockPos.DownCopy()); + if (blockBelow is BlockSoil) + { + // found soil floor - record it + bool isNewFloor = false; + if (!soilFloorCache.TryGetValue(xz, out int existingFloor) || blockPos.Y < existingFloor) + { + soilFloorCache[xz] = blockPos.Y; + isNewFloor = true; + } + + if (isNewFloor) + cursor.CountAndSkipBlocksBelowY(blockPos.Y, xz, offsetTables); + } + } + + if (PlantRecognitionUtilities.IsCrop(block)) + { + cropPositions.Add(structPos); + flowerPositions.Remove(structPos); + } + else if (PlantRecognitionUtilities.IsFlower(block, accessor, blockPos)) + { + flowerPositions.Add(structPos); + cropPositions.Remove(structPos); + } + else + { + flowerPositions.Remove(structPos); + cropPositions.Remove(structPos); + } + } + + stopwatch.Stop(); + if (stopwatch.Elapsed.TotalSeconds > 0.2) + { + Mod.Logger.Warning($"{nameof(PlantPositionRegistryModSystem2)} {nameof(OnTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s (beehives: {beehives.Count})."); + } + } + + /// + /// 1. pick the beehive with the least blocks checked in its radius + /// 2. pick the closest horizontal space vertically + /// 3. check the next closest block to the beehive in this horizontal space + /// + private (BeehiveScanCursor Cursor, BlockPos BlockPos)? GetNextBlockToCheck() + { + if (beehives.Count == 0) + return null; + + bool allCompleted = true; + foreach (var cursor in beehives.Values) + { + var table = offsetTables[cursor.Radius]; + if (cursor.BlocksCheckedCount < table.Count) + { + allCompleted = false; + break; + } + } + + BeehiveScanCursor? bestCursor = null; + + if (!allCompleted) + { + var lowestRelativeProgress = float.MaxValue; + foreach (var cursor in beehives.Values) + { + var table = offsetTables[cursor.Radius]; + if (cursor.BlocksCheckedCount >= table.Count) + continue; + + var relativeProgress = (float)cursor.BlocksCheckedCount / table.Count; + if (relativeProgress < lowestRelativeProgress) + { + lowestRelativeProgress = relativeProgress; + bestCursor = cursor; + } + } + } + else + { + var beehivesList = beehives.Values.ToList(); + lastScannedBeehiveIndex = (lastScannedBeehiveIndex + 1) % beehivesList.Count; + bestCursor = beehivesList[lastScannedBeehiveIndex]; + } + + if (bestCursor == null) + return null; + + if (bestCursor.IsStartingNewCycle()) + soilFloorCache.Clear(); + + var blockPos = bestCursor.GetCurrentBlockPos(offsetTables); + return (bestCursor, blockPos); + } + + private class BeehiveScanCursor(BlockPos hivePos, int radius) + { + public BlockPos HivePos { get; } = hivePos; + public int Radius { get; } = radius; + public int BlocksCheckedCount { get; private set; } = 0; + + private int currentIndex = 0; + + public BlockPos GetCurrentBlockPos(Dictionary tables) + { + var table = tables[Radius]; + var (X, Y, Z) = table.GetOffset(currentIndex); + return new BlockPos( + HivePos.X + X, + HivePos.Y + Y, + HivePos.Z + Z, + HivePos.dimension + ); + } + + public void Advance() + { + var table = offsetTables[Radius]; + currentIndex++; + + if (currentIndex >= table.Count) + currentIndex = 0; + + if (BlocksCheckedCount < table.Count) + BlocksCheckedCount++; + } + + public bool IsStartingNewCycle() + { + return currentIndex == 0 && BlocksCheckedCount >= offsetTables[Radius].Count; + } + + public int GetCurrentIndex() + { + return currentIndex; + } + + public void CountAndSkipBlocksBelowY(int floorY, (int X, int Z) xz, Dictionary tables) + { + var table = tables[Radius]; + int skippedCount = 0; + + for (int i = currentIndex + 1; i < table.Count; i++) + { + var (offsetX, offsetY, offsetZ) = table.GetOffset(i); + int worldX = HivePos.X + offsetX; + int worldZ = HivePos.Z + offsetZ; + int worldY = HivePos.Y + offsetY; + + if (worldX == xz.X && worldZ == xz.Z && worldY < floorY) + { + skippedCount++; + if (BlocksCheckedCount < table.Count) + BlocksCheckedCount++; + } + } + } + + public float GetRescanProgress(Dictionary tables) + { + var table = tables[Radius]; + if (table.Count == 0) + return 0.0f; + + if (BlocksCheckedCount < table.Count) + return 0.0f; + + return (float)currentIndex / table.Count; + } + } + + /// + /// shared table of offsets for a given radius, sorted by priority. + /// only one instance per radius is created and shared across all beehives. + /// + private class ScanOffsetTable + { + private readonly (int X, int Y, int Z)[] offsets; + public int Count => offsets.Length; + + public ScanOffsetTable(int radius) + { + int radiusSq = radius * radius; + var offsets = new List<(int X, int Y, int Z, int YDist, int HorizontalDistSq)>(); + + for (int dy = -radius; dy <= radius; dy++) + { + for (int dx = -radius; dx <= radius; dx++) + { + for (int dz = -radius; dz <= radius; dz++) + { + int distSq = dx * dx + dy * dy + dz * dz; + if (distSq <= radiusSq) + { + int yDist = Math.Abs(dy); + int horizontalDistSq = dx * dx + dz * dz; + offsets.Add((dx, dy, dz, yDist, horizontalDistSq)); + } + } + } + } + + this.offsets = [.. offsets + .OrderBy(o => o.YDist) + .ThenBy(o => o.HorizontalDistSq) + .Select(o => (o.X, o.Y, o.Z))]; + } + + public (int X, int Y, int Z) GetOffset(int index) + { + return offsets[index]; + } + } +} diff --git a/OrekiWoofsBees.Common/PlantRecognitionUtilities.cs b/OrekiWoofsBees.Common/PlantRecognitionUtilities.cs new file mode 100644 index 0000000..5b9f765 --- /dev/null +++ b/OrekiWoofsBees.Common/PlantRecognitionUtilities.cs @@ -0,0 +1,45 @@ +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace OrekiWoofsBees.Common; + +public static class PlantRecognitionUtilities +{ + public static bool IsCrop(Block block) + { + return block is BlockCrop; + } + + public static bool IsFlower(Block block, IBlockAccessor accessor, BlockPos pos) + { + if (block.FirstCodePart() == "flower") + return true; + + if (block is BlockPlantContainer) + { + var plantContainer = block.GetBlockEntity(pos); + if (plantContainer is null) + return false; + + var contents = plantContainer.GetContents(); + if (contents is null) + return false; + if (contents.Block?.FirstCodePart() == "flower") + return true; + } + + if (block is BlockBerryBush && accessor.GetBlockEntity(pos) is BlockEntityBerryBush blockEntityBerryBush) + return blockEntityBerryBush.IsFlowering; + + if (block is BlockFruitTreePart && accessor.GetBlockEntity(pos) is BlockEntityFruitTreeFoliage fruitTreeFoliage) + return fruitTreeFoliage.FoliageState == EnumFoliageState.Flowering; + + return false; + } + + public static bool IsPlant(Block block, IBlockAccessor accessor, BlockPos pos) + { + return IsCrop(block) || IsFlower(block, accessor, pos); + } +} diff --git a/OrekiWoofsBees.Common/StructVec3i.cs b/OrekiWoofsBees.Common/StructVec3i.cs new file mode 100644 index 0000000..59d3737 --- /dev/null +++ b/OrekiWoofsBees.Common/StructVec3i.cs @@ -0,0 +1,8 @@ +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBees.Common; + +public readonly record struct StructVec3i(int X, int Y, int Z) +{ + public static StructVec3i FromBlockPos(BlockPos pos) => new(pos.X, pos.Y, pos.Z); +} diff --git a/OrekiWoofsBees.Common/SwarmState.cs b/OrekiWoofsBees.Common/SwarmState.cs new file mode 100644 index 0000000..4f8a4b4 --- /dev/null +++ b/OrekiWoofsBees.Common/SwarmState.cs @@ -0,0 +1,8 @@ +namespace OrekiWoofsBees.Common; + +public enum SwarmState +{ + BuildingSwarm, + HangingOut, + MigratingToNewHive, +} diff --git a/OrekiWoofsBees.Common/VectorConversionUtils.cs b/OrekiWoofsBees.Common/VectorConversionUtils.cs new file mode 100644 index 0000000..7b54cde --- /dev/null +++ b/OrekiWoofsBees.Common/VectorConversionUtils.cs @@ -0,0 +1,10 @@ +using System.Numerics; +using Vintagestory.API.MathTools; + +namespace OrekiWoofsBees.Common; + +public static class VectorConversionUtils +{ + public static Vector3 ToVector3(this Vec3f v) => new(v.X, v.Y, v.Z); + public static Vec3f ToVec3f(this Vector3 v) => new(v.X, v.Y, v.Z); +} diff --git a/OrekiWoofsBees.Full/.gitignore b/OrekiWoofsBees.Full/.gitignore new file mode 100644 index 0000000..2e947e8 --- /dev/null +++ b/OrekiWoofsBees.Full/.gitignore @@ -0,0 +1 @@ +Mods/ diff --git a/OrekiWoofsBees.Full/OrekiWoofsBees.Full.csproj b/OrekiWoofsBees.Full/OrekiWoofsBees.Full.csproj new file mode 100644 index 0000000..8a82097 --- /dev/null +++ b/OrekiWoofsBees.Full/OrekiWoofsBees.Full.csproj @@ -0,0 +1,38 @@ + + + net8.0 + Debug;Release;Debug22 + false + true + + + + + net10.0 + --dataPath "..\VintageStoryDataBeehives\Data22" + + + + + + + + + + + + + + + <_BeehivesFiles Include="$(ProjectDir)../OrekiWoofsBeehives/bin/$(Configuration)/Mods/orekiwoofsbeehives/**/*" /> + <_RoamingBeesFiles Include="$(ProjectDir)../RoamingBees/RoamingBees/bin/$(Configuration)/Mods/roamingbees/**/*" /> + <_BeehivesAssets Include="$(ProjectDir)../OrekiWoofsBeehives/assets/**/*" /> + <_RoamingBeesAssets Include="$(ProjectDir)../RoamingBees/RoamingBees/assets/**/*" /> + + + + + + + + diff --git a/OrekiWoofsBees.Full/Properties/launchSettings.json b/OrekiWoofsBees.Full/Properties/launchSettings.json new file mode 100644 index 0000000..ecb4ce1 --- /dev/null +++ b/OrekiWoofsBees.Full/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Client": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/Mods\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + }, + "Server": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/Mods\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + } + } +} diff --git a/RoamingBees/.gitignore b/RoamingBees/.gitignore new file mode 100644 index 0000000..8e4f89c --- /dev/null +++ b/RoamingBees/.gitignore @@ -0,0 +1,675 @@ +# Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode,rider +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio,visualstudiocode,rider + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### Rider ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### VisualStudioCode ### +!.vscode/*.code-snippets + +# Local History for Visual Studio Code + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### VisualStudio ### + +# User-specific files + +# User-specific files (MonoDevelop/Xamarin Studio) + +# Mono auto generated files + +# Build results + +# Visual Studio 2015/2017 cache/options directory +# Uncomment if you have tasks that create the project's static files in wwwroot + +# Visual Studio 2017 auto generated files + +# MSTest test Results + +# NUnit + +# Build Results of an ATL Project + +# Benchmark Results + +# .NET Core + +# ASP.NET Scaffolding + +# StyleCop + +# Files built by Visual Studio + +# Chutzpah Test files + +# Visual C++ cache files + +# Visual Studio profiler + +# Visual Studio Trace Files + +# TFS 2012 Local Workspace + +# Guidance Automation Toolkit + +# ReSharper is a .NET coding add-in + +# TeamCity is a build add-in + +# DotCover is a Code Coverage Tool + +# AxoCover is a Code Coverage Tool + +# Coverlet is a free, cross platform Code Coverage Tool + +# Visual Studio code coverage results + +# NCrunch + +# MightyMoose + +# Web workbench (sass) + +# Installshield output folder + +# DocProject is a documentation generator add-in + +# Click-Once directory + +# Publish Web Output +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted + +# NuGet Packages +# NuGet Symbol Packages +# The packages folder can be ignored because of Package Restore +# except build/, which is used as an MSBuild target. +# Uncomment if necessary however generally it will be regenerated when needed +# NuGet v3's project.json files produces more ignorable files + +# Microsoft Azure Build Output + +# Microsoft Azure Emulator + +# Windows Store app package directories and files + +# Visual Studio cache files +# files ending in .cache can be ignored +# but keep track of directories ending in .cache + +# Others + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) + +# RIA/Silverlight projects + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) + +# SQL Server files + +# Business Intelligence projects + +# Microsoft Fakes + +# GhostDoc plugin setting file + +# Node.js Tools for Visual Studio + +# Visual Studio 6 build log + +# Visual Studio 6 workspace options file + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output + +# Paket dependency manager + +# FAKE - F# Make + +# CodeRush personal settings + +# Python Tools for Visual Studio (PTVS) + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio + +# Telerik's JustMock configuration file + +# BizTalk build output + +# OpenCover UI analysis results + +# Azure Stream Analytics local run output + +# MSBuild Binary and Structured Log + +# NVidia Nsight GPU debugger configuration file + +# MFractors (Xamarin productivity tool) working folder + +# Local History for Visual Studio + +# Visual Studio History (VSHistory) files + +# BeatPulse healthcheck temp database + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 + +# Ionide (cross platform F# VS Code tools) working folder + +# Fody - auto-generated XML schema + +# VS Code files for those working on multiple tools + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs + +# JetBrains Rider + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode,rider \ No newline at end of file diff --git a/RoamingBees/RoamingBees/Behaviors/BlockBehaviorBeeSwarm.cs b/RoamingBees/RoamingBees/Behaviors/BlockBehaviorBeeSwarm.cs new file mode 100644 index 0000000..2bd42cc --- /dev/null +++ b/RoamingBees/RoamingBees/Behaviors/BlockBehaviorBeeSwarm.cs @@ -0,0 +1,24 @@ +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Behaviors; + +public class BlockBehaviorBeeSwarm(Block block) : BlockBehavior(block) +{ + public override string GetPlacedBlockInfo(IWorldAccessor world, BlockPos pos, IPlayer forPlayer) + { + if (!world.EntityDebugMode) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var entityBehavior = block.GetBEBehavior(pos); + if (entityBehavior is null) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var str = $"Active bees: {entityBehavior.ActiveBeesCount} (hover: {entityBehavior.ActiveHoveringCount}, travel: {entityBehavior.ActiveTravelingCount}, scout: {entityBehavior.ActiveScoutingCount})\n"; + str += $"Target bees: hover {entityBehavior.TargetHoveringCount}, travel {entityBehavior.TargetTravelingCount}, scout {entityBehavior.TargetScoutingCount}\n"; + str += $"State: {entityBehavior.SwarmState}\n"; + str += $"Time since last spawn: {entityBehavior.TimeSinceLastSpawn:F1} | hover: {entityBehavior.TimeSinceLastHoverSpawn:F1}\n"; + + return base.GetPlacedBlockInfo(world, pos, forPlayer) + str; + } +} diff --git a/RoamingBees/RoamingBees/Behaviors/BlockBehaviorRoamingBees.cs b/RoamingBees/RoamingBees/Behaviors/BlockBehaviorRoamingBees.cs new file mode 100644 index 0000000..f6e0630 --- /dev/null +++ b/RoamingBees/RoamingBees/Behaviors/BlockBehaviorRoamingBees.cs @@ -0,0 +1,26 @@ +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Behaviors; + +public class BlockBehaviorRoamingBees(Block block) : BlockBehavior(block) +{ + public override string GetPlacedBlockInfo(IWorldAccessor world, BlockPos pos, IPlayer forPlayer) + { + if (!world.EntityDebugMode) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var entityBehavior = block.GetBEBehavior(pos); + if (entityBehavior is null) + return base.GetPlacedBlockInfo(world, pos, forPlayer); + + var str = $"Roaming bees: {entityBehavior.ActiveBeesCount}\n"; + str += $"Target roaming bees: {entityBehavior.TargetParticleCount}\n"; + str += $"Time since last spawn + cooldown: {entityBehavior.TimeSinceLastSpawn:F1}\n"; + + str += $"Flowers around: {entityBehavior.FlowerCount}, crops around: {entityBehavior.CropCount}\n"; + str += $"Scan progress: {entityBehavior.InitialScanProgress * 100f:F1}%, rescan progress: {entityBehavior.RescanProgress * 100f:F1}%\n"; + + return base.GetPlacedBlockInfo(world, pos, forPlayer) + str; + } +} diff --git a/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorBeeSwarm.cs b/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorBeeSwarm.cs new file mode 100644 index 0000000..df24587 --- /dev/null +++ b/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorBeeSwarm.cs @@ -0,0 +1,615 @@ +using OrekiWoofsBees.Common; +using RoamingBees.Particles; +using RoamingBees.Particles.Catchup; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Behaviors; + +public class BlockEntityBehaviorBeeSwarm(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity), IBeeSpawnHandler, IBeeSpawnCatchup +{ + private const float spawn_cooldown_seconds = 2f; + private const float hover_spawn_cooldown_seconds = 0.1f; + private const int server_tick_frequency_decrease = 1; + private const bool swarm_collision_enabled = true; + + private static readonly Random random = new(); + + private readonly List activeBees = []; + private readonly List candidateHivePositions = []; + + private RoamingBeesModSystem? modSystem; + private bool initialized; + + private BlockPos? originHivePos; + private BlockPos? targetHivePos; + + private Vec3f[]? hoverSurfaceMap; + private Cuboidf[] shapeElements = []; + private Vector3 hiddenAnchor; + private string swarmSide = "north"; + private double stateStartTotalHours; + private double buildingDurationHours = 3.0; + private double hangingDurationHours = 5.0; + private double migrationPhaseDurationHours = 5.0; + + public int ActiveBeesCount => activeBees.Count; + public int ActiveHoveringCount => activeBees.Count(b => b.Role == BeeRole.Hovering); + public int ActiveTravelingCount => activeBees.Count(b => b.Role == BeeRole.Traveling); + public int ActiveScoutingCount => activeBees.Count(b => b.Role == BeeRole.Scouting); + public int TargetHoveringCount => ComputeHoverTarget(Config.Instance); + public int TargetTravelingCount => ComputeTravelingTarget(Config.Instance); + public int TargetScoutingCount => SwarmState == SwarmState.HangingOut ? Math.Max(0, Config.Instance.BeesPerSwarmTraveling) : 0; + + public IEnumerable ActiveBeesPackets => activeBees.Select(x => x.SpawnPacket); + + public float TimeSinceLastSpawn { get; private set; } = (float)(random.NextDouble() * spawn_cooldown_seconds); + public float TimeSinceLastHoverSpawn { get; private set; } = hover_spawn_cooldown_seconds; + public SwarmState SwarmState { get; private set; } + + public override void Initialize(ICoreAPI api, JsonObject properties) + { + if (initialized) + return; + + base.Initialize(api, properties); + initialized = true; + + modSystem = api.ModLoader.GetModSystem(); + modSystem?.BeeSpawnPacketDistributor?.Register(Blockentity.Pos, this); + modSystem?.ClientChannel?.SendPacket(new BeeCatchupRequestPacket { HivePosition = Pos }); + if (modSystem?.Mod.Info.Version.Contains("dev") == true && modSystem?.ClientChannel != null) + modSystem?.Mod.Logger.Event($"{nameof(BlockEntityBehaviorBeeSwarm)} sent {nameof(BeeCatchupRequestPacket)} Pos: {Pos}"); + + swarmSide = Blockentity.Block?.Variant?.ContainsKey("side") == true + ? Blockentity.Block.Variant["side"] + : "north"; + + BuildHoverSurfaceMap(); + + var updateFrequency = 20; + if (api.Side == EnumAppSide.Server) + updateFrequency *= server_tick_frequency_decrease; + Blockentity.RegisterGameTickListener(OnTick, updateFrequency); + } + + private void BuildHoverSurfaceMap() + { + shapeElements = ComputeShapeElements(); + + // find the union bounding rect of all elements' outward-facing surface + var uMin = new Vector3(float.MaxValue); + var uMax = new Vector3(float.MinValue); + foreach (var elem in shapeElements) + { + var eMin = elem.Start.ToVector3(); + var eMax = elem.End.ToVector3(); + uMin = Vector3.Min(uMin, eMin); + uMax = Vector3.Max(uMax, eMax); + } + + var samples = new List(); + var sampleCount = 12; + for (var i = 0; i < sampleCount; i++) + { + float x, y, z; + switch (swarmSide) + { + case "north": + x = uMin.X + (float)random.NextDouble() * (uMax.X - uMin.X); + y = uMin.Y + (float)random.NextDouble() * (uMax.Y - uMin.Y); + z = uMax.Z; + break; + case "south": + x = uMin.X + (float)random.NextDouble() * (uMax.X - uMin.X); + y = uMin.Y + (float)random.NextDouble() * (uMax.Y - uMin.Y); + z = uMin.Z; + break; + case "east": + x = uMin.X; + y = uMin.Y + (float)random.NextDouble() * (uMax.Y - uMin.Y); + z = uMin.Z + (float)random.NextDouble() * (uMax.Z - uMin.Z); + break; + case "west": + x = uMax.X; + y = uMin.Y + (float)random.NextDouble() * (uMax.Y - uMin.Y); + z = uMin.Z + (float)random.NextDouble() * (uMax.Z - uMin.Z); + break; + case "up": + x = uMin.X + (float)random.NextDouble() * (uMax.X - uMin.X); + y = uMax.Y; + z = uMin.Z + (float)random.NextDouble() * (uMax.Z - uMin.Z); + break; + default: // down + x = uMin.X + (float)random.NextDouble() * (uMax.X - uMin.X); + y = uMin.Y; + z = uMin.Z + (float)random.NextDouble() * (uMax.Z - uMin.Z); + break; + } + samples.Add(new Vec3f(x, y, z)); + } + + hoverSurfaceMap = [.. samples]; + hiddenAnchor = SwarmBeePathGeneration.ComputeHiddenAnchor(hoverSurfaceMap, swarmSide); + } + + private Cuboidf[] ComputeShapeElements() + { + var block = Blockentity.Block; + if (block?.Shape?.Base is null) + return [new Cuboidf(0.25f, 0.25f, 0.25f, 0.75f, 0.75f, 0.75f)]; + + var shapeLocation = block.Shape.Base.Clone(); + shapeLocation.Path = "shapes/" + shapeLocation.Path + ".json"; + var shapeAsset = Api.Assets.TryGet(shapeLocation); + if (shapeAsset is null) + return [new Cuboidf(0.25f, 0.25f, 0.25f, 0.75f, 0.75f, 0.75f)]; + + var shape = shapeAsset.ToObject(); + if (shape?.Elements is null || shape.Elements.Length == 0) + return [new Cuboidf(0.25f, 0.25f, 0.25f, 0.75f, 0.75f, 0.75f)]; + + var rotX = block.Shape.rotateX; + var rotY = block.Shape.rotateY; + var rotZ = block.Shape.rotateZ; + + var cuboids = new List(); + CollectElementCuboids(shape.Elements, cuboids, rotX, rotY, rotZ); + return cuboids.Count > 0 ? [.. cuboids] : [new Cuboidf(0.25f, 0.25f, 0.25f, 0.75f, 0.75f, 0.75f)]; + } + + private static void CollectElementCuboids(ShapeElement[] elements, List cuboids, float rotX, float rotY, float rotZ) + { + foreach (var element in elements) + { + if (element.From is not null && element.To is not null) + { + var x1 = MathF.Min((float)element.From[0], (float)element.To[0]) / 16f; + var y1 = MathF.Min((float)element.From[1], (float)element.To[1]) / 16f; + var z1 = MathF.Min((float)element.From[2], (float)element.To[2]) / 16f; + var x2 = MathF.Max((float)element.From[0], (float)element.To[0]) / 16f; + var y2 = MathF.Max((float)element.From[1], (float)element.To[1]) / 16f; + var z2 = MathF.Max((float)element.From[2], (float)element.To[2]) / 16f; + + if (rotX != 0 || rotY != 0 || rotZ != 0) + RotateBounds(ref x1, ref y1, ref z1, ref x2, ref y2, ref z2, rotX, rotY, rotZ); + + cuboids.Add(new Cuboidf( + Math.Clamp(x1, 0f, 1f), Math.Clamp(y1, 0f, 1f), Math.Clamp(z1, 0f, 1f), + Math.Clamp(x2, 0f, 1f), Math.Clamp(y2, 0f, 1f), Math.Clamp(z2, 0f, 1f))); + } + + if (element.Children is not null) + CollectElementCuboids(element.Children, cuboids, rotX, rotY, rotZ); + } + } + + private static void RotateBounds(ref float minX, ref float minY, ref float minZ, ref float maxX, ref float maxY, ref float maxZ, float rotXDeg, float rotYDeg, float rotZDeg) + { + // rotate all 8 corners of the AABB around (0.5, 0.5, 0.5) center and recompute bounds + var corners = new Vector3[8] + { + new(minX, minY, minZ), new(maxX, minY, minZ), + new(minX, maxY, minZ), new(maxX, maxY, minZ), + new(minX, minY, maxZ), new(maxX, minY, maxZ), + new(minX, maxY, maxZ), new(maxX, maxY, maxZ), + }; + + var center = new Vector3(0.5f, 0.5f, 0.5f); + var rotation = Matrix4x4.CreateFromYawPitchRoll( + rotYDeg * MathF.PI / 180f, + rotXDeg * MathF.PI / 180f, + rotZDeg * MathF.PI / 180f); + + var newMin = new Vector3(float.MaxValue); + var newMax = new Vector3(float.MinValue); + + foreach (var corner in corners) + { + var rotated = Vector3.Transform(corner - center, rotation) + center; + newMin = Vector3.Min(newMin, rotated); + newMax = Vector3.Max(newMax, rotated); + } + + minX = newMin.X; minY = newMin.Y; minZ = newMin.Z; + maxX = newMax.X; maxY = newMax.Y; maxZ = newMax.Z; + } + + public override void OnBlockRemoved() + { + Clear(); + base.OnBlockRemoved(); + } + + public override void OnBlockUnloaded() + { + Clear(); + base.OnBlockUnloaded(); + } + + public void Clear() + { + if (modSystem is null) + return; + + modSystem.GlobalActiveBees -= activeBees.Count; + modSystem.BeeSpawnPacketDistributor?.Unregister(Blockentity.Pos); + activeBees.Clear(); + } + + private readonly TreeAttribute swarmAttributeSnapshot = new(); + + private void ReadSwarmAttributesFromBlockEntity() + { + swarmAttributeSnapshot.Clear(); + Blockentity.ToTreeAttributes(swarmAttributeSnapshot); + + SwarmState = (SwarmState)swarmAttributeSnapshot.GetInt("roamingbees_swarm_state"); + originHivePos = swarmAttributeSnapshot.GetBlockPos("roamingbees_swarm_originHivePos"); + targetHivePos = swarmAttributeSnapshot.GetBlockPos("roamingbees_swarm_targetHivePos"); + stateStartTotalHours = swarmAttributeSnapshot.GetDouble("stateStartTotalHours", stateStartTotalHours); + buildingDurationHours = swarmAttributeSnapshot.GetDouble("buildingDurationHours", buildingDurationHours); + hangingDurationHours = swarmAttributeSnapshot.GetDouble("hangingDurationHours", hangingDurationHours); + migrationPhaseDurationHours = swarmAttributeSnapshot.GetDouble("MigrationPhaseDurationHours", migrationPhaseDurationHours); + + candidateHivePositions.Clear(); + if (swarmAttributeSnapshot["roamingbees_swarm_candidateHives"] is not TreeArrayAttribute candidates) + return; + + foreach (var entry in candidates.value.OfType()) + { + var pos = entry?.GetBlockPos("pos"); + if (pos is not null) + candidateHivePositions.Add(pos); + } + } + + public void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false) + { + if (Blockentity.Pos != packet.HivePosition) + return; + + if (packet.Path is null || packet.Path.Length == 0) + return; + + var points = packet.Path.Select(x => x.ToPoint()).ToArray(); + var bee = new InternalBeeParticle(packet.EntrancePosition, GetFrontDirection(), points, packet.Role, packet.DespawnPosition, packet); + if (packet.Role == BeeRole.Hovering) + bee.LookDirection = -GetFrontDirection(); + activeBees.Add(bee); + if (catchup) + { + var totalDelta = Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds; + if (modSystem?.Mod.Info.Version.Contains("dev") == true) + modSystem.Mod.Logger.Notification($"HandleBeeParticleSpawn catchup totalDelta: {totalDelta}s"); + for (var i = 0; i < totalDelta / 0.1f; i++) + bee.Step(0.1f, 0.1f); // todo + } + } + + private void OnTick(float dt) + { + var stopwatch = Stopwatch.StartNew(); + Update(dt); + stopwatch.Stop(); + if (stopwatch.Elapsed.TotalSeconds > 0.2) + modSystem?.Mod?.Logger.Warning($"{nameof(BlockEntityBehaviorBeeSwarm)} {nameof(OnTick)} took {stopwatch.Elapsed.TotalSeconds:F2}s"); + } + + private void Update(float dt) + { + if (dt > 10f) + return; + dt = Math.Min(0.5f, dt); + + ReadSwarmAttributesFromBlockEntity(); + + var windVec = Api.World.BlockAccessor.GetWindSpeedAt(Blockentity.Pos); + var windSpeed = (float)Math.Min(windVec.Length(), 1.0); + + for (int i = activeBees.Count - 1; i >= 0; i--) + { + var bee = activeBees[i]; + + if (Api.Side == EnumAppSide.Client) + HandleBeeClientSide(dt, windSpeed, bee); + + if (Api.Side == EnumAppSide.Server) + { + for (var j = 0; j < server_tick_frequency_decrease; j++) + bee.Step(dt / server_tick_frequency_decrease, windSpeed); + } + + if (bee.ShouldBeDespawned && modSystem != null) + { + activeBees.RemoveAt(i); + modSystem.GlobalActiveBees--; + } + } + + TimeSinceLastSpawn += dt; + TimeSinceLastHoverSpawn += dt; + if (Api.Side == EnumAppSide.Server) + { + TrySpawnSwarmBee(); + TrySpawnHoverBee(); + } + } + + private void HandleBeeClientSide(float dt, float windSpeed, InternalBeeParticle bee) + { + bee.Step(dt, windSpeed); + + // Collision response: push the bee outward if it has drifted inside any + // of the swarm's shape element cuboids, unless heading to a despawn/slowdown point. + if (swarm_collision_enabled && (bee.Target is null || !bee.Target.Value.IsSlowdown)) + { + var p = bee.Position; + var outDir = swarmSide switch + { + "north" => new Vector3(0f, 0f, 1f), + "south" => new Vector3(0f, 0f, -1f), + "east" => new Vector3(-1f, 0f, 0f), + "west" => new Vector3(1f, 0f, 0f), + "up" => new Vector3(0f, 1f, 0f), + "down" => new Vector3(0f, -1f, 0f), + _ => new Vector3(0f, 0f, 1f), + }; + foreach (var elem in shapeElements) + { + var eMin = elem.Start.ToVector3(); + var eMax = elem.End.ToVector3(); + if (p.X >= eMin.X && p.X <= eMax.X && + p.Y >= eMin.Y && p.Y <= eMax.Y && + p.Z >= eMin.Z && p.Z <= eMax.Z) + { + bee.SolveCollision(eMin, eMax, outDir); + break; + } + } + } + + if (Config.Instance.ReceiveParticles && Api.World is IClientWorldAccessor clientWorld) + BeeVisualParticleRenderer.Spawn(clientWorld, Blockentity.Pos, bee); + } + + private void TrySpawnSwarmBee() + { + if (TimeSinceLastSpawn < spawn_cooldown_seconds) + return; + + if (modSystem?.ServerChannel is null) + return; + + var cfg = Config.Instance; + if (modSystem.GlobalActiveBees >= cfg.MaxGlobalRoamingBees) + return; + + var (path, role, startingPosition, despawnPosition) = GeneratePathForCurrentState(); + + if (path is null || path.Length == 0) + { + TimeSinceLastSpawn = 0f; + return; + } + + var message = new BeeSpawnPacket + { + HivePosition = Blockentity.Pos, + Path = [.. path.Select(x => BeePlannedPathPointContract.FromPoint(x))], + Facing = swarmSide, + Role = role, + EntrancePosition = startingPosition, + DespawnPosition = despawnPosition, + TimeElapsedSeconds = Api.World.Calendar.ElapsedSeconds, + }; + + activeBees.Add(new InternalBeeParticle(startingPosition, GetFrontDirection(), path, role, despawnPosition, message)); + modSystem.GlobalActiveBees++; + TimeSinceLastSpawn = 0f; + modSystem.ServerChannel.BroadcastPacket(message); + } + + private void TrySpawnHoverBee() + { + if (TimeSinceLastHoverSpawn < hover_spawn_cooldown_seconds) + return; + + if (modSystem?.ServerChannel is null) + return; + + var cfg = Config.Instance; + if (modSystem.GlobalActiveBees >= cfg.MaxGlobalRoamingBees) + return; + + if (hoverSurfaceMap is null || hoverSurfaceMap.Length == 0) + return; + + if (ActiveHoveringCount >= ComputeHoverTarget(cfg)) + return; + + var path = SwarmBeePathGeneration.GenerateHoverPath(Api.World.BlockAccessor, Blockentity.Pos, hoverSurfaceMap, hiddenAnchor, swarmSide); + if (path is null || path.Length == 0) + return; + + var message = new BeeSpawnPacket + { + HivePosition = Blockentity.Pos, + Path = [.. path.Select(x => BeePlannedPathPointContract.FromPoint(x))], + Facing = swarmSide, + Role = BeeRole.Hovering, + EntrancePosition = hiddenAnchor, + DespawnPosition = hiddenAnchor, + TimeElapsedSeconds = Api.World.Calendar.ElapsedSeconds, + }; + + var hoverBee = new InternalBeeParticle(hiddenAnchor, GetFrontDirection(), path, BeeRole.Hovering, hiddenAnchor, message); + hoverBee.LookDirection = -GetFrontDirection(); + activeBees.Add(hoverBee); + modSystem.GlobalActiveBees++; + TimeSinceLastHoverSpawn = 0f; + modSystem.ServerChannel.BroadcastPacket(message); + } + + private int ComputeHoverTarget(Config cfg) + { + var maxHover = Math.Max(0, cfg.BeesPerSwarmHovering); + return SwarmState switch + { + SwarmState.BuildingSwarm => ComputePhaseScaledTarget(maxHover, GetPhaseProgress(buildingDurationHours), invert: false), + SwarmState.HangingOut => maxHover, + SwarmState.MigratingToNewHive => ComputePhaseScaledTarget(maxHover, GetPhaseProgress(migrationPhaseDurationHours), invert: true), + _ => maxHover, + }; + } + + private int ComputeTravelingTarget(Config cfg) + { + var maxTravel = Math.Max(0, cfg.BeesPerSwarmTraveling); + return SwarmState switch + { + SwarmState.MigratingToNewHive => GetPhaseProgress(migrationPhaseDurationHours) >= 0.9d ? 0 : maxTravel, + _ => maxTravel, + }; + } + + private double GetPhaseProgress(double durationHours) + { + if (Api?.World?.Calendar is null || durationHours <= 0.0001) + return 1; + + return Math.Clamp((Api.World.Calendar.TotalHours - stateStartTotalHours) / durationHours, 0d, 1d); + } + + private static int ComputePhaseScaledTarget(int maxTarget, double progress, bool invert) + { + var scaledProgress = Math.Clamp(progress / 0.9d, 0d, 1d); + var ratio = invert ? 1d - scaledProgress : scaledProgress; + return (int)Math.Round(maxTarget * ratio, MidpointRounding.AwayFromZero); + } + + private (BeePlannedPathPoint[]? path, BeeRole role, Vector3 startingPosition, Vector3 despawnPosition) GeneratePathForCurrentState() + { + if (hoverSurfaceMap is null || hoverSurfaceMap.Length == 0) + return (null, BeeRole.Hovering, hiddenAnchor, hiddenAnchor); + + var cfg = Config.Instance; + var blockAccessor = Api.World.BlockAccessor; + + switch (SwarmState) + { + case SwarmState.BuildingSwarm: + { + var path = GenerateBuildingSwarmPath(blockAccessor, cfg); + var start = path is not null ? path[0].Position : hiddenAnchor; + return (path, BeeRole.Traveling, start, hiddenAnchor); + } + case SwarmState.HangingOut: + return (GenerateHangingOutPath(blockAccessor, cfg), BeeRole.Scouting, hiddenAnchor, hiddenAnchor); + case SwarmState.MigratingToNewHive: + { + var (path, despawn) = GenerateMigrationStatePath(blockAccessor, cfg); + return (path, BeeRole.Traveling, hiddenAnchor, despawn); + } + default: + return (null, BeeRole.Hovering, hiddenAnchor, hiddenAnchor); + } + } + + private BeePlannedPathPoint[]? GenerateBuildingSwarmPath(IBlockAccessor blockAccessor, Config cfg) + { + var targetTraveling = ComputeTravelingTarget(cfg); + if (originHivePos is null || targetTraveling <= 0 || ActiveTravelingCount >= targetTraveling) + return null; + + var originBehavior = GetRoamingBehavior(originHivePos); + if (originBehavior is null) + return null; + + var originEntrance = ToSwarmRelative(originHivePos) + originBehavior.GetEntrancePosition(); + var originFront = originBehavior.GetFrontDirection(); + return SwarmBeePathGeneration.GenerateBuildingPath(blockAccessor, Blockentity.Pos, originEntrance, originFront, hiddenAnchor); + } + + private BeePlannedPathPoint[]? GenerateHangingOutPath(IBlockAccessor blockAccessor, Config cfg) + { + if (candidateHivePositions.Count == 0 || ActiveScoutingCount >= cfg.BeesPerSwarmTraveling || random.NextDouble() >= 0.3) + return null; + + var candidate = candidateHivePositions[random.Next(candidateHivePositions.Count)]; + var candidateBehavior = GetRoamingBehavior(candidate); + if (candidateBehavior is null) + return null; + + var targetEntrance = ToSwarmRelative(candidate) + candidateBehavior.GetEntrancePosition(); + var targetFront = candidateBehavior.GetFrontDirection(); + return SwarmBeePathGeneration.GenerateScoutPath(blockAccessor, Blockentity.Pos, targetEntrance, targetFront, hiddenAnchor); + } + + private (BeePlannedPathPoint[]? path, Vector3 despawnPosition) GenerateMigrationStatePath(IBlockAccessor blockAccessor, Config cfg) + { + var targetTraveling = ComputeTravelingTarget(cfg); + if (targetHivePos is null || targetTraveling <= 0 || ActiveTravelingCount >= targetTraveling) + return (null, hiddenAnchor); + + var targetBehavior = GetRoamingBehavior(targetHivePos); + if (targetBehavior is null) + return (null, hiddenAnchor); + + var targetEntrance = ToSwarmRelative(targetHivePos) + targetBehavior.GetEntrancePosition(); + var targetFront = targetBehavior.GetFrontDirection(); + return (SwarmBeePathGeneration.GenerateMigrationPath(blockAccessor, Blockentity.Pos, targetEntrance, targetFront, hiddenAnchor), targetEntrance); + } + + private BlockEntityBehaviorRoamingBees? GetRoamingBehavior(BlockPos pos) + { + var be = Api.World.BlockAccessor.GetBlockEntity(pos); + return be?.GetBehavior(); + } + + private Vector3 ToSwarmRelative(BlockPos worldPos) + { + return new Vector3( + worldPos.X - Blockentity.Pos.X, + worldPos.Y - Blockentity.Pos.Y, + worldPos.Z - Blockentity.Pos.Z); + } + + private Vector3 GetFrontDirection() + { + return swarmSide switch + { + "north" => new Vector3(0f, 0f, 1f), + "east" => new Vector3(-1f, 0f, 0f), + "south" => new Vector3(0f, 0f, -1f), + "west" => new Vector3(1f, 0f, 0f), + "down" => new Vector3(0f, -1f, 0f), + "up" => new Vector3(0f, 1f, 0f), + _ => new Vector3(0f, 0f, 1f), + }; + } + + public override void ToTreeAttributes(ITreeAttribute tree) + { + base.ToTreeAttributes(tree); + tree.SetFloat("dbg_timeSinceLastSpawn", TimeSinceLastSpawn); + tree.SetFloat("dbg_timeSinceLastHoverSpawn", TimeSinceLastHoverSpawn); + } + + public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve) + { + base.FromTreeAttributes(tree, worldAccessForResolve); + if (Api?.Side.IsClient() == true) + { + TimeSinceLastSpawn = tree.GetFloat("dbg_timeSinceLastSpawn", TimeSinceLastSpawn); + TimeSinceLastHoverSpawn = tree.GetFloat("dbg_timeSinceLastHoverSpawn", TimeSinceLastHoverSpawn); + } + } +} diff --git a/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorRoamingBees.cs b/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorRoamingBees.cs new file mode 100644 index 0000000..fbb6bff --- /dev/null +++ b/RoamingBees/RoamingBees/Behaviors/BlockEntityBehaviorRoamingBees.cs @@ -0,0 +1,433 @@ +using OrekiWoofsBees.Common; +using RoamingBees.Particles; +using RoamingBees.Particles.Catchup; +using RoamingBees.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace RoamingBees.Behaviors; + +public class BlockEntityBehaviorRoamingBees(BlockEntity blockEntity) : BlockEntityBehavior(blockEntity), IBeeSpawnHandler, IBeeSpawnCatchup +{ + private const float spawn_cooldown_seconds = 1.5f; + private const string flower_count_attribute = "roamingbees_flowerCount"; + private const string crop_count_attribute = "roamingbees_cropCount"; + private const string initial_scan_progress_attribute = "roamingbees_initialScanProgress"; + private const string rescan_progress_attribute = "roamingbees_rescanProgress"; + + private static readonly Random random = new(); + + private readonly List activeBees = []; + private readonly TreeAttribute entityAttributeSnapshot = new(); + + private string facingVariantKey = "side"; + private Dictionary? entrancePositions; + private Dictionary? frontDirections; + private RoamingBeesModSystem? modSystem; + private IPlantPositionRegistry? plantPositionRegistry; + private float rainfall; + private float temperature; + private bool initialized; + public const string TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE = "roamingbees_targetBeeParticleCount"; + public const string RADIUS_ATTRIBUTE = "roamingbees_radius"; + public const string NETWORK_CHANNEL_NAME = "bee particle spawns"; + public const int SERVER_TICK_FREQUENCY_DECREASE = 50; + + public int ActiveBeesCount => activeBees.Count; + public IEnumerable ActiveBeesPackets => activeBees.Select(x => x.SpawnPacket); + public float TimeSinceLastSpawn { get; private set; } = (float)(random.NextDouble() * spawn_cooldown_seconds); + public int TargetParticleCount { get; private set; } + public int ScanRadius => GetRadiusFromBlockEntity(); + public int FlowerCount { get; private set; } + public int CropCount { get; private set; } + public float InitialScanProgress { get; private set; } + public float RescanProgress { get; private set; } + + + public override void Initialize(ICoreAPI api, JsonObject properties) + { + if (initialized) + return; + + base.Initialize(api, properties); + initialized = true; + + facingVariantKey = properties["facingVariantKey"].AsString("side"); + entrancePositions = VectorParsing.ParseVector3Map(properties["entrancePositions"]); + frontDirections = VectorParsing.ParseVector3Map(properties["frontDirections"]); + + modSystem = api.ModLoader.GetModSystem(); + modSystem?.BeeSpawnPacketDistributor?.Register(Blockentity.Pos, this); + modSystem?.ClientChannel?.SendPacket(new BeeCatchupRequestPacket { HivePosition = Pos }); + if (modSystem?.Mod.Info.Version.Contains("dev") == true && modSystem?.ClientChannel != null) + modSystem?.Mod.Logger.Event($"{nameof(BlockEntityBehaviorRoamingBees)} sent {nameof(BeeCatchupRequestPacket)} Pos: {Pos}"); + + plantPositionRegistry = api.GetPlantPositionRegistry(); + var radius = GetRadiusFromBlockEntity(); + plantPositionRegistry?.RegisterBeehive(Blockentity.Pos, radius); + + var updateFrequency = 20; + if (api.Side == EnumAppSide.Server) + updateFrequency *= SERVER_TICK_FREQUENCY_DECREASE; + Blockentity.RegisterGameTickListener(OnParticleTick, updateFrequency); + } + + private int GetRadiusFromBlockEntity() + { + var radius = properties[RADIUS_ATTRIBUTE].AsInt(10); + + var tree = new TreeAttribute(); + Blockentity.ToTreeAttributes(tree); + radius = tree.GetAsInt(RADIUS_ATTRIBUTE, radius); + return radius; + } + + private int GetTargetBeeParticleCountFromBlockEntity() + { + var targetBeeParticleCount = properties[TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE].AsInt(10); + targetBeeParticleCount = entityAttributeSnapshot.GetInt(TARGET_BEE_PARTICLE_COUNT_ATTRIBUTE, targetBeeParticleCount); + return targetBeeParticleCount; + } + + public override void OnBlockRemoved() + { + Clear(); + base.OnBlockRemoved(); + } + + public override void OnBlockUnloaded() + { + Clear(); + base.OnBlockUnloaded(); + } + + public override void ToTreeAttributes(ITreeAttribute tree) + { + base.ToTreeAttributes(tree); + + tree.SetInt(flower_count_attribute, FlowerCount); + tree.SetInt(crop_count_attribute, CropCount); + tree.SetFloat(initial_scan_progress_attribute, InitialScanProgress); + tree.SetFloat(rescan_progress_attribute, RescanProgress); + } + + public override void FromTreeAttributes(ITreeAttribute tree, IWorldAccessor worldAccessForResolve) + { + base.FromTreeAttributes(tree, worldAccessForResolve); + + if (Api is null || Api.Side == EnumAppSide.Server) + return; + FlowerCount = tree.GetInt(flower_count_attribute); + CropCount = tree.GetInt(crop_count_attribute); + InitialScanProgress = tree.GetFloat(initial_scan_progress_attribute); + RescanProgress = tree.GetFloat(rescan_progress_attribute); + } + + public void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false) + { + if (Blockentity.Pos != packet.HivePosition) + return; + + if (packet.Path is null) + { + modSystem?.Mod.Logger.Warning("Received packet with Path == null"); + return; + } + + if (packet.Path.Length == 0) + { + modSystem?.Mod.Logger.Warning("Received packet with Path.Length == 0"); + return; + } + + var facing = packet.Facing ?? GetFacing(); + Vector3 entrancePosition = GetEntrancePosition(facing); + var bee = new InternalBeeParticle(entrancePosition, GetFrontDirection(facing), [.. packet.Path.Select(x => x.ToPoint())], BeeRole.Forager, entrancePosition, packet); + activeBees.Add(bee); + if (catchup) + { + var totalDelta = Api.World.Calendar.ElapsedSeconds - packet.TimeElapsedSeconds; + if (modSystem?.Mod.Info.Version.Contains("dev") == true) + modSystem.Mod.Logger.Notification($"HandleBeeParticleSpawn catchup totalDelta: {totalDelta}s"); + for (var i = 0; i < totalDelta / 0.1f; i++) + bee.Step(0.1f, 0.1f); // todo + } + } + + public void Clear() + { + if (modSystem is null) + return; + + modSystem.GlobalActiveBees -= activeBees.Count; + modSystem.BeeSpawnPacketDistributor?.Unregister(Blockentity.Pos); + plantPositionRegistry?.UnregisterBeehive(Blockentity.Pos); + + activeBees.Clear(); + } + + private void OnParticleTick(float dt) + { + if (Block is BlockSkep && !Config.Instance.EnableOnVanillaSkeps) + return; + + var isFgc = Block?.Code?.Domain == "fromgoldencombs"; + var path = Block?.Code?.Path ?? string.Empty; + var isFgcLangstroth = isFgc && path.Contains("langstrothstack", StringComparison.OrdinalIgnoreCase); + var isFgcCeramic = isFgc && path.Contains("ceramicbroodpot", StringComparison.OrdinalIgnoreCase); + + if (isFgcLangstroth) + { + if (!Config.Instance.EnableOnFgcLangstroth) + return; + if (!HasRequiredLangstrothBase()) + return; + } + + if (isFgcCeramic && !Config.Instance.EnableOnFgcCeramic) + return; + + entityAttributeSnapshot.Clear(); + Blockentity.ToTreeAttributes(entityAttributeSnapshot); + + var targetBeeParticleCount = GetTargetBeeParticleCountFromBlockEntity(); + if (targetBeeParticleCount <= 0) + return; + + TargetParticleCount = Math.Max(0, targetBeeParticleCount); + if (Block is BlockSkep) + TargetParticleCount = Config.Instance.RoamingBeesPerVanillaSkep; + if (isFgcLangstroth) + TargetParticleCount = Config.Instance.RoamingBeesPerFgcLangstroth; + if (isFgcCeramic) + TargetParticleCount = Config.Instance.RoamingBeesPerFgcCeramic; + var nearbyPlantPositions = Api.Side == EnumAppSide.Server + ? GetNearbyPlantPositions() + : []; + + Update(dt, nearbyPlantPositions); + } + + private void Update(float dt, IEnumerable nearbyPlantPositions) + { + if (dt > 10f) + return; + dt = Math.Min(0.5f, dt); + + var windVec = Api.World.BlockAccessor.GetWindSpeedAt(Blockentity.Pos); + var windSpeed = (float)Math.Min(windVec.Length(), 1.0); + var climate = Api.World.BlockAccessor.GetClimateAt(Blockentity.Pos, EnumGetClimateMode.NowValues); + rainfall = climate?.Rainfall ?? 0f; + temperature = climate?.Temperature ?? 20f; + + // iterate from end to easily remove from list + for (int i = activeBees.Count - 1; i >= 0; i--) + { + var bee = activeBees[i]; + + if (Api.Side == EnumAppSide.Client) + { + bee.Step(dt, windSpeed); + if (Config.Instance.ReceiveParticles && Api.World is IClientWorldAccessor clientWorld) + BeeVisualParticleRenderer.Spawn(clientWorld, Blockentity.Pos, bee); + } + if (Api.Side == EnumAppSide.Server) + { + for (var j = 0; j < SERVER_TICK_FREQUENCY_DECREASE; j++) + bee.Step(dt / SERVER_TICK_FREQUENCY_DECREASE, windSpeed); + } + + if (bee.ShouldBeDespawned && modSystem != null) + { + activeBees.RemoveAt(i); + modSystem.GlobalActiveBees--; + } + } + + TimeSinceLastSpawn += dt; + if (Api.Side == EnumAppSide.Server) + TrySpawnNewBee(TargetParticleCount, nearbyPlantPositions); + } + + private void TrySpawnNewBee(int targetParticleCount, IEnumerable nearbyPlantPositions) + { + if (TimeSinceLastSpawn < spawn_cooldown_seconds) + return; + + if (!nearbyPlantPositions.Any()) + return; + + var cfg = Config.Instance; + + var sunPos = Api.World.Calendar.GetSunPosition(new Vec3d(Blockentity.Pos.X, Blockentity.Pos.Y, Blockentity.Pos.Z), Api.World.Calendar.TotalDays); + float sunAltitudeDegrees = MathF.Asin(sunPos.Y) * 180f / GameMath.PI; + if (sunPos.Y <= cfg.SunAltitudeMinDegrees) + return; + + if (rainfall >= cfg.RainfallSpawnStopThreshold) + return; + + int maxGlobal = cfg.MaxGlobalRoamingBees; + if (modSystem?.ServerChannel is null) + return; + + if (modSystem.GlobalActiveBees >= maxGlobal) + return; + + int desiredParticleCount = targetParticleCount; + if (activeBees.Count >= desiredParticleCount) + return; + + float sunAltitudeModifier = 1 - Math.Clamp((sunAltitudeDegrees - cfg.SunAltitudeMinDegrees) / cfg.SunAltitudeRangeDegrees, 0f, 1f); + float sunAltitudeCooldownPenalty = sunAltitudeModifier * cfg.MaxSunAltitudeCooldownPenalty; + + float rainfallFactor = rainfall / cfg.RainfallSpawnStopThreshold; + float rainfallCooldownPenalty = rainfallFactor * cfg.MaxRainfallCooldownPenalty; + float spawnCooldownSeconds = rainfallCooldownPenalty + sunAltitudeCooldownPenalty; + + float temperatureRange = cfg.MaxTemperatureParticleSpawn - cfg.MinTemperatureParticleSpawn; + float temperatureFactor = Math.Clamp((cfg.MaxTemperatureParticleSpawn - temperature) / temperatureRange, 0f, 1f); + + var randomChance = random.NextDouble() > temperatureFactor && random.NextDouble() > sunAltitudeModifier && random.NextDouble() > 0.3f; + if (!randomChance) + { + var noise = random.NextDouble() * 2; + TimeSinceLastSpawn = -(float)(random.NextDouble() * spawnCooldownSeconds + noise); + return; + } + + var facing = GetFacing(); + var entrancePos = GetEntrancePosition(facing); + var frontDirection = GetFrontDirection(facing); + var path = BeePathGeneration.GeneratePath(Api.World.BlockAccessor, Blockentity.Pos, entrancePos, frontDirection, nearbyPlantPositions); + if (path is null || path.Length == 0) + return; + + var message = new BeeSpawnPacket + { + HivePosition = Blockentity.Pos, + Path = [.. path.Select(x => BeePlannedPathPointContract.FromPoint(x))], + Facing = facing, + Role = BeeRole.Forager, + TimeElapsedSeconds = Api.World.Calendar.ElapsedSeconds, + EntrancePosition = entrancePos, + DespawnPosition = entrancePos, + }; + activeBees.Add(new InternalBeeParticle(entrancePos, frontDirection, path, BeeRole.Forager, entrancePos, message)); + modSystem.GlobalActiveBees++; + TimeSinceLastSpawn = 0f; + + modSystem.ServerChannel.BroadcastPacket(message); + } + + private bool HasRequiredLangstrothBase() + { + if (Blockentity is not BlockEntityDisplay blockEntityDisplay) + return false; + + return blockEntityDisplay.Inventory.Any(x => x.Itemstack?.Block?.Code?.Path?.Contains("langstrothbase", StringComparison.OrdinalIgnoreCase) == true); + } + + public Vector3 GetEntrancePosition() => GetEntrancePosition(GetFacing()); + public Vector3 GetFrontDirection() => GetFrontDirection(GetFacing()); + + private Vector3 GetEntrancePosition(string facing) + { + if (entrancePositions != null) + { + if (entrancePositions.TryGetValue(facing, out var entrance)) + return entrance; + if (entrancePositions.TryGetValue("*", out entrance)) + return entrance; + } + + return facing switch + { + "north" => new Vector3(0.5f, 0.2f, 0.9f), + "east" => new Vector3(0.1f, 0.2f, 0.5f), + "south" => new Vector3(0.5f, 0.2f, 0.1f), + "west" => new Vector3(0.9f, 0.2f, 0.5f), + _ => new Vector3(0.5f, 0.2f, 0.9f) + }; + } + + private Vector3[] GetNearbyPlantPositions() + { + int radius = Config.Instance.BeeRoamingRadius; + if (radius <= 0) + { + radius = entityAttributeSnapshot.GetInt(RADIUS_ATTRIBUTE, 10); + if (radius <= 0) + return []; + } + + var plantPositions = new List(); + var plantRegistry = Api.GetPlantPositionRegistry(); + if (plantRegistry is null) + return []; + + var (flowerCount, cropCount, initialScanProgress, rescanProgress) = plantRegistry.GetPlantCountsNearPosition(Blockentity.Pos, radius); + FlowerCount = flowerCount; + CropCount = cropCount; + InitialScanProgress = initialScanProgress; + RescanProgress = rescanProgress; + + var (flowers, crops, _, _) = plantRegistry.GetPlantsNearPosition(Blockentity.Pos, radius); + foreach (var pos in flowers.Concat(crops)) + { + float relX = pos.X - Blockentity.Pos.X + 0.5f; + float relY = pos.Y - Blockentity.Pos.Y + 0.5f; + float relZ = pos.Z - Blockentity.Pos.Z + 0.5f; + plantPositions.Add(new Vector3(relX, relY, relZ)); + } + + return [.. plantPositions]; + } + + private Vector3 GetFrontDirection(string facing) + { + if (frontDirections != null) + { + if (frontDirections.TryGetValue(facing, out var frontDirection)) + return frontDirection; + if (frontDirections.TryGetValue("*", out frontDirection)) + return frontDirection; + } + + return facing switch + { + "north" => new Vector3(0f, 0f, 1f), + "east" => new Vector3(-1f, 0f, 0f), + "south" => new Vector3(0f, 0f, -1f), + "west" => new Vector3(1f, 0f, 0f), + _ => new Vector3(0f, 0f, 1f) + }; + } + + private string GetFacing() + { + if (!string.IsNullOrWhiteSpace(facingVariantKey)) + { + var block = Blockentity.Block; + if (block?.Variant?.ContainsKey(facingVariantKey) == true) + return block.Variant[facingVariantKey]; + } + + // omnidirectional block, randomly pick from available entrance directions + if (entrancePositions is { Count: > 0 }) + { + var keys = entrancePositions.Keys.Where(k => k != "*").ToList(); + if (keys.Count > 0) + return keys[random.Next(keys.Count)]; + } + + return "north"; + } +} diff --git a/RoamingBees/RoamingBees/ChatCommands.cs b/RoamingBees/RoamingBees/ChatCommands.cs new file mode 100644 index 0000000..f2897af --- /dev/null +++ b/RoamingBees/RoamingBees/ChatCommands.cs @@ -0,0 +1,83 @@ +using OrekiWoofsBees.Common.Configs; +using OrekiWoofsBees.Common; +using Vintagestory.API.Client; +using Vintagestory.API.Config; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace RoamingBees; + +public partial class RoamingBeesModSystem +{ + private const string lang_domain = "roamingbees"; + private const string config_filename = "RoamingBees.json"; + + public void SetupClientCommands(ICoreClientAPI api) + { + var rootCommand = api.ChatCommands.Create("roamingbees"); + ConfigCommands.Register(rootCommand, api.ChatCommands, lang_domain, serverSide: false, () => Config.Instance, SaveClientConfig); + + rootCommand + .BeginSubCommand("debugpaths") + .WithDescription(Lang.Get($"{lang_domain}:debugpaths-desc")) + .WithArgs(api.ChatCommands.Parsers.OptionalIntRange("value", 0, 1)) + .HandleWith(HandleDebugPathsCommand) + .EndSubCommand(); + } + + public void SetupServerCommands(ICoreServerAPI api) + { + var rootCommand = api.ChatCommands.Create("roamingbees"); + ConfigCommands.Register(rootCommand, api.ChatCommands, lang_domain, serverSide: true, () => Config.Instance, SaveServerConfig); + + rootCommand + .BeginSubCommand("PlantReg") + .BeginSubCommand("BlocksPerTick") + .WithDescription(Lang.Get($"{lang_domain}:plantreg-blockpertick-desc")) + .WithArgs(api.ChatCommands.Parsers.OptionalIntRange("value", 0, 1000)) + .HandleWith(HandlePlantRegBlockPerTickCommand) + .EndSubCommand() + .EndSubCommand(); + } + + private TextCommandResult HandleDebugPathsCommand(TextCommandCallingArgs args) + { + if (args.Parsers[0].IsMissing) + return TextCommandResult.Success($"debugpaths={(DebugPathsEnabled ? 1 : 0)}"); + + if (args.Parsers[0].GetValue() is not int value) + return TextCommandResult.Error(Lang.Get($"{lang_domain}:debugpaths-parse-error")); + + SetDebugPathsEnabled(value == 1); + return TextCommandResult.Success($"debugpaths={(DebugPathsEnabled ? 1 : 0)}"); + } + + private void SaveClientConfig(bool _) + { + var temp = api?.LoadModConfig(config_filename) ?? new Config(); + temp.ReceiveParticles = Config.Instance.ReceiveParticles; + api?.StoreModConfig(temp, config_filename); + } + + private void SaveServerConfig(bool _) + { + api?.StoreModConfig(Config.Instance, config_filename); + serverApi?.Network.GetChannel(CONFIG_CHANNEL_NAME).BroadcastPacket(Config.Instance); + } + + private TextCommandResult HandlePlantRegBlockPerTickCommand(TextCommandCallingArgs args) + { + var registry = api?.ModLoader.GetModSystem(); + if (registry == null) + return TextCommandResult.Error(Lang.Get($"{lang_domain}:plantreg-unavailable")); + + if (args.Parsers[0].IsMissing) + return TextCommandResult.Success($"PlantReg BlocksPerTick={registry.BlocksPerTick}"); + + if (args.Parsers[0].GetValue() is not int value) + return TextCommandResult.Error(Lang.Get($"{lang_domain}:plantreg-blockpertick-parse-error")); + + registry.BlocksPerTick = value; + return TextCommandResult.Success($"PlantReg BlocksPerTick={registry.BlocksPerTick}"); + } +} diff --git a/RoamingBees/RoamingBees/Config.cs b/RoamingBees/RoamingBees/Config.cs new file mode 100644 index 0000000..a425aaf --- /dev/null +++ b/RoamingBees/RoamingBees/Config.cs @@ -0,0 +1,113 @@ +using OrekiWoofsBees.Common.Configs; +using ProtoBuf; + +namespace RoamingBees; + +[ProtoContract(SkipConstructor = true)] +public class Config +{ + public static Config Instance { get; internal set; } = new(); + + // client-side + [ProtoMember(16)] + [ConfigCommand(serverSide: false)] + public bool ReceiveParticles { get; set; } = true; + + // general + [ProtoMember(17)] + [ConfigCommand(serverSide: true)] + public bool EnableOnVanillaSkeps { get; set; } = true; + + [ProtoMember(18)] + [ConfigCommand(serverSide: true, Min = 0, Max = 100)] + public int RoamingBeesPerVanillaSkep { get; set; } = 15; + + [ProtoMember(19)] + [ConfigCommand(serverSide: true)] + public bool EnableOnFgcCeramic { get; set; } = true; + + [ProtoMember(20)] + [ConfigCommand(serverSide: true, Min = 0, Max = 100)] + public int RoamingBeesPerFgcCeramic { get; set; } = 15; + + [ProtoMember(21)] + [ConfigCommand(serverSide: true)] + public bool EnableOnFgcLangstroth { get; set; } = true; + + [ProtoMember(22)] + [ConfigCommand(serverSide: true, Min = 0, Max = 100)] + public int RoamingBeesPerFgcLangstroth { get; set; } = 25; + + [ProtoMember(1)] + [ConfigCommand(serverSide: true, Min = 0, Max = 200)] + public int BeeRoamingRadius { get; set; } = 0; + + // swarms + [ProtoMember(23)] + [ConfigCommand(serverSide: true, Min = 0, Max = 50)] + public int BeesPerSwarmHovering { get; set; } = 15; + + [ProtoMember(24)] + [ConfigCommand(serverSide: true, Min = 0, Max = 50)] + public int BeesPerSwarmTraveling { get; set; } = 15; + + // weather effects + [ProtoMember(2)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float RainfallSpawnStopThreshold { get; set; } = 0.1f; + + [ProtoMember(3)] + [ConfigCommand(serverSide: true, Min = 0, Max = 300)] + public float MaxRainfallCooldownPenalty { get; set; } = 60f; + + [ProtoMember(4)] + [ConfigCommand(serverSide: true)] + public bool GreenhouseAffectsBeehive { get; set; } = true; + + [ProtoMember(5)] + [ConfigCommand(serverSide: true, Min = -20, Max = 40)] + public float MinTemperatureParticleSpawn { get; set; } = 8f; + + [ProtoMember(6)] + [ConfigCommand(serverSide: true, Min = -20, Max = 50)] + public float MaxTemperatureParticleSpawn { get; set; } = 18f; + + [ProtoMember(7)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float OptimalTemperatureSpawnChance { get; set; } = 0.75f; + + [ProtoMember(8)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float BaseWindFrequency { get; set; } = 0.1f; + + [ProtoMember(9)] + [ConfigCommand(serverSide: true, Min = 0, Max = 20)] + public float WindFrequencyMultiplier { get; set; } = 4f; + + [ProtoMember(10)] + [ConfigCommand(serverSide: true, Min = 0, Max = 1)] + public float BaseWindNoiseStrength { get; set; } = 0.2f; + + [ProtoMember(11)] + [ConfigCommand(serverSide: true, Min = 0, Max = 5)] + public float MaxWindNoiseStrength { get; set; } = 1f; + + [ProtoMember(12)] + [ConfigCommand(serverSide: true, Min = -90, Max = 90)] + public float SunAltitudeMinDegrees { get; set; } = -5f; + + [ProtoMember(13)] + [ConfigCommand(serverSide: true, Min = 0, Max = 180)] + public float SunAltitudeRangeDegrees { get; set; } = 10f; + + [ProtoMember(14)] + [ConfigCommand(serverSide: true, Min = 0, Max = 300)] + public float MaxSunAltitudeCooldownPenalty { get; set; } = 30f; + + // particles + [ProtoMember(15)] + [ConfigCommand(serverSide: true, Min = 1, Max = 2000)] + public int MaxGlobalRoamingBees { get; set; } = 200; + // hidden + public bool EyesAndAntennaeEnabled { get; set; } = false; +} diff --git a/RoamingBees/RoamingBees/ModConfigSetup.cs b/RoamingBees/RoamingBees/ModConfigSetup.cs new file mode 100644 index 0000000..46a08d5 --- /dev/null +++ b/RoamingBees/RoamingBees/ModConfigSetup.cs @@ -0,0 +1,100 @@ +using ConfigLib; +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace RoamingBees; + +public partial class RoamingBeesModSystem : ModSystem +{ + private ConfigLibModSystem? configLibSystem; + + private void SubscribeToConfigChange(ICoreAPI api) + { + configLibSystem = api.ModLoader.GetModSystem(); + if (configLibSystem == null) + return; + + configLibSystem.SettingChanged += OnSettingChanged; + configLibSystem.ConfigsLoaded += OnConfigsLoaded; + } + + private void UnsubscribeFromConfigChange() + { + if (configLibSystem is null) + return; + configLibSystem.SettingChanged -= OnSettingChanged; + configLibSystem.ConfigsLoaded -= OnConfigsLoaded; + } + + private void OnConfigsLoaded() + { + configLibSystem?.GetConfig("roamingbees")?.AssignSettingsValues(Config.Instance); + } + + private void OnSettingChanged(string domain, IConfig config, ISetting setting) + { + if (domain != "roamingbees") + return; + + try + { + if (api != null) + Config.Instance = api.LoadModConfig("RoamingBees.json") ?? Config.Instance; + setting.AssignSettingValue(Config.Instance); + serverApi?.Network.GetChannel(CONFIG_CHANNEL_NAME).BroadcastPacket(Config.Instance); + } + catch (Exception ex) + { + Mod.Logger.Error(ex); + } + } + + private void InitializeServerConfig(ICoreServerAPI api) + { + try + { + Config.Instance = api.LoadModConfig("RoamingBees.json"); + Config.Instance ??= new Config(); + api.StoreModConfig(Config.Instance, "RoamingBees.json"); + api.Event.PlayerJoin += OnPlayerJoin; + } + catch (Exception e) + { + Mod.Logger.Error("Could not load config! Loading default settings instead."); + Mod.Logger.Error(e); + Config.Instance = new Config(); + } + } + + private void InitializeClientConfig(ICoreClientAPI api) + { + try + { + Config? clientConfig = api.LoadModConfig("RoamingBees.json"); + if (clientConfig != null) + Config.Instance.ReceiveParticles = clientConfig.ReceiveParticles; + } + catch (Exception e) + { + Mod.Logger.Error("Could not load config! Loading default settings instead."); + Mod.Logger.Error(e); + Config.Instance = new Config(); + } + } + + private void OnPlayerJoin(IServerPlayer byPlayer) + { + serverApi!.Network.GetChannel(CONFIG_CHANNEL_NAME).SendPacket(Config.Instance, byPlayer); + } + + private void OnConfigReceivedFromServer(Config packet) + { + var currentConfig = Config.Instance; + Config.Instance = packet; + if (currentConfig is null) + return; + Config.Instance.ReceiveParticles = currentConfig.ReceiveParticles; + } +} diff --git a/RoamingBees/RoamingBees/Particles/BeePathGeneration.cs b/RoamingBees/RoamingBees/Particles/BeePathGeneration.cs new file mode 100644 index 0000000..98696ab --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeePathGeneration.cs @@ -0,0 +1,432 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.GameContent; + +namespace RoamingBees.Particles; + +public static class BeePathGeneration +{ + private const int max_plants_low = 2; + private const int max_plants_high = 14; + private static Random random { get; } = new(); + private static List temp_list { get; } = []; + private static List temp_plants { get; } = []; + private static HashSet used_plants { get; } = []; + internal static Vector3[] Offsets { get; } + + static BeePathGeneration() + { + Offsets = [ + new Vector3(1, 0, 0), + new Vector3(-1, 0, 0), + new Vector3(0, 0, 1), + new Vector3(0, 0, -1), + new Vector3(1, 0, 1), + new Vector3(1, 0, -1), + new Vector3(-1, 0, 1), + new Vector3(-1, 0, -1), + new Vector3(0, 1.5f, 0), + new Vector3(0, -1.5f, 0), + new Vector3(2, 0, 0), + new Vector3(-2, 0, 0), + new Vector3(0, 0, 2), + new Vector3(0, 0, -2), + new Vector3(2, 0, 1), + new Vector3(2, 0, -1), + new Vector3(-2, 0, 1), + new Vector3(-2, 0, -1), + new Vector3(1, 0, 2), + new Vector3(1, 0, -2), + new Vector3(-1, 0, 2), + new Vector3(-1, 0, -2), + new Vector3(3, 0, 0), + new Vector3(-3, 0, 0), + new Vector3(0, 0, 3), + new Vector3(0, 0, -3), + new Vector3(0, 3, 0), + new Vector3(0, -3, 0), + ]; + } + + public static BeePlannedPathPoint[] GeneratePath( + IBlockAccessor blockAccessor, + BlockPos hiveWorldPos, + Vector3 beehiveEntrancePosition, + Vector3 frontDirection, + IEnumerable plantPositions) + { + temp_list.Clear(); + temp_plants.Clear(); + used_plants.Clear(); + + foreach (var plant in plantPositions) + temp_plants.Add(plant); + + var plant_count = random.Next(max_plants_low, max_plants_high + 1); + for (var i = 0; i < plant_count; i++) + { + var lastPosition = temp_list.Count > 0 ? temp_list[^1].Position : GetFrontOfEntrance(beehiveEntrancePosition, frontDirection); + foreach (var plantPathPoint in PickTargetAndGeneratePathPoints(blockAccessor, hiveWorldPos, lastPosition)) + temp_list.Add(plantPathPoint); + } + + if (temp_list.Count == 0) + return []; + + foreach (var returnPathPoint in GetReturnPath(blockAccessor, hiveWorldPos, temp_list.Last().Position, beehiveEntrancePosition, frontDirection)) + temp_list.Add(new BeePlannedPathPoint(returnPathPoint, true, false)); + + temp_list[^1] = temp_list[^1] with { IsSlowdown = true }; + temp_list.Add(new BeePlannedPathPoint(beehiveEntrancePosition, false, true)); + + return [.. temp_list]; + } + + public static bool TryGetTargetBoxAtWorldBlock(IBlockAccessor blockAccessor, BlockPos blockPosWorld, out Cuboidf targetBox) + { + targetBox = new Cuboidf(); + + var blockCenter = new Vector3(blockPosWorld.X + 0.5f, blockPosWorld.Y + 0.5f, blockPosWorld.Z + 0.5f); + var (plantOffset, plantSize) = GetPlantOffsetAndSize(blockAccessor, blockCenter, new BlockPos(0, 0, 0)); + if (plantOffset is null || plantSize is null) + return false; + + var adjustedCenter = AdjustCenter(blockCenter, plantOffset.Value, plantSize.Value); + var halfSize = plantSize.Value * 0.5f; + + targetBox = new Cuboidf( + adjustedCenter.X - halfSize.X, + adjustedCenter.Y - halfSize.Y, + adjustedCenter.Z - halfSize.Z, + adjustedCenter.X + halfSize.X, + adjustedCenter.Y + halfSize.Y, + adjustedCenter.Z + halfSize.Z + ); + + return true; + } + + private static Vector3 AdjustCenter(Vector3 blockCenter, Vector3 plantOffset, Vector3 plantSize) + { + var blockPosFloored = new Vector3((int)Math.Floor(blockCenter.X), (int)Math.Floor(blockCenter.Y), (int)Math.Floor(blockCenter.Z)); + return new Vector3( + blockPosFloored.X + plantOffset.X, + blockPosFloored.Y + plantOffset.Y + plantSize.Y * 0.5f, + blockPosFloored.Z + plantOffset.Z); + } + + private static (Vector3?, Vector3?) GetPlantOffsetAndSize(IBlockAccessor blockAccessor, Vector3 blockCenter, BlockPos hiveWorldPos) + { + var blockPos = new BlockPos((int)Math.Floor(blockCenter.X), (int)Math.Floor(blockCenter.Y), (int)Math.Floor(blockCenter.Z)); + var blockPosWorld = blockPos + hiveWorldPos; + + var block = blockAccessor.GetBlock(blockPosWorld); + var attributes = block.GetAttributes(blockAccessor, blockPos); + + if (TryGetPlantContainerOffsetAndSize(blockAccessor, blockPosWorld, block, attributes, out var containerOffset, out var containerSize)) + return (containerOffset, containerSize); + + if (TryGetBlockPlantOffsetAndSize(blockAccessor, blockPosWorld, block, attributes, out var plantOffset, out var plantSize)) + return (plantOffset, plantSize); + + return (null, null); + } + + private static bool TryGetPlantContainerOffsetAndSize( + IBlockAccessor blockAccessor, + BlockPos blockPosWorld, + Block block, + JsonObject attributes, + out Vector3 offset, + out Vector3 size) + { + offset = default; + size = default; + + if (block.Class != "BlockPlantContainer") + return false; + + Cuboidf[] containerSelectionBoxes = block.GetSelectionBoxes(blockAccessor, blockPosWorld); + if (containerSelectionBoxes.Length == 0) + return false; + + Cuboidf containerSelectionBox = containerSelectionBoxes.First(); + var fillHeight = attributes["fillHeight"]?.AsFloat(0.5f) ?? 0.5f; + var sitHeight = attributes["sitHeight"]?.AsFloat(Math.Min(0.9f, fillHeight + 0.2f)) ?? Math.Min(0.9f, fillHeight + 0.2f); + + var targetMinY = Math.Clamp(fillHeight + 0.05f, 0f, 0.95f); + var targetMaxY = Math.Clamp(sitHeight + 0.1f, targetMinY + 0.05f, 0.98f); + var targetHeight = targetMaxY - targetMinY; + + offset = new Vector3((float)containerSelectionBox.Center.X, targetMinY, (float)containerSelectionBox.Center.Z); + size = new Vector3(containerSelectionBox.Width, targetHeight, containerSelectionBox.Length); + return true; + } + + private static bool TryGetBlockPlantOffsetAndSize( + IBlockAccessor blockAccessor, + BlockPos blockPosWorld, + Block block, + JsonObject attributes, + out Vector3 offset, + out Vector3 size) + { + offset = default; + size = default; + + if (block is not BlockPlant blockPlant) + return false; + + if (blockPlant.RandomDrawOffset == 0) + return false; + + Cuboidf[] selectionBoxes = blockPlant.GetSelectionBoxes(blockAccessor, blockPosWorld); + if (selectionBoxes.Length == 0) + return false; + + Cuboidf selectionBox = selectionBoxes.First(); + var height = blockPlant.drawnHeight / 32f; + var sitHeightAttr = attributes["sitHeight"]; + if (sitHeightAttr != null) + { + var sitHeight = sitHeightAttr.AsFloat(1); + height *= sitHeight; + } + + offset = new Vector3((float)selectionBox.Center.X, 0f, (float)selectionBox.Center.Z); + size = new Vector3(selectionBox.Width, height, selectionBox.Length); + return true; + } + + private static IEnumerable PickTargetAndGeneratePathPoints(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 lastPosition) + { + for (var i = 0; i < 2; i++) + { + var nearby_only = i == 1; + if (!TryPickPlantPosition(lastPosition, nearby_only, out var blockCenter)) + yield break; + + var (plantOffset, plantSize) = GetPlantOffsetAndSize(blockAccessor, blockCenter, hiveWorldPos); + if (plantSize is not null && plantOffset is not null) + blockCenter = AdjustCenter(blockCenter, plantOffset.Value, plantSize.Value); + + var size = plantSize ?? new Vector3(1, 1, 1); + if (TryPickPlantTarget(blockAccessor, hiveWorldPos, lastPosition, blockCenter, size, out var target, out var waypoint, true)) + { + used_plants.Add(blockCenter); + if (waypoint is not null) + yield return new BeePlannedPathPoint(waypoint.Value, true, false); + foreach (var plantTarget in GetPlantTargets(blockCenter, size, target)) + yield return new BeePlannedPathPoint(plantTarget, false, false); + yield break; + } + } + } + + private static bool TryPickPlantPosition(Vector3 lastPosition, bool nearbyOnly, out Vector3 blockCenter) + { + blockCenter = Vector3.Zero; + int valid_count = 0; + + for (var i = 0; i < temp_plants.Count; i++) + { + var plant = temp_plants[i]; + if (used_plants.Contains(plant)) + continue; + + if (nearbyOnly && Vector3.Distance(lastPosition, plant) > 5f) + continue; + + valid_count++; + if (random.Next(valid_count) == 0) + blockCenter = plant; + } + + return valid_count > 0; + } + + private static IEnumerable GetPlantTargets(Vector3 blockCenter, Vector3 plantSize, Vector3 firstTarget) + { + yield return firstTarget; + + var total_targets = random.Next(3, 10); + for (var i = 1; i < total_targets; i++) + yield return GetRandomPointInPlantBlock(blockCenter, plantSize); + } + + private static bool TryPickPlantTarget(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 lastPosition, Vector3 blockCenter, Vector3 plantSize, out Vector3 target, out Vector3? waypoint, bool biasUpwards = false) + { + waypoint = null; + target = GetRandomPointInPlantBlock(blockCenter, plantSize, biasUpwards); + + if (HasLineOfSight(blockAccessor, hiveWorldPos, lastPosition, target)) + return true; + + if (TryFindWaypoint(blockAccessor, hiveWorldPos, lastPosition, target, out var waypointPosition)) + { + waypoint = waypointPosition; + return true; + } + + return false; + } + + private static Vector3 GetRandomPointInPlantBlock(Vector3 blockCenter, Vector3 plantSize, bool biasUpwards = false) + { + double horizontal_range_x = plantSize.X; + double horizontal_half_range_x = horizontal_range_x / 2d; + float offsetX = (float)(random.NextDouble() * horizontal_range_x - horizontal_half_range_x); + + double horizontal_range_z = plantSize.Z; + double horizontal_half_range_z = horizontal_range_z / 2d; + float offsetZ = (float)(random.NextDouble() * horizontal_range_z - horizontal_half_range_z); + + double vertical_range = plantSize.Y; + double vertical_half_range = vertical_range / 2d; + float offsetY = biasUpwards + ? (float)vertical_half_range + : (float)(random.NextDouble() * vertical_range - vertical_half_range); + return blockCenter + new Vector3(offsetX, offsetY, offsetZ); + } + + internal static bool HasLineOfSight(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 from, Vector3 to) + { + var direction = to - from; + float distance = direction.Length(); + if (distance < 0.1f) + return true; + + var normalized = Vector3.Normalize(direction); + var startMargin = 0.1f; + var endMargin = 0.1f; + if (distance <= startMargin + endMargin) + return true; + + // The block the ray starts in should never count as an obstacle - + // the bee is already there (e.g. the swarm block itself). + var fromBlock = new Vec3i((int)MathF.Floor(from.X), (int)MathF.Floor(from.Y), (int)MathF.Floor(from.Z)); + + var stepSize = 0.4f; + var lateralOffset = 0.25f; + var lateral = new Vector3(-normalized.Z, 0f, normalized.X); + var lateralNormalized = lateral.Length() > 0.001f ? Vector3.Normalize(lateral) : Vector3.Zero; + var leftOffset = lateralNormalized * lateralOffset; + var rightOffset = lateralNormalized * -lateralOffset; + + for (float t = startMargin; t <= distance - endMargin; t += stepSize) + { + var checkPosRelative = from + normalized * t; + if (IsBlockedAt(blockAccessor, hiveWorldPos, checkPosRelative.X, checkPosRelative.Y, checkPosRelative.Z, fromBlock)) + return false; + + if (leftOffset != Vector3.Zero && IsBlockedAt(blockAccessor, hiveWorldPos, checkPosRelative.X + leftOffset.X, checkPosRelative.Y, checkPosRelative.Z + leftOffset.Z, fromBlock)) + return false; + + if (rightOffset != Vector3.Zero && IsBlockedAt(blockAccessor, hiveWorldPos, checkPosRelative.X + rightOffset.X, checkPosRelative.Y, checkPosRelative.Z + rightOffset.Z, fromBlock)) + return false; + } + + return true; + } + + private static bool IsBlockedAt(IBlockAccessor blockAccessor, BlockPos anchor, float rx, float ry, float rz, Vec3i skipBlock) + { + var block = new Vec3i((int)MathF.Floor(rx), (int)MathF.Floor(ry), (int)MathF.Floor(rz)); + if (block == skipBlock) + return false; + return BlocksLineOfSight(GetBlockAtRelative(blockAccessor, anchor, rx, ry, rz)); + } + + private static Block GetBlockAtRelative(IBlockAccessor blockAccessor, BlockPos anchor, float rx, float ry, float rz) + { + var pos = new BlockPos(anchor.X + (int)MathF.Floor(rx), anchor.Y + (int)MathF.Floor(ry), anchor.Z + (int)MathF.Floor(rz)); + return blockAccessor.GetBlock(pos); + } + + internal static bool TryFindWaypoint(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 from, Vector3 to, out Vector3 waypoint) + { + var midPoint = (from + to) * 0.5f; + + if (TryWaypointsAroundPosition(blockAccessor, hiveWorldPos, from, to, basePos: midPoint, Offsets, out waypoint)) + return true; + + if (TryWaypointsAroundPosition(blockAccessor, hiveWorldPos, from, to, basePos: from, Offsets, out waypoint)) + return true; + + if (TryWaypointsAroundPosition(blockAccessor, hiveWorldPos, from, to, basePos: to, Offsets, out waypoint)) + return true; + + return false; + } + + private static bool TryWaypointsAroundPosition(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 from, Vector3 to, Vector3 basePos, Vector3[] offsets, out Vector3 waypoint) + { + waypoint = Vector3.Zero; + + foreach (var offset in offsets) + { + var waypointCandidate = GetOuterEdgePosition(basePos, offset); + + if (HasLineOfSight(blockAccessor, hiveWorldPos, from, waypointCandidate) && HasLineOfSight(blockAccessor, hiveWorldPos, waypointCandidate, to)) + { + waypoint = waypointCandidate; + return true; + } + } + + return false; + } + + internal static Vector3 GetOuterEdgePosition(Vector3 basePos, Vector3 offset) + { + return basePos + new Vector3( + offset.X != 0 ? offset.X + 0.5f * MathF.Sign(offset.X) : 0, + offset.Y != 0 ? offset.Y + 0.5f * MathF.Sign(offset.Y) : 0, + offset.Z != 0 ? offset.Z + 0.5f * MathF.Sign(offset.Z) : 0 + ); + } + + internal static bool BlocksLineOfSight(Block block) + { + if (block == null || block.BlockId == 0) + return false; + + if (block.IsLiquid()) + return true; + + return block.CollisionBoxes != null && block.CollisionBoxes.Length > 0; + } + + private static IEnumerable GetReturnPath(IBlockAccessor blockAccessor, BlockPos hiveWorldPos, Vector3 lastPosition, Vector3 beehiveEntrancePosition, Vector3 frontDirection) + { + var frontOfEntrance = GetFrontOfEntrance(beehiveEntrancePosition, frontDirection); + var targetPos = frontOfEntrance; + if (HasLineOfSight(blockAccessor, hiveWorldPos, lastPosition, targetPos)) + { + yield return targetPos; + yield break; + } + + if (TryFindWaypoint(blockAccessor, hiveWorldPos, lastPosition, targetPos, out var waypoint)) + { + yield return waypoint; + yield return targetPos; + yield break; + } + + foreach (var pathPoint in temp_list.Reverse()) + yield return pathPoint.Position; + + yield return frontOfEntrance; + } + + internal static Vector3 GetFrontOfEntrance(Vector3 beehiveEntrancePosition, Vector3 frontDirection) + { + return beehiveEntrancePosition + Vector3.Normalize(frontDirection) * 0.5f + Vector3.UnitY * 0.2f; + } +} diff --git a/RoamingBees/RoamingBees/Particles/BeePlannedPathPoint.cs b/RoamingBees/RoamingBees/Particles/BeePlannedPathPoint.cs new file mode 100644 index 0000000..bcc4f80 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeePlannedPathPoint.cs @@ -0,0 +1,5 @@ +using System.Numerics; + +namespace RoamingBees.Particles; + +public readonly record struct BeePlannedPathPoint(Vector3 Position, bool IsIntermediate, bool IsSlowdown, Vector3? LookDirection = null); diff --git a/RoamingBees/RoamingBees/Particles/BeePlannedPathPointContract.cs b/RoamingBees/RoamingBees/Particles/BeePlannedPathPointContract.cs new file mode 100644 index 0000000..85619c6 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeePlannedPathPointContract.cs @@ -0,0 +1,37 @@ +using OrekiWoofsBees.Common; +using ProtoBuf; +using System.Numerics; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +[ProtoContract] +public record struct BeePlannedPathPointContract(Vec3f Position, bool IsIntermediate, bool IsSlowdown, Vector3? LookDirection = null) +{ + [ProtoMember(1)] + public Vec3f Position = Position; + + [ProtoMember(2)] + public bool IsIntermediate = IsIntermediate; + + [ProtoMember(3)] + public bool IsSlowdown = IsSlowdown; + + [ProtoMember(4)] + public Vec3f _LookDirection = LookDirection?.ToVec3f() ?? new(); + public Vector3? LookDirection + { + readonly get => (_LookDirection.X * _LookDirection.X + _LookDirection.Y * _LookDirection.Y + _LookDirection.Z * _LookDirection.Z) > 0.0001f ? _LookDirection.ToVector3() : null; + init => _LookDirection = value.HasValue ? value.Value.ToVec3f() : new Vec3f(); + } + + public readonly BeePlannedPathPoint ToPoint() + { + return new BeePlannedPathPoint(new(Position.X, Position.Y, Position.Z), IsIntermediate, IsSlowdown, LookDirection); + } + + public static BeePlannedPathPointContract FromPoint(BeePlannedPathPoint p) + { + return new BeePlannedPathPointContract(new(p.Position.X, p.Position.Y, p.Position.Z), p.IsIntermediate, p.IsSlowdown, p.LookDirection); + } +} diff --git a/RoamingBees/RoamingBees/Particles/BeeRole.cs b/RoamingBees/RoamingBees/Particles/BeeRole.cs new file mode 100644 index 0000000..0932384 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeeRole.cs @@ -0,0 +1,9 @@ +namespace RoamingBees.Particles; + +public enum BeeRole +{ + Forager, + Hovering, + Traveling, + Scouting, +} diff --git a/RoamingBees/RoamingBees/Particles/BeeSpawnPacket.cs b/RoamingBees/RoamingBees/Particles/BeeSpawnPacket.cs new file mode 100644 index 0000000..c8caf20 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeeSpawnPacket.cs @@ -0,0 +1,34 @@ +using OrekiWoofsBees.Common; +using ProtoBuf; +using System.Numerics; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +[ProtoContract] +public record struct BeeSpawnPacket +{ + [ProtoMember(1)] + public required BlockPos HivePosition; + + [ProtoMember(2)] + public required BeePlannedPathPointContract[] Path; + + [ProtoMember(3)] + public required string Facing; + + [ProtoMember(4)] + public required BeeRole Role; + + [ProtoMember(5)] + public Vec3f _EntrancePosition; + + [ProtoMember(6)] + public Vec3f _DespawnPosition; + + public required Vector3 EntrancePosition { readonly get => _EntrancePosition.ToVector3(); set => _EntrancePosition = value.ToVec3f(); } + public required Vector3 DespawnPosition { readonly get => _DespawnPosition.ToVector3(); set => _DespawnPosition = value.ToVec3f(); } + + [ProtoMember(7)] + public required long TimeElapsedSeconds; +} diff --git a/RoamingBees/RoamingBees/Particles/BeeSpawnPacketDistributor.cs b/RoamingBees/RoamingBees/Particles/BeeSpawnPacketDistributor.cs new file mode 100644 index 0000000..d88c0f7 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeeSpawnPacketDistributor.cs @@ -0,0 +1,85 @@ +using OrekiWoofsBees.Common; +using RoamingBees.Particles.Catchup; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; +using Vintagestory.API.Server; + +namespace RoamingBees.Particles; + +internal class BeeSpawnPacketDistributor(RoamingBeesModSystem modSystem, EnumAppSide appSide) +{ + private readonly RoamingBeesModSystem modSystem = modSystem; + private readonly EnumAppSide appSide = appSide; + + private Dictionary spawnHandlers { get; } = []; + private Dictionary catchupHandlers { get; } = []; + + public void Register(BlockPos position, object manager) + { + if (manager is IBeeSpawnHandler spawnHandler) + spawnHandlers.TryAdd(new(position.X, position.Y, position.Z), spawnHandler); + if (manager is IBeeSpawnCatchup beeSpawnCatchup) + catchupHandlers.TryAdd(new(position.X, position.Y, position.Z), beeSpawnCatchup); + } + + public void Unregister(BlockPos position) + { + var pos = new Vector3(position.X, position.Y, position.Z); + spawnHandlers.Remove(pos); + catchupHandlers.Remove(pos); + } + + public void HandleBeeParticleSpawn(BeeSpawnPacket packet) + { + if (!Config.Instance.ReceiveParticles || appSide.HasFlag(EnumAppSide.Server)) + return; + + if (spawnHandlers.TryGetValue(new(packet.HivePosition.X, packet.HivePosition.Y, packet.HivePosition.Z), out var hive)) + hive.HandleBeeParticleSpawn(packet); + } + + public void HandleBeeParticleCatchup(BeeCatchupPacket catchupPacket) + { + if (modSystem.Mod.Info.Version.Contains("dev")) + { + modSystem.Mod.Logger.Event($"HandleBeeParticleCatchup, Pos: {catchupPacket.HivePosition.ToVec3f().ToVector3()}, Count: {catchupPacket.SpawnPackets.Count()}"); + } + + if (!Config.Instance.ReceiveParticles || appSide.HasFlag(EnumAppSide.Server)) + return; + + if (!spawnHandlers.TryGetValue(new(catchupPacket.HivePosition.X, catchupPacket.HivePosition.Y, catchupPacket.HivePosition.Z), out var hive)) + return; + foreach (var spawnPacket in catchupPacket.SpawnPackets) + hive.HandleBeeParticleSpawn(spawnPacket, catchup: true); + } + + internal void OnBeeCatchupRequest(IServerPlayer fromPlayer, BeeCatchupRequestPacket catchupRequest) + { + if (!appSide.HasFlag(EnumAppSide.Server)) + return; + + if (!catchupHandlers.TryGetValue(new(catchupRequest.HivePosition.X, catchupRequest.HivePosition.Y, catchupRequest.HivePosition.Z), out var hive)) + return; + + var spawnPackets = hive.ActiveBeesPackets.ToList(); + if (spawnPackets.Count is 0) + return; + + var message = new BeeCatchupPacket + { + HivePosition = catchupRequest.HivePosition, + SpawnPackets = spawnPackets, + }; + + if (modSystem.Mod.Info.Version.Contains("dev")) + modSystem.Mod.Logger.Event($"OnBeeCatchupRequest, Pos: {catchupRequest.HivePosition.ToVec3f().ToVector3()}, Count: {message.SpawnPackets.Count()}"); + + modSystem.ServerChannel?.SendPacket(message, fromPlayer); + } + + public void Clear() => spawnHandlers.Clear(); +} diff --git a/RoamingBees/RoamingBees/Particles/BeeVisualParticleRenderer.cs b/RoamingBees/RoamingBees/Particles/BeeVisualParticleRenderer.cs new file mode 100644 index 0000000..16f6a56 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/BeeVisualParticleRenderer.cs @@ -0,0 +1,235 @@ +using System.Numerics; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +public static class BeeVisualParticleRenderer +{ + private static SimpleParticleProperties? _baseProperties; + private static SimpleParticleProperties BaseProperties + { + get => _baseProperties ??= new SimpleParticleProperties() + { + MinPos = new Vec3d(), + MinVelocity = new Vec3f(), + MinSize = 0.5f, + MaxSize = 0.5f, + LifeLength = 0.02f, + WithTerrainCollision = false, + ParticleModel = EnumParticleModel.Cube, + MinQuantity = 1, + AddQuantity = 0, + Color = ColorUtil.ToRgba(255, 180, 160, 0), // ARGB: alpha=255, yellow RGB + GravityEffect = 0f, + SelfPropelled = false, + RandomVelocityChange = false, + WindAffected = false, + }; + } + + public static float BodyOffsetDivider { get; set; } = 32f; + public static float HeadOffsetMultiplier { get; set; } = 1f; + public static float TailOffsetMultiplier { get; set; } = 1f; + public static float HeadVerticalOffsetMultiplier { get; set; } = 0.5f; + public static float TailVerticalOffsetMultiplier { get; set; } = 0.5f; + + public static float WingSize { get; set; } = 0.28f; + public static float WingHorizontalOffsetMultiplier { get; set; } = 1f; + public static float WingForwardOffsetMultiplierWhenFlap { get; set; } = -2f; + public static float WingForwardOffsetMultiplierWhenRest { get; set; } = -1f; + public static float WingVerticalOffsetMultiplierWhenFlap { get; set; } = 0f; + public static float WingVerticalOffsetMultiplierWhenRest { get; set; } = 1f; + public static Vec3d WingPosRandomness { get; set; } = new Vec3d(1, 0, 1) * 0.005; + + public static float EyeSize { get; set; } = 0.18f; + public static float EyeForwardOffsetMultiplier { get; set; } = 1.7f; + public static float EyeHorizontalOffsetMultiplier { get; set; } = 0.5f; + public static float EyeVerticalOffsetMultiplier { get; set; } = 0.6f; + + public static float AntennaSize { get; set; } = 0.12f; + public static int AntennaSegments { get; set; } = 5; + public static float AntennaForwardOffsetMultiplier { get; set; } = 1.5f; + public static float AntennaHorizontalOffsetMultiplier { get; set; } = 0.35f; + public static float AntennaVerticalStartMultiplier { get; set; } = 0.9f; + public static float AntennaVerticalStepMultiplier { get; set; } = 0.3f; + + public static void Spawn(IClientWorldAccessor world, BlockPos hivePosition, InternalBeeParticle bee) + { + var relativePosition = bee.Position; + var velocity = bee.Velocity; + + var middleParticleProps = SpawnBody(world, hivePosition, relativePosition, velocity); + + var accDirXZ = bee.AccelerationDirection with { Y = 0 }; + Vector3 direction = bee.LookDirection.HasValue + ? bee.LookDirection.Value with { Y = 0 } is var ld && ld.LengthSquared() > 0.001f ? Vector3.Normalize(ld) : Vector3.UnitZ + : accDirXZ.Length() > 0.001f ? Vector3.Normalize(accDirXZ) : Vector3.UnitZ; + double offset = middleParticleProps.MinSize / BodyOffsetDivider; + + var wingPhase = bee.GetWingPhase(); + + SpawnHead(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize); + SpawnTail(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize); + SpawnWing(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize, wingPhase, WingSide.Left); + SpawnWing(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize, wingPhase, WingSide.Right); + if (Config.Instance.EyesAndAntennaeEnabled) + { + SpawnEyes(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize); + SpawnAntennae(world, hivePosition, relativePosition, velocity, direction, offset, middleParticleProps.MinSize); + } + } + + private static SimpleParticleProperties SpawnBody(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity) + { + var bodyParticleProps = BaseProperties.Clone(world); + bodyParticleProps.MinPos = new Vec3d(relativePosition.X, relativePosition.Y, relativePosition.Z).Add(hivePosition); + bodyParticleProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + + world.SpawnParticles(bodyParticleProps); + return bodyParticleProps; + } + + private static void SpawnHead(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity, Vector3 direction, double offset, float bodySize) + { + var headParticleProps = BaseProperties.Clone(world); + headParticleProps.Color = ColorUtil.ToRgba(255, 0, 0, 0); + headParticleProps.MinPos = new Vec3d( + relativePosition.X + direction.X * offset * HeadOffsetMultiplier, + relativePosition.Y + (bodySize / BodyOffsetDivider) * HeadVerticalOffsetMultiplier, + relativePosition.Z + direction.Z * offset * HeadOffsetMultiplier + ).Add(hivePosition); + headParticleProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + + world.SpawnParticles(headParticleProps); + } + + private static void SpawnTail(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity, Vector3 direction, double offset, float bodySize) + { + var tailParticleProps = BaseProperties.Clone(world); + tailParticleProps.Color = ColorUtil.ToRgba(255, 0, 0, 0); + tailParticleProps.MinPos = new Vec3d( + relativePosition.X - direction.X * offset * TailOffsetMultiplier, + relativePosition.Y - (bodySize / BodyOffsetDivider) * TailVerticalOffsetMultiplier, + relativePosition.Z - direction.Z * offset * TailOffsetMultiplier + ).Add(hivePosition); + tailParticleProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + + world.SpawnParticles(tailParticleProps); + } + + private static void SpawnWing(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity, Vector3 direction, double offset, float bodySize, bool wingPhase, WingSide side) + { + var wingDirection = side == WingSide.Left + ? new Vector3(-direction.Z, 0f, direction.X) + : new Vector3(direction.Z, 0f, -direction.X); + var wingParticleProps = CreateWingProps(world); + var wingForwardOffsetMultiplier = wingPhase ? WingForwardOffsetMultiplierWhenFlap : WingForwardOffsetMultiplierWhenRest; + var wingVerticalOffsetMultiplier = wingPhase ? WingVerticalOffsetMultiplierWhenFlap : WingVerticalOffsetMultiplierWhenRest; + + wingParticleProps.MinPos = new Vec3d( + relativePosition.X + wingDirection.X * offset * WingHorizontalOffsetMultiplier + direction.X * offset * wingForwardOffsetMultiplier, + relativePosition.Y + (bodySize / BodyOffsetDivider) * wingVerticalOffsetMultiplier, + relativePosition.Z + wingDirection.Z * offset * WingHorizontalOffsetMultiplier + direction.Z * offset * wingForwardOffsetMultiplier + ).Add(hivePosition).Add(WingPosRandomness * -0.5); + wingParticleProps.AddPos = WingPosRandomness; + wingParticleProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + + world.SpawnParticles(wingParticleProps); + } + + private static void SpawnEyes(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity, Vector3 direction, double offset, float bodySize) + { + var sideDirection = new Vector3(-direction.Z, 0f, direction.X); + if (sideDirection.Length() < 0.001f) + return; + + sideDirection = Vector3.Normalize(sideDirection); + var forward = direction * (float)offset * EyeForwardOffsetMultiplier; + var vertical = (bodySize / BodyOffsetDivider) * EyeVerticalOffsetMultiplier; + var horizontal = sideDirection * (float)offset * EyeHorizontalOffsetMultiplier; + + var leftEye = CreateEyeProps(world); + leftEye.MinPos = new Vec3d( + relativePosition.X + forward.X + horizontal.X, + relativePosition.Y + vertical, + relativePosition.Z + forward.Z + horizontal.Z + ).Add(hivePosition); + leftEye.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + world.SpawnParticles(leftEye); + + var rightEye = CreateEyeProps(world); + rightEye.MinPos = new Vec3d( + relativePosition.X + forward.X - horizontal.X, + relativePosition.Y + vertical, + relativePosition.Z + forward.Z - horizontal.Z + ).Add(hivePosition); + rightEye.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + world.SpawnParticles(rightEye); + } + + private static void SpawnAntennae(IClientWorldAccessor world, BlockPos hivePosition, Vector3 relativePosition, Vector3 velocity, Vector3 direction, double offset, float bodySize) + { + var sideDirection = new Vector3(-direction.Z, 0f, direction.X); + if (sideDirection.Length() < 0.001f) + return; + + sideDirection = Vector3.Normalize(sideDirection); + var forward = direction * (float)offset * AntennaForwardOffsetMultiplier; + var horizontal = sideDirection * (float)offset * AntennaHorizontalOffsetMultiplier; + var baseVertical = (bodySize / BodyOffsetDivider) * AntennaVerticalStartMultiplier; + + for (var i = 0; i < AntennaSegments; i++) + { + var stepVertical = baseVertical + (bodySize / BodyOffsetDivider) * AntennaVerticalStepMultiplier * i; + var segmentProps = CreateEyeProps(world); + segmentProps.MinSize = AntennaSize; + segmentProps.MaxSize = AntennaSize; + + segmentProps.MinPos = new Vec3d( + relativePosition.X + forward.X + horizontal.X, + relativePosition.Y + stepVertical, + relativePosition.Z + forward.Z + horizontal.Z + ).Add(hivePosition); + segmentProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + world.SpawnParticles(segmentProps); + + var mirroredSegmentProps = segmentProps.Clone(world); + mirroredSegmentProps.MinPos = new Vec3d( + relativePosition.X + forward.X - horizontal.X, + relativePosition.Y + stepVertical, + relativePosition.Z + forward.Z - horizontal.Z + ).Add(hivePosition); + mirroredSegmentProps.MinVelocity = new Vec3f(velocity.X, velocity.Y, velocity.Z); + world.SpawnParticles(mirroredSegmentProps); + } + } + + private static SimpleParticleProperties CreateWingProps(IClientWorldAccessor world) + { + var wingParticleProps = BaseProperties.Clone(world); + wingParticleProps.Color = ColorUtil.ToRgba(50, 255, 255, 100); + wingParticleProps.ParticleModel = EnumParticleModel.Cube; + wingParticleProps.MinSize = WingSize; + wingParticleProps.MaxSize = WingSize; + + return wingParticleProps; + } + + private static SimpleParticleProperties CreateEyeProps(IClientWorldAccessor world) + { + var eyeParticleProps = BaseProperties.Clone(world); + eyeParticleProps.Color = ColorUtil.ToRgba(255, 51, 51, 51); + eyeParticleProps.ParticleModel = EnumParticleModel.Cube; + eyeParticleProps.MinSize = EyeSize; + eyeParticleProps.MaxSize = EyeSize; + return eyeParticleProps; + } + + private enum WingSide + { + Left, + Right, + } +} diff --git a/RoamingBees/RoamingBees/Particles/Catchup/BeeSpawnCatchupPacket.cs b/RoamingBees/RoamingBees/Particles/Catchup/BeeSpawnCatchupPacket.cs new file mode 100644 index 0000000..c8a261b --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/Catchup/BeeSpawnCatchupPacket.cs @@ -0,0 +1,15 @@ +using ProtoBuf; +using System.Collections.Generic; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles.Catchup; + +[ProtoContract] +public record struct BeeCatchupPacket +{ + [ProtoMember(1)] + public required BlockPos HivePosition; + + [ProtoMember(2)] + public required IEnumerable SpawnPackets; +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/Particles/Catchup/CatchupRequestPacket.cs b/RoamingBees/RoamingBees/Particles/Catchup/CatchupRequestPacket.cs new file mode 100644 index 0000000..ff913e1 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/Catchup/CatchupRequestPacket.cs @@ -0,0 +1,11 @@ +using ProtoBuf; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles.Catchup; + +[ProtoContract] +public record struct BeeCatchupRequestPacket +{ + [ProtoMember(1)] + public required BlockPos HivePosition; +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/Particles/Catchup/IParticleCatchup.cs b/RoamingBees/RoamingBees/Particles/Catchup/IParticleCatchup.cs new file mode 100644 index 0000000..f8a0b7c --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/Catchup/IParticleCatchup.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace RoamingBees.Particles.Catchup; + +public interface IBeeSpawnCatchup +{ + IEnumerable ActiveBeesPackets { get; } +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/Particles/Catchup/ParticleCatchup.cs b/RoamingBees/RoamingBees/Particles/Catchup/ParticleCatchup.cs new file mode 100644 index 0000000..bc9b146 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/Catchup/ParticleCatchup.cs @@ -0,0 +1,17 @@ +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace RoamingBees.Particles.Catchup; + +public class ParticleCatchupManager(IBeeSpawnCatchup origin, ICoreAPI api) +{ + public IBeeSpawnCatchup Origin { get; } = origin; + public ICoreServerAPI? ServerApi { get; } = api as ICoreServerAPI; + public ICoreClientAPI? ClientApi { get; } = api as ICoreClientAPI; + + public void SendCatchup() + { + + } +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/Particles/DebugPathBoxRenderer.cs b/RoamingBees/RoamingBees/Particles/DebugPathBoxRenderer.cs new file mode 100644 index 0000000..6384a5c --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/DebugPathBoxRenderer.cs @@ -0,0 +1,92 @@ +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +internal static class DebugPathBoxRenderer +{ + private const double edge_step = 0.04; + private static SimpleParticleProperties? _markerProperties; + + private static SimpleParticleProperties MarkerProperties() + { + return _markerProperties ??= new SimpleParticleProperties() + { + MinPos = new Vec3d(), + MinVelocity = new Vec3f(), + MinSize = 0.16f, + MaxSize = 0.16f, + LifeLength = 0.45f, + WithTerrainCollision = false, + ParticleModel = EnumParticleModel.Cube, + MinQuantity = 1, + AddQuantity = 0, + Color = ColorUtil.ToRgba(235, 40, 255, 255), + GravityEffect = 0f, + SelfPropelled = false, + RandomVelocityChange = false, + WindAffected = false, + }; + } + + public static void Render(IClientWorldAccessor world, Cuboidf box) + { + var p000 = new Vec3d(box.X1, box.Y1, box.Z1); + var p100 = new Vec3d(box.X2, box.Y1, box.Z1); + var p010 = new Vec3d(box.X1, box.Y2, box.Z1); + var p110 = new Vec3d(box.X2, box.Y2, box.Z1); + var p001 = new Vec3d(box.X1, box.Y1, box.Z2); + var p101 = new Vec3d(box.X2, box.Y1, box.Z2); + var p011 = new Vec3d(box.X1, box.Y2, box.Z2); + var p111 = new Vec3d(box.X2, box.Y2, box.Z2); + + SpawnEdge(world, p000, p100); + SpawnEdge(world, p000, p010); + SpawnEdge(world, p000, p001); + + SpawnEdge(world, p111, p011); + SpawnEdge(world, p111, p101); + SpawnEdge(world, p111, p110); + + SpawnEdge(world, p100, p101); + SpawnEdge(world, p100, p110); + + SpawnEdge(world, p010, p011); + SpawnEdge(world, p010, p110); + + SpawnEdge(world, p001, p011); + SpawnEdge(world, p001, p101); + } + + private static void SpawnEdge(IClientWorldAccessor world, Vec3d from, Vec3d to) + { + var direction = to - from; + var length = direction.Length(); + if (length <= 0) + { + SpawnPoint(world, from.X, from.Y, from.Z); + return; + } + + var steps = Math.Max(1, (int)Math.Ceiling(length / edge_step)); + for (var i = 0; i <= steps; i++) + { + var t = (double)i / steps; + SpawnPoint( + world, + from.X + direction.X * t, + from.Y + direction.Y * t, + from.Z + direction.Z * t + ); + } + } + + private static void SpawnPoint(IClientWorldAccessor world, double x, double y, double z) + { + var props = MarkerProperties().Clone(world); + props.MinPos = new Vec3d(x, y, z); + world.SpawnParticles(props); + } +} diff --git a/RoamingBees/RoamingBees/Particles/DebugPathRenderManager.cs b/RoamingBees/RoamingBees/Particles/DebugPathRenderManager.cs new file mode 100644 index 0000000..8197d5f --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/DebugPathRenderManager.cs @@ -0,0 +1,65 @@ +using Vintagestory.API.Client; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +internal static class DebugPathRenderManager +{ + private const int debug_paths_radius = 5; + private const int debug_paths_update_interval_ms = 100; + + private static ICoreClientAPI? clientApi; + private static long? tickListenerId; + + public static void SetEnabled(ICoreClientAPI? api, bool enabled) + { + if (clientApi != api) + { + DisableCurrent(); + clientApi = api; + } + + if (clientApi == null) + return; + + if (enabled) + { + if (!tickListenerId.HasValue) + tickListenerId = clientApi.Event.RegisterGameTickListener(OnTick, debug_paths_update_interval_ms); + return; + } + + DisableCurrent(); + } + + private static void DisableCurrent() + { + if (clientApi != null && tickListenerId.HasValue) + clientApi.Event.UnregisterGameTickListener(tickListenerId.Value); + tickListenerId = null; + } + + private static void OnTick(float _) + { + if (clientApi?.World is not IClientWorldAccessor clientWorld) + return; + + var playerEntity = clientWorld.Player?.Entity; + if (playerEntity == null) + return; + + var playerPos = playerEntity.Pos.AsBlockPos; + var blockAccessor = clientWorld.BlockAccessor; + + for (var dx = -debug_paths_radius; dx <= debug_paths_radius; dx++) + for (var dy = -debug_paths_radius; dy <= debug_paths_radius; dy++) + for (var dz = -debug_paths_radius; dz <= debug_paths_radius; dz++) + { + var blockPos = new BlockPos(playerPos.X + dx, playerPos.Y + dy, playerPos.Z + dz); + if (!BeePathGeneration.TryGetTargetBoxAtWorldBlock(blockAccessor, blockPos, out var box)) + continue; + + DebugPathBoxRenderer.Render(clientWorld, box); + } + } +} diff --git a/RoamingBees/RoamingBees/Particles/IBeeSpawnHandler.cs b/RoamingBees/RoamingBees/Particles/IBeeSpawnHandler.cs new file mode 100644 index 0000000..8cf37da --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/IBeeSpawnHandler.cs @@ -0,0 +1,6 @@ +namespace RoamingBees.Particles; + +public interface IBeeSpawnHandler +{ + void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false); +} diff --git a/RoamingBees/RoamingBees/Particles/InternalBeeParticle.cs b/RoamingBees/RoamingBees/Particles/InternalBeeParticle.cs new file mode 100644 index 0000000..5748fd9 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/InternalBeeParticle.cs @@ -0,0 +1,283 @@ +using System; +using System.Linq; +using System.Numerics; + +namespace RoamingBees.Particles; + +public class InternalBeeParticle(Vector3 startingPosition, Vector3 frontDirection, BeePlannedPathPoint[] path, BeeRole role, Vector3 despawnPosition, BeeSpawnPacket spawnPacket) +{ + private readonly BeePlannedPathPoint[] path = path; + + private float totalDt = 0; + private int currentTargetIndex = 0; + + public float MaxSpeedTraveling { get; } = 2.0f; + public float TargetSpeedApproaching { get; } = 0.3f; + public float TargetSpeedHovering { get; } = 0.1f; + public float TargetSpeedReturning { get; } = 0.02f; + + public float SpeedChangeTimeSeconds { get; } = 1f; + + public float IntermediateWaypointDistanceThreshold { get; } = 0.6f; + public float ApproachingDistanceThreshold { get; } = 2.5f; + public float HoverDistanceThreshold { get; } = 0.3f; + + public Vector3 StartingPosition { get; } = startingPosition; + public Vector3 DespawnPosition { get; } = despawnPosition; + public BeeSpawnPacket SpawnPacket { get; } = spawnPacket; + + public Vector3 Position { get; private set; } = startingPosition; + public Vector3 Velocity { get; private set; } = frontDirection.Length() > 0.01f ? (Vector3.Normalize(frontDirection) * 0.3f + Vector3.UnitY * 0.1f) : Vector3.Zero; + public Vector3 AccelerationDirection { get; private set; } + + /// from 0 to 1 + public float AccelerationMagnitude { get; private set; } + + public BeePlannedPathPoint? Target => path.ElementAtOrDefault(currentTargetIndex); + public Vector3? NextTarget { get; private set; } + public float TimeAtTarget { get; private set; } + public float DesiredTimeAtTarget { get; } = 4f; + + public bool ShouldBeDespawned { get; private set; } + + public int Ticks { get; private set; } + + public BeeRole Role { get; } = role; + + /// + /// unset = use AccelerationDirection + /// + public Vector3? LookDirection { get; set; } = path.ElementAtOrDefault(0).LookDirection; + + public void SolveCollision(Vector3 min, Vector3 max, Vector3 outDir) + { + var p = Position; + + // push along the outward axis to the matching face + Position = outDir switch + { + { X: > 0.5f } => p with { X = max.X + 0.01f }, + { X: < -0.5f } => p with { X = min.X - 0.01f }, + { Y: > 0.5f } => p with { Y = max.Y + 0.01f }, + { Y: < -0.5f } => p with { Y = min.Y - 0.01f }, + { Z: > 0.5f } => p with { Z = max.Z + 0.01f }, + _ => p with { Z = min.Z - 0.01f }, + }; + + // kill velocity + var vDotOut = Vector3.Dot(Velocity, outDir); + if (vDotOut < 0f) + Velocity -= outDir * vDotOut; + } + + + public void Step(float dt, float windSpeed) + { + Ticks++; + totalDt += dt; + + + SetAccelerationTowardsTarget(); + if (AccelerationDirection.Length() > 0.1f) + Velocity += Vector3.Normalize(AccelerationDirection) * dt * Math.Clamp(AccelerationMagnitude, 0f, 2f); + + var cfg = Config.Instance; + var windNoiseStrength = cfg.BaseWindNoiseStrength + (windSpeed * (cfg.MaxWindNoiseStrength - cfg.BaseWindNoiseStrength)); + var noise = GetAccelerationNoise(windSpeed); + if (noise.Length() > 0.01f) + Velocity += Vector3.Normalize(noise) * windNoiseStrength * dt; + + if (Velocity.Length() > MaxSpeedTraveling) + Velocity = Vector3.Normalize(Velocity) * MaxSpeedTraveling; + + Position += Velocity * dt; + + if (Target is null) + return; + + + var state = GetTravelState(); + + if (state is TravelState.IntermediateWaypoint) + { + currentTargetIndex++; + LookDirection = Target?.LookDirection; + TimeAtTarget = 0; + return; + } + + if (state is TravelState.Hovering) + { + // here we have to decrease Velocity over SpeedChangeTimeSeconds onto the appropriate speed + TimeAtTarget += dt; + // decrease speed gradually from approach speed to "at target" speed over SpeedChangeTimeSeconds + var needsBraking = IsMovingAwayOrSideways(); + if (!Target.Value.IsSlowdown) + { + var hoverSpeed = Role == BeeRole.Hovering ? 0.06f : TargetSpeedHovering; + var brakeDt = Role == BeeRole.Hovering ? dt * 2f : dt; + DecreaseVelocityTowardsSpeed(needsBraking ? 0f : hoverSpeed, brakeDt); + HandleBeingAtPlantTarget(); + } + else + { + DecreaseVelocityTowardsSpeed(needsBraking ? 0 : TargetSpeedHovering, dt * 2); + HandleEndOfJourney(); + } + } + else if (state is TravelState.Approaching) + { + // same shit but from traveling speed to approaching speed + var needsBraking = IsMovingAwayOrSideways(); + var targetSpeed = Target.Value.IsSlowdown ? TargetSpeedReturning : TargetSpeedApproaching; + DecreaseVelocityTowardsSpeed(needsBraking ? 0 : targetSpeed, dt); + } + } + + public bool GetWingPhase() + { + var quarterSeconds = (int)(totalDt * 16); + return quarterSeconds % 2 == 0; + } + + private void HandleEndOfJourney() + { + var distanceToTarget = Vector3.Distance(Target!.Value.Position, Position); + if (Target.Value.Position != DespawnPosition && distanceToTarget < 0.1f && TimeAtTarget > 1) + { + currentTargetIndex++; + LookDirection = Target?.LookDirection; + TimeAtTarget = 0f; + } + + if (Target!.Value.Position == DespawnPosition && TimeAtTarget > 1f && Vector3.Distance(DespawnPosition, Position) < 0.1f) + ShouldBeDespawned = true; + } + + private void HandleBeingAtPlantTarget() + { + if (TimeAtTarget <= DesiredTimeAtTarget || Target!.Value.Position == DespawnPosition) + return; + + TimeAtTarget = 0; + currentTargetIndex++; + LookDirection = Target?.LookDirection; + } + + private void DecreaseVelocityTowardsSpeed(float toSpeed, float dt) + { + var currentSpeed = Velocity.Length(); + if (currentSpeed < 0.01f) + return; + + float lerpFactor = Math.Clamp(dt * 2f, 0f, 1f); + float newSpeed = currentSpeed + (toSpeed - currentSpeed) * lerpFactor; + + Velocity = Vector3.Normalize(Velocity) * newSpeed; + } + + private bool IsMovingAwayOrSideways() + { + if (Target == null || Velocity.Length() < 0.01f) + return false; + + var directionToTarget = Target.Value.Position - Position; + if (directionToTarget.Length() < 0.01f) + return false; + + var velocityNorm = Vector3.Normalize(Velocity); + var targetDirNorm = Vector3.Normalize(directionToTarget); + float alignment = Vector3.Dot(velocityNorm, targetDirNorm); + + if (Target.Value.IsSlowdown) + return alignment < 0.97f; + + return alignment < 0.6f; + } + + private void SetAccelerationTowardsTarget() + { + var distanceToEntrance = Vector3.Distance(StartingPosition, Position); + if (Target == null || (totalDt <= 1 && distanceToEntrance <= 0.1f)) + { + if (Velocity.LengthSquared() > 0.1) + { + AccelerationDirection = Vector3.Normalize(Velocity); + AccelerationMagnitude = 0.01f; + } + return; + } + + var directionToTarget = Target.Value.Position - Position; + var distance = directionToTarget.Length(); + + if (distance > 0.01f) + { + directionToTarget = Vector3.Normalize(directionToTarget); + AccelerationDirection = directionToTarget; + AccelerationMagnitude = 0.7f; + } + else + { + AccelerationDirection = Vector3.Zero; + AccelerationMagnitude = 0f; + } + + if (Target.Value.IsSlowdown) + { + AccelerationMagnitude = 1f; + return; + } + + var state = GetTravelState(); + if (state is TravelState.Approaching) + AccelerationMagnitude = 0.3f; + + if (state is TravelState.Hovering) + AccelerationMagnitude = Role == BeeRole.Hovering ? 0.2f : 0.1f; + } + + private Vector3 GetAccelerationNoise(float windSpeed) + { + if ((GetTravelState() is TravelState.Approaching or TravelState.Hovering) && Target?.IsSlowdown == true) + return Vector3.Zero; + + // hovering-role bees hug the swarm - suppress noise so they don't sway off + if (Role == BeeRole.Hovering && GetTravelState() is TravelState.Hovering or TravelState.Approaching) + return Vector3.Zero; + + var cfg = Config.Instance; + float windFrequency = cfg.BaseWindFrequency + (windSpeed * cfg.BaseWindFrequency * cfg.WindFrequencyMultiplier); + float t = Ticks * windFrequency; + + float x = MathF.Sin(t); + float y = MathF.Sin(t * 1.3f + 2.1f); + float z = MathF.Sin(t * 0.7f + 4.2f); + + return new Vector3(x, y, z); + } + + private TravelState GetTravelState() + { + if (Target is null) + return TravelState.Idle; + + var distance = Vector3.Distance(Position, Target.Value.Position); + if (Target.Value.IsIntermediate && distance < IntermediateWaypointDistanceThreshold) + return TravelState.IntermediateWaypoint; + if (distance < HoverDistanceThreshold) + return TravelState.Hovering; + if (distance < ApproachingDistanceThreshold) + return TravelState.Approaching; + return TravelState.FastTraveling; + } + + private enum TravelState + { + FastTraveling, + Approaching, + Hovering, + IntermediateWaypoint, + Idle, + } +} diff --git a/RoamingBees/RoamingBees/Particles/SwarmBeePathGeneration.cs b/RoamingBees/RoamingBees/Particles/SwarmBeePathGeneration.cs new file mode 100644 index 0000000..43fccc1 --- /dev/null +++ b/RoamingBees/RoamingBees/Particles/SwarmBeePathGeneration.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Vintagestory.API.Common; +using Vintagestory.API.MathTools; + +namespace RoamingBees.Particles; + +public static class SwarmBeePathGeneration +{ + private const bool force_path_even_when_blocked = false; + private static Random random { get; } = new(); + private static List temp_list { get; } = []; + + public static Vector3 ComputeHiddenAnchor(Vec3f[] hoverSurfaceMap, string swarmSide) + { + var centroid = GetCentroid(hoverSurfaceMap); + + return swarmSide switch + { + "north" => centroid with { Z = 0f }, + "south" => centroid with { Z = 1f }, + "east" => centroid with { X = 1f }, + "west" => centroid with { X = 0f }, + "down" => centroid with { Y = 0f }, + "up" => centroid with { Y = 1f }, + _ => centroid with { Z = 0f }, + }; + } + + public static BeePlannedPathPoint[] GenerateBuildingPath( + IBlockAccessor blockAccessor, + BlockPos swarmPos, + Vector3 originEntranceRelative, + Vector3 originFrontDir, + Vector3 hiddenAnchor) + { + temp_list.Clear(); + + // spawn at the origin hive entrance, first waypoint exits through the front + temp_list.Add(new BeePlannedPathPoint(originEntranceRelative, true, false)); + var exitPoint = BeePathGeneration.GetFrontOfEntrance(originEntranceRelative, originFrontDir); + temp_list.Add(new BeePlannedPathPoint(exitPoint, false, false)); + + // random approach point near the swarm block center for variety + var approach = new Vector3(0.5f, 0.5f, 0.5f) + new Vector3( + (float)(random.NextDouble() * 0.6 - 0.3), + (float)(random.NextDouble() * 0.6 - 0.3), + (float)(random.NextDouble() * 0.6 - 0.3)); + if (!AddLineOfSightSegment(blockAccessor, swarmPos, exitPoint, approach)) + return []; + temp_list.Add(new BeePlannedPathPoint(approach, false, false)); + + if (!AddLineOfSightSegment(blockAccessor, swarmPos, approach, hiddenAnchor)) + return []; + temp_list.Add(new BeePlannedPathPoint(hiddenAnchor, false, true)); + + return [.. temp_list]; + } + + public static BeePlannedPathPoint[] GenerateHoverPath( + IBlockAccessor blockAccessor, + BlockPos swarmPos, + Vec3f[] hoverSurfaceMap, + Vector3 hiddenAnchor, + string swarmSide) + { + if (hoverSurfaceMap.Length == 0) + return []; + + temp_list.Clear(); + + var outDir = swarmSide switch + { + "north" => new Vector3(0f, 0f, 1f), + "south" => new Vector3(0f, 0f, -1f), + "east" => new Vector3(-1f, 0f, 0f), + "west" => new Vector3( 1f, 0f, 0f), + "up" => new Vector3(0f, 1f, 0f), + "down" => new Vector3(0f, -1f, 0f), + _ => new Vector3(0f, 0f, 1f), + }; + var up = Math.Abs(outDir.Y) < 0.99f ? Vector3.UnitY : Vector3.UnitX; + var tangent = Vector3.Normalize(Vector3.Cross(outDir, up)); + + if (random.NextDouble() < 0.5) + { + // tight: shape-based points only + var shapeCount = Math.Min(10, hoverSurfaceMap.Length); + for (var i = 0; i < shapeCount; i++) + { + var sample = hoverSurfaceMap[random.Next(hoverSurfaceMap.Length)]; + temp_list.Add(new BeePlannedPathPoint(new Vector3(sample.X, sample.Y, sample.Z), false, false)); + } + } + else + { + // loose: outward points in front of the swarm face + var outwardCount = random.Next(2, 4); + var faceCenter = hiddenAnchor with { }; + for (var i = 0; i < outwardCount; i++) + { + var dist = (float)(random.NextDouble() * 0.4 + 0.1); + var lateral = (float)(random.NextDouble() * 0.27 - 0.13); + var vertical = Math.Abs(outDir.Y) < 0.99f ? (float)(random.NextDouble() * 0.2 - 0.1) : 0f; + var outPos = faceCenter + outDir * dist + tangent * lateral + Vector3.UnitY * vertical; + outPos = NudgeOutOfSolid(blockAccessor, swarmPos, outPos) ?? faceCenter; + temp_list.Add(new BeePlannedPathPoint(outPos, false, false)); + } + } + + // shuffle all waypoints for variety, then append anchor once at end + for (var i = temp_list.Count - 1; i > 0; i--) + { + var j = random.Next(i + 1); + (temp_list[i], temp_list[j]) = (temp_list[j], temp_list[i]); + } + temp_list.Add(new BeePlannedPathPoint(hiddenAnchor, false, true)); + + return [.. temp_list]; + } + + public static BeePlannedPathPoint[] GenerateScoutPath( + IBlockAccessor blockAccessor, + BlockPos swarmPos, + Vector3 targetEntranceRelative, + Vector3 targetFrontDir, + Vector3 hiddenAnchor) + { + temp_list.Clear(); + + // targetFrontDir is the outward direction from the candidate hive entrance. + var front = Vector3.Normalize(targetFrontDir); + + // far approach point: bee must clear the hive block before moving in close + var farApproach = targetEntranceRelative + front * 1f; + farApproach = NudgeOutOfSolid(blockAccessor, swarmPos, farApproach) ?? farApproach; + if (!AddLineOfSightSegment(blockAccessor, swarmPos, hiddenAnchor, farApproach)) + return []; + temp_list.Add(new BeePlannedPathPoint(farApproach, true, false)); + + var entryPoint = targetEntranceRelative + front * 0.3f; + entryPoint = NudgeOutOfSolid(blockAccessor, swarmPos, entryPoint) ?? entryPoint; + var lookInward = -front; + temp_list.Add(new BeePlannedPathPoint(entryPoint, false, false, lookInward)); + + var hoverCount = random.Next(2, 4); + var lastHoverPos = entryPoint; + for (var i = 0; i < hoverCount; i++) + { + var outward = (float)(random.NextDouble() * 0.6); + var vertical = (float)(random.NextDouble() * 0.4 - 0.2); + + var angle = (float)(random.NextDouble() * MathF.PI * 2); + var up = Math.Abs(front.Y) < 0.99f ? Vector3.UnitY : Vector3.UnitX; + var tangent = Vector3.Normalize(Vector3.Cross(front, up)); + var bitangent = Vector3.Cross(front, tangent); + var lateralOffset = tangent * MathF.Cos(angle) * 0.4f + bitangent * MathF.Sin(angle) * 0.4f; + + var hoverPos = entryPoint + front * outward + lateralOffset + Vector3.UnitY * vertical; + hoverPos = NudgeOutOfSolid(blockAccessor, swarmPos, hoverPos) ?? entryPoint; + temp_list.Add(new BeePlannedPathPoint(hoverPos, false, false, lookInward)); + lastHoverPos = hoverPos; + } + + if (!AddLineOfSightSegment(blockAccessor, swarmPos, lastHoverPos, hiddenAnchor)) + return []; + temp_list.Add(new BeePlannedPathPoint(hiddenAnchor, false, true)); + + return [.. temp_list]; + } + + public static BeePlannedPathPoint[] GenerateMigrationPath( + IBlockAccessor blockAccessor, + BlockPos swarmPos, + Vector3 targetEntranceRelative, + Vector3 targetFrontDir, + Vector3 hiddenAnchor) + { + temp_list.Clear(); + + // start at hidden anchor, consumed immediately as intermediate + temp_list.Add(new BeePlannedPathPoint(hiddenAnchor, true, false)); + + // random drift midpoint for variety + var mid = new Vector3(0.5f, 0.5f, 0.5f) + new Vector3( + (float)(random.NextDouble() * 0.8 - 0.4), + (float)(random.NextDouble() * 0.6 - 0.3), + (float)(random.NextDouble() * 0.8 - 0.4)); + if (!AddLineOfSightSegment(blockAccessor, swarmPos, hiddenAnchor, mid)) + return []; + temp_list.Add(new BeePlannedPathPoint(mid, true, false)); + + // slow approach to front of entrance, then enter + var targetFront = BeePathGeneration.GetFrontOfEntrance(targetEntranceRelative, targetFrontDir); + if (!AddLineOfSightSegment(blockAccessor, swarmPos, mid, targetFront)) + return []; + temp_list.Add(new BeePlannedPathPoint(targetFront, true, true)); + temp_list.Add(new BeePlannedPathPoint(targetEntranceRelative, false, true)); + + return [.. temp_list]; + } + + private static Vector3 GetCentroid(Vec3f[] points) + { + if (points.Length == 0) + return new Vector3(0.5f, 0.5f, 0.5f); + + var sum = Vector3.Zero; + foreach (var p in points) + sum += new Vector3(p.X, p.Y, p.Z); + + return sum / points.Length; + } + + /// + /// If falls inside a solid block, tries nudging in Y+, Y-, X+, X-, Z+, Z- + /// in steps of 0.25 up to 1 block. Returns the first non-solid position found, or null if all fail. + /// + private static Vector3? NudgeOutOfSolid(IBlockAccessor blockAccessor, BlockPos swarmPos, Vector3 relPos) + { + if (!IsSolidAt(blockAccessor, swarmPos, relPos)) + return relPos; + + Span directions = [ + Vector3.UnitY, -Vector3.UnitY, + Vector3.UnitX, -Vector3.UnitX, + Vector3.UnitZ, -Vector3.UnitZ, + ]; + + for (var step = 1; step <= 4; step++) + { + var nudge = step * 0.25f; + foreach (var dir in directions) + { + var candidate = relPos + dir * nudge; + if (!IsSolidAt(blockAccessor, swarmPos, candidate)) + return candidate; + } + } + + return null; + } + + private static bool IsSolidAt(IBlockAccessor blockAccessor, BlockPos swarmPos, Vector3 relPos) + { + var worldPos = new BlockPos( + swarmPos.X + (int)MathF.Floor(relPos.X), + swarmPos.Y + (int)MathF.Floor(relPos.Y), + swarmPos.Z + (int)MathF.Floor(relPos.Z)); + var block = blockAccessor.GetBlock(worldPos); + return block is not null && block.BlockMaterial != EnumBlockMaterial.Air && block.Replaceable < 6000; + } + + private static bool AddLineOfSightSegment( + IBlockAccessor blockAccessor, + BlockPos anchor, + Vector3 from, + Vector3 to) + { + if (BeePathGeneration.HasLineOfSight(blockAccessor, anchor, from, to)) + return true; + + if (BeePathGeneration.TryFindWaypoint(blockAccessor, anchor, from, to, out var waypoint)) + { + temp_list.Add(new BeePlannedPathPoint(waypoint, true, false)); + return true; + } + + return force_path_even_when_blocked; + } +} diff --git a/RoamingBees/RoamingBees/Properties/launchSettings.json b/RoamingBees/RoamingBees/Properties/launchSettings.json new file mode 100644 index 0000000..f1f3c56 --- /dev/null +++ b/RoamingBees/RoamingBees/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "profiles": { + "Client": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + }, + "Server": { + "commandName": "Executable", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"$(VintageStoryDataPathArgs)", + "workingDirectory": "$(VINTAGE_STORY)" + } + } +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/RoamingBees.csproj b/RoamingBees/RoamingBees/RoamingBees.csproj new file mode 100644 index 0000000..3b610c1 --- /dev/null +++ b/RoamingBees/RoamingBees/RoamingBees.csproj @@ -0,0 +1,83 @@ + + + + net8.0 + false + bin\$(Configuration)\Mods\roamingbees + enable + Debug;Release;Debug22 + + + + + net10.0 + $(VINTAGE_STORY_22PRE2) + --dataPath "..\VintageStoryDataBeehives\Data22" + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + false + + + $(VINTAGE_STORY)/Mods/VSSurvivalMod.dll + False + + + $(VINTAGE_STORY)/Mods/VSEssentials.dll + False + + + $(VINTAGE_STORY)/Mods/VSCreativeMod.dll + False + + + $(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll + False + + + $(VINTAGE_STORY)/Lib/0Harmony.dll + False + + + $(VINTAGE_STORY)/VintagestoryLib.dll + false + + + $(VINTAGE_STORY)/Lib/protobuf-net.dll + False + + + $(VINTAGE_STORY)/Lib/cairo-sharp.dll + False + + + $(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll + False + + + E:\Code\VintageStory\configlib_1.10.14\configlib.dll + False + + + + + + PreserveNewest + + + + + + PreserveNewest + + + + + + Common\%(RecursiveDir)%(Filename)%(Extension) + + + + diff --git a/RoamingBees/RoamingBees/RoamingBeesModSystem.cs b/RoamingBees/RoamingBees/RoamingBeesModSystem.cs new file mode 100644 index 0000000..e29bfd6 --- /dev/null +++ b/RoamingBees/RoamingBees/RoamingBeesModSystem.cs @@ -0,0 +1,96 @@ +using RoamingBees.Behaviors; +using RoamingBees.Particles; +using RoamingBees.Particles.Catchup; +using System; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Server; + +namespace RoamingBees; + +public partial class RoamingBeesModSystem : ModSystem +{ + private ICoreServerAPI? serverApi; + private ICoreClientAPI? clientApi; + private ICoreAPI? api; + public const string CONFIG_CHANNEL_NAME = "roamingbees-config"; + + internal BeeSpawnPacketDistributor? BeeSpawnPacketDistributor { get; private set; } + internal int GlobalActiveBees { get; set; } + internal bool DebugPathsEnabled { get; set; } + public IServerNetworkChannel? ServerChannel { get; private set; } + public IClientNetworkChannel? ClientChannel { get; private set; } + + public override void Start(ICoreAPI api) + { + this.api = api; + api.RegisterBlockBehaviorClass(nameof(BlockBehaviorRoamingBees), typeof(BlockBehaviorRoamingBees)); + api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorRoamingBees), typeof(BlockEntityBehaviorRoamingBees)); + api.RegisterBlockBehaviorClass(nameof(BlockBehaviorBeeSwarm), typeof(BlockBehaviorBeeSwarm)); + api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorBeeSwarm), typeof(BlockEntityBehaviorBeeSwarm)); + + try + { + if (api.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem")) + SubscribeToConfigChange(api); + } + catch (Exception) + { + Mod.Logger.VerboseDebug("Failed to subscribe to config change"); + } + } + + public override void StartServerSide(ICoreServerAPI serverApi) + { + InitializeServerConfig(serverApi); + this.serverApi = serverApi; + SetupServerCommands(serverApi); + BeeSpawnPacketDistributor = new(this, serverApi.Side); + ServerChannel = serverApi.Network + .RegisterChannel(BlockEntityBehaviorRoamingBees.NETWORK_CHANNEL_NAME) + .RegisterMessageType() + .RegisterMessageType().SetMessageHandler(BeeSpawnPacketDistributor.OnBeeCatchupRequest) + .RegisterMessageType(); + serverApi.Network + .RegisterChannel(CONFIG_CHANNEL_NAME) + .RegisterMessageType(); + } + + public override void StartClientSide(ICoreClientAPI clientApi) + { + this.clientApi = clientApi; + InitializeClientConfig(clientApi); + SetupClientCommands(clientApi); + BeeSpawnPacketDistributor = new(this, clientApi.Side); + ClientChannel = clientApi.Network + // in case something ever magically doesn't work.. the docs say: + // "Must be registered in the same order as on the server." + .RegisterChannel(BlockEntityBehaviorRoamingBees.NETWORK_CHANNEL_NAME) + .RegisterMessageType() + .RegisterMessageType().SetMessageHandler(BeeSpawnPacketDistributor.HandleBeeParticleSpawn) + .RegisterMessageType().SetMessageHandler(BeeSpawnPacketDistributor.HandleBeeParticleCatchup); + + clientApi.Network + .RegisterChannel(CONFIG_CHANNEL_NAME) + .RegisterMessageType() + .SetMessageHandler(OnConfigReceivedFromServer); + } + + public override double ExecuteOrder() => 0.12; + + public override void Dispose() + { + SetDebugPathsEnabled(false); + BeeSpawnPacketDistributor?.Clear(); + if (serverApi != null) + serverApi.Event.PlayerJoin -= OnPlayerJoin; + if (api?.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem") == true) + UnsubscribeFromConfigChange(); + } + + internal void SetDebugPathsEnabled(bool enabled) + { + DebugPathsEnabled = enabled; + DebugPathRenderManager.SetEnabled(clientApi, enabled); + } +} diff --git a/RoamingBees/RoamingBees/Utilities/ApiExtensions.cs b/RoamingBees/RoamingBees/Utilities/ApiExtensions.cs new file mode 100644 index 0000000..e506e4e --- /dev/null +++ b/RoamingBees/RoamingBees/Utilities/ApiExtensions.cs @@ -0,0 +1,10 @@ +using OrekiWoofsBees.Common; +using Vintagestory.API.Common; + +namespace RoamingBees.Utilities; + +internal static class ApiExtensions +{ + public static IPlantPositionRegistry? GetPlantPositionRegistry(this ICoreAPI api) => api.ModLoader.GetModSystem(); + public static RoamingBeesModSystem? GetFlyingBeesModSystem(this ICoreAPI api) => api.ModLoader.GetModSystem(); +} diff --git a/RoamingBees/RoamingBees/Utilities/VectorParsing.cs b/RoamingBees/RoamingBees/Utilities/VectorParsing.cs new file mode 100644 index 0000000..047092a --- /dev/null +++ b/RoamingBees/RoamingBees/Utilities/VectorParsing.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Vintagestory.API.Datastructures; + +namespace RoamingBees.Utilities; + +internal static class VectorParsing +{ + public static Vector3? TryParseVector3(JsonObject? obj) + { + if (obj is null || obj.Token is null) + return null; + + var values = obj.AsObject([]); + if (values.Length < 3) + return null; + + return new Vector3(values[0], values[1], values[2]); + } + + public static Dictionary? ParseVector3Map(JsonObject? obj) + { + if (obj is null || obj.Token is null) + return null; + + var raw = obj.AsObject>([]); + if (raw.Count == 0) + return null; + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in raw) + { + if (kvp.Value is null || kvp.Value.Length < 3) + continue; + result[kvp.Key] = new Vector3(kvp.Value[0], kvp.Value[1], kvp.Value[2]); + } + + return result.Count > 0 ? result : null; + } +} diff --git a/RoamingBees/RoamingBees/assets/roamingbees/config/configlib-patches.json b/RoamingBees/RoamingBees/assets/roamingbees/config/configlib-patches.json new file mode 100644 index 0000000..d77d209 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/config/configlib-patches.json @@ -0,0 +1,248 @@ +{ + "version": 0, + "file": "RoamingBees.json", + "patches": {}, + "settings": [ + { + "type": "separator", + "title": "Client-side" + }, + { + "code": "ReceiveParticles", + "comment": "Whether to spawn and show bee particles.", + "type": "boolean", + "default": true, + "clientSide": true + }, + { + "type": "separator", + "title": "General" + }, + { + "code": "EnableOnVanillaSkeps", + "comment": "Whether roaming bee models will come out of vanilla skeps.", + "type": "boolean", + "default": true + }, + { + "code": "RoamingBeesPerVanillaSkep", + "comment": "Max amount of bees roaming from a single vanilla skep.", + "type": "int", + "default": 15, + "range": { + "min": 0, + "max": 50 + } + }, + { + "code": "BeeRoamingRadius", + "comment": "The radius within which bees visit flowers. Set to 0 to use the source's radius attribute. If this is 0, and the source doesn't have this attribute, it defaults to 10.", + "type": "int", + "default": 0, + "range": { + "min": 0, + "max": 70 + } + }, + { + "code": "MaxGlobalRoamingBees", + "comment": "Maximum number of bee particle groups across all sources.", + "type": "int", + "default": 200, + "range": { + "min": 0, + "max": 1000 + } + }, + { + "type": "separator", + "title": "From Golden Combs compatibility" + }, + { + "code": "EnableOnFgcCeramic", + "comment": "Whether roaming bee models will come out of Ceramic Hives from the 'From Golden Combs' mod.", + "type": "boolean", + "default": true + }, + { + "code": "RoamingBeesPerFgcCeramic", + "comment": "Max amount of bees roaming from a single Ceramic Hive from the 'From Golden Combs' mod.", + "type": "int", + "default": 15, + "range": { + "min": 0, + "max": 50 + } + }, + { + "code": "EnableOnFgcLangstroth", + "comment": "Whether roaming bee models will come out of Langstroth Hives from the 'From Golden Combs' mod.", + "type": "boolean", + "default": true + }, + { + "code": "RoamingBeesPerFgcLangstroth", + "comment": "Max amount of bees roaming from a single Langstroth Hive from the 'From Golden Combs' mod.", + "type": "int", + "default": 25, + "range": { + "min": 0, + "max": 50 + } + }, + { + "type": "separator", + "title": "Swarms" + }, + { + "code": "BeesPerSwarmHovering", + "comment": "How many bees hang out around a swarm block.", + "type": "int", + "default": 15, + "range": { + "min": 0, + "max": 50 + } + }, + { + "code": "BeesPerSwarmTraveling", + "comment": "How many bees travel between a hive and a swarm block during migrations.", + "type": "int", + "default": 15, + "range": { + "min": 0, + "max": 50 + } + }, + { + "type": "separator", + "title": "Weather Effects" + }, + { + "code": "RainfallSpawnStopThreshold", + "comment": "Rainfall level at which bee spawning completely stops.", + "type": "float", + "default": 0.1, + "range": { + "min": 0.0, + "max": 1.0 + } + }, + { + "code": "MaxRainfallCooldownPenalty", + "comment": "Maximum seconds added to spawn cooldown at rainfall threshold.", + "type": "float", + "default": 60.0, + "range": { + "min": 0.0, + "max": 300.0 + } + }, + { + "code": "GreenhouseAffectsBeehive", + "comment": "Whether greenhouses give 5C degrees temperature boost to the beehive.", + "type": "boolean", + "default": true + }, + { + "code": "MinTemperatureParticleSpawn", + "comment": "Temperature at which bee particle spawn chance drops to zero.", + "type": "float", + "default": 8.0, + "range": { + "min": -10.0, + "max": 30.0 + } + }, + { + "code": "MaxTemperatureParticleSpawn", + "comment": "Temperature at which bee particle spawn chance is optimal.", + "type": "float", + "default": 18.0, + "range": { + "min": 0.0, + "max": 40.0 + } + }, + { + "code": "OptimalTemperatureSpawnChance", + "comment": "UNUSED ! Random spawn chance threshold at optimal temperature (lower = easier to spawn).", + "type": "float", + "default": 0.75, + "range": { + "min": 0.0, + "max": 1.0 + } + }, + { + "code": "BaseWindFrequency", + "comment": "Base frequency for particle movement sinusoidal noise at no wind.", + "type": "float", + "default": 0.1, + "range": { + "min": 0.01, + "max": 1.0 + } + }, + { + "code": "WindFrequencyMultiplier", + "comment": "Multiplier for how much wind increases noise frequency (4 = 5x faster at max wind).", + "type": "float", + "default": 4.0, + "range": { + "min": 0.0, + "max": 10.0 + } + }, + { + "code": "BaseWindNoiseStrength", + "comment": "Base acceleration strength from noise at no wind.", + "type": "float", + "default": 0.2, + "range": { + "min": 0.0, + "max": 2.0 + } + }, + { + "code": "MaxWindNoiseStrength", + "comment": "Maximum acceleration strength from noise at full wind.", + "type": "float", + "default": 1.0, + "range": { + "min": 0.0, + "max": 5.0 + } + }, + { + "code": "SunAltitudeMinDegrees", + "comment": "Minimum sun altitude in degrees for spawn modifier calculation (0 = horizon).", + "type": "float", + "default": -5.0, + "range": { + "min": -90.0, + "max": 90.0 + } + }, + { + "code": "SunAltitudeRangeDegrees", + "comment": "Range in degrees for sun altitude modifier (from min to min+range = 0% to 100%).", + "type": "float", + "default": 10.0, + "range": { + "min": 1.0, + "max": 90.0 + } + }, + { + "code": "MaxSunAltitudeCooldownPenalty", + "comment": "Maximum cooldown penalty in seconds at minimum sun altitude (linearly decreases to 0 at max altitude).", + "type": "float", + "default": 30.0, + "range": { + "min": 0.0, + "max": 300.0 + } + } + ] +} diff --git a/RoamingBees/RoamingBees/assets/roamingbees/lang/en.json b/RoamingBees/RoamingBees/assets/roamingbees/lang/en.json new file mode 100644 index 0000000..9580b60 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/lang/en.json @@ -0,0 +1,29 @@ +{ + "config-desc-ReceiveParticles": "Whether to receive and render roaming bee particles on the client.", + "config-desc-EnableOnVanillaSkeps": "Whether roaming bees spawn from vanilla skeps.", + "config-desc-RoamingBeesPerVanillaSkep": "Number of roaming bees per vanilla skep.", + "config-desc-EnableOnFgcCeramic": "Whether roaming bees spawn from From Golden Combs ceramic hives.", + "config-desc-RoamingBeesPerFgcCeramic": "Number of roaming bees per From Golden Combs ceramic hive.", + "config-desc-EnableOnFgcLangstroth": "Whether roaming bees spawn from From Golden Combs Langstroth hives.", + "config-desc-RoamingBeesPerFgcLangstroth": "Number of roaming bees per From Golden Combs Langstroth hive.", + "config-desc-BeeRoamingRadius": "Radius in blocks within which bees roam from their hive.", + "config-desc-RainfallSpawnStopThreshold": "Rain level (0-1) above which bee spawning stops.", + "config-desc-MaxRainfallCooldownPenalty": "Maximum cooldown penalty in seconds after rain stops before bees resume spawning.", + "config-desc-GreenhouseAffectsBeehive": "Whether being in a greenhouse affects roaming bee behavior.", + "config-desc-MinTemperatureParticleSpawn": "Minimum temperature (°C) at which bee particles start spawning.", + "config-desc-MaxTemperatureParticleSpawn": "Temperature (°C) at which bee particle spawn chance reaches maximum.", + "config-desc-OptimalTemperatureSpawnChance": "Spawn chance (0-1) at optimal temperature.", + "config-desc-BaseWindFrequency": "Base frequency of wind effect on bee movement.", + "config-desc-WindFrequencyMultiplier": "Multiplier applied to wind frequency.", + "config-desc-BaseWindNoiseStrength": "Base noise strength of wind effect on bee movement.", + "config-desc-MaxWindNoiseStrength": "Maximum noise strength of wind effect on bee movement.", + "config-desc-SunAltitudeMinDegrees": "Minimum sun altitude in degrees for bee spawning.", + "config-desc-SunAltitudeRangeDegrees": "Range of sun altitude degrees over which spawn chance ramps up.", + "config-desc-MaxSunAltitudeCooldownPenalty": "Maximum cooldown penalty in seconds when sun is below minimum altitude.", + "config-desc-MaxGlobalRoamingBees": "Maximum number of roaming bee particles active globally.", + "debugpaths-desc": "Toggle client-side debug rendering of target boxes around nearby blocks (0 or 1).", + "debugpaths-parse-error": "Couldn't parse. Use 0 or 1.", + "plantreg-blockpertick-desc": "Get or set plant registry scan BlocksPerTick (0-1000).", + "plantreg-blockpertick-parse-error": "Couldn't parse. Use an integer in range 0-1000.", + "plantreg-unavailable": "Plant registry mod system is not available." +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/assets/roamingbees/lang/ru.json b/RoamingBees/RoamingBees/assets/roamingbees/lang/ru.json new file mode 100644 index 0000000..e81af55 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/lang/ru.json @@ -0,0 +1,29 @@ +{ + "config-desc-ReceiveParticles": "Принимать ли и отображать частицы бродящих пчёл на клиенте.", + "config-desc-EnableOnVanillaSkeps": "Появляются ли бродячие пчёлы из ванильных ульев.", + "config-desc-RoamingBeesPerVanillaSkep": "Количество бродячих пчёл на ванильный улей.", + "config-desc-EnableOnFgcCeramic": "Появляются ли бродячие пчёлы из керамических ульев «From Golden Combs».", + "config-desc-RoamingBeesPerFgcCeramic": "Количество бродячих пчёл в керамическом улье «From Golden Combs».", + "config-desc-EnableOnFgcLangstroth": "Появляются ли бродячие пчёлы из ульев «Golden Combs Langstroth».", + "config-desc-RoamingBeesPerFgcLangstroth": "Количество бродячих пчёл в керамическом улье «Golden Combs Langstroth».", + "config-desc-BeeRoamingRadius": "Радиус в блоках от улья, внутри которых бродят пчёлы.", + "config-desc-RainfallSpawnStopThreshold": "Уровень осадков (0-1), выше которого прекращается появление пчёл.", + "config-desc-MaxRainfallCooldownPenalty": "Максимальное время в секундах после прекращения дождя до того, как пчёлы вновь начнут появляться.", + "config-desc-GreenhouseAffectsBeehive": "Влияет ли нахождение в теплице на поведение пчёл.", + "config-desc-MinTemperatureParticleSpawn": "Минимальная температура (°C), при которой пчёлы начинают появляться.", + "config-desc-MaxTemperatureParticleSpawn": "Температура (°C), при которой вероятность появления пчёл достигает максимума.", + "config-desc-OptimalTemperatureSpawnChance": "Шанс появления (0-1) при оптимальной температуре.", + "config-desc-BaseWindFrequency": "Базовая частота влияния ветра на движение пчёл.", + "config-desc-WindFrequencyMultiplier": "Множитель, применяемый к частоте ветра.", + "config-desc-BaseWindNoiseStrength": "Базовая сила шума ветра, влияющего на движение пчёл.", + "config-desc-MaxWindNoiseStrength": "Максимальная сила шума ветра, влияющего на движение пчёл.", + "config-desc-SunAltitudeMinDegrees": "Минимальная высота солнца в градусах для появления пчёл.", + "config-desc-SunAltitudeRangeDegrees": "Диапазон высоты солнца в градусах, в пределах которого постепенно увеличивается шанс появления пчёл.", + "config-desc-MaxSunAltitudeCooldownPenalty": "Максимальный штраф к перезарядке в секундах, когда солнце ниже минимальной высоты.", + "config-desc-MaxGlobalRoamingBees": "Максимальное глобальное количество частиц блуждающих пчёл.", + "debugpaths-desc": "Включить или выключить клиентскую отладочную визуализацию целевых рамок вокруг ближайших блоков (0 или 1).", + "debugpaths-parse-error": "Не удалось разобрать значение. Используйте 0 или 1.", + "plantreg-blockpertick-desc": "Получить или установить BlocksPerTick сканирования реестра растений (0–1000).", + "plantreg-blockpertick-parse-error": "Не удалось разобрать значение. Используйте целое число в диапазоне 0–1000.", + "plantreg-unavailable": "Система реестра растений недоступна." +} \ No newline at end of file diff --git a/RoamingBees/RoamingBees/assets/roamingbees/patches/beeswarm.json b/RoamingBees/RoamingBees/assets/roamingbees/patches/beeswarm.json new file mode 100644 index 0000000..5eca293 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/patches/beeswarm.json @@ -0,0 +1,18 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [{"name": "BlockEntityBehaviorBeeSwarm"}], + "file": "orekiwoofsbeehives:blocktypes/beeswarm", + "side": "Server", + "dependsOn": [{"modid": "orekiwoofsbeehives"}] + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [{"name": "BlockBehaviorBeeSwarm"}], + "file": "orekiwoofsbeehives:blocktypes/beeswarm", + "side": "Server", + "dependsOn": [{"modid": "orekiwoofsbeehives"}] + } +] diff --git a/RoamingBees/RoamingBees/assets/roamingbees/patches/ceramicbroodpot.json b/RoamingBees/RoamingBees/assets/roamingbees/patches/ceramicbroodpot.json new file mode 100644 index 0000000..56f956c --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/patches/ceramicbroodpot.json @@ -0,0 +1,145 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "", + "entrancePositions": { + "north": [0.5, 0.1, 0.8], + "east": [0.2, 0.1, 0.5], + "south": [0.5, 0.1, 0.2], + "west": [0.8, 0.1, 0.5] + }, + "frontDirections": { + "north": [0, 0, 1], + "east": [-1, 0, 0], + "south": [0, 0, -1], + "west": [1, 0, 0] + } + } + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + { + "op": "addMerge", + "path": "/behaviorsByType/*", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "", + "entrancePositions": { + "north": [0.5, 0.1, 0.8], + "east": [0.2, 0.1, 0.5], + "south": [0.5, 0.1, 0.2], + "west": [0.8, 0.1, 0.5] + }, + "frontDirections": { + "north": [0, 0, 1], + "east": [-1, 0, 0], + "south": [0, 0, -1], + "west": [1, 0, 0] + } + } + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-new", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + { + "op": "addMerge", + "path": "/behaviorsByType/*", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-new", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "", + "entrancePositions": { + "north": [0.5, 0.1, 0.8], + "east": [0.2, 0.1, 0.5], + "south": [0.5, 0.1, 0.2], + "west": [0.8, 0.1, 0.5] + }, + "frontDirections": { + "north": [0, 0, 1], + "east": [-1, 0, 0], + "south": [0, 0, -1], + "west": [1, 0, 0] + } + } + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-fancy", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + { + "op": "addMerge", + "path": "/behaviorsByType/*", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-fancy", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + } +] diff --git a/RoamingBees/RoamingBees/assets/roamingbees/patches/langstrothstack.json b/RoamingBees/RoamingBees/assets/roamingbees/patches/langstrothstack.json new file mode 100644 index 0000000..a38b010 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/patches/langstrothstack.json @@ -0,0 +1,49 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "side", + "entrancePositions": { + "north": [0.5, 0.27, 0.7], + "east": [0.3, 0.27, 0.5], + "south": [0.5, 0.27, 0.3], + "west": [0.7, 0.27, 0.5] + }, + "frontDirections": { + "north": [0, -0.5, 1], + "east": [-1, -0.5, 0], + "south": [0, -0.5, -1], + "west": [1, -0.5, 0] + } + } + } + ], + "file": "fromgoldencombs:blocktypes/langstroth/langstrothstack", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "fromgoldencombs:blocktypes/langstroth/langstrothstack", + "side": "Server", + "dependsOn": [ + { + "modid": "fromgoldencombs" + } + ] + } +] diff --git a/RoamingBees/RoamingBees/assets/roamingbees/patches/orekiwoofsbeehives.json b/RoamingBees/RoamingBees/assets/roamingbees/patches/orekiwoofsbeehives.json new file mode 100644 index 0000000..af94c4c --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/patches/orekiwoofsbeehives.json @@ -0,0 +1,49 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "side", + "entrancePositions": { + "north": [0.5, 0.2, 0.9], + "east": [0.1, 0.2, 0.5], + "south": [0.5, 0.2, 0.1], + "west": [0.9, 0.2, 0.5] + }, + "frontDirections": { + "north": [0, 0, 1], + "east": [-1, 0, 0], + "south": [0, 0, -1], + "west": [1, 0, 0] + } + } + } + ], + "file": "orekiwoofsbeehives:blocktypes/beehive", + "side": "Server", + "dependsOn": [ + { + "modid": "orekiwoofsbeehives" + } + ] + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "orekiwoofsbeehives:blocktypes/beehive", + "side": "Server", + "dependsOn": [ + { + "modid": "orekiwoofsbeehives" + } + ] + } +] diff --git a/RoamingBees/RoamingBees/assets/roamingbees/patches/skep.json b/RoamingBees/RoamingBees/assets/roamingbees/patches/skep.json new file mode 100644 index 0000000..03fb567 --- /dev/null +++ b/RoamingBees/RoamingBees/assets/roamingbees/patches/skep.json @@ -0,0 +1,39 @@ +[ + { + "op": "addMerge", + "path": "/entityBehaviors", + "value": [ + { + "name": "BlockEntityBehaviorRoamingBees", + "properties": { + "facingVariantKey": "side", + "entrancePositions": { + "north": [0.5, 0.1, 0.6], + "east": [0.4, 0.1, 0.5], + "south": [0.5, 0.1, 0.4], + "west": [0.6, 0.1, 0.5] + }, + "frontDirections": { + "north": [0, 0, 1], + "east": [-1, 0, 0], + "south": [0, 0, -1], + "west": [1, 0, 0] + } + } + } + ], + "file": "game:blocktypes/reed/skep", + "side": "Server" + }, + { + "op": "addMerge", + "path": "/behaviors", + "value": [ + { + "name": "BlockBehaviorRoamingBees" + } + ], + "file": "game:blocktypes/reed/skep", + "side": "Server" + } +] diff --git a/RoamingBees/RoamingBees/modicon.png b/RoamingBees/RoamingBees/modicon.png new file mode 100644 index 0000000000000000000000000000000000000000..df9bf605825ebe760356bd3e2b5276929dde5edd GIT binary patch literal 7544 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 O0000() + .Run(args); + } +} + +public class BuildContext : FrostingContext +{ + public const string PROJECT_NAME = "RoamingBees"; + public string BuildConfiguration { get; } + public string Version { get; } + public string Name { get; } + public bool SkipJsonValidation { get; } + public string GameVersion { get; } + + public BuildContext(ICakeContext context) + : base(context) + { + BuildConfiguration = context.Argument("configuration", "Release"); + SkipJsonValidation = context.Argument("skipJsonValidation", false); + var modInfo = context.DeserializeJsonFromFile($"../{PROJECT_NAME}/modinfo.json"); + Version = modInfo.Version; + Name = modInfo.ModID; + GameVersion = modInfo.Dependencies.First(x => x.ModID == "game").Version; + } +} + +[TaskName("ValidateJson")] +public sealed class ValidateJsonTask : FrostingTask +{ + public override void Run(BuildContext context) + { + if (context.SkipJsonValidation) + { + return; + } + var jsonFiles = context.GetFiles($"../{BuildContext.PROJECT_NAME}/assets/**/*.json"); + foreach (var file in jsonFiles) + { + try + { + var json = File.ReadAllText(file.FullPath); + JToken.Parse(json); + } + catch (JsonException ex) + { + throw new Exception($"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex); + } + } + } +} + +[TaskName("Build")] +[IsDependentOn(typeof(ValidateJsonTask))] +public sealed class BuildTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.DotNetClean($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj", + new DotNetCleanSettings + { + Configuration = context.BuildConfiguration + }); + + + context.DotNetPublish($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj", + new DotNetPublishSettings + { + Configuration = context.BuildConfiguration + }); + } +} + +[TaskName("Package")] +[IsDependentOn(typeof(BuildTask))] +public sealed class PackageTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.EnsureDirectoryExists("../Releases"); + context.CleanDirectory("../Releases"); + context.EnsureDirectoryExists($"../Releases/{context.Name}"); + context.CopyFiles($"../{BuildContext.PROJECT_NAME}/bin/{context.BuildConfiguration}/Mods/roamingbees/publish/*", $"../Releases/{context.Name}"); + if (context.DirectoryExists($"../{BuildContext.PROJECT_NAME}/assets")) + { + context.CopyDirectory($"../{BuildContext.PROJECT_NAME}/assets", $"../Releases/{context.Name}/assets"); + } + context.CopyFile($"../{BuildContext.PROJECT_NAME}/modinfo.json", $"../Releases/{context.Name}/modinfo.json"); + if (context.FileExists($"../{BuildContext.PROJECT_NAME}/modicon.png")) + { + context.CopyFile($"../{BuildContext.PROJECT_NAME}/modicon.png", $"../Releases/{context.Name}/modicon.png"); + } + context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_v{context.Version}-v{context.GameVersion}.zip"); + } +} + +[TaskName("Default")] +[IsDependentOn(typeof(PackageTask))] +public class DefaultTask : FrostingTask +{ +} \ No newline at end of file diff --git a/RoamingBees/ZZCakeBuild/RoamingBeesCakeBuild.csproj b/RoamingBees/ZZCakeBuild/RoamingBeesCakeBuild.csproj new file mode 100644 index 0000000..06b06e6 --- /dev/null +++ b/RoamingBees/ZZCakeBuild/RoamingBeesCakeBuild.csproj @@ -0,0 +1,20 @@ + + + Exe + net8.0 + $(MSBuildProjectDirectory) + Debug;Release;Debug22 + + + + + + + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + + + \ No newline at end of file diff --git a/RoamingBees/build.ps1 b/RoamingBees/build.ps1 new file mode 100644 index 0000000..f76fcc6 --- /dev/null +++ b/RoamingBees/build.ps1 @@ -0,0 +1,2 @@ +dotnet run --project ZZCakeBuild/RoamingBeesCakeBuild.csproj -- $args +exit $LASTEXITCODE; \ No newline at end of file diff --git a/RoamingBees/build.sh b/RoamingBees/build.sh new file mode 100644 index 0000000..1826512 --- /dev/null +++ b/RoamingBees/build.sh @@ -0,0 +1 @@ +dotnet run --project ./ZZCakeBuild/RoamingBeesCakeBuild.csproj -- "$@" diff --git a/TestScenarios/ConfigScenarios.md b/TestScenarios/ConfigScenarios.md new file mode 100644 index 0000000..b6c1efb --- /dev/null +++ b/TestScenarios/ConfigScenarios.md @@ -0,0 +1,54 @@ +# Config Scenarios + +## 1) ClientSide mode +- Server: `InformationVerbosityServerSideType=ClientSide`, `InformationVerbosityServer=4` +- Client: `InformationVerbosity=1` +- Expected: client uses `1` + +## 2) ServerRecommended mode (override OFF) +- Server: `InformationVerbosityServerSideType=ServerRecommended`, `InformationVerbosityServer=4` +- Client: `InformationVerbosity=1`, `DisableServerRecommended=false` +- Expected: client uses `4` + +## 3) ServerRecommended mode (override ON) +- Same as above, but `DisableServerRecommended=true` +- Expected: client uses `1` + +## 4) ServerForced mode +- Server: `InformationVerbosityServerSideType=ServerForced`, `InformationVerbosityServer=4` +- Client: `InformationVerbosity=1`, `DisableServerRecommended=true` +- Expected: client uses `4` + +## 5) Client-side settings stay client-side +- Client: set `BeehiveAmbientVolume=0.2`, `DisableServerRecommended=true` +- Join server with different values +- Expected: client keeps `0.2` and `true` + +## 6) MP -> SP check +- Join server in `ServerForced` with `InformationVerbosityServer=4` +- Leave, open singleplayer +- Expected: singleplayer uses local config value + +## 7) ConfigLib UI check +- Client-side section has: `InformationVerbosity`, `DisableServerRecommended`, `BeehiveAmbientVolume` +- Server-side section has: `InformationVerbosityServerSideType` with `ClientSide|ServerRecommended|ServerForced`, and `InformationVerbosityServer` + +## 8) Client command saves only client-side values to disk +- Manually edit config file: set `BeehiveRadius=50`, `YieldMultiplier=2.5` +- Join server (server broadcasts its own values, e.g. `BeehiveRadius=35`, `YieldMultiplier=1.2`) +- Run `/beehives BeehiveAmbientVolume 0.3` +- Open config file on disk +- Expected: `BeehiveAmbientVolume=0.3`, but `BeehiveRadius=50` and `YieldMultiplier=2.5` remain unchanged in the file (not overwritten with the server's in-memory values) + +## 9) Client command does not overwrite server values received via broadcast +- Server has `MaxBeePopulation=100000` +- Client config file has `MaxBeePopulation=50000` (default) +- Client joins, receives server broadcast (`MaxBeePopulation=100000` in memory) +- Run `/beehives InformationVerbosity 3` +- Open config file on disk +- Expected: `MaxBeePopulation=50000` still in file (not `100000`) + +## 10) Server command saves and broadcasts +- Run `/beehives BeehiveRadius 40` on server +- Expected: server config file has `BeehiveRadius=40` +- Expected: all connected clients have `BeehiveRadius=40` in memory diff --git a/TestScenarios/CropBoostScenarios.md b/TestScenarios/CropBoostScenarios.md new file mode 100644 index 0000000..5122d94 --- /dev/null +++ b/TestScenarios/CropBoostScenarios.md @@ -0,0 +1,223 @@ +# Crop Boost Scenarios + +## Shared setup +- Use one beehive in range of a crop/farmland. +- Keep distance fixed inside full-distance zone (`PlantsFullBoostDistanceToHive`) unless the scenario says otherwise. +- Enable both boosts when validating both paths: `SpeedBoost=true`, `YieldBoost=true`. +- Use non-zero bonuses to make differences visible: `GrowthSpeedBonus=0.2`, `YieldMultiplier=1.2`. + +### Shared config commands (copy/paste) +```text +/beehives YieldBoost true +/beehives SpeedBoost true +/beehives GrowthSpeedBonus 0.2 +/beehives YieldMultiplier 1.2 +/beehives BeehiveRadius 35 +/beehives PlantsFullBoostDistanceToHive 20 +/beehives MinBeesForPlantBoost 1000 +/beehives MaxBeePopulation 50000 +/beehives PopulationPercentForMaxBoost 100 +/beehives MultipleBeehivesBoostCombination Max +``` + +## 1) Full mode keeps legacy behavior +- Config: `BoostCalculation=Full` +- Set hive population below `MinBeesForPlantBoost` +- Expected: no crop boost from that hive. +- Raise population to `MinBeesForPlantBoost` +- Expected: hive contributes full population factor (`100%`) immediately (same as old behavior). + +### Commands +```text +/beehives BoostCalculation Full +``` + +## 2) LinearPopulation starts at 0% at min +- Config: `BoostCalculation=LinearPopulation`, `PopulationPercentForMaxBoost=100`, `MaxBeePopulation=50000`, `MinBeesForPlantBoost=1000` +- Set hive population to `1000` +- Expected: population factor is `0%`. + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 3) LinearPopulation reaches 100% at configured max percent +- Same config as scenario 2 +- Set hive population to `50000` (`100%` of `MaxBeePopulation`) +- Expected: population factor is `100%`. + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 4) LinearPopulation midpoint check +- Same config as scenario 2 +- Set hive population to midpoint between min and target max (`25500`) +- Expected: population factor is about `50%`. + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 5) NormalizedLogk10Population starts at 0% and ends at 100% +- Config: `BoostCalculation=NormalizedLogk10Population`, same thresholds as scenario 2 +- At `MinBeesForPlantBoost`, expected `0%`. +- At configured target max, expected `100%`. + +### Commands +```text +/beehives BoostCalculation NormalizedLogk10Population +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 6) NormalizedLogk10Population is front-loaded vs linear +- Compare `LinearPopulation` vs `NormalizedLogk10Population` at same midpoint population +- Expected: log mode gives a higher factor than linear at midpoint. +- Formula reference: `f(x)=ln(1+10x)/ln(11)` where `x` is normalized `[0..1]` between min and configured max. + +### Commands +```text +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 7) PopulationPercentForMaxBoost lower than 100 +- Config: `PopulationPercentForMaxBoost=50`, `MaxBeePopulation=50000` +- Target max population for full factor becomes `25000`. +- Expected: factor reaches `100%` at `25000` and stays clamped at `100%` above it. + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 50 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +``` + +## 8) PopulationPercentForMaxBoost = 0 boundary +- Config: `PopulationPercentForMaxBoost=0`, non-Full mode +- Expected: implementation falls back to immediate full factor once min threshold is met (avoids divide-by-zero / inverted range). + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 0 +``` + +## 9) Distance and population multiply together +- Use non-Full mode with population factor around `50%`. +- Move crop just outside full-distance zone so distance effectiveness is also around `50%`. +- Expected: per-hive contribution is about `25%` (`0.5 * 0.5`). + +### Commands +```text +/beehives BoostCalculation LinearPopulation +/beehives PopulationPercentForMaxBoost 100 +/beehives MaxBeePopulation 50000 +/beehives MinBeesForPlantBoost 1000 +/beehives PlantsFullBoostDistanceToHive 20 +/beehives BeehiveRadius 35 +``` + +## 10) Multi-hive accumulation still clamps at 100% +- Place multiple valid hives in range. +- Expected: summed effectiveness does not exceed `100%` total. + +### Commands +```text +/beehives BoostCalculation Full +/beehives YieldBoost true +/beehives SpeedBoost true +/beehives MultipleBeehivesBoostCombination Additive +``` + +## 10b) Multi-hive combination: Max mode +- Place multiple valid hives in range with noticeably different effectiveness. +- Expected: final effectiveness equals the highest single-hive effectiveness (no additive stacking). + +### Commands +```text +/beehives MultipleBeehivesBoostCombination Max +``` + +## 10c) Multi-hive combination: Additive mode +- Place multiple valid hives in range with partial effectiveness. +- Expected: effectiveness is additive across hives and capped at `100%`. + +### Commands +```text +/beehives MultipleBeehivesBoostCombination Additive +``` + +## 11) Yield and speed consumers are consistent +- Verify beehive info text and actual growth/drop behavior under same setup. +- Expected: displayed boost trend matches runtime trend for growth speed and yield bonus when population changes. + +### Commands +```text +/beehives YieldBoost true +/beehives SpeedBoost true +/beehives GrowthSpeedBonus 0.2 +/beehives YieldMultiplier 1.2 +``` + +## 12) Verbosity 1 uses level words (no percentages, no parentheses) +- Config: effective InformationVerbosity = `1` +- Expected beehive block info line: `Crop boost effectiveness: Low|Medium|High` +- Expected: no `%` and no `(...)` details. + +### Commands +```text +/beehives InformationVerbosityServerSideType ServerForced +/beehives InformationVerbosityServer 1 +/beehives YieldBoost true +/beehives SpeedBoost true +``` + +## 13) Verbosity > 1 shows detailed percentages and details +- Config: effective InformationVerbosity = `2` (or higher) +- Expected beehive block info line format: `Crop boost effectiveness: X% (+Y% yield, +Z% speed)`. + +### Commands +```text +/beehives InformationVerbosityServerSideType ServerForced +/beehives InformationVerbosityServer 2 +/beehives YieldBoost true +/beehives SpeedBoost true +``` + +## 14) Crop boost line hidden when boost is disabled +- Config: `YieldBoost=false`, `SpeedBoost=false` +- Expected on beehive block info: no `Crop boost effectiveness` line and no `Population too low for a crop boost` warning. + +### Commands +```text +/beehives YieldBoost false +/beehives SpeedBoost false +``` + +## 15) Low-population warning appears in yellow when boost is enabled +- Config: boost enabled, nearby hive population below `MinBeesForPlantBoost` +- Expected on beehive block info: yellow line `Population too low for a crop boost`. + +### Commands +```text +/beehives YieldBoost true +/beehives SpeedBoost true +/beehives MinBeesForPlantBoost 1000 +``` diff --git a/TestScenarios/SwarmScenarios.md b/TestScenarios/SwarmScenarios.md new file mode 100644 index 0000000..1182b79 --- /dev/null +++ b/TestScenarios/SwarmScenarios.md @@ -0,0 +1,190 @@ +# Swarm Scenarios + +## Shared setup +- Use at least 2 reusable beehives within `BeehiveRadius` of each other. +- Ensure one target hive can stay below `BeehiveConsideredEmptyBelowPopulation`. +- Keep at least one source hive well above swarm threshold. +- Use warm biome/daytime for temperature-gated scenarios. + +### Shared config commands (copy/paste) +```text +/beehives BeehiveRadius 35 +/beehives MaxBeePopulation 50000 +/beehives BeehiveConsideredEmptyBelowPopulation 500 +/beehives PopulationPercentRequirementForSwarm 80 +/beehives SwarmPopulationPercentage 0.2 +/beehives MaxTemperatureGrowth 10 +``` + +## 1) Open-for-incoming is always below-threshold +- Set hive A population to `499` while threshold is `500`. +- Expected: hive A is eligible as an incoming swarm target. +- Raise hive A to `500` or higher. +- Expected: hive A is no longer eligible. + +### Commands +```text +/beehives BeehiveConsideredEmptyBelowPopulation 500 +/beehives setPopulation 499 +``` + +## 2) Swarm trigger gates: population, dayhour, pre-swarm progress +- Source hive above required percent (`> 80%` of max by default). +- Test outside 8-12 dayhours. +- Expected: no swarm starts even if pre-swarm is at 100%. +- Test in 8-12 dayhours with pre-swarm at 100% (requires sustained optimal temperature over `PreSwarmDurationHours`). +- Expected: swarm starts. + +### Commands +```text +/beehives PopulationPercentRequirementForSwarm 80 +/beehives MaxBeePopulation 50000 +/beehives setPopulation 45000 +``` + +## 2b) Pre-swarm progress builds at optimal temperature +- Source hive above required percent, cooldown expired. +- Ensure temperature is at or above `MaxTemperatureGrowth`. +- Expected: block info shows "Bees are preparing to swarm" (v1) or progress % (v2+). +- Expected: progress increases from 0 to 100% over `PreSwarmDurationHours` hours. + +## 2c) Pre-swarm progress decreases in cold +- Start with some pre-swarm progress built up. +- Drop temperature below `MaxTemperatureGrowth`. +- Expected: pre-swarm progress decreases back toward 0. +- Expected: swarm does not trigger until progress reaches 100% again. + +## 2d) Wrench disables swarming +- Right-click a beehive with a wrench. +- Expected: block info shows "Swarming is disabled on this beehive." +- Expected: swarm does not trigger even if all conditions are met. +- Right-click again with wrench. +- Expected: swarming re-enabled and normal swarm behavior resumes. + +## 3) BuildingSwarm phase is gradual over 3h +- Trigger swarm from source hive. +- During first ~3 in-game hours, observe source population decreasing and swarm population increasing progressively. +- Expected: transition is gradual, not instant. + +## 4) HangingOut phase duration +- After BuildingSwarm finishes, swarm enters `HangingOut`. +- Expected: swarm remains in place for ~5 in-game hours. + +## 5) MigratingToNewHive phase is gradual over MigrationPhaseDurationHours +- After target pick, swarm enters `MigratingToNewHive`. +- Expected: population transfers gradually for the full migration duration. +- Expected: swarm block disappears at end of migration. + +## 6) Target ranking prefers distance * flowers * frame-factor +- Prepare two eligible target hives: + - B: closer, fewer flowers, no filled/feed frame. + - C: slightly farther, more flowers, has filled/feed frame. +- Expected: chosen target matches highest multiplicative score. +- Frame factor for any filled/feed frame should act as `2.0` multiplier. + +## 7) Vanilla skep target conversion path +- Place empty vanilla skep (`game:skep-reed-empty-*` or `game:skep-papyrus-empty-*`) in radius. +- Ensure it is registered as valid target by behavior patch. +- Expected: at migration start, empty skep is exchanged to populated skep. + +## 8) No-target retry behavior +- Let swarm finish hanging with zero eligible targets. +- Expected: no immediate disperse; retry occurs in next day swarm window (8-12). +- If still no valid target after retry, expected: swarm disperses. + +## 9) Spawn placement preference +- Trigger multiple swarm spawns with varied nearby geometry. +- Expected priority: + 1. side-attached on solid-faced wood blocks, + 2. side-attached on other solid-faced blocks, + 3. floor-attached fallback. + +## 10) Info text validation +- Beehive info: when source is ready, expected `Ready to swarm` line. +- Swarm block info: expected state + population + eligible target count + hours-until-pick lines. + +## 11) Command-assisted deterministic setup +- Use command to quickly shape scenario state. +- Expected: `/beehives setPopulation {int}` updates targeted reusable beehive population immediately. + +### Commands +```text +/beehives setPopulation 200 +/beehives setPopulation 45000 +``` + +## 12) Unload before swarm window (no missed swarm) +- Make source hive swarm-ready but unload area before 8:00. +- Re-enter area after 8:00 same day. +- Expected: catch-up applies population/honey/feed updates. +- Expected: swarm only starts if simulated timeline crosses valid window while ready. + +## 13) Unload during BuildingSwarm phase +- Trigger swarm and unload area while swarm is in `BuildingSwarm`. +- Stay away long enough to skip part or all of the 3h building phase. +- Re-enter area. +- Expected: swarm population is already advanced to the correct catch-up value. +- Expected: source hive population has already lost corresponding bees. + +## 14) Unload during HangingOut phase +- Trigger swarm and unload area after `BuildingSwarm` finishes. +- Stay away through part or all of `HangingOut`. +- Re-enter area. +- Expected: swarm resumes in correct post-catch-up state. +- Expected: if hanging duration elapsed and a target exists, migration may already have started or completed. + +## 15) Unload during MigratingToNewHive phase +- Trigger migration, then unload area before migration completes. +- Re-enter area after enough time has passed. +- Expected: transferred bee count reflects elapsed offline time. +- Expected: swarm block is gone if migration fully completed. +- Expected: target hive population increased according to migrated amount. + +## 16) Full lifecycle while unloaded (start -> finish) +- Make source hive swarm-ready in morning window. +- Unload immediately before/at expected swarm start and stay away for longer than full lifecycle. +- Re-enter area. +- Expected: catch-up simulation spawns swarm at historical time and fast-forwards it. +- Expected: resulting world state matches completed lifecycle outcome (target populated or swarm dispersed). +- Expected: source cooldown is based on simulated swarm start time. + +## 17) Retry path while unloaded (no targets first day) +- Ensure no eligible targets when hanging phase ends. +- Unload through first retry day window and re-enter later. +- Expected: retry-day behavior is honored by catch-up. +- Expected: swarm disperses if no valid target remains after retry. + +## 18) Debug chat output for unload catch-up +- Enable debug output before running unload scenarios. +- Expected: chat shows catch-up start/end summaries, population deltas, and swarm lifecycle catch-up events. + +### Commands +```text +/beehives debugUnload true +/beehives debugUnload false +``` + +## 19) SwarmPopulationPercentageWhenNoBeehivesAvailable: zero value skips spawn +- Ensure there are no eligible swarm targets in range (no empty beehives or skeps). +- Set `SwarmPopulationPercentageWhenNoBeehivesAvailable` to `0`. +- Trigger swarm conditions on source hive. +- Expected: no swarm block is spawned. +- Expected: cooldown is applied as if the swarm had occurred (`NextSwarmAllowedTotalDays` advances). + +### Commands +```text +/beehives SwarmPopulationPercentageWhenNoBeehivesAvailable 0 +``` + +## 20) SwarmPopulationPercentageWhenNoBeehivesAvailable: non-zero value spawns smaller swarm +- Ensure there are no eligible swarm targets in range. +- Set `SwarmPopulationPercentageWhenNoBeehivesAvailable` to a value greater than `0` (e.g., `5`). +- Trigger swarm conditions on source hive. +- Expected: a swarm block is spawned with a planned population equal to `5%` of the source hive population. +- Expected: swarm eventually disperses (no target found). + +### Commands +```text +/beehives SwarmPopulationPercentageWhenNoBeehivesAvailable 5 +``` + diff --git a/TestScenarios/readme.md b/TestScenarios/readme.md new file mode 100644 index 0000000..4af670d --- /dev/null +++ b/TestScenarios/readme.md @@ -0,0 +1 @@ +used AI to generate these scenarios but so far I've been too lazy to read and test them, will probably make a project for automated mod scenarios testing later. \ No newline at end of file diff --git a/ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj b/ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj new file mode 100644 index 0000000..cdec49f --- /dev/null +++ b/ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj @@ -0,0 +1,20 @@ + + + Exe + net8.0 + $(MSBuildProjectDirectory) + Debug;Release;Debug22 + + + + + + + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + + + \ No newline at end of file diff --git a/ZZCakeBuild/Program.cs b/ZZCakeBuild/Program.cs new file mode 100644 index 0000000..a3c733a --- /dev/null +++ b/ZZCakeBuild/Program.cs @@ -0,0 +1,122 @@ +using System; +using System.IO; +using System.Linq; +using Cake.Common; +using Cake.Common.IO; +using Cake.Common.Tools.DotNet; +using Cake.Common.Tools.DotNet.Clean; +using Cake.Common.Tools.DotNet.Publish; +using Cake.Core; +using Cake.Frosting; +using Cake.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Vintagestory.API.Common; + +namespace OrekiWoofsBeehivesCakeBuild; + +public static class Program +{ + public static int Main(string[] args) + { + return new CakeHost() + .UseContext() + .Run(args); + } +} + +public class BuildContext : FrostingContext +{ + public const string PROJECT_NAME = "OrekiWoofsBeehives"; + public string BuildConfiguration { get; } + public string Version { get; } + public string Name { get; } + public bool SkipJsonValidation { get; } + public string GameVersion { get; } + + public BuildContext(ICakeContext context) + : base(context) + { + BuildConfiguration = context.Argument("configuration", "Release"); + SkipJsonValidation = context.Argument("skipJsonValidation", false); + var modInfo = context.DeserializeJsonFromFile($"../{PROJECT_NAME}/modinfo.json"); + Version = modInfo.Version; + Name = modInfo.ModID; + GameVersion = modInfo.Dependencies.First(x => x.ModID == "game").Version; + } +} + +[TaskName("ValidateJson")] +public sealed class ValidateJsonTask : FrostingTask +{ + public override void Run(BuildContext context) + { + if (context.SkipJsonValidation) + { + return; + } + var jsonFiles = context.GetFiles($"../{BuildContext.PROJECT_NAME}/assets/**/*.json"); + foreach (var file in jsonFiles) + { + try + { + var json = File.ReadAllText(file.FullPath); + JToken.Parse(json); + } + catch (JsonException ex) + { + throw new Exception($"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex); + } + } + } +} + +[TaskName("Build")] +[IsDependentOn(typeof(ValidateJsonTask))] +public sealed class BuildTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.DotNetClean($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj", + new DotNetCleanSettings + { + Configuration = context.BuildConfiguration + }); + + + context.DotNetPublish($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj", + new DotNetPublishSettings + { + Configuration = context.BuildConfiguration + }); + } +} + +[TaskName("Package")] +[IsDependentOn(typeof(BuildTask))] +public sealed class PackageTask : FrostingTask +{ + public override void Run(BuildContext context) + { + context.EnsureDirectoryExists("../Releases"); + context.CleanDirectory("../Releases"); + context.EnsureDirectoryExists($"../Releases/{context.Name}"); + context.CopyFiles($"../{BuildContext.PROJECT_NAME}/bin/{context.BuildConfiguration}/Mods/orekiwoofsbeehives/publish/*", $"../Releases/{context.Name}"); + if (context.DirectoryExists($"../{BuildContext.PROJECT_NAME}/assets")) + { + context.CopyDirectory($"../{BuildContext.PROJECT_NAME}/assets", $"../Releases/{context.Name}/assets"); + } + context.CopyFile($"../{BuildContext.PROJECT_NAME}/modinfo.json", $"../Releases/{context.Name}/modinfo.json"); + if (context.FileExists($"../{BuildContext.PROJECT_NAME}/modicon.png")) + { + context.CopyFile($"../{BuildContext.PROJECT_NAME}/modicon.png", $"../Releases/{context.Name}/modicon.png"); + } + context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_v{context.Version}-v{context.GameVersion}.zip"); + } +} + +[TaskName("Default")] +[IsDependentOn(typeof(PackageTask))] +public class DefaultTask : FrostingTask +{ +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..dad9404 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,14 @@ +dotnet run --project ZZCakeBuild\OrekiWoofsBeehivesCakeBuild.csproj -- $args + +if ($LASTEXITCODE -eq 0) { + Set-Location .\RoamingBees + dotnet run --project ZZCakeBuild\RoamingBeesCakeBuild.csproj + Set-Location .. +} + +if ($LASTEXITCODE -eq 0) { + New-Item -ItemType Directory -Path .\Releases -Force | Out-Null + Copy-Item -Path .\RoamingBees\Releases\* -Destination .\Releases -Recurse -Force +} + +exit $LASTEXITCODE; \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..58c82b4 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +dotnet run --project ./ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj -- "$@" diff --git a/printFormattedCommits.ps1 b/printFormattedCommits.ps1 new file mode 100644 index 0000000..9f19b20 --- /dev/null +++ b/printFormattedCommits.ps1 @@ -0,0 +1,17 @@ +param( + [Alias('n')] + [int]$Count = 10 +) + +$messages = @(git -C $PSScriptRoot log -n $Count --pretty=format:%s) +[array]::Reverse($messages) + +$outputLines = @('
    ') + +foreach ($message in $messages) { + $encodedMessage = [System.Net.WebUtility]::HtmlEncode($message) + $outputLines += "
  • $encodedMessage
  • " +} + +$outputLines += '
' +$outputLines -join [Environment]::NewLine \ No newline at end of file diff --git a/printLatestCommits.ps1 b/printLatestCommits.ps1 new file mode 100644 index 0000000..992767d --- /dev/null +++ b/printLatestCommits.ps1 @@ -0,0 +1,3 @@ +$messages = git -C $PSScriptRoot log -n 10 --pretty=format:%s +[array]::Reverse($messages) +$messages diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ea89195 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# OrekiWoof's Beehives + +adds reharvestable beehives that also improve plant growth \ No newline at end of file