Private
Public Access
1
0

reinit branch

This commit is contained in:
2026-03-11 01:46:34 +01:00
commit bff9251737
129 changed files with 16115 additions and 0 deletions

265
.editorconfig Normal file
View File

@@ -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

675
.gitignore vendored Normal file
View File

@@ -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

1355
.timetracker Normal file

File diff suppressed because it is too large Load Diff

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "OrekiWoofsBeehives.slnx"
}

88
LICENSE.txt Normal file
View File

@@ -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.

18
OrekiWoofsBeehives.slnx Normal file
View File

@@ -0,0 +1,18 @@
<Solution>
<Configurations>
<BuildType Name="Debug22" />
<BuildType Name="Debug" />
<BuildType Name="Release" />
</Configurations>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
</Folder>
<Project Path="OrekiWoofsBeehives/OrekiWoofsBeehives.csproj" />
<Project Path="OrekiWoofsBees.Common/OrekiWoofsBees.Common.csproj" Id="e53bd51a-4c0e-4482-9e20-1361d42e72ea">
<BuildType Solution="Debug22|*" Project="Release" />
</Project>
<Project Path="RoamingBees/RoamingBees/RoamingBees.csproj" Id="7f545210-d3d4-4232-b6d6-7a10be19290c" />
<Project Path="RoamingBees/ZZCakeBuild/RoamingBeesCakeBuild.csproj" Id="742949f3-29ab-48ef-bb9a-903055795dfb" />
<Project Path="ZZCakeBuild/OrekiWoofsBeehivesCakeBuild.csproj" />
<Project Path="OrekiWoofsBees.Full/OrekiWoofsBees.Full.csproj" />
</Solution>

View File

@@ -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<string>(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<string>();
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<BlockEntityBehaviorBeehiveYieldMultiplier>(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<BlockEntityBehaviorBeehiveYieldMultiplier>(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<BlockEntityBehaviorBeehiveYieldMultiplier>(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<StructVec3i> 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);
}
}

View File

@@ -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<int> stagesWithBeehivePresent = [];
public IReadOnlyCollection<int> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
);

View File

@@ -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<SwarmTargetCandidate>? 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<SwarmTargetCandidate> FindEligibleTargets()
{
var candidates = new List<SwarmTargetCandidate>();
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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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,
};
}
}

View File

@@ -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<string, string>
{
["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<string, string> values)
{
var result = Lang.Get(langKey);
foreach (var kvp in values)
result = result.Replace($">>>{kvp.Key}<<<", kvp.Value);
return result;
}
}

View File

@@ -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<BlockEntityReusableBeehive>(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<Shape>();
var openShape = api.Assets.Get(new AssetLocation("orekiwoofsbeehives:shapes/block/beehive-open.json")).ToObject<Shape>();
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<WorldInteraction> { openCloseInteraction };
if (be.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation)
{
string[] existingValidSkeps = ["game:skep-reed-populated-east", "game:skep-papyrus-populated-east"];
var skepStacks = new List<ItemStack>();
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<WorldInteraction>();
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;
}
}

View File

@@ -0,0 +1,8 @@
namespace OrekiWoofsBeehives;
public enum BoostCalculationType
{
Full,
LinearPopulation,
NormalizedLogk10Population,
}

View File

@@ -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>(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<PlantPositionRegistryModSystem2>();
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);
}
}

View File

@@ -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,
};
}
}

View File

@@ -0,0 +1,12 @@
using System;
namespace OrekiWoofsBeehives;
[Flags]
public enum CropBonusType
{
None = 0,
Yield = 1,
Speed = 2,
YieldAndSpeed = Yield | Speed
}

View File

@@ -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($"<font color=\"#00bb00\">{Lang.Get("orekiwoofsbeehives:beehive-info-source-swarm-forming")}</font>");
else if (!beehive.IsReadyToStartSwarm() && beehive.IsReadyToStartSwarm(ignoreDayTime: true))
builder.AppendLine($"<font color=\"#00bb00\">{Lang.Get("orekiwoofsbeehives:beehive-info-ready-waiting-morning")}</font>");
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($"<font color=\"#00bb00\">{Lang.Get("orekiwoofsbeehives:beehive-info-incoming-swarm")}</font>");
if (!beehive.IsReceivingIncomingSwarm && beehive.BeePopulation < Config.Instance.BeehiveConsideredEmptyBelowPopulation)
{
builder.AppendLine($"<font color=\"#ffff00\">{Lang.GetWithFallback("orekiwoofsbeehives:beehive-info-no-bees-use-skep-or-wait-for-swarm", "orekiwoofsbeehives:beehive-info-no-bees")}</font>");
return;
}
if (beehive.IsReadyToStartSwarm())
builder.AppendLine($"<font color=\"#ffaa00\">{Lang.Get("orekiwoofsbeehives:beehive-info-ready-to-swarm")}</font>");
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($"<font color=\"#aaffaa\">{Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-building")}</font>");
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($"<font color=\"#aaffaa\">{Lang.Get("orekiwoofsbeehives:beehive-info-pre-swarm-progress", detail)}</font>");
}
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<string, string>
{
["beePopulation"] = $"{beehive.BeePopulation:N0}",
["maxBeePopulation"] = $"{cfg.MaxBeePopulation:N0}",
["beeProductionMultiplier"] = $"{stats.BeeProductionMultiplier:F1}"
}));
}
else
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-population", new Dictionary<string, string>
{
["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<string, string>
{
["flowersAround"] = $"{beehive.FlowersAround}",
["cropsAround"] = $"{beehive.CropsAround}"
}) + scoutingSuffix);
}
else
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-flowers", new Dictionary<string, string>
{
["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($"<font color=\"#ff0000\">{FormatLang("orekiwoofsbeehives:beehive-info-flowers-warning-causing-deaths", new Dictionary<string, string>
{
["flowersNeeded"] = $"{flowersNeeded}"
})}</font>");
}
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<string, string>
{
["filledFrames"] = $"{filledFrames}",
["filledFrameBonus"] = $"{filledFrames * cfg.BonusGrowthPerFilledFrame:N0}",
["emptyFrames"] = $"{emptyFrames}"
}));
}
else if (filledFrames > 0)
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-filled", new Dictionary<string, string>
{
["filledFrames"] = $"{filledFrames}",
["filledFrameBonus"] = $"{filledFrames * cfg.BonusGrowthPerFilledFrame:N0}"
}));
}
else if (emptyFrames > 0)
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-empty", new Dictionary<string, string>
{
["emptyFrames"] = $"{emptyFrames}"
}));
}
else
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-frames-total", new Dictionary<string, string>
{
["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<string, string>
{
["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<string, string>
{
["honeyProgress"] = $"{beehive.HoneyProgress * 100:F1}",
["daysToFill"] = $"{(1.0 - beehive.HoneyProgress) / stats.FramesPerDay:F1}"
}));
}
else
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-progress-noproduction", new Dictionary<string, string>
{
["honeyProgress"] = $"{beehive.HoneyProgress * 100:F1}"
}));
}
}
else if (totalFrames > 0 && verbosity < 3)
builder.AppendLine($"<font color=\"#00bb00\">{Lang.Get("orekiwoofsbeehives:beehive-info-all-filled")}</font>");
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<string, string>
{
["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($"<font color=\"#ff0000\">{Lang.Get("orekiwoofsbeehives:beehive-info-winter-starving")}</font>");
}
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<string, string>
{
["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($"<font color=\"#ffff00\">{Lang.Get("orekiwoofsbeehives:blockinfo-crop-boost-population-too-low")}</font>");
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<string>();
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<string, string>
{
["framesPerDay"] = $"{stats.FramesPerDay:F2}",
["daysPerFrame"] = $"{1.0 / stats.FramesPerDay:F1}"
}));
}
else
{
builder.AppendLine(FormatLang("orekiwoofsbeehives:beehive-info-honey-production-simple", new Dictionary<string, string>
{
["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<string, string>
{
["feedRemainingPercent"] = $"{remaining * 100:F0}",
["daysPerFrame"] = $"{daysPerFrame:F1}"
}));
}
private static string FormatLang(string langKey, Dictionary<string, string> values)
{
var result = Lang.Get(langKey);
foreach (var kvp in values)
result = result.Replace($">>>{kvp.Key}<<<", kvp.Value);
return result;
}
}

View File

@@ -0,0 +1,8 @@
namespace OrekiWoofsBeehives;
public enum InformationVerbosityServerSideType
{
ClientSide,
ServerRecommended,
ServerForced,
}

View File

@@ -0,0 +1,7 @@
namespace OrekiWoofsBeehives;
public class MetaConfig
{
public bool IsTester { get; set; }
public string? ModVersion { get; set; }
}

View File

@@ -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<ConfigLibModSystem>();
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>(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>(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>(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<ConfigLibModSystem>();
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}");
}
}

View File

@@ -0,0 +1,7 @@
namespace OrekiWoofsBeehives;
public enum MultipleBeehivesBoostCombinationType
{
Additive,
Max,
}

View File

@@ -0,0 +1,86 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>bin\$(Configuration)\Mods\orekiwoofsbeehives</OutputPath>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug22</Configurations>
<VintageStoryDataPathArgs></VintageStoryDataPathArgs>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug22'">
<TargetFramework>net10.0</TargetFramework>
<VINTAGE_STORY>$(VINTAGE_STORY_22PRE2)</VINTAGE_STORY>
<VintageStoryDataPathArgs> --dataPath "..\VintageStoryDataBeehives\Data22"</VintageStoryDataPathArgs>
</PropertyGroup>
<ItemGroup>
<Reference Include="VintagestoryAPI">
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="VSSurvivalMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSSurvivalMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSEssentials">
<HintPath>$(VINTAGE_STORY)/Mods/VSEssentials.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSCreativeMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSCreativeMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(VINTAGE_STORY)/Lib/0Harmony.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VintagestoryLib">
<HintPath>$(VINTAGE_STORY)/VintagestoryLib.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="protobuf-net">
<HintPath>$(VINTAGE_STORY)/Lib/protobuf-net.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="cairo-sharp">
<HintPath>$(VINTAGE_STORY)/Lib/cairo-sharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Data.Sqlite">
<HintPath>$(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Reference Include="configlib">
<HintPath>E:\Code\VintageStory\configlib_1.10.14\configlib.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Content Include="modinfo.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="modicon.png" Condition="Exists('modicon.png')">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Compile Include="..\OrekiWoofsBees.Common\**\*.cs" Exclude="..\OrekiWoofsBees.Common\bin\**\*;..\OrekiWoofsBees.Common\obj\**\*">
<Link>Common\%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
</ItemGroup>
</Project>

View File

@@ -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<Config>();
_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<Config>(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<MetaConfig>(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\"");
};
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<ItemStack>(__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<BlockEntityBehaviorBeehiveYieldMultiplier>(farmland.Pos);
if (tracker is null)
return false;
return tracker.IsPollinated(crop.CurrentCropStage);
}
}

View File

@@ -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)"
}
}
}

View File

@@ -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<OrekiWoofsBeehivesModSystem>();
public static IPlantPositionRegistry? GetPlantPositionRegistry(this ICoreAPI api) => api.ModLoader.GetModSystem<PlantPositionRegistryModSystem2>();
}

View File

@@ -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<StructVec3i> beehivePositions = [];
public IEnumerable<StructVec3i> 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);
}
}

View File

@@ -0,0 +1,24 @@
using OrekiWoofsBees.Common;
using System.Collections.Generic;
using Vintagestory.API.MathTools;
namespace OrekiWoofsBeehives.Utilities;
public class VanillaSkepRegistry
{
private readonly Dictionary<StructVec3i, string> emptySkepPositions = [];
public IEnumerable<KeyValuePair<StructVec3i, string>> 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);
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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
}
}
]
}

View File

@@ -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<<<x produkce medu)",
"beehive-info-population-v2": "Populace včel: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<<x produkce medu)",
"beehive-info-population-none": "Populace včel: Žádná",
"beehive-info-population-low": "Populace včel: Nízká",
"beehive-info-population-medium": "Populace včel: Střední",
"beehive-info-population-high": "Populace včel: Vysoká",
"beehive-info-population-veryhigh": "Populace včel: Velmi vysoká",
"beehive-info-flowers": "Květiny v okolí: >>>flowersAround<<<, Plodiny: >>>cropsAround<<<, Efektivně: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<<x produkce medu)",
"beehive-info-flowers-simple": "Květiny v okolí: >>>flowersAround<<<, Plodiny: >>>cropsAround<<<",
"beehive-info-flowers-scanning-suffix": "(oblast se stále prozkoumává)",
"beehive-info-frames-mixed": "Rámky: >>>filledFrames<<< naplněných (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> včel/den), >>>emptyFrames<<< prázdných (<font color=\"#994d00\">-0.5x</font> včel/den)",
"beehive-info-frames-filled": "Rámky: >>>filledFrames<<< naplněných (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> včel/den)",
"beehive-info-frames-empty": "Rámky: >>>emptyFrames<<< prázdných (<font color=\"#ffff00\">-0.5x</font> 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<<<x efektivita a změna populace)",
"beehive-info-temperature-cold": "Nízká teplota způsobuje, že včely zůstávají více uvnitř.",
"beehive-info-temperature-overwintering": "Včely přezimují.",
"beehive-info-population-change": "Změna populace: >>>changeSign<<<>>>netChange<<</den (růst: +>>>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<<<x (>>>daysPerFrame<<< dní na rámek)",
"beehive-info-honey-production-simple": "Produkce medu: >>>framesPerDay<<<x",
"beehive-info-feed-current": "Aktuální rámek s krmivem: >>>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": "- <a href=\"handbook://block-orekiwoofsbeehives:beehiveroof-north\">Střecha</a><br>- <a href=\"handbook://block-orekiwoofsbeehives:beehivestand-north\">Stojan</a>",
"handbook-beehive-title": "Dekorace"
}

View File

@@ -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<<<x honey production)",
"beehive-info-population-v2": "Bee population: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<<x honey production)",
"beehive-info-population-none": "Bee population: None",
"beehive-info-population-low": "Bee population: Low",
"beehive-info-population-medium": "Bee population: Medium",
"beehive-info-population-high": "Bee population: High",
"beehive-info-population-veryhigh": "Bee population: Very high",
"beehive-info-flowers": "Flowers around: >>>flowersAround<<<, Crops: >>>cropsAround<<<, Effectively: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<<x honey production)",
"beehive-info-flowers-simple": "Flowers around: >>>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 (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> bees/day), >>>emptyFrames<<< empty (<font color=\"#994d00\">-0.5x</font> bees/day)",
"beehive-info-frames-filled": "Frames: >>>filledFrames<<< filled (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> bees/day)",
"beehive-info-frames-empty": "Frames: >>>emptyFrames<<< empty (<font color=\"#ffff00\">-0.5x</font> 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<<<x efficiency and population change)",
"beehive-info-temperature-cold": "The cold temperature is making the bees stay inside more.",
"beehive-info-temperature-overwintering": "The bees are overwintering.",
"beehive-info-winter-starving": "Bees need additional food in the winter. Add a feed frame to keep them healthy.",
"beehive-info-population-change": "Population change: >>>changeSign<<<>>>netChange<<</day (growth: +>>>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<<<x (>>>daysPerFrame<<< days per frame)",
"beehive-info-honey-production-simple": "Honey production: >>>framesPerDay<<<x",
"beehive-info-feed-current": "Current frame with feed: >>>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": "- <a href=\"handbook://block-orekiwoofsbeehives:beehiveroof-north\">Roof</a><br>- <a href=\"handbook://block-orekiwoofsbeehives:beehivestand-north\">Stand</a>",
"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."
}

View File

@@ -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<<<x produkcji miodu)",
"beehive-info-population-v2": "Populacja pszczół: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<<x produkcji miodu)",
"beehive-info-population-none": "Populacja pszczół: Brak",
"beehive-info-population-low": "Populacja pszczół: Niska",
"beehive-info-population-medium": "Populacja pszczół: Średnia",
"beehive-info-population-high": "Populacja pszczół: Wysoka",
"beehive-info-population-veryhigh": "Populacja pszczół: Bardzo wysoka",
"beehive-info-flowers": "Kwiaty w okolicy: >>>flowersAround<<<, Uprawy: >>>cropsAround<<<, Efektywnie: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<<x produkcji miodu)",
"beehive-info-flowers-simple": "Kwiaty w okolicy: >>>flowersAround<<<, Uprawy: >>>cropsAround<<<",
"beehive-info-flowers-scanning-suffix": "(wciąż trwa sprawdzanie okolicy)",
"beehive-info-frames-mixed": "Wypełnione ramki: >>>filledFrames<<< (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> pszczół/dzień), Puste: >>>emptyFrames<<< (<font color=\"#ffff00\">-0.5x</font> pszczół/dzień)",
"beehive-info-frames-filled": "Wypełnione ramki: >>>filledFrames<<< (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> pszczół/dzień)",
"beehive-info-frames-empty": "Puste ramki: >>>emptyFrames<<< (<font color=\"#ffff00\">-0.5x</font> 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<<<x wydajności i zmiany populacji)",
"beehive-info-temperature-cold": "Przez zimno pszczoły wolą zostawać w ulu.",
"beehive-info-temperature-overwintering": "Pszczoły zimują.",
"beehive-info-population-change": "Zmiana populacji: >>>changeSign<<<>>>netChange<<</dzień (wzrost: +>>>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<<<x (>>>daysPerFrame<<< dni na ramkę)",
"beehive-info-honey-production-simple": "Produkcja miodu: >>>framesPerDay<<<x",
"beehive-info-feed-current": "Aktualna ramka z jedzeniem dla pszczół: >>>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": "- <a href=\"handbook://block-orekiwoofsbeehives:beehiveroof-north\">Daszek</a><br>- <a href=\"handbook://block-orekiwoofsbeehives:beehivestand-north\">Stojak</a>",
"handbook-beehive-title": "Urozmaicenia"
}

View File

@@ -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<<<x к производству мёда)",
"beehive-info-population-v2": "Популяция пчёл: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<<x к производству мёда)",
"beehive-info-population-none": "Популяция пчёл: отсутствует",
"beehive-info-population-low": "Популяция пчёл: низкая",
"beehive-info-population-medium": "Популяция пчёл: средняя",
"beehive-info-population-high": "Популяция пчёл: высокая",
"beehive-info-population-veryhigh": "Популяция пчёл: очень высокая",
"beehive-info-flowers": "Цветы вокруг: >>>flowersAround<<<, посевы: >>>cropsAround<<<, эффективно: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<<x к производству мёда)",
"beehive-info-flowers-simple": "Цветы вокруг: >>>flowersAround<<<, посевы: >>>cropsAround<<<",
"beehive-info-flowers-scanning-suffix": "(область всё ещё исследуется)",
"beehive-info-flowers-warning-causing-deaths": "Нехватка цветов приводит к сокращению популяции. Посадите ещё >>>flowersNeeded<<< цветов поблизости.",
"beehive-info-flowers-warning-suboptimal": "Улей был бы продуктивнее, если бы поблизости было больше цветов.",
"beehive-info-frames-mixed": "Рамки: >>>filledFrames<<< заполнено (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> пчёл/день), >>>emptyFrames<<< пусто (<font color=\"#994d00\">-0.5x</font> пчёл/день)",
"beehive-info-frames-filled": "Рамки: >>>filledFrames<<< заполнено (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> пчёл/день)",
"beehive-info-frames-empty": "Рамки: >>>emptyFrames<<< пусто (<font color=\"#ffff00\">-0.5x</font> пчёл/день)",
"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<<<x к эффективности и изменению популяции)",
"beehive-info-temperature-cold": "Низкая температура заставляет пчёл дольше оставаться внутри.",
"beehive-info-temperature-overwintering": "Пчелы перезимовывают.",
"beehive-info-winter-starving": "Зимой пчёлам необходим дополнительное питание. Добавьте кормовую рамку, чтобы сохранить их здоровье.",
"beehive-info-population-change": "Изменение популяции: >>>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<<<x (>>>daysPerFrame<<< дн. на рамку)",
"beehive-info-honey-production-simple": "Производство мёда: >>>framesPerDay<<<x",
"beehive-info-feed-current": "Текущая кормовая рамка улья: >>>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": "- <a href=\"handbook://block-orekiwoofsbeehives:beehiveroof-north\">Крыша улья</a><br>- <a href=\"handbook://block-orekiwoofsbeehives:beehivestand-north\">Подставка для улья</a>",
"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": "Время в часах, за которое у пчёл накапливается готовность к роению (0100%). Прогресс растёт только при MaxTemperatureGrowth или выше, иначе уменьшается.",
"config-desc-SwarmSettingAfterPlacing": "Будет ли в только что установленном улье включено или отключено роение. Можно изменить гаечным ключом.",
"setpopulation-desc": "Установить численность пчёл в улье, на который вы сейчас смотрите.",
"debugunload-desc": "Включить или отключить отладочные сообщения догоняющей обработки после выгрузки в чате сервера.",
"plantreg-blockpertick-desc": "Показать или задать параметр BlocksPerTick (01000) для сканирования реестра растений.",
"plantreg-blockpertick-parse-error": "Не удалось распознать значение. Используйте целое число от 0 до 1000.",
"plantreg-unavailable": "Система реестра растений недоступна."
}

View File

@@ -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<<<x виробництво меду)",
"beehive-info-population-v2": "Популяція бджіл: >>>beePopulation<<< / >>>maxBeePopulation<<< (>>>beeProductionMultiplier<<<x виробництво меду)",
"beehive-info-population-none": "Популяція бджіл: Немає",
"beehive-info-population-low": "Популяція бджіл: Низька",
"beehive-info-population-medium": "Популяція бджіл: Середня",
"beehive-info-population-high": "Популяція бджіл: Висока",
"beehive-info-population-veryhigh": "Популяція бджіл: Дуже висока",
"beehive-info-flowers": "Квіти навколо: >>>flowersAround<<<, Врожай: >>>cropsAround<<<, Effectively: >>>effectiveFlowers<<< (>>>flowerProductionMultiplier<<<x виробництво меду)",
"beehive-info-flowers-simple": "Квіти навколо: >>>flowersAround<<<, Врожай: >>>cropsAround<<<",
"beehive-info-flowers-scanning-suffix": "(все ще розвідує територію)",
"beehive-info-frames-mixed": "Рамки: >>>filledFrames<<< наповнена (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> бджіл/день), >>>emptyFrames<<< порожня (<font color=\"#994d00\">-0.5x</font> бджіл/день)",
"beehive-info-frames-filled": "Рамки: >>>filledFrames<<< наповнена (<font color=\"#00aa00\">+>>>filledFrameBonus<<<</font> бджіл/день)",
"beehive-info-frames-empty": "Рамки: >>>emptyFrames<<< порожня (<font color=\"#ffff00\">-0.5x</font> бджіл/день)",
"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<<<x ефективність та зміна чисельності населення)",
"beehive-info-temperature-cold": "Холодна температура змушує бджіл частіше залишатися всередині.",
"beehive-info-temperature-overwintering": "Бджоли зимують.",
"beehive-info-population-change": "Зміна популяції: >>>changeSign<<<>>>netChange<<</day (growth: +>>>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<<<x (>>>daysPerFrame<<< днів на рамку)",
"beehive-info-honey-production-simple": "Виробництво меду: >>>framesPerDay<<<x",
"beehive-info-feed-current": "Поточна рамка з кормом: >>>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": "- <a href=\"handbook://block-orekiwoofsbeehives:beehiveroof-north\">Дах</a><br>- <a href=\"handbook://block-orekiwoofsbeehives:beehivestand-north\">підставка</a>",
"handbook-beehive-title": "Прикраси"
}

View File

@@ -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"
}
]

View File

@@ -0,0 +1,13 @@
[
{
"op": "addMerge",
"path": "/behaviors",
"value": [
{
"name": "BlockBehaviorBeehiveAffected"
}
],
"file": "game:blocktypes/plant/**",
"side": "Server"
}
]

View File

@@ -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"
}
]

View File

@@ -0,0 +1,13 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorVanillaSkepSwarmTarget"
}
],
"file": "game:blocktypes/reed/skep",
"side": "Server"
}
]

View File

@@ -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" }
}

View File

@@ -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 }
}
]

View File

@@ -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 }
}

View File

@@ -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 }
}

View File

@@ -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
}
}

View File

@@ -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 ] }
}
}
]
}

View File

@@ -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 ] }
}
}
]
}

View File

@@ -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 ] }
}
}
]
}

View File

@@ -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 }
}
}]
}
]
}

View File

@@ -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 ] }
}
}
]
}

View File

@@ -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 ] }
}
}
]
}

View File

@@ -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 ] }
}
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -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"
}
}

View File

@@ -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<T>(
IChatCommandApi chatApi,
string commandName,
string langDomain,
bool serverSide,
Func<T> getInstance,
Action<bool> saveConfig)
{
Register(chatApi.Create(commandName), chatApi, langDomain, serverSide, getInstance, saveConfig);
}
public static void Register<T>(
IChatCommand builder,
IChatCommandApi chatApi,
string langDomain,
bool serverSide,
Func<T> getInstance,
Action<bool> saveConfig)
{
if (serverSide)
builder = builder.RequiresPrivilege(Privilege.controlserver);
var p = chatApi.Parsers;
foreach (var prop in typeof(T).GetProperties())
{
var attr = prop.GetCustomAttribute<ConfigCommandAttribute>();
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<string> formatValue, Action applyAndSave, Func<bool> 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()}");
}
}

View File

@@ -0,0 +1,23 @@
using System;
namespace OrekiWoofsBees.Common.Configs;
/// <summary>
/// Marks a Config property as having a /beehives or .beehives chat command.
/// The registration loop in ChatCommands.cs picks these up.
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ConfigCommandAttribute(bool serverSide) : Attribute
{
/// <summary>True = registered as a server command and broadcasts on change.</summary>
public bool ServerSide { get; } = serverSide;
/// <summary>Inclusive</summary>
public double Min { get; set; }
/// <summary>Inclusive</summary>
public double Max { get; set; }
/// <remarks>Allowed values for string enum properties (uses OptionalWordRange).</remarks>
public string[] AllowedValues { get; set; } = [];
}

View File

@@ -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);
}
}

View File

@@ -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<BlockPos, int>? CropEvent;
event Action<BlockPos, int>? 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<BlockPos> Flowers,
IEnumerable<BlockPos> 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);
}

View File

@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug22</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug22'">
<TargetFramework>net10.0</TargetFramework>
<VINTAGE_STORY>$(VINTAGE_STORY_22PRE2)</VINTAGE_STORY>
</PropertyGroup>
<ItemGroup>
<Reference Include="VintagestoryAPI">
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="VSSurvivalMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSSurvivalMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSEssentials">
<HintPath>$(VINTAGE_STORY)/Mods/VSEssentials.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSCreativeMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSCreativeMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(VINTAGE_STORY)/Lib/0Harmony.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VintagestoryLib">
<HintPath>$(VINTAGE_STORY)/VintagestoryLib.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="protobuf-net">
<HintPath>$(VINTAGE_STORY)/Lib/protobuf-net.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="cairo-sharp">
<HintPath>$(VINTAGE_STORY)/Lib/cairo-sharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Microsoft.Data.Sqlite">
<HintPath>$(VINTAGE_STORY)/Lib/Microsoft.Data.Sqlite.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="configlib">
<HintPath>E:\Code\VintageStory\configlib_1.10.14\configlib.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View File

@@ -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;
}
}

View File

@@ -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;
/// <summary>
/// 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
/// </summary>
public class PlantPositionRegistryModSystem2 : ModSystem, IPlantPositionRegistry
{
private const int default_blocks_per_tick = 20;
private const int tick_interval_ms = 20;
private static readonly Dictionary<int, ScanOffsetTable> offsetTables = [];
private long? tickListenerId;
private readonly Dictionary<StructVec3i, BeehiveScanCursor> beehives = [];
private readonly HashSet<StructVec3i> flowerPositions = [];
private readonly HashSet<StructVec3i> 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<BlockPos, int>? FlowerEvent;
public event Action<BlockPos, int>? 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<BlockPos> Flowers,
IEnumerable<BlockPos> 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}).");
}
}
/// <summary>
/// 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
/// </summary>
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<int, ScanOffsetTable> 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<int, ScanOffsetTable> 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<int, ScanOffsetTable> tables)
{
var table = tables[Radius];
if (table.Count == 0)
return 0.0f;
if (BlocksCheckedCount < table.Count)
return 0.0f;
return (float)currentIndex / table.Count;
}
}
/// <summary>
/// shared table of offsets for a given radius, sorted by priority.
/// only one instance per radius is created and shared across all beehives.
/// </summary>
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];
}
}
}

View File

@@ -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<BlockEntityPlantContainer?>(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);
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
namespace OrekiWoofsBees.Common;
public enum SwarmState
{
BuildingSwarm,
HangingOut,
MigratingToNewHive,
}

View File

@@ -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);
}

1
OrekiWoofsBees.Full/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
Mods/

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Configurations>Debug;Release;Debug22</Configurations>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableFastUpToDateCheck>true</DisableFastUpToDateCheck>
<VintageStoryDataPathArgs></VintageStoryDataPathArgs>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Debug22'">
<TargetFramework>net10.0</TargetFramework>
<VintageStoryDataPathArgs> --dataPath "..\VintageStoryDataBeehives\Data22"</VintageStoryDataPathArgs>
</PropertyGroup>
<Target Name="BuildBothMods" BeforeTargets="Build">
<Exec Command="dotnet build &quot;$(ProjectDir)../OrekiWoofsBeehives/OrekiWoofsBeehives.csproj&quot; -c $(Configuration)" />
<Exec Command="dotnet build &quot;$(ProjectDir)../RoamingBees/RoamingBees/RoamingBees.csproj&quot; -c $(Configuration)" />
<RemoveDir Directories="$(ProjectDir)Mods/orekiwoofsbeehives" />
<RemoveDir Directories="$(ProjectDir)Mods/roamingbees" />
<Delete Files="$(ProjectDir)Mods/orekiwoofsbeehives_v*.zip;$(ProjectDir)Mods/roamingbees_v*.zip" />
<MakeDir Directories="$(ProjectDir)Mods/orekiwoofsbeehives" />
<MakeDir Directories="$(ProjectDir)Mods/roamingbees" />
<ItemGroup>
<_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/**/*" />
</ItemGroup>
<Copy SourceFiles="@(_BeehivesFiles)" DestinationFolder="$(ProjectDir)Mods/orekiwoofsbeehives/%(RecursiveDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(_RoamingBeesFiles)" DestinationFolder="$(ProjectDir)Mods/roamingbees/%(RecursiveDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(_BeehivesAssets)" DestinationFolder="$(ProjectDir)Mods/orekiwoofsbeehives/assets/%(RecursiveDir)" SkipUnchangedFiles="true" />
<Copy SourceFiles="@(_RoamingBeesAssets)" DestinationFolder="$(ProjectDir)Mods/roamingbees/assets/%(RecursiveDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -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)"
}
}
}

675
RoamingBees/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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<BlockEntityBehaviorBeeSwarm>(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;
}
}

View File

@@ -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<BlockEntityBehaviorRoamingBees>(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;
}
}

View File

@@ -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<InternalBeeParticle> activeBees = [];
private readonly List<BlockPos> 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<BeeSpawnPacket> 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<RoamingBeesModSystem>();
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<Vec3f>();
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<Shape>();
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<Cuboidf>();
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<Cuboidf> 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<ITreeAttribute>())
{
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<BlockEntityBehaviorRoamingBees>();
}
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);
}
}
}

View File

@@ -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<InternalBeeParticle> activeBees = [];
private readonly TreeAttribute entityAttributeSnapshot = new();
private string facingVariantKey = "side";
private Dictionary<string, Vector3>? entrancePositions;
private Dictionary<string, Vector3>? 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<BeeSpawnPacket> 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<RoamingBeesModSystem>();
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<Vector3> 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<Vector3> 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<Vector3>();
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";
}
}

View File

@@ -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>(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<PlantPositionRegistryModSystem2>();
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}");
}
}

View File

@@ -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;
}

View File

@@ -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<ConfigLibModSystem>();
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<Config>("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<Config>("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<Config>("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;
}
}

View File

@@ -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<BeePlannedPathPoint> temp_list { get; } = [];
private static List<Vector3> temp_plants { get; } = [];
private static HashSet<Vector3> 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<Vector3> 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<BeePlannedPathPoint> 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<Vector3> 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<Vector3> 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<BeePlannedPathPoint>())
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;
}
}

View File

@@ -0,0 +1,5 @@
using System.Numerics;
namespace RoamingBees.Particles;
public readonly record struct BeePlannedPathPoint(Vector3 Position, bool IsIntermediate, bool IsSlowdown, Vector3? LookDirection = null);

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
namespace RoamingBees.Particles;
public enum BeeRole
{
Forager,
Hovering,
Traveling,
Scouting,
}

View File

@@ -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;
}

View File

@@ -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<Vector3, IBeeSpawnHandler> spawnHandlers { get; } = [];
private Dictionary<Vector3, IBeeSpawnCatchup> 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();
}

View File

@@ -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,
}
}

View File

@@ -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<BeeSpawnPacket> SpawnPackets;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace RoamingBees.Particles.Catchup;
public interface IBeeSpawnCatchup
{
IEnumerable<BeeSpawnPacket> ActiveBeesPackets { get; }
}

View File

@@ -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()
{
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,6 @@
namespace RoamingBees.Particles;
public interface IBeeSpawnHandler
{
void HandleBeeParticleSpawn(BeeSpawnPacket packet, bool catchup = false);
}

View File

@@ -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; }
/// <summary> from 0 to 1 </summary>
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;
/// <summary>
/// unset = use AccelerationDirection
/// </summary>
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,
}
}

View File

@@ -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<BeePlannedPathPoint> 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;
}
/// <summary>
/// If <paramref name="relPos"/> 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.
/// </summary>
private static Vector3? NudgeOutOfSolid(IBlockAccessor blockAccessor, BlockPos swarmPos, Vector3 relPos)
{
if (!IsSolidAt(blockAccessor, swarmPos, relPos))
return relPos;
Span<Vector3> 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;
}
}

View File

@@ -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)"
}
}
}

Some files were not shown because too many files have changed in this diff Show More