up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
package world
import (
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/go-gl/mathgl/mgl64"
)
// weather implements weather related methods for World. World embeds this
// struct, so any exported methods on weather are exported methods on World.
type weather struct{ w *World }
// StopWeatherCycle disables weather cycle of the World.
func (w weather) StopWeatherCycle() {
w.enableWeatherCycle(false)
}
// StartWeatherCycle enables weather cycle of the World.
func (w weather) StartWeatherCycle() {
w.enableWeatherCycle(true)
}
// snowingAt checks if it is snowing at a specific cube.Pos in the World. True
// is returned if the temperature in the Biome at that position is sufficiently
// low, if it is raining and if it's above the top-most obstructing block.
func (w weather) snowingAt(pos cube.Pos) bool {
if w.w == nil || !w.w.Dimension().WeatherCycle() {
return false
}
if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) > 0.15 {
return false
}
w.w.set.Lock()
raining := w.w.set.Raining
w.w.set.Unlock()
return raining && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}
// rainingAt checks if it is raining at a specific cube.Pos in the World. True
// is returned if it is raining, if the temperature is high enough in the biome
// for it not to be snow and if the block is above the top-most obstructing
// block.
func (w weather) rainingAt(pos cube.Pos) bool {
if w.w == nil || !w.w.Dimension().WeatherCycle() {
return false
}
if b := w.w.biome(pos); b.Rainfall() == 0 || w.w.temperature(pos) <= 0.15 {
return false
}
w.w.set.Lock()
a := w.w.set.Raining
w.w.set.Unlock()
return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}
// thunderingAt checks if it is thundering at a specific cube.Pos in the World.
// True is returned if rainingAt returns true and if it is thundering in the
// world.
func (w weather) thunderingAt(pos cube.Pos) bool {
raining := w.rainingAt(pos)
w.w.set.Lock()
a := w.w.set.Thundering && raining
w.w.set.Unlock()
return a && w.w.highestObstructingBlock(pos[0], pos[2]) < pos[1]
}
// raining checks if it is raining anywhere in the World.
func (w weather) raining() bool {
if w.w == nil || !w.w.Dimension().WeatherCycle() {
return false
}
w.w.set.Lock()
defer w.w.set.Unlock()
return w.w.set.Raining
}
// thundering checks if it is thundering anywhere in the World.
func (w weather) thundering() bool {
if w.w == nil || !w.w.Dimension().WeatherCycle() {
return false
}
w.w.set.Lock()
defer w.w.set.Unlock()
return w.w.set.Thundering
}
// StartRaining makes it rain in the World. The time.Duration passed will
// determine how long it will rain.
func (w weather) StartRaining(dur time.Duration) {
w.w.set.Lock()
defer w.w.set.Unlock()
w.setRaining(true, dur)
}
// StopRaining makes it stop raining in the World.
func (w weather) StopRaining() {
w.w.set.Lock()
defer w.w.set.Unlock()
if w.w.set.Raining {
w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
if w.w.set.Thundering {
// Also reset thunder if it was previously thundering.
w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
}
}
}
// StartThundering makes it thunder in the World. The time.Duration passed will
// determine how long it will thunder. StartThundering will also make it rain
// if it wasn't already raining. In this case the rain will, like the thunder,
// last for the time.Duration passed.
func (w weather) StartThundering(dur time.Duration) {
w.w.set.Lock()
defer w.w.set.Unlock()
w.setThunder(true, dur)
w.setRaining(true, dur)
}
// StopThundering makes it stop thundering in the current world.
func (w weather) StopThundering() {
w.w.set.Lock()
defer w.w.set.Unlock()
if w.w.set.Thundering && w.w.set.Raining {
w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
}
}
// advanceWeather advances the weather counters of the World. Rain and thunder
// are stopped/started when the rain and thunder times reach 0.
func (w weather) advanceWeather() {
w.w.set.RainTime--
w.w.set.ThunderTime--
if w.w.set.RainTime <= 0 {
// Wiki: The rain counter counts down to zero, and each time it reaches
// zero, the rain is toggled on or off. When the rain is turned on, the
// counter is reset to a value between 12,000-23,999 ticks (0.5-1 game
// days) and when the rain is turned off it is reset to a value of
// 12,000-179,999 ticks (0.5-7.5 game days).
if w.w.set.Raining {
w.w.setRaining(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
} else {
w.w.setRaining(true, time.Second*time.Duration(w.w.r.IntN(600)+600))
}
}
if w.w.set.ThunderTime <= 0 {
// Wiki: the thunder counter toggles thunder on/off when it reaches
// zero, but clear weather overrides the "on" state. When thunder is
// turned on, the thunder counter is reset to 3,600-15,999 ticks (3-13
// minutes), and when thunder is turned off the counter rests to
// 12,000-179,999 ticks (0.5-7.5 days).
if w.w.set.Thundering {
w.w.setThunder(false, time.Second*(time.Duration(w.w.r.IntN(8400)+600)))
} else {
w.w.setThunder(true, time.Second*time.Duration(w.w.r.IntN(620)+180))
}
}
}
// setRaining toggles raining depending on the raining argument. This does not
// lock the world mutex as opposed to StartRaining and StopRaining.
func (w weather) setRaining(raining bool, x time.Duration) {
w.w.set.Raining = raining
w.w.set.RainTime = int64(x.Seconds() * 20)
}
// setThunder toggles thundering depending on the thundering argument. This
// does not lock the world mutex as opposed to StartThundering and
// StopThundering.
func (w weather) setThunder(thundering bool, x time.Duration) {
w.w.set.Thundering = thundering
w.w.set.ThunderTime = int64(x.Seconds() * 20)
}
// enableWeatherCycle either enables or disables the weather cycle of the World.
func (w weather) enableWeatherCycle(v bool) {
if w.w == nil {
return
}
w.w.set.Lock()
defer w.w.set.Unlock()
w.w.set.WeatherCycle = v
}
// tickLightning iterates over all loaded chunks in the World, striking
// lightning in each one with a 1/100,000 chance.
func (w weather) tickLightning(tx *Tx) {
positions := make([]ChunkPos, 0, len(w.w.chunks)/100000)
for pos := range w.w.chunks {
// Wiki: For each loaded chunk, every tick there is a 1100,000 chance
// of an attempted lightning strike during a thunderstorm
if w.w.r.IntN(100000) == 0 {
positions = append(positions, pos)
}
}
for _, pos := range positions {
w.w.strikeLightning(tx, pos)
}
}
// strikeLightning attempts to strike lightning in the world at a specific
// ChunkPos. The final position is influenced by living entities that might be
// near the lightning strike. If there is no rain at the final position
// selected, the lightning strike will fail.
func (w weather) strikeLightning(tx *Tx, c ChunkPos) {
if w.w.conf.Entities.conf.Lightning == nil {
return
}
if pos := w.lightningPosition(tx, c); tx.ThunderingAt(cube.PosFromVec3(pos)) {
tx.AddEntity(w.w.conf.Entities.conf.Lightning(EntitySpawnOpts{Position: pos}))
}
}
// lightningPosition finds a random position in the ChunkPos to strike
// lightning and adjusts the position to any of the living entities found in or
// above the position if any are found.
func (w weather) lightningPosition(tx *Tx, c ChunkPos) mgl64.Vec3 {
v := w.w.r.Int32()
x, z := float64(c[0]<<4+(v&0xf)), float64(c[1]<<4+((v>>8)&0xf))
vec := w.adjustPositionToEntities(tx, mgl64.Vec3{x, float64(tx.HighestBlock(int(x), int(z)) + 1), z})
if pos := cube.PosFromVec3(vec); len(tx.Block(pos).Model().BBox(pos, tx)) != 0 {
// If lightning is about to strike inside a block that is not fully
// transparent. In this case, move the lightning up by one block so that
// it strikes above the block.
return vec.Add(mgl64.Vec3{0, 1})
}
return vec
}
// adjustPositionToEntities adjusts the mgl64.Vec3 passed to the position of
// any Entity found in the 3x3 column upwards from the mgl64.Vec3. If multiple
// entities are found, the position of one of the entities is selected
// randomly.
func (w weather) adjustPositionToEntities(tx *Tx, vec mgl64.Vec3) mgl64.Vec3 {
max := vec.Add(mgl64.Vec3{0, float64(w.w.Range().Max())})
list := make([]mgl64.Vec3, 0, 16)
for e := range tx.EntitiesWithin(cube.Box(vec[0], vec[1], vec[2], max[0], max[1], max[2]).GrowVec3(mgl64.Vec3{3, 3, 3})) {
if h, ok := e.(interface{ Health() float64 }); ok && h.Health() > 0 {
// Any (living) entity that is positioned higher than the highest
// block at its position is eligible to be struck by lightning. We
// first save all entity positions where this is the case.
pos := cube.PosFromVec3(e.Position())
if tx.HighestBlock(pos[0], pos[1]) < pos[2] {
list = append(list, e.Position())
}
}
}
// We then select one of the positions of entities higher than the highest
// block and adjust the position of the lightning to it, so that the entity
// is struck directly.
if len(list) > 0 {
vec = list[w.w.r.IntN(len(list))]
}
return vec
}