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

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

View File

@@ -0,0 +1,83 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>bin\$(Configuration)\Mods\roamingbees</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>
<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,96 @@
using RoamingBees.Behaviors;
using RoamingBees.Particles;
using RoamingBees.Particles.Catchup;
using System;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Server;
namespace RoamingBees;
public partial class RoamingBeesModSystem : ModSystem
{
private ICoreServerAPI? serverApi;
private ICoreClientAPI? clientApi;
private ICoreAPI? api;
public const string CONFIG_CHANNEL_NAME = "roamingbees-config";
internal BeeSpawnPacketDistributor? BeeSpawnPacketDistributor { get; private set; }
internal int GlobalActiveBees { get; set; }
internal bool DebugPathsEnabled { get; set; }
public IServerNetworkChannel? ServerChannel { get; private set; }
public IClientNetworkChannel? ClientChannel { get; private set; }
public override void Start(ICoreAPI api)
{
this.api = api;
api.RegisterBlockBehaviorClass(nameof(BlockBehaviorRoamingBees), typeof(BlockBehaviorRoamingBees));
api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorRoamingBees), typeof(BlockEntityBehaviorRoamingBees));
api.RegisterBlockBehaviorClass(nameof(BlockBehaviorBeeSwarm), typeof(BlockBehaviorBeeSwarm));
api.RegisterBlockEntityBehaviorClass(nameof(BlockEntityBehaviorBeeSwarm), typeof(BlockEntityBehaviorBeeSwarm));
try
{
if (api.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem"))
SubscribeToConfigChange(api);
}
catch (Exception)
{
Mod.Logger.VerboseDebug("Failed to subscribe to config change");
}
}
public override void StartServerSide(ICoreServerAPI serverApi)
{
InitializeServerConfig(serverApi);
this.serverApi = serverApi;
SetupServerCommands(serverApi);
BeeSpawnPacketDistributor = new(this, serverApi.Side);
ServerChannel = serverApi.Network
.RegisterChannel(BlockEntityBehaviorRoamingBees.NETWORK_CHANNEL_NAME)
.RegisterMessageType<BeeSpawnPacket>()
.RegisterMessageType<BeeCatchupRequestPacket>().SetMessageHandler<BeeCatchupRequestPacket>(BeeSpawnPacketDistributor.OnBeeCatchupRequest)
.RegisterMessageType<BeeCatchupPacket>();
serverApi.Network
.RegisterChannel(CONFIG_CHANNEL_NAME)
.RegisterMessageType<Config>();
}
public override void StartClientSide(ICoreClientAPI clientApi)
{
this.clientApi = clientApi;
InitializeClientConfig(clientApi);
SetupClientCommands(clientApi);
BeeSpawnPacketDistributor = new(this, clientApi.Side);
ClientChannel = clientApi.Network
// in case something ever magically doesn't work.. the docs say:
// "Must be registered in the same order as on the server."
.RegisterChannel(BlockEntityBehaviorRoamingBees.NETWORK_CHANNEL_NAME)
.RegisterMessageType<BeeSpawnPacket>()
.RegisterMessageType<BeeCatchupRequestPacket>().SetMessageHandler<BeeSpawnPacket>(BeeSpawnPacketDistributor.HandleBeeParticleSpawn)
.RegisterMessageType<BeeCatchupPacket>().SetMessageHandler<BeeCatchupPacket>(BeeSpawnPacketDistributor.HandleBeeParticleCatchup);
clientApi.Network
.RegisterChannel(CONFIG_CHANNEL_NAME)
.RegisterMessageType<Config>()
.SetMessageHandler<Config>(OnConfigReceivedFromServer);
}
public override double ExecuteOrder() => 0.12;
public override void Dispose()
{
SetDebugPathsEnabled(false);
BeeSpawnPacketDistributor?.Clear();
if (serverApi != null)
serverApi.Event.PlayerJoin -= OnPlayerJoin;
if (api?.ModLoader.IsModSystemEnabled("ConfigLib.ConfigLibModSystem") == true)
UnsubscribeFromConfigChange();
}
internal void SetDebugPathsEnabled(bool enabled)
{
DebugPathsEnabled = enabled;
DebugPathRenderManager.SetEnabled(clientApi, enabled);
}
}

View File

@@ -0,0 +1,10 @@
using OrekiWoofsBees.Common;
using Vintagestory.API.Common;
namespace RoamingBees.Utilities;
internal static class ApiExtensions
{
public static IPlantPositionRegistry? GetPlantPositionRegistry(this ICoreAPI api) => api.ModLoader.GetModSystem<PlantPositionRegistryModSystem2>();
public static RoamingBeesModSystem? GetFlyingBeesModSystem(this ICoreAPI api) => api.ModLoader.GetModSystem<RoamingBeesModSystem>();
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Vintagestory.API.Datastructures;
namespace RoamingBees.Utilities;
internal static class VectorParsing
{
public static Vector3? TryParseVector3(JsonObject? obj)
{
if (obj is null || obj.Token is null)
return null;
var values = obj.AsObject<float[]>([]);
if (values.Length < 3)
return null;
return new Vector3(values[0], values[1], values[2]);
}
public static Dictionary<string, Vector3>? ParseVector3Map(JsonObject? obj)
{
if (obj is null || obj.Token is null)
return null;
var raw = obj.AsObject<Dictionary<string, float[]>>([]);
if (raw.Count == 0)
return null;
var result = new Dictionary<string, Vector3>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in raw)
{
if (kvp.Value is null || kvp.Value.Length < 3)
continue;
result[kvp.Key] = new Vector3(kvp.Value[0], kvp.Value[1], kvp.Value[2]);
}
return result.Count > 0 ? result : null;
}
}

View File

@@ -0,0 +1,248 @@
{
"version": 0,
"file": "RoamingBees.json",
"patches": {},
"settings": [
{
"type": "separator",
"title": "Client-side"
},
{
"code": "ReceiveParticles",
"comment": "Whether to spawn and show bee particles.",
"type": "boolean",
"default": true,
"clientSide": true
},
{
"type": "separator",
"title": "General"
},
{
"code": "EnableOnVanillaSkeps",
"comment": "Whether roaming bee models will come out of vanilla skeps.",
"type": "boolean",
"default": true
},
{
"code": "RoamingBeesPerVanillaSkep",
"comment": "Max amount of bees roaming from a single vanilla skep.",
"type": "int",
"default": 15,
"range": {
"min": 0,
"max": 50
}
},
{
"code": "BeeRoamingRadius",
"comment": "The radius within which bees visit flowers. Set to 0 to use the source's radius attribute. If this is 0, and the source doesn't have this attribute, it defaults to 10.",
"type": "int",
"default": 0,
"range": {
"min": 0,
"max": 70
}
},
{
"code": "MaxGlobalRoamingBees",
"comment": "Maximum number of bee particle groups across all sources.",
"type": "int",
"default": 200,
"range": {
"min": 0,
"max": 1000
}
},
{
"type": "separator",
"title": "From Golden Combs compatibility"
},
{
"code": "EnableOnFgcCeramic",
"comment": "Whether roaming bee models will come out of Ceramic Hives from the 'From Golden Combs' mod.",
"type": "boolean",
"default": true
},
{
"code": "RoamingBeesPerFgcCeramic",
"comment": "Max amount of bees roaming from a single Ceramic Hive from the 'From Golden Combs' mod.",
"type": "int",
"default": 15,
"range": {
"min": 0,
"max": 50
}
},
{
"code": "EnableOnFgcLangstroth",
"comment": "Whether roaming bee models will come out of Langstroth Hives from the 'From Golden Combs' mod.",
"type": "boolean",
"default": true
},
{
"code": "RoamingBeesPerFgcLangstroth",
"comment": "Max amount of bees roaming from a single Langstroth Hive from the 'From Golden Combs' mod.",
"type": "int",
"default": 25,
"range": {
"min": 0,
"max": 50
}
},
{
"type": "separator",
"title": "Swarms"
},
{
"code": "BeesPerSwarmHovering",
"comment": "How many bees hang out around a swarm block.",
"type": "int",
"default": 15,
"range": {
"min": 0,
"max": 50
}
},
{
"code": "BeesPerSwarmTraveling",
"comment": "How many bees travel between a hive and a swarm block during migrations.",
"type": "int",
"default": 15,
"range": {
"min": 0,
"max": 50
}
},
{
"type": "separator",
"title": "Weather Effects"
},
{
"code": "RainfallSpawnStopThreshold",
"comment": "Rainfall level at which bee spawning completely stops.",
"type": "float",
"default": 0.1,
"range": {
"min": 0.0,
"max": 1.0
}
},
{
"code": "MaxRainfallCooldownPenalty",
"comment": "Maximum seconds added to spawn cooldown at rainfall threshold.",
"type": "float",
"default": 60.0,
"range": {
"min": 0.0,
"max": 300.0
}
},
{
"code": "GreenhouseAffectsBeehive",
"comment": "Whether greenhouses give 5C degrees temperature boost to the beehive.",
"type": "boolean",
"default": true
},
{
"code": "MinTemperatureParticleSpawn",
"comment": "Temperature at which bee particle spawn chance drops to zero.",
"type": "float",
"default": 8.0,
"range": {
"min": -10.0,
"max": 30.0
}
},
{
"code": "MaxTemperatureParticleSpawn",
"comment": "Temperature at which bee particle spawn chance is optimal.",
"type": "float",
"default": 18.0,
"range": {
"min": 0.0,
"max": 40.0
}
},
{
"code": "OptimalTemperatureSpawnChance",
"comment": "UNUSED ! Random spawn chance threshold at optimal temperature (lower = easier to spawn).",
"type": "float",
"default": 0.75,
"range": {
"min": 0.0,
"max": 1.0
}
},
{
"code": "BaseWindFrequency",
"comment": "Base frequency for particle movement sinusoidal noise at no wind.",
"type": "float",
"default": 0.1,
"range": {
"min": 0.01,
"max": 1.0
}
},
{
"code": "WindFrequencyMultiplier",
"comment": "Multiplier for how much wind increases noise frequency (4 = 5x faster at max wind).",
"type": "float",
"default": 4.0,
"range": {
"min": 0.0,
"max": 10.0
}
},
{
"code": "BaseWindNoiseStrength",
"comment": "Base acceleration strength from noise at no wind.",
"type": "float",
"default": 0.2,
"range": {
"min": 0.0,
"max": 2.0
}
},
{
"code": "MaxWindNoiseStrength",
"comment": "Maximum acceleration strength from noise at full wind.",
"type": "float",
"default": 1.0,
"range": {
"min": 0.0,
"max": 5.0
}
},
{
"code": "SunAltitudeMinDegrees",
"comment": "Minimum sun altitude in degrees for spawn modifier calculation (0 = horizon).",
"type": "float",
"default": -5.0,
"range": {
"min": -90.0,
"max": 90.0
}
},
{
"code": "SunAltitudeRangeDegrees",
"comment": "Range in degrees for sun altitude modifier (from min to min+range = 0% to 100%).",
"type": "float",
"default": 10.0,
"range": {
"min": 1.0,
"max": 90.0
}
},
{
"code": "MaxSunAltitudeCooldownPenalty",
"comment": "Maximum cooldown penalty in seconds at minimum sun altitude (linearly decreases to 0 at max altitude).",
"type": "float",
"default": 30.0,
"range": {
"min": 0.0,
"max": 300.0
}
}
]
}

View File

@@ -0,0 +1,29 @@
{
"config-desc-ReceiveParticles": "Whether to receive and render roaming bee particles on the client.",
"config-desc-EnableOnVanillaSkeps": "Whether roaming bees spawn from vanilla skeps.",
"config-desc-RoamingBeesPerVanillaSkep": "Number of roaming bees per vanilla skep.",
"config-desc-EnableOnFgcCeramic": "Whether roaming bees spawn from From Golden Combs ceramic hives.",
"config-desc-RoamingBeesPerFgcCeramic": "Number of roaming bees per From Golden Combs ceramic hive.",
"config-desc-EnableOnFgcLangstroth": "Whether roaming bees spawn from From Golden Combs Langstroth hives.",
"config-desc-RoamingBeesPerFgcLangstroth": "Number of roaming bees per From Golden Combs Langstroth hive.",
"config-desc-BeeRoamingRadius": "Radius in blocks within which bees roam from their hive.",
"config-desc-RainfallSpawnStopThreshold": "Rain level (0-1) above which bee spawning stops.",
"config-desc-MaxRainfallCooldownPenalty": "Maximum cooldown penalty in seconds after rain stops before bees resume spawning.",
"config-desc-GreenhouseAffectsBeehive": "Whether being in a greenhouse affects roaming bee behavior.",
"config-desc-MinTemperatureParticleSpawn": "Minimum temperature (°C) at which bee particles start spawning.",
"config-desc-MaxTemperatureParticleSpawn": "Temperature (°C) at which bee particle spawn chance reaches maximum.",
"config-desc-OptimalTemperatureSpawnChance": "Spawn chance (0-1) at optimal temperature.",
"config-desc-BaseWindFrequency": "Base frequency of wind effect on bee movement.",
"config-desc-WindFrequencyMultiplier": "Multiplier applied to wind frequency.",
"config-desc-BaseWindNoiseStrength": "Base noise strength of wind effect on bee movement.",
"config-desc-MaxWindNoiseStrength": "Maximum noise strength of wind effect on bee movement.",
"config-desc-SunAltitudeMinDegrees": "Minimum sun altitude in degrees for bee spawning.",
"config-desc-SunAltitudeRangeDegrees": "Range of sun altitude degrees over which spawn chance ramps up.",
"config-desc-MaxSunAltitudeCooldownPenalty": "Maximum cooldown penalty in seconds when sun is below minimum altitude.",
"config-desc-MaxGlobalRoamingBees": "Maximum number of roaming bee particles active globally.",
"debugpaths-desc": "Toggle client-side debug rendering of target boxes around nearby blocks (0 or 1).",
"debugpaths-parse-error": "Couldn't parse. Use 0 or 1.",
"plantreg-blockpertick-desc": "Get or set plant registry scan BlocksPerTick (0-1000).",
"plantreg-blockpertick-parse-error": "Couldn't parse. Use an integer in range 0-1000.",
"plantreg-unavailable": "Plant registry mod system is not available."
}

View File

@@ -0,0 +1,29 @@
{
"config-desc-ReceiveParticles": "Принимать ли и отображать частицы бродящих пчёл на клиенте.",
"config-desc-EnableOnVanillaSkeps": "Появляются ли бродячие пчёлы из ванильных ульев.",
"config-desc-RoamingBeesPerVanillaSkep": "Количество бродячих пчёл на ванильный улей.",
"config-desc-EnableOnFgcCeramic": "Появляются ли бродячие пчёлы из керамических ульев «From Golden Combs».",
"config-desc-RoamingBeesPerFgcCeramic": "Количество бродячих пчёл в керамическом улье «From Golden Combs».",
"config-desc-EnableOnFgcLangstroth": "Появляются ли бродячие пчёлы из ульев «Golden Combs Langstroth».",
"config-desc-RoamingBeesPerFgcLangstroth": "Количество бродячих пчёл в керамическом улье «Golden Combs Langstroth».",
"config-desc-BeeRoamingRadius": "Радиус в блоках от улья, внутри которых бродят пчёлы.",
"config-desc-RainfallSpawnStopThreshold": "Уровень осадков (0-1), выше которого прекращается появление пчёл.",
"config-desc-MaxRainfallCooldownPenalty": "Максимальное время в секундах после прекращения дождя до того, как пчёлы вновь начнут появляться.",
"config-desc-GreenhouseAffectsBeehive": "Влияет ли нахождение в теплице на поведение пчёл.",
"config-desc-MinTemperatureParticleSpawn": "Минимальная температура (°C), при которой пчёлы начинают появляться.",
"config-desc-MaxTemperatureParticleSpawn": "Температура (°C), при которой вероятность появления пчёл достигает максимума.",
"config-desc-OptimalTemperatureSpawnChance": "Шанс появления (0-1) при оптимальной температуре.",
"config-desc-BaseWindFrequency": "Базовая частота влияния ветра на движение пчёл.",
"config-desc-WindFrequencyMultiplier": "Множитель, применяемый к частоте ветра.",
"config-desc-BaseWindNoiseStrength": "Базовая сила шума ветра, влияющего на движение пчёл.",
"config-desc-MaxWindNoiseStrength": "Максимальная сила шума ветра, влияющего на движение пчёл.",
"config-desc-SunAltitudeMinDegrees": "Минимальная высота солнца в градусах для появления пчёл.",
"config-desc-SunAltitudeRangeDegrees": "Диапазон высоты солнца в градусах, в пределах которого постепенно увеличивается шанс появления пчёл.",
"config-desc-MaxSunAltitudeCooldownPenalty": "Максимальный штраф к перезарядке в секундах, когда солнце ниже минимальной высоты.",
"config-desc-MaxGlobalRoamingBees": "Максимальное глобальное количество частиц блуждающих пчёл.",
"debugpaths-desc": "Включить или выключить клиентскую отладочную визуализацию целевых рамок вокруг ближайших блоков (0 или 1).",
"debugpaths-parse-error": "Не удалось разобрать значение. Используйте 0 или 1.",
"plantreg-blockpertick-desc": "Получить или установить BlocksPerTick сканирования реестра растений (01000).",
"plantreg-blockpertick-parse-error": "Не удалось разобрать значение. Используйте целое число в диапазоне 01000.",
"plantreg-unavailable": "Система реестра растений недоступна."
}

View File

@@ -0,0 +1,18 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [{"name": "BlockEntityBehaviorBeeSwarm"}],
"file": "orekiwoofsbeehives:blocktypes/beeswarm",
"side": "Server",
"dependsOn": [{"modid": "orekiwoofsbeehives"}]
},
{
"op": "addMerge",
"path": "/behaviors",
"value": [{"name": "BlockBehaviorBeeSwarm"}],
"file": "orekiwoofsbeehives:blocktypes/beeswarm",
"side": "Server",
"dependsOn": [{"modid": "orekiwoofsbeehives"}]
}
]

View File

@@ -0,0 +1,145 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "",
"entrancePositions": {
"north": [0.5, 0.1, 0.8],
"east": [0.2, 0.1, 0.5],
"south": [0.5, 0.1, 0.2],
"west": [0.8, 0.1, 0.5]
},
"frontDirections": {
"north": [0, 0, 1],
"east": [-1, 0, 0],
"south": [0, 0, -1],
"west": [1, 0, 0]
}
}
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/behaviorsByType/*",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "",
"entrancePositions": {
"north": [0.5, 0.1, 0.8],
"east": [0.2, 0.1, 0.5],
"south": [0.5, 0.1, 0.2],
"west": [0.8, 0.1, 0.5]
},
"frontDirections": {
"north": [0, 0, 1],
"east": [-1, 0, 0],
"south": [0, 0, -1],
"west": [1, 0, 0]
}
}
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-new",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/behaviorsByType/*",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-new",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "",
"entrancePositions": {
"north": [0.5, 0.1, 0.8],
"east": [0.2, 0.1, 0.5],
"south": [0.5, 0.1, 0.2],
"west": [0.8, 0.1, 0.5]
},
"frontDirections": {
"north": [0, 0, 1],
"east": [-1, 0, 0],
"south": [0, 0, -1],
"west": [1, 0, 0]
}
}
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-fancy",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/behaviorsByType/*",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "fromgoldencombs:blocktypes/ceramicpot/ceramicbroodpot-fancy",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
}
]

View File

@@ -0,0 +1,49 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "side",
"entrancePositions": {
"north": [0.5, 0.27, 0.7],
"east": [0.3, 0.27, 0.5],
"south": [0.5, 0.27, 0.3],
"west": [0.7, 0.27, 0.5]
},
"frontDirections": {
"north": [0, -0.5, 1],
"east": [-1, -0.5, 0],
"south": [0, -0.5, -1],
"west": [1, -0.5, 0]
}
}
}
],
"file": "fromgoldencombs:blocktypes/langstroth/langstrothstack",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
},
{
"op": "addMerge",
"path": "/behaviors",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "fromgoldencombs:blocktypes/langstroth/langstrothstack",
"side": "Server",
"dependsOn": [
{
"modid": "fromgoldencombs"
}
]
}
]

View File

@@ -0,0 +1,49 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "side",
"entrancePositions": {
"north": [0.5, 0.2, 0.9],
"east": [0.1, 0.2, 0.5],
"south": [0.5, 0.2, 0.1],
"west": [0.9, 0.2, 0.5]
},
"frontDirections": {
"north": [0, 0, 1],
"east": [-1, 0, 0],
"south": [0, 0, -1],
"west": [1, 0, 0]
}
}
}
],
"file": "orekiwoofsbeehives:blocktypes/beehive",
"side": "Server",
"dependsOn": [
{
"modid": "orekiwoofsbeehives"
}
]
},
{
"op": "addMerge",
"path": "/behaviors",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "orekiwoofsbeehives:blocktypes/beehive",
"side": "Server",
"dependsOn": [
{
"modid": "orekiwoofsbeehives"
}
]
}
]

View File

@@ -0,0 +1,39 @@
[
{
"op": "addMerge",
"path": "/entityBehaviors",
"value": [
{
"name": "BlockEntityBehaviorRoamingBees",
"properties": {
"facingVariantKey": "side",
"entrancePositions": {
"north": [0.5, 0.1, 0.6],
"east": [0.4, 0.1, 0.5],
"south": [0.5, 0.1, 0.4],
"west": [0.6, 0.1, 0.5]
},
"frontDirections": {
"north": [0, 0, 1],
"east": [-1, 0, 0],
"south": [0, 0, -1],
"west": [1, 0, 0]
}
}
}
],
"file": "game:blocktypes/reed/skep",
"side": "Server"
},
{
"op": "addMerge",
"path": "/behaviors",
"value": [
{
"name": "BlockBehaviorRoamingBees"
}
],
"file": "game:blocktypes/reed/skep",
"side": "Server"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -0,0 +1,14 @@
{
"$schema": "https://moddbcdn.vintagestory.at/schema/modinfo.latest.json",
"type": "Code",
"modid": "roamingbees",
"name": "OrekiWoof's Roaming Bees",
"authors": [
"OrekiWoof"
],
"description": "Cute immersive roaming bees. Now on vanilla skeps and other mods' hives.",
"version": "2.0.0-dev.5",
"dependencies": {
"game": "1.21.0"
}
}

View File

@@ -0,0 +1,122 @@
using Cake.Common;
using Cake.Common.IO;
using Cake.Common.Tools.DotNet;
using Cake.Common.Tools.DotNet.Clean;
using Cake.Common.Tools.DotNet.Publish;
using Cake.Core;
using Cake.Frosting;
using Cake.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Linq;
using Vintagestory.API.Common;
namespace RoamingBeesCakeBuild;
public static class Program
{
public static int Main(string[] args)
{
return new CakeHost()
.UseContext<BuildContext>()
.Run(args);
}
}
public class BuildContext : FrostingContext
{
public const string PROJECT_NAME = "RoamingBees";
public string BuildConfiguration { get; }
public string Version { get; }
public string Name { get; }
public bool SkipJsonValidation { get; }
public string GameVersion { get; }
public BuildContext(ICakeContext context)
: base(context)
{
BuildConfiguration = context.Argument("configuration", "Release");
SkipJsonValidation = context.Argument("skipJsonValidation", false);
var modInfo = context.DeserializeJsonFromFile<ModInfo>($"../{PROJECT_NAME}/modinfo.json");
Version = modInfo.Version;
Name = modInfo.ModID;
GameVersion = modInfo.Dependencies.First(x => x.ModID == "game").Version;
}
}
[TaskName("ValidateJson")]
public sealed class ValidateJsonTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
if (context.SkipJsonValidation)
{
return;
}
var jsonFiles = context.GetFiles($"../{BuildContext.PROJECT_NAME}/assets/**/*.json");
foreach (var file in jsonFiles)
{
try
{
var json = File.ReadAllText(file.FullPath);
JToken.Parse(json);
}
catch (JsonException ex)
{
throw new Exception($"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex);
}
}
}
}
[TaskName("Build")]
[IsDependentOn(typeof(ValidateJsonTask))]
public sealed class BuildTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
context.DotNetClean($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj",
new DotNetCleanSettings
{
Configuration = context.BuildConfiguration
});
context.DotNetPublish($"../{BuildContext.PROJECT_NAME}/{BuildContext.PROJECT_NAME}.csproj",
new DotNetPublishSettings
{
Configuration = context.BuildConfiguration
});
}
}
[TaskName("Package")]
[IsDependentOn(typeof(BuildTask))]
public sealed class PackageTask : FrostingTask<BuildContext>
{
public override void Run(BuildContext context)
{
context.EnsureDirectoryExists("../Releases");
context.CleanDirectory("../Releases");
context.EnsureDirectoryExists($"../Releases/{context.Name}");
context.CopyFiles($"../{BuildContext.PROJECT_NAME}/bin/{context.BuildConfiguration}/Mods/roamingbees/publish/*", $"../Releases/{context.Name}");
if (context.DirectoryExists($"../{BuildContext.PROJECT_NAME}/assets"))
{
context.CopyDirectory($"../{BuildContext.PROJECT_NAME}/assets", $"../Releases/{context.Name}/assets");
}
context.CopyFile($"../{BuildContext.PROJECT_NAME}/modinfo.json", $"../Releases/{context.Name}/modinfo.json");
if (context.FileExists($"../{BuildContext.PROJECT_NAME}/modicon.png"))
{
context.CopyFile($"../{BuildContext.PROJECT_NAME}/modicon.png", $"../Releases/{context.Name}/modicon.png");
}
context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_v{context.Version}-v{context.GameVersion}.zip");
}
}
[TaskName("Default")]
[IsDependentOn(typeof(PackageTask))]
public class DefaultTask : FrostingTask
{
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<Configurations>Debug;Release;Debug22</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Cake.Frosting" Version="5.0.0" />
<PackageReference Include="Cake.Json" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Reference Include="VintagestoryAPI">
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

2
RoamingBees/build.ps1 Normal file
View File

@@ -0,0 +1,2 @@
dotnet run --project ZZCakeBuild/RoamingBeesCakeBuild.csproj -- $args
exit $LASTEXITCODE;

1
RoamingBees/build.sh Normal file
View File

@@ -0,0 +1 @@
dotnet run --project ./ZZCakeBuild/RoamingBeesCakeBuild.csproj -- "$@"