From 646907ecbdaf64ed03107b96217b8fba33a48417 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 00:30:22 -0600 Subject: [PATCH 01/30] Initial implementation of nether portals (WIP) --- server/block/fire.go | 213 ++++++++++++------------ server/block/hash.go | 5 + server/block/portal.go | 53 ++++++ server/block/register.go | 3 + server/player/player.go | 46 ++++++ server/world/dimension.go | 4 +- server/world/portal/nether.go | 295 ++++++++++++++++++++++++++++++++++ server/world/portal/scan.go | 131 +++++++++++++++ server/world/world.go | 7 + 9 files changed, 656 insertions(+), 101 deletions(-) create mode 100644 server/block/portal.go create mode 100644 server/world/portal/nether.go create mode 100644 server/world/portal/scan.go diff --git a/server/block/fire.go b/server/block/fire.go index 728ad3b35..68c1d0a08 100644 --- a/server/block/fire.go +++ b/server/block/fire.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/entity" "github.com/df-mc/dragonfly/server/entity/damage" "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" "math/rand" "time" ) @@ -24,40 +25,98 @@ type Fire struct { Age int } -// flammableBlock returns true if a block is flammable. -func flammableBlock(block world.Block) bool { - flammable, ok := block.(Flammable) - return ok && flammable.FlammabilityInfo().Encouragement > 0 +// Place ... +func (Fire) Place(pos cube.Pos, w *world.World) bool { + for _, f := range cube.Faces() { + if o, ok := w.Block(pos.Side(f)).(Obsidian); ok && !o.Crying { + if p, ok := portal.NetherPortalFromPos(w, pos); ok && p.Framed() && !p.Activated() { + p.Activate() + return false + } + return true + } + } + return true } -// neighboursFlammable returns true if one a block adjacent to the passed position is flammable. -func neighboursFlammable(pos cube.Pos, w *world.World) bool { - for _, i := range cube.Faces() { - if flammableBlock(w.Block(pos.Side(i))) { - return true +// EntityInside ... +func (f Fire) EntityInside(_ cube.Pos, _ *world.World, e world.Entity) { + if flammable, ok := e.(entity.Flammable); ok { + if l, ok := e.(entity.Living); ok && !l.AttackImmune() { + l.Hurt(1, damage.SourceFire{}) + } + if flammable.OnFireDuration() < time.Second*8 { + flammable.SetOnFire(8 * time.Second) + } + } +} + +// ScheduledTick ... +func (f Fire) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) { + f.tick(pos, w, r) +} + +// RandomTick ... +func (f Fire) RandomTick(pos cube.Pos, w *world.World, r *rand.Rand) { + f.tick(pos, w, r) +} + +// NeighbourUpdateTick ... +func (f Fire) NeighbourUpdateTick(pos, neighbour cube.Pos, w *world.World) { + below := w.Block(pos.Side(cube.FaceDown)) + if diffuser, ok := below.(LightDiffuser); (ok && diffuser.LightDiffusionLevel() != 15) && (!neighboursFlammable(pos, w) || f.Type == SoulFire()) { + w.BreakBlockWithoutParticles(pos) + return + } + switch below.(type) { + case SoulSand, SoulSoil: + f.Type = SoulFire() + w.PlaceBlock(pos, f) + case Water: + if neighbour == pos { + w.BreakBlockWithoutParticles(pos) + } + default: + if f.Type == SoulFire() { + w.BreakBlockWithoutParticles(pos) + return } } +} + +// HasLiquidDrops ... +func (f Fire) HasLiquidDrops() bool { return false } -// max ... -func max(a, b int) int { - if a > b { - return a +// LightEmissionLevel ... +func (f Fire) LightEmissionLevel() uint8 { + return f.Type.LightLevel() +} + +// EncodeBlock ... +func (f Fire) EncodeBlock() (name string, properties map[string]interface{}) { + switch f.Type { + case NormalFire(): + return "minecraft:fire", map[string]interface{}{"age": int32(f.Age)} + case SoulFire(): + return "minecraft:soul_fire", map[string]interface{}{"age": int32(f.Age)} } - return b + panic("unknown fire type") } -// infinitelyBurning returns true if fire can infinitely burn at the specified position. -func infinitelyBurning(pos cube.Pos, w *world.World) bool { - switch block := w.Block(pos.Side(cube.FaceDown)).(type) { - //TODO: Magma Block - case Netherrack: - return true - case Bedrock: - return block.InfiniteBurning +// Start starts a fire at a position in the world. The position passed must be either air or tall grass and conditions +// for a fire to be present must be present. +func (f Fire) Start(w *world.World, pos cube.Pos) { + b := w.Block(pos) + _, isAir := b.(Air) + _, isTallGrass := b.(TallGrass) + if isAir || isTallGrass { + below := w.Block(pos.Side(cube.FaceDown)) + if below.Model().FaceSolid(pos, cube.FaceUp, w) || neighboursFlammable(pos, w) { + w.PlaceBlock(pos, Fire{}) + } } - return false } // burn attempts to burn a block. @@ -75,18 +134,6 @@ func (f Fire) burn(pos cube.Pos, w *world.World, r *rand.Rand, chanceBound int) } } -// rainingAround checks if it is raining either at the cube.Pos passed or at any of its horizontal neighbours. -func rainingAround(pos cube.Pos, w *world.World) bool { - raining := w.RainingAt(pos) - for _, face := range cube.HorizontalFaces() { - if raining { - break - } - raining = w.RainingAt(pos.Side(face)) - } - return raining -} - // tick ... func (f Fire) tick(pos cube.Pos, w *world.World, r *rand.Rand) { if f.Type == SoulFire() { @@ -174,84 +221,52 @@ func (f Fire) tick(pos cube.Pos, w *world.World, r *rand.Rand) { } } -// EntityInside ... -func (f Fire) EntityInside(_ cube.Pos, _ *world.World, e world.Entity) { - if flammable, ok := e.(entity.Flammable); ok { - if l, ok := e.(entity.Living); ok && !l.AttackImmune() { - l.Hurt(1, damage.SourceFire{}) - } - if flammable.OnFireDuration() < time.Second*8 { - flammable.SetOnFire(8 * time.Second) - } - } -} - -// ScheduledTick ... -func (f Fire) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) { - f.tick(pos, w, r) -} - -// RandomTick ... -func (f Fire) RandomTick(pos cube.Pos, w *world.World, r *rand.Rand) { - f.tick(pos, w, r) +// flammableBlock returns true if a block is flammable. +func flammableBlock(block world.Block) bool { + flammable, ok := block.(Flammable) + return ok && flammable.FlammabilityInfo().Encouragement > 0 } -// NeighbourUpdateTick ... -func (f Fire) NeighbourUpdateTick(pos, neighbour cube.Pos, w *world.World) { - below := w.Block(pos.Side(cube.FaceDown)) - if diffuser, ok := below.(LightDiffuser); (ok && diffuser.LightDiffusionLevel() != 15) && (!neighboursFlammable(pos, w) || f.Type == SoulFire()) { - w.BreakBlockWithoutParticles(pos) - return - } - switch below.(type) { - case SoulSand, SoulSoil: - f.Type = SoulFire() - w.PlaceBlock(pos, f) - case Water: - if neighbour == pos { - w.BreakBlockWithoutParticles(pos) - } - default: - if f.Type == SoulFire() { - w.BreakBlockWithoutParticles(pos) - return +// neighboursFlammable returns true if one a block adjacent to the passed position is flammable. +func neighboursFlammable(pos cube.Pos, w *world.World) bool { + for _, i := range cube.Faces() { + if flammableBlock(w.Block(pos.Side(i))) { + return true } } -} - -// HasLiquidDrops ... -func (f Fire) HasLiquidDrops() bool { return false } -// LightEmissionLevel ... -func (f Fire) LightEmissionLevel() uint8 { - return f.Type.LightLevel() +// max ... +func max(a, b int) int { + if a > b { + return a + } + return b } -// EncodeBlock ... -func (f Fire) EncodeBlock() (name string, properties map[string]interface{}) { - switch f.Type { - case NormalFire(): - return "minecraft:fire", map[string]interface{}{"age": int32(f.Age)} - case SoulFire(): - return "minecraft:soul_fire", map[string]interface{}{"age": int32(f.Age)} +// infinitelyBurning returns true if fire can infinitely burn at the specified position. +func infinitelyBurning(pos cube.Pos, w *world.World) bool { + switch block := w.Block(pos.Side(cube.FaceDown)).(type) { + //TODO: Magma Block + case Netherrack: + return true + case Bedrock: + return block.InfiniteBurning } - panic("unknown fire type") + return false } -// Start starts a fire at a position in the world. The position passed must be either air or tall grass and conditions -// for a fire to be present must be present. -func (f Fire) Start(w *world.World, pos cube.Pos) { - b := w.Block(pos) - _, isAir := b.(Air) - _, isTallGrass := b.(TallGrass) - if isAir || isTallGrass { - below := w.Block(pos.Side(cube.FaceDown)) - if below.Model().FaceSolid(pos, cube.FaceUp, w) || neighboursFlammable(pos, w) { - w.PlaceBlock(pos, Fire{}) +// rainingAround checks if it is raining either at the cube.Pos passed or at any of its horizontal neighbours. +func rainingAround(pos cube.Pos, w *world.World) bool { + raining := w.RainingAt(pos) + for _, face := range cube.HorizontalFaces() { + if raining { + break } + raining = w.RainingAt(pos.Side(face)) } + return raining } // allFire ... diff --git a/server/block/hash.go b/server/block/hash.go index c18f9ae89..f53242731 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -95,6 +95,7 @@ const ( hashPackedIce hashPlanks hashPodzol + hashPortal hashPotato hashPrismarine hashPumpkin @@ -506,6 +507,10 @@ func (Podzol) Hash() uint64 { return hashPodzol } +func (p Portal) Hash() uint64 { + return hashPortal | uint64(p.Axis)<<8 +} + func (p Potato) Hash() uint64 { return hashPotato | uint64(p.Growth)<<8 } diff --git a/server/block/portal.go b/server/block/portal.go new file mode 100644 index 000000000..819773fdf --- /dev/null +++ b/server/block/portal.go @@ -0,0 +1,53 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" +) + +// Portal is the translucent part of the nether portal that teleports the player to and from the Nether. +type Portal struct { + empty + transparent + + // Axis is the axis which the chain faces. + Axis cube.Axis +} + +// EncodeBlock ... +func (p Portal) EncodeBlock() (string, map[string]interface{}) { + return "minecraft:portal", map[string]interface{}{"portal_axis": p.Axis.String()} +} + +// NeighbourUpdateTick ... +func (p Portal) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { + valid := func(pos cube.Pos) bool { + b := w.Block(pos) + _, isPortal := b.(Portal) + _, isFrame := b.(Obsidian) + return isPortal || isFrame + } + + shouldKeep := true + if pos.Y() < w.Range().Max()-1 { + shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{0, 1, 0})) + } + if pos.Y() > w.Range().Min() { + shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{0, 1, 0})) + } + + if p.Axis == cube.X { + shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{1, 0, 0})) + shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{1, 0, 0})) + } else { + shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{0, 0, 1})) + shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{0, 0, 1})) + } + + if !shouldKeep { + if n, ok := portal.NetherPortalFromPos(w, pos); ok { + n.Deactivate() + } + } +} diff --git a/server/block/register.go b/server/block/register.go index bf926864f..4c0f4e3ee 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -1,6 +1,7 @@ package block import ( + "github.com/df-mc/dragonfly/server/block/cube" _ "github.com/df-mc/dragonfly/server/internal/block_internal" "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" @@ -29,6 +30,8 @@ func init() { world.RegisterBlock(Bedrock{InfiniteBurning: true}) world.RegisterBlock(Obsidian{}) world.RegisterBlock(Obsidian{Crying: true}) + world.RegisterBlock(Portal{Axis: cube.X}) + world.RegisterBlock(Portal{Axis: cube.Z}) world.RegisterBlock(DiamondBlock{}) world.RegisterBlock(Glass{}) world.RegisterBlock(Glowstone{}) diff --git a/server/player/player.go b/server/player/player.go index 1a87d8eb3..e80e5cc0d 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -23,6 +23,7 @@ import ( "github.com/df-mc/dragonfly/server/session" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/particle" + "github.com/df-mc/dragonfly/server/world/portal" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" @@ -90,6 +91,10 @@ type Player struct { breakingPos atomic.Value lastBreakDuration time.Duration + portalTimeout atomic.Bool + portalTime atomic.Value + awaitingPortalTransfer atomic.Bool + breakParticleCounter atomic.Uint32 hunger *hungerManager @@ -2036,6 +2041,23 @@ func (p *Player) Tick(w *world.World, current int64) { } p.cooldownMu.Unlock() + if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { + if _, ok := w.Block(cube.PosFromVec3(p.Position())).(block.Portal); ok { + if !p.portalTimeout.Load() { + if p.GameMode().CreativeInventory() || (p.awaitingPortalTransfer.Load() && time.Since(p.portalTime.Load().(time.Time)) >= time.Second*4) { + d, _ := w.PortalDestinations() + go p.travel(w, d) + } else if !p.awaitingPortalTransfer.Load() { + p.portalTime.Store(time.Now()) + p.awaitingPortalTransfer.Store(true) + } + } + } else { + p.portalTimeout.Store(false) + p.awaitingPortalTransfer.Store(false) + } + } + if p.session() == session.Nop && !p.Immobile() { m := p.mc.TickMovement(p, p.Position(), p.Velocity(), p.yaw.Load(), p.pitch.Load()) m.Send() @@ -2047,6 +2069,30 @@ func (p *Player) Tick(w *world.World, current int64) { } } +// travel moves the player to the given Nether or Overworld world, and translates the player's current position +// based on the source world. +func (p *Player) travel(source, destination *world.World) { + sourceDimension, targetDimension := source.Dimension(), destination.Dimension() + pos := cube.PosFromVec3(p.Position()) + if sourceDimension == world.Overworld { + pos = cube.Pos{pos.X() / 8, pos.Y() + sourceDimension.Range().Min(), pos.Z() / 8} + } else { + pos = cube.Pos{pos.X() * 8, pos.Y() - targetDimension.Range().Min(), pos.Z() * 8} + } + + p.portalTimeout.Store(true) + p.awaitingPortalTransfer.Store(false) + if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { + destination.AddEntity(p) + p.Teleport(netherPortal.Spawn().Vec3Middle()) + return + } + + // Java edition spawns the player at the translated position if all else fails, so we do the same. + destination.AddEntity(p) + p.Teleport(pos.Vec3Middle()) +} + // tickFood ticks food related functionality, such as the depletion of the food bar and regeneration if it // is full enough. func (p *Player) tickFood(w *world.World) { diff --git a/server/world/dimension.go b/server/world/dimension.go index 7a00a20a4..b817b3350 100644 --- a/server/world/dimension.go +++ b/server/world/dimension.go @@ -10,7 +10,7 @@ var ( // has a sun, clouds, stars and a moon. Overworld has a building range of [-64, 320]. Overworld overworld // Nether is a Dimension implementation with a lower base light level and a darker sky without sun/moon. It has a - // building range of [0, 256]. + // building range of [0, 128]. Nether nether // End is a Dimension implementation with a dark sky. It has a building range of [0, 256]. End end @@ -40,7 +40,7 @@ func (overworld) WeatherCycle() bool { return true } func (overworld) TimeCycle() bool { return true } func (overworld) String() string { return "Overworld" } -func (nether) Range() cube.Range { return cube.Range{0, 256} } +func (nether) Range() cube.Range { return cube.Range{0, 128} } func (nether) EncodeDimension() int { return 1 } func (nether) WaterEvaporates() bool { return true } func (nether) LavaSpreadDuration() time.Duration { return time.Second / 4 } diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go new file mode 100644 index 000000000..48acf8e5f --- /dev/null +++ b/server/world/portal/nether.go @@ -0,0 +1,295 @@ +package portal + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" + "github.com/df-mc/dragonfly/server/world" + "math" + "math/rand" +) + +// Nether contains information about a nether portal structure. +type Nether struct { + w, h int + framed bool + axis cube.Axis + world *world.World + spawnPos cube.Pos + positions []cube.Pos +} + +const ( + // minimumNetherPortalWidth, maximumNetherPortalWidth controls the minimum and maximum width of a portal. + minimumNetherPortalWidth, maximumNetherPortalWidth = 2, 21 + // minimumNetherPortalHeight, maximumNetherPortalHeight controls the minimum and maximum height of a portal. + minimumNetherPortalHeight, maximumNetherPortalHeight = 3, 21 +) + +// NetherPortalFromPos returns Nether portal information from a given position in the frame. +func NetherPortalFromPos(w *world.World, pos cube.Pos) (Nether, bool) { + if w.Dimension() == world.End { + // Don't waste our time - we can't make a portal in the end. + return Nether{}, false + } + + axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{ + portal(cube.X), + portal(cube.Z), + air(), + }) + if !ok { + axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []world.Block{ + portal(cube.X), + portal(cube.Z), + }) + } + return Nether{ + w: width, h: height, + spawnPos: pos, + positions: positions, + framed: completed, + axis: axis, + world: w, + }, ok +} + +// FindOrCreateNetherPortal finds or creates a Nether portal at the given position. +func FindOrCreateNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { + n, ok := FindNetherPortal(w, pos, radius) + if ok { + return n, true + } + return CreateNetherPortal(w, pos) +} + +// FindNetherPortal searches a provided radius for a Nether portal. +func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { + if w.Dimension() == world.End { + // Don't waste our time - we can't make a portal in the end. + return Nether{}, false + } + + closestPos, closestDist, ok := cube.Pos{}, math.MaxFloat64, false + topMatchers := []world.Block{portal(cube.X), portal(cube.Z)} + bottomMatcher := []world.Block{obsidian()} + + for x := pos.X() - radius/2; x < (pos.X() + radius/2); x++ { + for z := pos.Z() - radius/2; z < (pos.Z() + radius/2); z++ { + for y := w.Dimension().Range().Max(); y >= w.Dimension().Range().Min(); y-- { + selectedPos := cube.Pos{x, y, z} + if satisfiesMatchers(w.Block(selectedPos), topMatchers) { + belowPos := selectedPos.Side(cube.FaceDown) + if satisfiesMatchers(w.Block(belowPos), bottomMatcher) { + dist := world.Distance(pos.Vec3(), selectedPos.Vec3()) + if dist < closestDist { + closestDist, closestPos, ok = dist, selectedPos, true + } + } + } + } + } + } + if !ok { + // Don't waste our time if the search didn't work out. + return Nether{}, false + } + return NetherPortalFromPos(w, closestPos) +} + +// CreateNetherPortal creates a Nether portal at the given position. +func CreateNetherPortal(w *world.World, pos cube.Pos) (Nether, bool) { + if w.Dimension() == world.End { + // You can't create a nether portal in the end. + return Nether{}, false + } + + resultPos, random, distance, a, r := pos, rand.Intn(4), -1.0, 0, w.Range() + searchValidArea := func(directions int, valid func(pos cube.Pos, riv int, coEff1, coEff2 int) bool) { + for tempX := pos.X() - 16; tempX <= pos.X()+16; tempX++ { + offsetX := float64(tempX-pos.X()) + 0.5 + for tempZ := pos.Z() - 16; tempZ <= pos.Z()+16; tempZ++ { + offsetZ := float64(tempZ-pos.Z()) + 0.5 + for tempY := r.Max() - 1; tempY >= r.Min(); tempY-- { + entryPos := cube.Pos{tempX, tempY, tempZ} + if w.Block(entryPos) != air() { + continue + } + + for tempY > r.Min() && w.Block(entryPos.Side(cube.FaceDown)) == air() { + tempY-- + entryPos[1]-- + } + + for riv := random; riv < random+directions; riv++ { + coEff1 := riv % 2 + coEff2 := 1 - coEff1 + + if !valid(entryPos, riv, coEff1, coEff2) { + break + } + + offsetY := float64(tempY-pos.Y()) + 0.5 + newDist := offsetX*offsetX + offsetY*offsetY + offsetZ*offsetZ + if distance < 0.0 || newDist < distance { + distance = newDist + a = riv % directions + resultPos = cube.Pos{tempX, tempY, tempZ} + } + } + } + } + } + } + + // Search for a valid area in all four directions, adding some extra space for comfort. + searchValidArea(4, func(pos cube.Pos, riv int, coEff1, coEff2 int) bool { + if riv%4 >= 2 { + coEff1 = -coEff1 + coEff2 = -coEff2 + } + + for safeSpace1 := 0; safeSpace1 < 3; safeSpace1++ { + for safeSpace2 := -1; safeSpace2 < 3; safeSpace2++ { + for height := -1; height < 4; height++ { + b := w.Block(cube.Pos{ + pos.X() + safeSpace2*coEff1 + safeSpace1*coEff2, + pos.Y() + height, + pos.Z() + safeSpace2*coEff2 - safeSpace1*coEff1, + }) + _, solid := b.Model().(model.Solid) + if height < 0 && !solid || height >= 0 && b != air() { + return false + } + } + } + } + return true + }) + + if distance < 0.0 { + // If we couldn't find a valid area under those specifications, we can search the two main directions instead, + // reducing comfort but at least allowing us to have a portal in the area. + searchValidArea(2, func(pos cube.Pos, riv int, coEff1, coEff2 int) bool { + for safeSpace := 0; safeSpace < 3; safeSpace++ { + for height := -1; height < 4; height++ { + b := w.Block(cube.Pos{ + pos.X() + safeSpace*coEff1, + pos.Y() + height, + pos.Z() + safeSpace*coEff2, + }) + _, solid := b.Model().(model.Solid) + if height < 0 && !solid || height >= 0 && b != air() { + return false + } + } + } + return true + }) + } + + coEff1 := a % 2 + coEff2 := 1 - coEff1 + if a%4 >= 2 { + coEff1 = -coEff1 + coEff2 = -coEff2 + } + + axis := cube.X + if coEff1 == 0 { + axis = cube.Z + } + + if distance < 0.0 { + // If all else fails, we can simply create a floating platform in the void with the portal on it. + resultPos[1] = int(math.Min(math.Max(float64(resultPos[1]), 70), float64(r.Max()-10))) + for safeBeforeAfter := -1; safeBeforeAfter <= 1; safeBeforeAfter++ { + for safeWidth := 0; safeWidth < 2; safeWidth++ { + for height := -1; height < 3; height++ { + entryPos := cube.Pos{ + resultPos.X() + safeWidth*coEff1 + safeBeforeAfter*coEff2, + resultPos.Y() + height, + resultPos.Z() + safeWidth*coEff2 - safeBeforeAfter*coEff1, + } + + w.SetBlock(entryPos, air()) + if height < 0 { + w.SetBlock(entryPos, obsidian()) + } + } + } + } + } + + // Build the portal frame and activate it. + var positions []cube.Pos + for width := -1; width < 3; width++ { + for height := -1; height < 4; height++ { + entryPos := cube.Pos{ + resultPos.X() + width*coEff1, + resultPos.Y() + height, + resultPos.Z() + width*coEff2, + } + + if width == -1 || width == 2 || height == -1 || height == 3 { + w.SetBlock(entryPos, obsidian()) + continue + } + positions = append(positions, entryPos) + w.SetBlock(entryPos, portal(axis)) + } + } + + return Nether{ + w: minimumNetherPortalWidth, + h: minimumNetherPortalHeight, + framed: true, + spawnPos: resultPos, + positions: positions, + axis: axis, + world: w, + }, true +} + +// Bounds ... +func (n Nether) Bounds() (int, int) { + return n.w, n.h +} + +// Activate ... +func (n Nether) Activate() { + for _, pos := range n.Positions() { + n.world.SetBlock(pos, portal(n.axis)) + } +} + +// Deactivate ... +func (n Nether) Deactivate() { + for _, pos := range n.Positions() { + n.world.BreakBlockWithoutParticles(pos) + } +} + +// Framed ... +func (n Nether) Framed() bool { + return n.framed +} + +// Activated ... +func (n Nether) Activated() bool { + for _, pos := range n.Positions() { + if n.world.Block(pos) != portal(n.axis) { + return false + } + } + return true +} + +// Spawn ... +func (n Nether) Spawn() cube.Pos { + return n.spawnPos +} + +// Positions ... +func (n Nether) Positions() []cube.Pos { + return n.positions +} diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go new file mode 100644 index 000000000..a248c2710 --- /dev/null +++ b/server/world/portal/scan.go @@ -0,0 +1,131 @@ +package portal + +import ( + "container/list" + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// scanIteration contains data about a nether portal scan iteration. +type scanIteration struct { + lastPos cube.Pos + face cube.Face + first bool +} + +// multiAxisScan performs a scan first on the Z axis, and then on the X axis. +func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { + axis := cube.Z + positions, width, height, completed := scan(axis, framePos, w, matchers) + if len(positions) == 0 { + axis = cube.X + positions, width, height, completed = scan(axis, framePos, w, matchers) + } + return axis, positions, width, height, completed, len(positions) > 0 +} + +// scan performs a scan on the given axis for any of the provided matchers using a position and a world. +func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Block) ([]cube.Pos, int, int, bool) { + var width, height int + positionsMap := make(map[cube.Pos]bool) + + completed := true + queue := list.New() + queue.PushBack(scanIteration{lastPos: framePos, first: true}) + for queue.Len() > 0 { + e := queue.Front() + queue.Remove(e) + + // Parse the latest iteration. + iteration := e.Value.(scanIteration) + posFace := iteration.face + pos := iteration.lastPos + if !iteration.first { + pos = pos.Side(posFace) + } + + b := w.Block(pos) + if _, ok := positionsMap[pos]; !ok && satisfiesMatchers(b, matchers) { + // Add the position to the map. + positionsMap[pos] = true + + // If we are on the same X or Z axis as the portal, we can assume that our height is being changed. + if pos.X() == framePos.X() && pos.Z() == framePos.Z() && posFace < cube.FaceNorth { + height++ + } + + // If we are on the same Y axis as the portal, we can assume that our width is being changed. + if pos.Y() == framePos.Y() { + width++ + } + + // Make sure we don't exceed the maximum portal width or height. + if width > maximumNetherPortalWidth || height > maximumNetherPortalHeight { + return []cube.Pos{}, 0, 0, false + } + + // Plan new iterations. + if axis == cube.Z { + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceSouth}) + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceNorth}) + } else if axis == cube.X { + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceWest}) + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceEast}) + } + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceUp}) + queue.PushBack(scanIteration{lastPos: pos, face: cube.FaceDown}) + } else if _, ok = positionsMap[pos]; !(ok || b == obsidian()) { + completed = false + } + } + + // Make sure we at least reach the minimum portal width and height. + area, expectedArea := len(positionsMap), width*height + if width < minimumNetherPortalWidth || height < minimumNetherPortalHeight || area != expectedArea { + return []cube.Pos{}, 0, 0, false + } + + // Get the actual positions from the map. + positions := make([]cube.Pos, 0, expectedArea) + for pos := range positionsMap { + positions = append(positions, pos) + } + return positions, width, height, completed +} + +// satisfiesMatchers checks if the given block satisfies all matchers. +func satisfiesMatchers(b world.Block, matchers []world.Block) bool { + for _, matcher := range matchers { + if b == matcher { + return true + } + } + return false +} + +// air returns an air block. +func air() world.Block { + a, ok := world.BlockByName("minecraft:air", nil) + if !ok { + panic("could not find air block") + } + return a +} + +// portal returns a portal block. +func portal(axis cube.Axis) world.Block { + p, ok := world.BlockByName("minecraft:portal", map[string]interface{}{"portal_axis": axis.String()}) + if !ok { + panic("could not find portal block") + } + return p +} + +// obsidian returns an obsidian block. +func obsidian() world.Block { + o, ok := world.BlockByName("minecraft:obsidian", nil) + if !ok { + panic("could not find obsidian block") + } + return o +} diff --git a/server/world/world.go b/server/world/world.go index da3345e5e..f6e8e0c33 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -265,6 +265,13 @@ func (w *World) SetBlock(pos cube.Pos, b Block) { return } + if b, ok := b.(interface { + Place(pos cube.Pos, w *World) bool + }); ok && !b.Place(pos, w) { + // Don't place the block. + return + } + rid, ok := BlockRuntimeID(b) if !ok { w.log.Errorf("runtime ID of block %+v not found", b) From 78f1ade916e3ed6d8dd58211e6d0492e99148fe7 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 00:32:18 -0600 Subject: [PATCH 02/30] block/portal.go: Simply portal deactivation on neighbour change. --- server/block/portal.go | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/server/block/portal.go b/server/block/portal.go index 819773fdf..34bea0937 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -22,32 +22,7 @@ func (p Portal) EncodeBlock() (string, map[string]interface{}) { // NeighbourUpdateTick ... func (p Portal) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { - valid := func(pos cube.Pos) bool { - b := w.Block(pos) - _, isPortal := b.(Portal) - _, isFrame := b.(Obsidian) - return isPortal || isFrame - } - - shouldKeep := true - if pos.Y() < w.Range().Max()-1 { - shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{0, 1, 0})) - } - if pos.Y() > w.Range().Min() { - shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{0, 1, 0})) - } - - if p.Axis == cube.X { - shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{1, 0, 0})) - shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{1, 0, 0})) - } else { - shouldKeep = shouldKeep && valid(pos.Subtract(cube.Pos{0, 0, 1})) - shouldKeep = shouldKeep && valid(pos.Add(cube.Pos{0, 0, 1})) - } - - if !shouldKeep { - if n, ok := portal.NetherPortalFromPos(w, pos); ok { - n.Deactivate() - } + if n, ok := portal.NetherPortalFromPos(w, pos); ok && !n.Framed() { + n.Deactivate() } } From f77786511d1c63136b6ecef0950fa2ced468b53d Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 10:57:18 -0600 Subject: [PATCH 03/30] player/player.go: Make Travel exported. --- server/player/player.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index e80e5cc0d..5f6b0a687 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2046,7 +2046,7 @@ func (p *Player) Tick(w *world.World, current int64) { if !p.portalTimeout.Load() { if p.GameMode().CreativeInventory() || (p.awaitingPortalTransfer.Load() && time.Since(p.portalTime.Load().(time.Time)) >= time.Second*4) { d, _ := w.PortalDestinations() - go p.travel(w, d) + p.Travel(w, d) } else if !p.awaitingPortalTransfer.Load() { p.portalTime.Store(time.Now()) p.awaitingPortalTransfer.Store(true) @@ -2069,9 +2069,9 @@ func (p *Player) Tick(w *world.World, current int64) { } } -// travel moves the player to the given Nether or Overworld world, and translates the player's current position +// Travel moves the player to the given Nether or Overworld world, and translates the player's current position // based on the source world. -func (p *Player) travel(source, destination *world.World) { +func (p *Player) Travel(source, destination *world.World) { sourceDimension, targetDimension := source.Dimension(), destination.Dimension() pos := cube.PosFromVec3(p.Position()) if sourceDimension == world.Overworld { @@ -2082,15 +2082,17 @@ func (p *Player) travel(source, destination *world.World) { p.portalTimeout.Store(true) p.awaitingPortalTransfer.Store(false) - if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { - destination.AddEntity(p) - p.Teleport(netherPortal.Spawn().Vec3Middle()) - return - } + go func() { + if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { + destination.AddEntity(p) + p.Teleport(netherPortal.Spawn().Vec3Middle()) + return + } - // Java edition spawns the player at the translated position if all else fails, so we do the same. - destination.AddEntity(p) - p.Teleport(pos.Vec3Middle()) + // Java edition spawns the player at the translated position if all else fails, so we do the same. + destination.AddEntity(p) + p.Teleport(pos.Vec3Middle()) + }() } // tickFood ticks food related functionality, such as the depletion of the food bar and regeneration if it From 694f4c299d34b4978f83eb31b44c00b2a16b10d1 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 11:35:10 -0600 Subject: [PATCH 04/30] world/world.go: Moved Place checks to the PlaceBlock function. --- server/world/world.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/world/world.go b/server/world/world.go index f6e8e0c33..03c25dc11 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -265,13 +265,6 @@ func (w *World) SetBlock(pos cube.Pos, b Block) { return } - if b, ok := b.(interface { - Place(pos cube.Pos, w *World) bool - }); ok && !b.Place(pos, w) { - // Don't place the block. - return - } - rid, ok := BlockRuntimeID(b) if !ok { w.log.Errorf("runtime ID of block %+v not found", b) @@ -364,6 +357,14 @@ func (w *World) PlaceBlock(pos cube.Pos, b Block) { if w == nil { return } + + if b, ok := b.(interface { + Place(pos cube.Pos, w *World) bool + }); ok && !b.Place(pos, w) { + // Don't place the block. + return + } + var liquid Liquid if displacer, ok := b.(LiquidDisplacer); ok { liq, ok := w.Liquid(pos) From 5b7230808a0110b8f20a330aa732245471a63380 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 15:04:41 -0600 Subject: [PATCH 05/30] block/portal.go: Fix destroying a frame not destroying all frames. --- server/block/portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/block/portal.go b/server/block/portal.go index 34bea0937..9048d305d 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -22,7 +22,7 @@ func (p Portal) EncodeBlock() (string, map[string]interface{}) { // NeighbourUpdateTick ... func (p Portal) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { - if n, ok := portal.NetherPortalFromPos(w, pos); ok && !n.Framed() { + if n, ok := portal.NetherPortalFromPos(w, pos); ok && (!n.Framed() || !n.Activated()) { n.Deactivate() } } From d6e58bd937036fed22961cf726ce02427bed42da Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 15:08:06 -0600 Subject: [PATCH 06/30] player/player.go: Default the portal timeout to true. --- server/player/player.go | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index 5f6b0a687..763b0edb6 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -111,22 +111,23 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { p.broadcastItems(slot, item) } }), - uuid: uuid.New(), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - hunger: newHungerManager(), - health: entity.NewHealthManager(), - effects: entity.NewEffectManager(), - gameMode: world.GameModeSurvival, - h: NopHandler{}, - name: name, - skin: skin, - speed: *atomic.NewFloat64(0.1), - nameTag: *atomic.NewString(name), - heldSlot: atomic.NewUint32(0), - locale: language.BritishEnglish, - scale: *atomic.NewFloat64(1), - cooldowns: make(map[itemHash]time.Time), + uuid: uuid.New(), + offHand: inventory.New(1, p.broadcastItems), + armour: inventory.NewArmour(p.broadcastArmour), + hunger: newHungerManager(), + health: entity.NewHealthManager(), + effects: entity.NewEffectManager(), + gameMode: world.GameModeSurvival, + h: NopHandler{}, + name: name, + skin: skin, + speed: *atomic.NewFloat64(0.1), + nameTag: *atomic.NewString(name), + heldSlot: atomic.NewUint32(0), + locale: language.BritishEnglish, + scale: *atomic.NewFloat64(1), + cooldowns: make(map[itemHash]time.Time), + portalTimeout: *atomic.NewBool(true), } p.mc = &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true} p.pos.Store(pos) From 50b9b7384dad50bfeabe5a9a6228f095e7e27234 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 15:21:15 -0600 Subject: [PATCH 07/30] player/player.go: Don't request a new transfer during a transfer if the player leaves the portal. --- server/player/player.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index 763b0edb6..e0247bbcc 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -91,8 +91,9 @@ type Player struct { breakingPos atomic.Value lastBreakDuration time.Duration - portalTimeout atomic.Bool portalTime atomic.Value + portalTimeout atomic.Bool + portalTransfer atomic.Bool awaitingPortalTransfer atomic.Bool breakParticleCounter atomic.Uint32 @@ -2053,7 +2054,7 @@ func (p *Player) Tick(w *world.World, current int64) { p.awaitingPortalTransfer.Store(true) } } - } else { + } else if !p.portalTransfer.Load() { p.portalTimeout.Store(false) p.awaitingPortalTransfer.Store(false) } @@ -2081,18 +2082,21 @@ func (p *Player) Travel(source, destination *world.World) { pos = cube.Pos{pos.X() * 8, pos.Y() - targetDimension.Range().Min(), pos.Z() * 8} } + p.portalTransfer.Store(true) p.portalTimeout.Store(true) p.awaitingPortalTransfer.Store(false) go func() { + // Java edition spawns the player at the translated position if all else fails, so we do the same. + spawn := pos.Vec3Middle() if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { - destination.AddEntity(p) - p.Teleport(netherPortal.Spawn().Vec3Middle()) - return + spawn = netherPortal.Spawn().Vec3Middle() } - // Java edition spawns the player at the translated position if all else fails, so we do the same. + // Add the entity to the destination dimension and stop the portal transfer status. destination.AddEntity(p) - p.Teleport(pos.Vec3Middle()) + p.Teleport(spawn) + + p.portalTransfer.Store(false) }() } From bed9456ba38c2c16824c52e7a92c1a4cb89c96c0 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 16:32:20 -0600 Subject: [PATCH 08/30] block/portal.go: Destroy portals when water flows into a frame. --- server/block/portal.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/block/portal.go b/server/block/portal.go index 9048d305d..60d1a1798 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -20,6 +20,11 @@ func (p Portal) EncodeBlock() (string, map[string]interface{}) { return "minecraft:portal", map[string]interface{}{"portal_axis": p.Axis.String()} } +// HasLiquidDrops ... +func (p Portal) HasLiquidDrops() bool { + return false +} + // NeighbourUpdateTick ... func (p Portal) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { if n, ok := portal.NetherPortalFromPos(w, pos); ok && (!n.Framed() || !n.Activated()) { From 55772682a70b3737d95df871f4dcb17012bfc2ce Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 17:30:54 -0600 Subject: [PATCH 09/30] portal/scan.go: Allow portals that don't specify requirements to be marked as incomplete. --- server/world/portal/scan.go | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index a248c2710..9b2bcf1bf 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -13,15 +13,15 @@ type scanIteration struct { first bool } -// multiAxisScan performs a scan first on the Z axis, and then on the X axis. +// multiAxisScan performs a scan on the Z and X axis, returning the result that had the most positions, although +// favouring the Z axis. func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { - axis := cube.Z - positions, width, height, completed := scan(axis, framePos, w, matchers) - if len(positions) == 0 { - axis = cube.X - positions, width, height, completed = scan(axis, framePos, w, matchers) + positions, width, height, completed := scan(cube.Z, framePos, w, matchers) + positionsTwo, widthTwo, heightTwo, completedTwo := scan(cube.X, framePos, w, matchers) + if len(positionsTwo) > len(positions) && !completed { + return cube.X, positionsTwo, widthTwo, heightTwo, completedTwo, len(positionsTwo) > 0 } - return axis, positions, width, height, completed, len(positions) > 0 + return cube.Z, positions, width, height, completed, len(positions) > 0 } // scan performs a scan on the given axis for any of the provided matchers using a position and a world. @@ -38,10 +38,9 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl // Parse the latest iteration. iteration := e.Value.(scanIteration) - posFace := iteration.face pos := iteration.lastPos if !iteration.first { - pos = pos.Side(posFace) + pos = pos.Side(iteration.face) } b := w.Block(pos) @@ -50,7 +49,7 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl positionsMap[pos] = true // If we are on the same X or Z axis as the portal, we can assume that our height is being changed. - if pos.X() == framePos.X() && pos.Z() == framePos.Z() && posFace < cube.FaceNorth { + if pos.X() == framePos.X() && pos.Z() == framePos.Z() && iteration.face < cube.FaceNorth { height++ } @@ -81,9 +80,7 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl // Make sure we at least reach the minimum portal width and height. area, expectedArea := len(positionsMap), width*height - if width < minimumNetherPortalWidth || height < minimumNetherPortalHeight || area != expectedArea { - return []cube.Pos{}, 0, 0, false - } + completed = width >= minimumNetherPortalWidth && height >= minimumNetherPortalHeight && area == expectedArea // Get the actual positions from the map. positions := make([]cube.Pos, 0, expectedArea) From 656a992f272c5c38db9509af4539ae15ec670058 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 17:40:08 -0600 Subject: [PATCH 10/30] portal/scan.go: Fix completion check. --- server/world/portal/scan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index 9b2bcf1bf..7819e9d59 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -80,7 +80,7 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl // Make sure we at least reach the minimum portal width and height. area, expectedArea := len(positionsMap), width*height - completed = width >= minimumNetherPortalWidth && height >= minimumNetherPortalHeight && area == expectedArea + completed = completed && width >= minimumNetherPortalWidth && height >= minimumNetherPortalHeight && area == expectedArea // Get the actual positions from the map. positions := make([]cube.Pos, 0, expectedArea) From 69c1c07af41cfe1db10546fdb86b949634f34a88 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 17:50:10 -0600 Subject: [PATCH 11/30] player/player.go: Ensure the position is only translated if the source world is the overworld or nether. --- server/player/player.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index e0247bbcc..229e4610a 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2071,14 +2071,14 @@ func (p *Player) Tick(w *world.World, current int64) { } } -// Travel moves the player to the given Nether or Overworld world, and translates the player's current position -// based on the source world. +// Travel moves the player to the given Nether or Overworld world, and translates the player's current position based +// on the source world. func (p *Player) Travel(source, destination *world.World) { sourceDimension, targetDimension := source.Dimension(), destination.Dimension() pos := cube.PosFromVec3(p.Position()) if sourceDimension == world.Overworld { pos = cube.Pos{pos.X() / 8, pos.Y() + sourceDimension.Range().Min(), pos.Z() / 8} - } else { + } else if sourceDimension == world.Nether { pos = cube.Pos{pos.X() * 8, pos.Y() - targetDimension.Range().Min(), pos.Z() * 8} } From 8711c6742a616d4430ee734d63cb2c74acff00eb Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 18:02:02 -0600 Subject: [PATCH 12/30] portal/scan.go: Simplify logic. --- server/world/portal/scan.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index 7819e9d59..edbd012b6 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -13,12 +13,12 @@ type scanIteration struct { first bool } -// multiAxisScan performs a scan on the Z and X axis, returning the result that had the most positions, although +// multiAxisScan performs a scan on the Z and X axis, returning the result that was the most successful, although // favouring the Z axis. func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { positions, width, height, completed := scan(cube.Z, framePos, w, matchers) positionsTwo, widthTwo, heightTwo, completedTwo := scan(cube.X, framePos, w, matchers) - if len(positionsTwo) > len(positions) && !completed { + if !completed { return cube.X, positionsTwo, widthTwo, heightTwo, completedTwo, len(positionsTwo) > 0 } return cube.Z, positions, width, height, completed, len(positions) > 0 From 38c392899d8e2c8fad9a13fc68596df7f2f5d7e9 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 19 Feb 2022 19:45:45 -0600 Subject: [PATCH 13/30] Revert "portal/scan.go: Simplify logic." --- server/world/portal/scan.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index edbd012b6..7819e9d59 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -13,12 +13,12 @@ type scanIteration struct { first bool } -// multiAxisScan performs a scan on the Z and X axis, returning the result that was the most successful, although +// multiAxisScan performs a scan on the Z and X axis, returning the result that had the most positions, although // favouring the Z axis. func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { positions, width, height, completed := scan(cube.Z, framePos, w, matchers) positionsTwo, widthTwo, heightTwo, completedTwo := scan(cube.X, framePos, w, matchers) - if !completed { + if len(positionsTwo) > len(positions) && !completed { return cube.X, positionsTwo, widthTwo, heightTwo, completedTwo, len(positionsTwo) > 0 } return cube.Z, positions, width, height, completed, len(positions) > 0 From fc2cf593af5d2f0d7ca35e248b948938700a4277 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sun, 20 Feb 2022 12:39:11 -0600 Subject: [PATCH 14/30] Fix multi-axis scan logic --- server/world/portal/nether.go | 2 ++ server/world/portal/scan.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index 48acf8e5f..12de596a5 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -23,6 +23,8 @@ const ( minimumNetherPortalWidth, maximumNetherPortalWidth = 2, 21 // minimumNetherPortalHeight, maximumNetherPortalHeight controls the minimum and maximum height of a portal. minimumNetherPortalHeight, maximumNetherPortalHeight = 3, 21 + // minimumArea is the minimum area of a portal. + minimumArea = minimumNetherPortalWidth * minimumNetherPortalHeight ) // NetherPortalFromPos returns Nether portal information from a given position in the frame. diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index 7819e9d59..98404509f 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -18,7 +18,7 @@ type scanIteration struct { func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { positions, width, height, completed := scan(cube.Z, framePos, w, matchers) positionsTwo, widthTwo, heightTwo, completedTwo := scan(cube.X, framePos, w, matchers) - if len(positionsTwo) > len(positions) && !completed { + if len(positions) < minimumArea && len(positionsTwo) >= minimumArea { return cube.X, positionsTwo, widthTwo, heightTwo, completedTwo, len(positionsTwo) > 0 } return cube.Z, positions, width, height, completed, len(positions) > 0 From ade7b4da32d7964273a9b20beb985d41b0cec326 Mon Sep 17 00:00:00 2001 From: JustTal Date: Mon, 21 Feb 2022 20:44:39 -0600 Subject: [PATCH 15/30] Detect portal collision using their block models --- server/block/model/portal.go | 28 ++++++++++++++++++++ server/block/portal.go | 7 ++++- server/entity/movement.go | 37 +++++++------------------- server/player/player.go | 51 ++++++++++++++++++++++++------------ server/world/world.go | 19 ++++++++++++++ 5 files changed, 97 insertions(+), 45 deletions(-) create mode 100644 server/block/model/portal.go diff --git a/server/block/model/portal.go b/server/block/model/portal.go new file mode 100644 index 000000000..9c35218d5 --- /dev/null +++ b/server/block/model/portal.go @@ -0,0 +1,28 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/entity/physics" + "github.com/df-mc/dragonfly/server/world" + "github.com/go-gl/mathgl/mgl64" +) + +// Portal is a model used by portal blocks. +type Portal struct { + // Axis is the axis which the portal faces. + Axis cube.Axis +} + +// AABB ... +func (p Portal) AABB(cube.Pos, *world.World) []physics.AABB { + min, max := mgl64.Vec3{0, 0, .375}, mgl64.Vec3{1, 1, 0.25} + if p.Axis == cube.Z { + min[0], min[2], max[0], max[2] = 0.375, 0, 0.25, 1 + } + return []physics.AABB{physics.NewAABB(min, max)} +} + +// FaceSolid ... +func (Portal) FaceSolid(cube.Pos, cube.Face, *world.World) bool { + return false +} diff --git a/server/block/portal.go b/server/block/portal.go index 60d1a1798..54ecacdaf 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -2,19 +2,24 @@ package block import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/block/model" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/portal" ) // Portal is the translucent part of the nether portal that teleports the player to and from the Nether. type Portal struct { - empty transparent // Axis is the axis which the chain faces. Axis cube.Axis } +// Model ... +func (p Portal) Model() world.BlockModel { + return model.Portal{Axis: p.Axis} +} + // EncodeBlock ... func (p Portal) EncodeBlock() (string, map[string]interface{}) { return "minecraft:portal", map[string]interface{}{"portal_axis": p.Axis.String()} diff --git a/server/entity/movement.go b/server/entity/movement.go index 4d81e8c6a..58df348a2 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -5,7 +5,6 @@ import ( "github.com/df-mc/dragonfly/server/entity/physics" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" - "math" ) // MovementComputer is used to compute movement of an entity. When constructed, the Gravity of the entity @@ -125,8 +124,17 @@ func (c *MovementComputer) checkCollision(e world.Entity, pos, vel mgl64.Vec3) ( deltaX, deltaY, deltaZ := vel[0], vel[1], vel[2] // Entities only ever have a single bounding box. + w := e.World() entityAABB := e.AABB().Translate(pos) - blocks := blockAABBsAround(e, entityAABB.Extend(vel)) + positions := w.BlocksAround(entityAABB.Extend(vel)) + + var blocks []physics.AABB + for _, blockPos := range positions { + boxes := w.Block(blockPos).Model().AABB(blockPos, w) + for _, box := range boxes { + blocks = append(blocks, box.Translate(blockPos.Vec3())) + } + } if !mgl64.FloatEqualThreshold(deltaY, 0, epsilon) { // First we move the entity AABB on the Y axis. @@ -169,28 +177,3 @@ func (c *MovementComputer) checkCollision(e world.Entity, pos, vel mgl64.Vec3) ( } return mgl64.Vec3{deltaX, deltaY, deltaZ}, vel } - -// blockAABBsAround returns all blocks around the entity passed, using the AABB passed to make a prediction of -// what blocks need to have their AABB returned. -func blockAABBsAround(e world.Entity, aabb physics.AABB) []physics.AABB { - w := e.World() - grown := aabb.Grow(0.25) - min, max := grown.Min(), grown.Max() - minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) - maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) - - // A prediction of one AABB per block, plus an additional 2, in case - blockAABBs := make([]physics.AABB, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)+2) - for y := minY; y <= maxY; y++ { - for x := minX; x <= maxX; x++ { - for z := minZ; z <= maxZ; z++ { - pos := cube.Pos{x, y, z} - boxes := w.Block(pos).Model().AABB(pos, w) - for _, box := range boxes { - blockAABBs = append(blockAABBs, box.Translate(mgl64.Vec3{float64(x), float64(y), float64(z)})) - } - } - } - } - return blockAABBs -} diff --git a/server/player/player.go b/server/player/player.go index 229e4610a..2a5e11c99 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2007,6 +2007,7 @@ func (p *Player) Tick(w *world.World, current int64) { } p.checkBlockCollisions(w) + p.checkPortalCollisions(w) p.onGround.Store(p.checkOnGround(w)) p.tickFood(w) @@ -2043,23 +2044,6 @@ func (p *Player) Tick(w *world.World, current int64) { } p.cooldownMu.Unlock() - if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { - if _, ok := w.Block(cube.PosFromVec3(p.Position())).(block.Portal); ok { - if !p.portalTimeout.Load() { - if p.GameMode().CreativeInventory() || (p.awaitingPortalTransfer.Load() && time.Since(p.portalTime.Load().(time.Time)) >= time.Second*4) { - d, _ := w.PortalDestinations() - p.Travel(w, d) - } else if !p.awaitingPortalTransfer.Load() { - p.portalTime.Store(time.Now()) - p.awaitingPortalTransfer.Store(true) - } - } - } else if !p.portalTransfer.Load() { - p.portalTimeout.Store(false) - p.awaitingPortalTransfer.Store(false) - } - } - if p.session() == session.Nop && !p.Immobile() { m := p.mc.TickMovement(p, p.Position(), p.Velocity(), p.yaw.Load(), p.pitch.Load()) m.Send() @@ -2071,6 +2055,39 @@ func (p *Player) Tick(w *world.World, current int64) { } } +// checkPortalCollisions checks if the player is colliding with a nether portal block. If so, it teleports the player +// to the other dimension. +func (p *Player) checkPortalCollisions(w *world.World) { + if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { + // Get all blocks that could touch the player and check if any of them intersect with a portal block. + for _, pos := range w.BlocksAround(p.AABB().Translate(p.Position())) { + b := w.Block(pos) + if _, ok := b.(block.Portal); ok { + for _, aabb := range b.Model().AABB(pos, w) { + if aabb.Translate(pos.Vec3()).IntersectsWith(p.AABB().Translate(p.Position())) { + if !p.portalTimeout.Load() { + if p.GameMode().CreativeInventory() || (p.awaitingPortalTransfer.Load() && time.Since(p.portalTime.Load().(time.Time)) >= time.Second*4) { + d, _ := w.PortalDestinations() + p.Travel(w, d) + } else if !p.awaitingPortalTransfer.Load() { + p.portalTime.Store(time.Now()) + p.awaitingPortalTransfer.Store(true) + } + } + return + } + } + } + } + + // No portals found, check if we aren't transferring and if so, reset. + if !p.portalTransfer.Load() { + p.portalTimeout.Store(false) + p.awaitingPortalTransfer.Store(false) + } + } +} + // Travel moves the player to the given Nether or Overworld world, and translates the player's current position based // on the source world. func (p *Player) Travel(source, destination *world.World) { diff --git a/server/world/world.go b/server/world/world.go index 03c25dc11..5288e3cd0 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -9,6 +9,7 @@ import ( "github.com/df-mc/dragonfly/server/world/chunk" "github.com/go-gl/mathgl/mgl64" "go.uber.org/atomic" + "math" "math/rand" "sync" "time" @@ -186,6 +187,24 @@ func (w *World) Biome(pos cube.Pos) Biome { return b } +// BlocksAround returns all block positions around the AABB passed. +func (w *World) BlocksAround(aabb physics.AABB) []cube.Pos { + grown := aabb.Grow(0.25) + min, max := grown.Min(), grown.Max() + minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) + maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) + + blockAABBs := make([]cube.Pos, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)) + for y := minY; y <= maxY; y++ { + for x := minX; x <= maxX; x++ { + for z := minZ; z <= maxZ; z++ { + blockAABBs = append(blockAABBs, cube.Pos{x, y, z}) + } + } + } + return blockAABBs +} + // blockInChunk reads a block from the world at the position passed. The block is assumed to be in the chunk // passed, which is also assumed to be locked already or otherwise not yet accessible. func (w *World) blockInChunk(c *chunkData, pos cube.Pos) (Block, error) { From cdc901c8548b904649fe03784b1e11c47e00e5e2 Mon Sep 17 00:00:00 2001 From: JustTal Date: Mon, 21 Feb 2022 20:45:31 -0600 Subject: [PATCH 16/30] player/player.go: Fix typo. --- server/player/player.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/player/player.go b/server/player/player.go index 2a5e11c99..5251025aa 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2080,7 +2080,7 @@ func (p *Player) checkPortalCollisions(w *world.World) { } } - // No portals found, check if we aren't transferring and if so, reset. + // No portals found. Check if we aren't transferring and if so, reset. if !p.portalTransfer.Load() { p.portalTimeout.Store(false) p.awaitingPortalTransfer.Store(false) From 95bc01e3d24ef8d251e028fdfc9a2f094122fa5f Mon Sep 17 00:00:00 2001 From: JustTal Date: Mon, 21 Feb 2022 20:52:01 -0600 Subject: [PATCH 17/30] portal/nether.go: Fixed intersecting portals. --- server/world/portal/nether.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index 12de596a5..25b276fd7 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -30,15 +30,11 @@ const ( // NetherPortalFromPos returns Nether portal information from a given position in the frame. func NetherPortalFromPos(w *world.World, pos cube.Pos) (Nether, bool) { if w.Dimension() == world.End { - // Don't waste our time - we can't make a portal in the end. + // Don't waste our time; we can't make a portal in the end. return Nether{}, false } - axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{ - portal(cube.X), - portal(cube.Z), - air(), - }) + axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{air()}) if !ok { axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []world.Block{ portal(cube.X), From 7a76d607512ab3089aaaf85feb89dca935b89625 Mon Sep 17 00:00:00 2001 From: JustTal Date: Tue, 22 Feb 2022 09:48:46 -0600 Subject: [PATCH 18/30] model/portal.go: Fix missing zero before decimal point. --- server/block/model/portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/block/model/portal.go b/server/block/model/portal.go index 9c35218d5..9e4faa370 100644 --- a/server/block/model/portal.go +++ b/server/block/model/portal.go @@ -15,7 +15,7 @@ type Portal struct { // AABB ... func (p Portal) AABB(cube.Pos, *world.World) []physics.AABB { - min, max := mgl64.Vec3{0, 0, .375}, mgl64.Vec3{1, 1, 0.25} + min, max := mgl64.Vec3{0, 0, 0.375}, mgl64.Vec3{1, 1, 0.25} if p.Axis == cube.Z { min[0], min[2], max[0], max[2] = 0.375, 0, 0.25, 1 } From eedec39528caa0575bd9ac2f54668771615bb60e Mon Sep 17 00:00:00 2001 From: JustTal Date: Wed, 23 Feb 2022 12:05:40 -0600 Subject: [PATCH 19/30] Allow fire blocks to be overridden by portal ignition --- server/world/portal/nether.go | 9 +++------ server/world/portal/scan.go | 12 +++++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index 25b276fd7..d53569502 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -34,12 +34,9 @@ func NetherPortalFromPos(w *world.World, pos cube.Pos) (Nether, bool) { return Nether{}, false } - axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{air()}) + axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{air(), fire()}) if !ok { - axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []world.Block{ - portal(cube.X), - portal(cube.Z), - }) + axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []world.Block{portal(cube.X)}) } return Nether{ w: width, h: height, @@ -68,7 +65,7 @@ func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { } closestPos, closestDist, ok := cube.Pos{}, math.MaxFloat64, false - topMatchers := []world.Block{portal(cube.X), portal(cube.Z)} + topMatchers := []world.Block{portal(cube.X)} bottomMatcher := []world.Block{obsidian()} for x := pos.X() - radius/2; x < (pos.X() + radius/2); x++ { diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index 98404509f..c4ca8794f 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -92,8 +92,9 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl // satisfiesMatchers checks if the given block satisfies all matchers. func satisfiesMatchers(b world.Block, matchers []world.Block) bool { + name, _ := b.EncodeBlock() for _, matcher := range matchers { - if b == matcher { + if otherName, _ := matcher.EncodeBlock(); name == otherName { return true } } @@ -109,6 +110,15 @@ func air() world.Block { return a } +// fire returns a fire block. +func fire() world.Block { + f, ok := world.BlockByName("minecraft:fire", map[string]interface{}{"age": int32(0)}) + if !ok { + panic("could not find fire block") + } + return f +} + // portal returns a portal block. func portal(axis cube.Axis) world.Block { p, ok := world.BlockByName("minecraft:portal", map[string]interface{}{"portal_axis": axis.String()}) From fca683d578d94805d799f8945971498507be3100 Mon Sep 17 00:00:00 2001 From: JustTal Date: Wed, 23 Feb 2022 13:01:52 -0600 Subject: [PATCH 20/30] Implement TravelComputers for other entities that can travel through portals --- server/entity/item.go | 80 ++++++++++++++--------- server/entity/travel.go | 103 ++++++++++++++++++++++++++++++ server/player/player.go | 115 +++++++++------------------------- server/world/entity.go | 2 + server/world/portal/nether.go | 16 ++--- server/world/portal/scan.go | 17 ++--- 6 files changed, 196 insertions(+), 137 deletions(-) create mode 100644 server/entity/travel.go diff --git a/server/entity/item.go b/server/entity/item.go index cd6ca1721..1a4f95f1a 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -18,6 +18,7 @@ type Item struct { age, pickupDelay int i item.Stack + t *TravelComputer c *MovementComputer } @@ -30,11 +31,17 @@ func NewItem(i item.Stack, pos mgl64.Vec3) *Item { } i = nbtconv.ReadItem(nbtconv.WriteItem(i, true), nil) - it := &Item{i: i, pickupDelay: 10, c: &MovementComputer{ - Gravity: 0.04, - DragBeforeGravity: true, - Drag: 0.02, - }} + it := &Item{ + i: i, + pickupDelay: 10, + + t: &TravelComputer{Instantaneous: func(entity world.Entity) bool { return true }}, + c: &MovementComputer{ + Gravity: 0.04, + DragBeforeGravity: true, + Drag: 0.02, + }, + } it.transform = newTransform(it, pos) return it } @@ -76,6 +83,8 @@ func (it *Item) Tick(w *world.World, current int64) { it.pos, it.vel = m.pos, m.vel it.mu.Unlock() + it.t.TickTravelling(it) + m.Send() if m.pos[1] < float64(w.Range()[0]) && current%10 == 0 { @@ -94,6 +103,42 @@ func (it *Item) Tick(w *world.World, current int64) { } } +// Teleport teleports the item to the given position. +func (it *Item) Teleport(pos mgl64.Vec3) { + it.mu.Lock() + defer it.mu.Unlock() + + for _, v := range it.World().Viewers(pos) { + v.ViewEntityTeleport(it, pos) + } + it.pos = pos +} + +// DecodeNBT decodes the properties in a map to an Item and returns a new Item entity. +func (it *Item) DecodeNBT(data map[string]interface{}) interface{} { + i := nbtconv.MapItem(data, "Item") + if i.Empty() { + return nil + } + n := NewItem(i, nbtconv.MapVec3(data, "Pos")) + n.SetVelocity(nbtconv.MapVec3(data, "Motion")) + n.age = int(nbtconv.MapInt16(data, "Age")) + n.pickupDelay = int(nbtconv.MapInt64(data, "PickupDelay")) + return n +} + +// EncodeNBT encodes the Item entity's properties as a map and returns it. +func (it *Item) EncodeNBT() map[string]interface{} { + return map[string]interface{}{ + "Age": int16(it.age), + "PickupDelay": int64(it.pickupDelay), + "Pos": nbtconv.Vec3ToFloat32Slice(it.Position()), + "Motion": nbtconv.Vec3ToFloat32Slice(it.Velocity()), + "Health": int16(5), + "Item": nbtconv.WriteItem(it.Item(), true), + } +} + // checkNearby checks the entities of the chunks around for item collectors and other item stacks. If a // collector is found in range, the item will be picked up. If another item stack with the same item type is // found in range, the item stacks will merge. @@ -166,31 +211,6 @@ func (it *Item) collect(w *world.World, collector Collector, pos mgl64.Vec3) { _ = it.Close() } -// DecodeNBT decodes the properties in a map to an Item and returns a new Item entity. -func (it *Item) DecodeNBT(data map[string]interface{}) interface{} { - i := nbtconv.MapItem(data, "Item") - if i.Empty() { - return nil - } - n := NewItem(i, nbtconv.MapVec3(data, "Pos")) - n.SetVelocity(nbtconv.MapVec3(data, "Motion")) - n.age = int(nbtconv.MapInt16(data, "Age")) - n.pickupDelay = int(nbtconv.MapInt64(data, "PickupDelay")) - return n -} - -// EncodeNBT encodes the Item entity's properties as a map and returns it. -func (it *Item) EncodeNBT() map[string]interface{} { - return map[string]interface{}{ - "Age": int16(it.age), - "PickupDelay": int64(it.pickupDelay), - "Pos": nbtconv.Vec3ToFloat32Slice(it.Position()), - "Motion": nbtconv.Vec3ToFloat32Slice(it.Velocity()), - "Health": int16(5), - "Item": nbtconv.WriteItem(it.Item(), true), - } -} - // Collector represents an entity in the world that is able to collect an item, typically an entity such as // a player or a zombie. type Collector interface { diff --git a/server/entity/travel.go b/server/entity/travel.go new file mode 100644 index 000000000..8021a7271 --- /dev/null +++ b/server/entity/travel.go @@ -0,0 +1,103 @@ +package entity + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" + "github.com/go-gl/mathgl/mgl64" + "sync" + "time" +) + +// TravelComputer handles the interdimensional travelling of an entity. +type TravelComputer struct { + // Instantaneous is a function that returns true if the entity given can travel instantly. + Instantaneous func(world.Entity) bool + + mu sync.RWMutex + start time.Time + awaitingTravel bool + travelling bool + timedOut bool +} + +// Traveller represents a world.Entity that can travel between worlds. +type Traveller interface { + // Teleport teleports the entity to the position given. + Teleport(pos mgl64.Vec3) + + world.Entity +} + +// TickTravelling checks if the player is colliding with a nether portal block. If so, it teleports the player +// to the other dimension after four seconds or instantly if instantaneous is true. +func (t *TravelComputer) TickTravelling(e Traveller) { + w := e.World() + aabb := e.AABB().Translate(e.Position()).Grow(0.1) + if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { + // Get all blocks that could touch the player and check if any of them intersect with a portal block. + for _, pos := range w.BlocksAround(aabb) { + b := w.Block(pos) + if name, _ := w.Block(pos).EncodeBlock(); name == "minecraft:portal" { + for _, a := range b.Model().AABB(pos, w) { + if a.Translate(pos.Vec3()).IntersectsWith(aabb) { + t.mu.Lock() + timeOut, awaitingTravel, start := t.timedOut, t.awaitingTravel, t.start + t.mu.Unlock() + + if !timeOut { + if t.Instantaneous(e) || (awaitingTravel && time.Since(start) >= time.Second*4) { + d, _ := w.PortalDestinations() + t.Travel(e, w, d) + } else if !awaitingTravel { + t.mu.Lock() + t.start, t.awaitingTravel = time.Now(), true + t.mu.Unlock() + } + } + return + } + } + } + } + + // No portals found. Check if we aren't transferring and if so, reset. + t.mu.Lock() + defer t.mu.Unlock() + if !t.travelling { + t.timedOut, t.awaitingTravel = false, false + } + } +} + +// Travel moves the player to the given Nether or Overworld world, and translates the player's current position based +// on the source world. +func (t *TravelComputer) Travel(e Traveller, source *world.World, destination *world.World) { + sourceDimension, targetDimension := source.Dimension(), destination.Dimension() + pos := cube.PosFromVec3(e.Position()) + if sourceDimension == world.Overworld { + pos = cube.Pos{pos.X() / 8, pos.Y() + sourceDimension.Range().Min(), pos.Z() / 8} + } else if sourceDimension == world.Nether { + pos = cube.Pos{pos.X() * 8, pos.Y() - targetDimension.Range().Min(), pos.Z() * 8} + } + + t.mu.Lock() + t.travelling, t.timedOut, t.awaitingTravel = true, true, false + t.mu.Unlock() + + go func() { + // Java edition spawns the player at the translated position if all else fails, so we do the same. + spawn := pos.Vec3Middle() + if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { + spawn = netherPortal.Spawn().Vec3Middle() + } + + // Add the entity to the destination dimension and stop the portal transfer status. + destination.AddEntity(e) + e.Teleport(spawn) + + t.mu.Lock() + t.travelling = false + t.mu.Unlock() + }() +} diff --git a/server/player/player.go b/server/player/player.go index 5251025aa..0909c27ab 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -23,7 +23,6 @@ import ( "github.com/df-mc/dragonfly/server/session" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/particle" - "github.com/df-mc/dragonfly/server/world/portal" "github.com/df-mc/dragonfly/server/world/sound" "github.com/go-gl/mathgl/mgl64" "github.com/google/uuid" @@ -85,17 +84,13 @@ type Player struct { effects *entity.EffectManager immunity atomic.Value + tc *entity.TravelComputer mc *entity.MovementComputer breaking atomic.Bool breakingPos atomic.Value lastBreakDuration time.Duration - portalTime atomic.Value - portalTimeout atomic.Bool - portalTransfer atomic.Bool - awaitingPortalTransfer atomic.Bool - breakParticleCounter atomic.Uint32 hunger *hungerManager @@ -107,30 +102,37 @@ type Player struct { func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { p := &Player{} *p = Player{ + name: name, + skin: skin, + uuid: uuid.New(), + gameMode: world.GameModeSurvival, + inv: inventory.New(36, func(slot int, item item.Stack) { if slot == int(p.heldSlot.Load()) { p.broadcastItems(slot, item) } }), - uuid: uuid.New(), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - hunger: newHungerManager(), - health: entity.NewHealthManager(), - effects: entity.NewEffectManager(), - gameMode: world.GameModeSurvival, - h: NopHandler{}, - name: name, - skin: skin, - speed: *atomic.NewFloat64(0.1), - nameTag: *atomic.NewString(name), - heldSlot: atomic.NewUint32(0), - locale: language.BritishEnglish, - scale: *atomic.NewFloat64(1), - cooldowns: make(map[itemHash]time.Time), - portalTimeout: *atomic.NewBool(true), - } - p.mc = &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true} + offHand: inventory.New(1, p.broadcastItems), + armour: inventory.NewArmour(p.broadcastArmour), + + cooldowns: make(map[itemHash]time.Time), + heldSlot: atomic.NewUint32(0), + + hunger: newHungerManager(), + health: entity.NewHealthManager(), + effects: entity.NewEffectManager(), + + speed: *atomic.NewFloat64(0.1), + scale: *atomic.NewFloat64(1), + + nameTag: *atomic.NewString(name), + locale: language.BritishEnglish, + + tc: &entity.TravelComputer{Instantaneous: func(world.Entity) bool { return p.GameMode().CreativeInventory() }}, + mc: &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true}, + + h: NopHandler{}, + } p.pos.Store(pos) p.vel.Store(mgl64.Vec3{}) p.immunity.Store(time.Now()) @@ -2007,7 +2009,6 @@ func (p *Player) Tick(w *world.World, current int64) { } p.checkBlockCollisions(w) - p.checkPortalCollisions(w) p.onGround.Store(p.checkOnGround(w)) p.tickFood(w) @@ -2053,68 +2054,8 @@ func (p *Player) Tick(w *world.World, current int64) { } else { p.vel.Store(mgl64.Vec3{}) } -} -// checkPortalCollisions checks if the player is colliding with a nether portal block. If so, it teleports the player -// to the other dimension. -func (p *Player) checkPortalCollisions(w *world.World) { - if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { - // Get all blocks that could touch the player and check if any of them intersect with a portal block. - for _, pos := range w.BlocksAround(p.AABB().Translate(p.Position())) { - b := w.Block(pos) - if _, ok := b.(block.Portal); ok { - for _, aabb := range b.Model().AABB(pos, w) { - if aabb.Translate(pos.Vec3()).IntersectsWith(p.AABB().Translate(p.Position())) { - if !p.portalTimeout.Load() { - if p.GameMode().CreativeInventory() || (p.awaitingPortalTransfer.Load() && time.Since(p.portalTime.Load().(time.Time)) >= time.Second*4) { - d, _ := w.PortalDestinations() - p.Travel(w, d) - } else if !p.awaitingPortalTransfer.Load() { - p.portalTime.Store(time.Now()) - p.awaitingPortalTransfer.Store(true) - } - } - return - } - } - } - } - - // No portals found. Check if we aren't transferring and if so, reset. - if !p.portalTransfer.Load() { - p.portalTimeout.Store(false) - p.awaitingPortalTransfer.Store(false) - } - } -} - -// Travel moves the player to the given Nether or Overworld world, and translates the player's current position based -// on the source world. -func (p *Player) Travel(source, destination *world.World) { - sourceDimension, targetDimension := source.Dimension(), destination.Dimension() - pos := cube.PosFromVec3(p.Position()) - if sourceDimension == world.Overworld { - pos = cube.Pos{pos.X() / 8, pos.Y() + sourceDimension.Range().Min(), pos.Z() / 8} - } else if sourceDimension == world.Nether { - pos = cube.Pos{pos.X() * 8, pos.Y() - targetDimension.Range().Min(), pos.Z() * 8} - } - - p.portalTransfer.Store(true) - p.portalTimeout.Store(true) - p.awaitingPortalTransfer.Store(false) - go func() { - // Java edition spawns the player at the translated position if all else fails, so we do the same. - spawn := pos.Vec3Middle() - if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { - spawn = netherPortal.Spawn().Vec3Middle() - } - - // Add the entity to the destination dimension and stop the portal transfer status. - destination.AddEntity(p) - p.Teleport(spawn) - - p.portalTransfer.Store(false) - }() + p.tc.TickTravelling(p) } // tickFood ticks food related functionality, such as the depletion of the food bar and regeneration if it diff --git a/server/world/entity.go b/server/world/entity.go index b779bb8e1..2970a1941 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -38,6 +38,8 @@ type Entity interface { type TickerEntity interface { // Tick ticks the entity with the current World and tick passed. Tick(w *World, current int64) + + Entity } // SaveableEntity is an Entity that can be saved and loaded with the World it was added to. These entities can be diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index d53569502..ced2635cc 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -34,9 +34,12 @@ func NetherPortalFromPos(w *world.World, pos cube.Pos) (Nether, bool) { return Nether{}, false } - axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []world.Block{air(), fire()}) + axis, positions, width, height, completed, ok := multiAxisScan(pos, w, []string{ + "minecraft:air", + "minecraft:fire", + }) if !ok { - axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []world.Block{portal(cube.X)}) + axis, positions, width, height, completed, ok = multiAxisScan(pos, w, []string{"minecraft:portal"}) } return Nether{ w: width, h: height, @@ -65,16 +68,15 @@ func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { } closestPos, closestDist, ok := cube.Pos{}, math.MaxFloat64, false - topMatchers := []world.Block{portal(cube.X)} - bottomMatcher := []world.Block{obsidian()} - for x := pos.X() - radius/2; x < (pos.X() + radius/2); x++ { for z := pos.Z() - radius/2; z < (pos.Z() + radius/2); z++ { for y := w.Dimension().Range().Max(); y >= w.Dimension().Range().Min(); y-- { selectedPos := cube.Pos{x, y, z} - if satisfiesMatchers(w.Block(selectedPos), topMatchers) { + name, _ := w.Block(selectedPos).EncodeBlock() + if name == "minecraft:portal" { belowPos := selectedPos.Side(cube.FaceDown) - if satisfiesMatchers(w.Block(belowPos), bottomMatcher) { + name, _ = w.Block(belowPos).EncodeBlock() + if name == "minecraft:obsidian" { dist := world.Distance(pos.Vec3(), selectedPos.Vec3()) if dist < closestDist { closestDist, closestPos, ok = dist, selectedPos, true diff --git a/server/world/portal/scan.go b/server/world/portal/scan.go index c4ca8794f..22a6c5d68 100644 --- a/server/world/portal/scan.go +++ b/server/world/portal/scan.go @@ -15,7 +15,7 @@ type scanIteration struct { // multiAxisScan performs a scan on the Z and X axis, returning the result that had the most positions, although // favouring the Z axis. -func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (cube.Axis, []cube.Pos, int, int, bool, bool) { +func multiAxisScan(framePos cube.Pos, w *world.World, matchers []string) (cube.Axis, []cube.Pos, int, int, bool, bool) { positions, width, height, completed := scan(cube.Z, framePos, w, matchers) positionsTwo, widthTwo, heightTwo, completedTwo := scan(cube.X, framePos, w, matchers) if len(positions) < minimumArea && len(positionsTwo) >= minimumArea { @@ -25,7 +25,7 @@ func multiAxisScan(framePos cube.Pos, w *world.World, matchers []world.Block) (c } // scan performs a scan on the given axis for any of the provided matchers using a position and a world. -func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Block) ([]cube.Pos, int, int, bool) { +func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []string) ([]cube.Pos, int, int, bool) { var width, height int positionsMap := make(map[cube.Pos]bool) @@ -91,10 +91,10 @@ func scan(axis cube.Axis, framePos cube.Pos, w *world.World, matchers []world.Bl } // satisfiesMatchers checks if the given block satisfies all matchers. -func satisfiesMatchers(b world.Block, matchers []world.Block) bool { +func satisfiesMatchers(b world.Block, matchers []string) bool { name, _ := b.EncodeBlock() for _, matcher := range matchers { - if otherName, _ := matcher.EncodeBlock(); name == otherName { + if name == matcher { return true } } @@ -110,15 +110,6 @@ func air() world.Block { return a } -// fire returns a fire block. -func fire() world.Block { - f, ok := world.BlockByName("minecraft:fire", map[string]interface{}{"age": int32(0)}) - if !ok { - panic("could not find fire block") - } - return f -} - // portal returns a portal block. func portal(axis cube.Axis) world.Block { p, ok := world.BlockByName("minecraft:portal", map[string]interface{}{"portal_axis": axis.String()}) From 8538faf98c7d9cf687df35ed9333c935a387ed76 Mon Sep 17 00:00:00 2001 From: JustTal Date: Wed, 23 Feb 2022 13:12:18 -0600 Subject: [PATCH 21/30] entity/travel.go: Slightly increase AABB growth. --- server/entity/travel.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/entity/travel.go b/server/entity/travel.go index 8021a7271..fc5ed5ab0 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -33,7 +33,7 @@ type Traveller interface { // to the other dimension after four seconds or instantly if instantaneous is true. func (t *TravelComputer) TickTravelling(e Traveller) { w := e.World() - aabb := e.AABB().Translate(e.Position()).Grow(0.1) + aabb := e.AABB().Translate(e.Position()).Grow(0.15) if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { // Get all blocks that could touch the player and check if any of them intersect with a portal block. for _, pos := range w.BlocksAround(aabb) { @@ -61,7 +61,7 @@ func (t *TravelComputer) TickTravelling(e Traveller) { } } - // No portals found. Check if we aren't transferring and if so, reset. + // No portals found. Check if we aren't travelling and if so, reset. t.mu.Lock() defer t.mu.Unlock() if !t.travelling { @@ -92,7 +92,7 @@ func (t *TravelComputer) Travel(e Traveller, source *world.World, destination *w spawn = netherPortal.Spawn().Vec3Middle() } - // Add the entity to the destination dimension and stop the portal transfer status. + // Add the entity to the destination dimension and stop the portal travel status. destination.AddEntity(e) e.Teleport(spawn) From 51f7e3615f9c808e922b5648e18d3a704565ad95 Mon Sep 17 00:00:00 2001 From: JustTal Date: Wed, 23 Feb 2022 15:31:09 -0600 Subject: [PATCH 22/30] Use the *world.World provided in Tick. --- server/entity/falling_block.go | 2 +- server/entity/item.go | 4 ++-- server/entity/movement.go | 3 +-- server/entity/travel.go | 3 +-- server/player/player.go | 4 ++-- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/server/entity/falling_block.go b/server/entity/falling_block.go index cd226e2f1..4087dd75d 100644 --- a/server/entity/falling_block.go +++ b/server/entity/falling_block.go @@ -53,7 +53,7 @@ func (f *FallingBlock) Block() world.Block { // Tick ... func (f *FallingBlock) Tick(w *world.World, _ int64) { f.mu.Lock() - m := f.c.TickMovement(f, f.pos, f.vel, 0, 0) + m := f.c.TickMovement(w, f, f.pos, f.vel, 0, 0) f.pos, f.vel = m.pos, m.vel f.mu.Unlock() diff --git a/server/entity/item.go b/server/entity/item.go index 1a4f95f1a..09d469cb9 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -79,11 +79,11 @@ func (it *Item) SetPickupDelay(d time.Duration) { // Tick ticks the entity, performing movement. func (it *Item) Tick(w *world.World, current int64) { it.mu.Lock() - m := it.c.TickMovement(it, it.pos, it.vel, 0, 0) + m := it.c.TickMovement(w, it, it.pos, it.vel, 0, 0) it.pos, it.vel = m.pos, m.vel it.mu.Unlock() - it.t.TickTravelling(it) + it.t.TickTravelling(w, it) m.Send() diff --git a/server/entity/movement.go b/server/entity/movement.go index 58df348a2..8725d53c3 100644 --- a/server/entity/movement.go +++ b/server/entity/movement.go @@ -62,8 +62,7 @@ func (m *Movement) Rotation() (yaw, pitch float64) { // of its Drag and Gravity. // The new position of the entity after movement is returned. // The resulting Movement can be sent to viewers by calling Movement.Send. -func (c *MovementComputer) TickMovement(e world.Entity, pos, vel mgl64.Vec3, yaw, pitch float64) *Movement { - w := e.World() +func (c *MovementComputer) TickMovement(w *world.World, e world.Entity, pos, vel mgl64.Vec3, yaw, pitch float64) *Movement { viewers := w.Viewers(pos) velBefore := vel diff --git a/server/entity/travel.go b/server/entity/travel.go index fc5ed5ab0..2d7bdef5f 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -31,8 +31,7 @@ type Traveller interface { // TickTravelling checks if the player is colliding with a nether portal block. If so, it teleports the player // to the other dimension after four seconds or instantly if instantaneous is true. -func (t *TravelComputer) TickTravelling(e Traveller) { - w := e.World() +func (t *TravelComputer) TickTravelling(w *world.World, e Traveller) { aabb := e.AABB().Translate(e.Position()).Grow(0.15) if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { // Get all blocks that could touch the player and check if any of them intersect with a portal block. diff --git a/server/player/player.go b/server/player/player.go index 0909c27ab..88dfa1cfc 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2046,7 +2046,7 @@ func (p *Player) Tick(w *world.World, current int64) { p.cooldownMu.Unlock() if p.session() == session.Nop && !p.Immobile() { - m := p.mc.TickMovement(p, p.Position(), p.Velocity(), p.yaw.Load(), p.pitch.Load()) + m := p.mc.TickMovement(w, p, p.Position(), p.Velocity(), p.yaw.Load(), p.pitch.Load()) m.Send() p.vel.Store(m.Velocity()) @@ -2055,7 +2055,7 @@ func (p *Player) Tick(w *world.World, current int64) { p.vel.Store(mgl64.Vec3{}) } - p.tc.TickTravelling(p) + p.tc.TickTravelling(w, p) } // tickFood ticks food related functionality, such as the depletion of the food bar and regeneration if it From 4431fa370ef6ccd36efa92ff2eaa1b17e56b1a03 Mon Sep 17 00:00:00 2001 From: JustTal Date: Wed, 23 Feb 2022 15:36:02 -0600 Subject: [PATCH 23/30] entity/travel.go: Only grow AABB after the BlocksAround call --- server/entity/travel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/entity/travel.go b/server/entity/travel.go index 2d7bdef5f..3b67e64ac 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -32,14 +32,14 @@ type Traveller interface { // TickTravelling checks if the player is colliding with a nether portal block. If so, it teleports the player // to the other dimension after four seconds or instantly if instantaneous is true. func (t *TravelComputer) TickTravelling(w *world.World, e Traveller) { - aabb := e.AABB().Translate(e.Position()).Grow(0.15) + aabb := e.AABB().Translate(e.Position()) if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { // Get all blocks that could touch the player and check if any of them intersect with a portal block. for _, pos := range w.BlocksAround(aabb) { b := w.Block(pos) if name, _ := w.Block(pos).EncodeBlock(); name == "minecraft:portal" { for _, a := range b.Model().AABB(pos, w) { - if a.Translate(pos.Vec3()).IntersectsWith(aabb) { + if a.Translate(pos.Vec3()).IntersectsWith(aabb.Grow(0.25)) { t.mu.Lock() timeOut, awaitingTravel, start := t.timedOut, t.awaitingTravel, t.start t.mu.Unlock() From d8d8f3a28960a3f9ff9958489f16f86e56914780 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 26 Feb 2022 12:44:03 -0600 Subject: [PATCH 24/30] block/portal.go: Fix typo. --- server/block/portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/block/portal.go b/server/block/portal.go index 54ecacdaf..63cf2f512 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -11,7 +11,7 @@ import ( type Portal struct { transparent - // Axis is the axis which the chain faces. + // Axis is the axis which the portal faces. Axis cube.Axis } From b45deaecc364804c3a000fd272a356b64176fde0 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 26 Feb 2022 22:59:15 -0600 Subject: [PATCH 25/30] Remove world.Entity as an Instantaneous parameter --- server/entity/item.go | 2 +- server/entity/travel.go | 4 ++-- server/player/player.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/entity/item.go b/server/entity/item.go index 09d469cb9..18d317781 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -35,7 +35,7 @@ func NewItem(i item.Stack, pos mgl64.Vec3) *Item { i: i, pickupDelay: 10, - t: &TravelComputer{Instantaneous: func(entity world.Entity) bool { return true }}, + t: &TravelComputer{Instantaneous: func() bool { return true }}, c: &MovementComputer{ Gravity: 0.04, DragBeforeGravity: true, diff --git a/server/entity/travel.go b/server/entity/travel.go index 3b67e64ac..05fff164e 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -12,7 +12,7 @@ import ( // TravelComputer handles the interdimensional travelling of an entity. type TravelComputer struct { // Instantaneous is a function that returns true if the entity given can travel instantly. - Instantaneous func(world.Entity) bool + Instantaneous func() bool mu sync.RWMutex start time.Time @@ -45,7 +45,7 @@ func (t *TravelComputer) TickTravelling(w *world.World, e Traveller) { t.mu.Unlock() if !timeOut { - if t.Instantaneous(e) || (awaitingTravel && time.Since(start) >= time.Second*4) { + if t.Instantaneous() || (awaitingTravel && time.Since(start) >= time.Second*4) { d, _ := w.PortalDestinations() t.Travel(e, w, d) } else if !awaitingTravel { diff --git a/server/player/player.go b/server/player/player.go index f85a6b5c8..849cbc245 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -129,7 +129,7 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { nameTag: *atomic.NewString(name), locale: language.BritishEnglish, - tc: &entity.TravelComputer{Instantaneous: func(world.Entity) bool { return p.GameMode().CreativeInventory() }}, + tc: &entity.TravelComputer{Instantaneous: func() bool { return p.GameMode().CreativeInventory() }}, mc: &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true}, h: NopHandler{}, From b431bd75ff7a17cc7f9032bd417111a00ea4a60f Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 26 Feb 2022 23:10:18 -0600 Subject: [PATCH 26/30] Undo unnecessary ordering --- server/player/player.go | 41 +++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index 849cbc245..034efcb8f 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -103,36 +103,29 @@ type Player struct { func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { p := &Player{} *p = Player{ - name: name, - skin: skin, - uuid: uuid.New(), - gameMode: world.GameModeSurvival, - inv: inventory.New(36, func(slot int, item item.Stack) { if slot == int(p.heldSlot.Load()) { p.broadcastItems(slot, item) } }), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - - cooldowns: make(map[itemHash]time.Time), + uuid: uuid.New(), + offHand: inventory.New(1, p.broadcastItems), + armour: inventory.NewArmour(p.broadcastArmour), + hunger: newHungerManager(), + health: entity.NewHealthManager(), + effects: entity.NewEffectManager(), + gameMode: world.GameModeSurvival, + h: NopHandler{}, + name: name, + skin: skin, + speed: *atomic.NewFloat64(0.1), + nameTag: *atomic.NewString(name), heldSlot: atomic.NewUint32(0), - - hunger: newHungerManager(), - health: entity.NewHealthManager(), - effects: entity.NewEffectManager(), - - speed: *atomic.NewFloat64(0.1), - scale: *atomic.NewFloat64(1), - - nameTag: *atomic.NewString(name), - locale: language.BritishEnglish, - - tc: &entity.TravelComputer{Instantaneous: func() bool { return p.GameMode().CreativeInventory() }}, - mc: &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true}, - - h: NopHandler{}, + locale: language.BritishEnglish, + scale: *atomic.NewFloat64(1), + cooldowns: make(map[itemHash]time.Time), + mc: &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true}, + tc: &entity.TravelComputer{Instantaneous: func() bool { return p.GameMode().CreativeInventory() }}, } p.pos.Store(pos) p.vel.Store(mgl64.Vec3{}) From 3bb09b0609995762d47856b3a18b23f149e8d43a Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 26 Feb 2022 23:17:04 -0600 Subject: [PATCH 27/30] entity/travel.go: Update Traveller doc. --- server/entity/travel.go | 2 +- server/player/player.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/entity/travel.go b/server/entity/travel.go index 05fff164e..8d5bf958e 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -21,7 +21,7 @@ type TravelComputer struct { timedOut bool } -// Traveller represents a world.Entity that can travel between worlds. +// Traveller represents a world.Entity that can travel between dimensions. type Traveller interface { // Teleport teleports the entity to the position given. Teleport(pos mgl64.Vec3) diff --git a/server/player/player.go b/server/player/player.go index 034efcb8f..0b9125e3b 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -124,7 +124,7 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { locale: language.BritishEnglish, scale: *atomic.NewFloat64(1), cooldowns: make(map[itemHash]time.Time), - mc: &entity.MovementComputer{Gravity: 0.06, Drag: 0.02, DragBeforeGravity: true}, + mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, tc: &entity.TravelComputer{Instantaneous: func() bool { return p.GameMode().CreativeInventory() }}, } p.pos.Store(pos) From 611f4aa450b91760ea06e8e5234bdf1a2f2db118 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sat, 26 Feb 2022 23:23:02 -0600 Subject: [PATCH 28/30] Use an interface to detect a portal collision --- server/block/portal.go | 11 ++++++++--- server/entity/travel.go | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/server/block/portal.go b/server/block/portal.go index 63cf2f512..99bbe7257 100644 --- a/server/block/portal.go +++ b/server/block/portal.go @@ -20,9 +20,9 @@ func (p Portal) Model() world.BlockModel { return model.Portal{Axis: p.Axis} } -// EncodeBlock ... -func (p Portal) EncodeBlock() (string, map[string]interface{}) { - return "minecraft:portal", map[string]interface{}{"portal_axis": p.Axis.String()} +// Portal ... +func (Portal) Portal() world.Dimension { + return world.Nether } // HasLiquidDrops ... @@ -30,6 +30,11 @@ func (p Portal) HasLiquidDrops() bool { return false } +// EncodeBlock ... +func (p Portal) EncodeBlock() (string, map[string]interface{}) { + return "minecraft:portal", map[string]interface{}{"portal_axis": p.Axis.String()} +} + // NeighbourUpdateTick ... func (p Portal) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) { if n, ok := portal.NetherPortalFromPos(w, pos); ok && (!n.Framed() || !n.Activated()) { diff --git a/server/entity/travel.go b/server/entity/travel.go index 8d5bf958e..7472e2009 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -37,7 +37,10 @@ func (t *TravelComputer) TickTravelling(w *world.World, e Traveller) { // Get all blocks that could touch the player and check if any of them intersect with a portal block. for _, pos := range w.BlocksAround(aabb) { b := w.Block(pos) - if name, _ := w.Block(pos).EncodeBlock(); name == "minecraft:portal" { + if p, ok := b.(interface { + // Portal returns the dimension the portal block takes you to. + Portal() world.Dimension + }); ok && p.Portal() == world.Nether { for _, a := range b.Model().AABB(pos, w) { if a.Translate(pos.Vec3()).IntersectsWith(aabb.Grow(0.25)) { t.mu.Lock() From 5e3b77331444ce1d86f0bb1757a09527607fe610 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sun, 31 Jul 2022 02:37:02 -0500 Subject: [PATCH 29/30] portal/nether.go: Various fixes. --- server/block/fire.go | 15 +++++++ server/block/model/portal.go | 7 ++-- server/block/obsidian.go | 6 +++ server/entity/falling_block.go | 2 +- server/entity/item.go | 2 +- server/entity/travel.go | 72 ++++++++++++++++++++-------------- server/player/player.go | 2 +- server/world/portal/nether.go | 42 ++++++++++++-------- server/world/world.go | 25 +++--------- 9 files changed, 102 insertions(+), 71 deletions(-) diff --git a/server/block/fire.go b/server/block/fire.go index acbbc9987..e160ae748 100644 --- a/server/block/fire.go +++ b/server/block/fire.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/entity/damage" "github.com/df-mc/dragonfly/server/event" "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/portal" "math/rand" "time" ) @@ -204,6 +205,20 @@ func (f Fire) EntityInside(_ cube.Pos, _ *world.World, e world.Entity) { } } +// Place ... +func (Fire) Place(pos cube.Pos, w *world.World) bool { + for _, f := range cube.Faces() { + if o, ok := w.Block(pos.Side(f)).(Obsidian); ok && !o.Crying { + if p, ok := portal.NetherPortalFromPos(w, pos); ok && p.Framed() && !p.Activated() { + p.Activate() + return false + } + return true + } + } + return true +} + // ScheduledTick ... func (f Fire) ScheduledTick(pos cube.Pos, w *world.World, r *rand.Rand) { f.tick(pos, w, r) diff --git a/server/block/model/portal.go b/server/block/model/portal.go index 9e4faa370..81fa34ed3 100644 --- a/server/block/model/portal.go +++ b/server/block/model/portal.go @@ -2,7 +2,6 @@ package model import ( "github.com/df-mc/dragonfly/server/block/cube" - "github.com/df-mc/dragonfly/server/entity/physics" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" ) @@ -13,13 +12,13 @@ type Portal struct { Axis cube.Axis } -// AABB ... -func (p Portal) AABB(cube.Pos, *world.World) []physics.AABB { +// BBox ... +func (p Portal) BBox(cube.Pos, *world.World) []cube.BBox { min, max := mgl64.Vec3{0, 0, 0.375}, mgl64.Vec3{1, 1, 0.25} if p.Axis == cube.Z { min[0], min[2], max[0], max[2] = 0.375, 0, 0.25, 1 } - return []physics.AABB{physics.NewAABB(min, max)} + return []cube.BBox{cube.Box(min[0], min[1], min[2], max[0], max[1], max[2])} } // FaceSolid ... diff --git a/server/block/obsidian.go b/server/block/obsidian.go index 5d99bd4a0..1e0393b9d 100644 --- a/server/block/obsidian.go +++ b/server/block/obsidian.go @@ -2,6 +2,7 @@ package block import ( "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" ) // Obsidian is a dark purple block known for its high blast resistance and strength, most commonly found when @@ -37,6 +38,11 @@ func (o Obsidian) EncodeBlock() (string, map[string]any) { return "minecraft:obsidian", nil } +// Frame ... +func (o Obsidian) Frame(dimension world.Dimension) bool { + return dimension == world.Nether +} + // BreakInfo ... func (o Obsidian) BreakInfo() BreakInfo { return newBreakInfo(35, func(t item.Tool) bool { diff --git a/server/entity/falling_block.go b/server/entity/falling_block.go index 84a22ff66..f19e2ec1f 100644 --- a/server/entity/falling_block.go +++ b/server/entity/falling_block.go @@ -81,7 +81,7 @@ type landable interface { // Tick ... func (f *FallingBlock) Tick(w *world.World, _ int64) { f.mu.Lock() - m := f.c.TickMovement(w, f, f.pos, f.vel, 0, 0) + m := f.c.TickMovement(f, f.pos, f.vel, 0, 0) f.pos, f.vel = m.pos, m.vel f.mu.Unlock() diff --git a/server/entity/item.go b/server/entity/item.go index f4fa99075..409747e46 100644 --- a/server/entity/item.go +++ b/server/entity/item.go @@ -84,7 +84,7 @@ func (it *Item) Tick(w *world.World, current int64) { it.pos, it.vel = m.pos, m.vel it.mu.Unlock() - it.t.TickTravelling(w, it) + it.t.TickTravelling(it) m.Send() diff --git a/server/entity/travel.go b/server/entity/travel.go index 7472e2009..04f8d94b6 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -5,6 +5,7 @@ import ( "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/portal" "github.com/go-gl/mathgl/mgl64" + "math" "sync" "time" ) @@ -29,46 +30,61 @@ type Traveller interface { world.Entity } +// portalBlock represents a block that can be used as a portal to travel between dimensions. +type portalBlock interface { + // Portal returns the dimension that the portal leads to. + Portal() world.Dimension +} + // TickTravelling checks if the player is colliding with a nether portal block. If so, it teleports the player // to the other dimension after four seconds or instantly if instantaneous is true. -func (t *TravelComputer) TickTravelling(w *world.World, e Traveller) { - aabb := e.AABB().Translate(e.Position()) - if w.Dimension() == world.Overworld || w.Dimension() == world.Nether { - // Get all blocks that could touch the player and check if any of them intersect with a portal block. - for _, pos := range w.BlocksAround(aabb) { - b := w.Block(pos) - if p, ok := b.(interface { - // Portal returns the dimension the portal block takes you to. - Portal() world.Dimension - }); ok && p.Portal() == world.Nether { - for _, a := range b.Model().AABB(pos, w) { - if a.Translate(pos.Vec3()).IntersectsWith(aabb.Grow(0.25)) { - t.mu.Lock() - timeOut, awaitingTravel, start := t.timedOut, t.awaitingTravel, t.start - t.mu.Unlock() +func (t *TravelComputer) TickTravelling(e Traveller) { + w := e.World() + box := e.BBox().Translate(e.Position()).Grow(0.25) - if !timeOut { - if t.Instantaneous() || (awaitingTravel && time.Since(start) >= time.Second*4) { - d, _ := w.PortalDestinations() - t.Travel(e, w, d) - } else if !awaitingTravel { - t.mu.Lock() - t.start, t.awaitingTravel = time.Now(), true - t.mu.Unlock() - } + min, max := box.Min(), box.Max() + minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) + maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) + travelling, target := false, world.Dimension(nil) + for y := minY; y <= maxY; y++ { + for x := minX; x <= maxX; x++ { + for z := minZ; z <= maxZ; z++ { + pos := cube.Pos{x, y, z} + b := w.Block(pos) + if p, ok := b.(portalBlock); ok { + for _, blockBox := range b.Model().BBox(pos, w) { + if blockBox.Translate(pos.Vec3()).IntersectsWith(box) { + travelling, target = true, p.Portal() + break } - return } } } } + } + t.mu.Lock() + defer t.mu.Unlock() + if !travelling { // No portals found. Check if we aren't travelling and if so, reset. - t.mu.Lock() - defer t.mu.Unlock() if !t.travelling { t.timedOut, t.awaitingTravel = false, false } + return + } + + switch target { + case world.Nether: + timeOut, awaitingTravel, start := t.timedOut, t.awaitingTravel, t.start + if !timeOut { + if t.Instantaneous() || (awaitingTravel && time.Since(start) >= time.Second*4) { + t.mu.Unlock() + t.Travel(e, w, w.PortalDestination(world.Nether)) + t.mu.Lock() + } else if !awaitingTravel { + t.start, t.awaitingTravel = time.Now(), true + } + } } } @@ -88,13 +104,11 @@ func (t *TravelComputer) Travel(e Traveller, source *world.World, destination *w t.mu.Unlock() go func() { - // Java edition spawns the player at the translated position if all else fails, so we do the same. spawn := pos.Vec3Middle() if netherPortal, ok := portal.FindOrCreateNetherPortal(destination, pos, 128); ok { spawn = netherPortal.Spawn().Vec3Middle() } - // Add the entity to the destination dimension and stop the portal travel status. destination.AddEntity(e) e.Teleport(spawn) diff --git a/server/player/player.go b/server/player/player.go index 7803b863f..31f1ec6f6 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2262,7 +2262,7 @@ func (p *Player) Tick(w *world.World, current int64) { p.vel.Store(mgl64.Vec3{}) } - p.tc.TickTravelling(w, p) + p.tc.TickTravelling(p) } // tickAirSupply tick's the player's air supply, consuming it when underwater, and replenishing it when out of water. diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index ced2635cc..156fec417 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -60,6 +60,18 @@ func FindOrCreateNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, return CreateNetherPortal(w, pos) } +// portalBlock represents a block that can be used as a portal to travel between dimensions. +type portalBlock interface { + // Portal returns the dimension that the portal leads to. + Portal() world.Dimension +} + +// frameBlock represents a block that can be used as a frame for a Nether portal. +type frameBlock interface { + // Frame returns true if the block is used as a frame for the given dimension. + Frame(dimension world.Dimension) bool +} + // FindNetherPortal searches a provided radius for a Nether portal. func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { if w.Dimension() == world.End { @@ -67,26 +79,24 @@ func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { return Nether{}, false } - closestPos, closestDist, ok := cube.Pos{}, math.MaxFloat64, false - for x := pos.X() - radius/2; x < (pos.X() + radius/2); x++ { - for z := pos.Z() - radius/2; z < (pos.Z() + radius/2); z++ { + closestPos, closestDist, found := cube.Pos{}, math.MaxFloat64, false + for x := pos.X() - radius; x < pos.X()+radius; x++ { + for z := pos.Z() - radius; z < pos.Z()+radius; z++ { for y := w.Dimension().Range().Max(); y >= w.Dimension().Range().Min(); y-- { selectedPos := cube.Pos{x, y, z} - name, _ := w.Block(selectedPos).EncodeBlock() - if name == "minecraft:portal" { + if p, ok := w.Block(selectedPos).(portalBlock); ok && p.Portal() == world.Nether { belowPos := selectedPos.Side(cube.FaceDown) - name, _ = w.Block(belowPos).EncodeBlock() - if name == "minecraft:obsidian" { - dist := world.Distance(pos.Vec3(), selectedPos.Vec3()) + if f, ok := w.Block(belowPos).(frameBlock); ok && f.Frame(world.Nether) { + dist := selectedPos.Vec3().Sub(pos.Vec3()).Len() if dist < closestDist { - closestDist, closestPos, ok = dist, selectedPos, true + closestDist, closestPos, found = dist, selectedPos, true } } } } } } - if !ok { + if !found { // Don't waste our time if the search didn't work out. return Nether{}, false } @@ -208,9 +218,9 @@ func CreateNetherPortal(w *world.World, pos cube.Pos) (Nether, bool) { resultPos.Z() + safeWidth*coEff2 - safeBeforeAfter*coEff1, } - w.SetBlock(entryPos, air()) + w.SetBlock(entryPos, nil, nil) if height < 0 { - w.SetBlock(entryPos, obsidian()) + w.SetBlock(entryPos, obsidian(), nil) } } } @@ -228,11 +238,11 @@ func CreateNetherPortal(w *world.World, pos cube.Pos) (Nether, bool) { } if width == -1 || width == 2 || height == -1 || height == 3 { - w.SetBlock(entryPos, obsidian()) + w.SetBlock(entryPos, obsidian(), nil) continue } positions = append(positions, entryPos) - w.SetBlock(entryPos, portal(axis)) + w.SetBlock(entryPos, portal(axis), nil) } } @@ -255,14 +265,14 @@ func (n Nether) Bounds() (int, int) { // Activate ... func (n Nether) Activate() { for _, pos := range n.Positions() { - n.world.SetBlock(pos, portal(n.axis)) + n.world.SetBlock(pos, portal(n.axis), nil) } } // Deactivate ... func (n Nether) Deactivate() { for _, pos := range n.Positions() { - n.world.BreakBlockWithoutParticles(pos) + n.world.SetBlock(pos, nil, nil) } } diff --git a/server/world/world.go b/server/world/world.go index 66b1b4391..98afa2d62 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -11,7 +11,6 @@ import ( "github.com/google/uuid" "golang.org/x/exp/maps" "golang.org/x/exp/slices" - "math" "math/rand" "sync" "time" @@ -140,24 +139,6 @@ func (w *World) Biome(pos cube.Pos) Biome { return b } -// BlocksAround returns all block positions around the AABB passed. -func (w *World) BlocksAround(aabb physics.AABB) []cube.Pos { - grown := aabb.Grow(0.25) - min, max := grown.Min(), grown.Max() - minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) - maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) - - blockAABBs := make([]cube.Pos, 0, (maxX-minX)*(maxY-minY)*(maxZ-minZ)) - for y := minY; y <= maxY; y++ { - for x := minX; x <= maxX; x++ { - for z := minZ; z <= maxZ; z++ { - blockAABBs = append(blockAABBs, cube.Pos{x, y, z}) - } - } - } - return blockAABBs -} - // blockInChunk reads a block from the world at the position passed. The block is assumed to be in the chunk // passed, which is also assumed to be locked already or otherwise not yet accessible. func (w *World) blockInChunk(c *chunkData, pos cube.Pos) Block { @@ -245,6 +226,12 @@ func (w *World) SetBlock(pos cube.Pos, b Block, opts *SetOpts) { if opts == nil { opts = &SetOpts{} } + if p, ok := b.(interface { + Place(pos cube.Pos, w *World) bool + }); ok && !p.Place(pos, w) { + // Don't place the block. + return + } x, y, z := uint8(pos[0]), int16(pos[1]), uint8(pos[2]) c := w.chunk(chunkPosFromBlockPos(pos)) From cfc74d7ce4f483a82d4a2397dad8768d83aabde9 Mon Sep 17 00:00:00 2001 From: JustTal Date: Sun, 31 Jul 2022 11:51:18 -0500 Subject: [PATCH 30/30] entity/travel.go: Various improvements. --- server/entity/travel.go | 53 ++++++++++++++++++----------------- server/world/portal/nether.go | 3 +- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/server/entity/travel.go b/server/entity/travel.go index 04f8d94b6..d0fcc65d7 100644 --- a/server/entity/travel.go +++ b/server/entity/travel.go @@ -24,14 +24,14 @@ type TravelComputer struct { // Traveller represents a world.Entity that can travel between dimensions. type Traveller interface { + world.Entity // Teleport teleports the entity to the position given. Teleport(pos mgl64.Vec3) - - world.Entity } // portalBlock represents a block that can be used as a portal to travel between dimensions. type portalBlock interface { + world.Block // Portal returns the dimension that the portal leads to. Portal() world.Dimension } @@ -45,18 +45,19 @@ func (t *TravelComputer) TickTravelling(e Traveller) { min, max := box.Min(), box.Max() minX, minY, minZ := int(math.Floor(min[0])), int(math.Floor(min[1])), int(math.Floor(min[2])) maxX, maxY, maxZ := int(math.Ceil(max[0])), int(math.Ceil(max[1])), int(math.Ceil(max[2])) - travelling, target := false, world.Dimension(nil) + found, target := false, world.Dimension(nil) for y := minY; y <= maxY; y++ { for x := minX; x <= maxX; x++ { for z := minZ; z <= maxZ; z++ { pos := cube.Pos{x, y, z} - b := w.Block(pos) - if p, ok := b.(portalBlock); ok { - for _, blockBox := range b.Model().BBox(pos, w) { - if blockBox.Translate(pos.Vec3()).IntersectsWith(box) { - travelling, target = true, p.Portal() - break - } + p, ok := w.Block(pos).(portalBlock) + if !ok { + continue + } + for _, blockBox := range p.Model().BBox(pos, w) { + if blockBox.Translate(pos.Vec3()).IntersectsWith(box) { + found, target = true, p.Portal() + break } } } @@ -65,25 +66,27 @@ func (t *TravelComputer) TickTravelling(e Traveller) { t.mu.Lock() defer t.mu.Unlock() - if !travelling { - // No portals found. Check if we aren't travelling and if so, reset. - if !t.travelling { - t.timedOut, t.awaitingTravel = false, false + if !found { + if t.travelling { + // Don't reset if we're travelling. + return } + t.timedOut, t.awaitingTravel = false, false return } switch target { case world.Nether: - timeOut, awaitingTravel, start := t.timedOut, t.awaitingTravel, t.start - if !timeOut { - if t.Instantaneous() || (awaitingTravel && time.Since(start) >= time.Second*4) { - t.mu.Unlock() - t.Travel(e, w, w.PortalDestination(world.Nether)) - t.mu.Lock() - } else if !awaitingTravel { - t.start, t.awaitingTravel = time.Now(), true - } + if t.timedOut { + // Timed out, we can't travel through Nether portals. + return + } + if t.Instantaneous() || (t.awaitingTravel && time.Since(t.start) >= time.Second*4) { + t.mu.Unlock() + t.Travel(e, w, w.PortalDestination(world.Nether)) + t.mu.Lock() + } else if !t.awaitingTravel { + t.start, t.awaitingTravel = time.Now(), true } } } @@ -100,8 +103,8 @@ func (t *TravelComputer) Travel(e Traveller, source *world.World, destination *w } t.mu.Lock() + defer t.mu.Unlock() t.travelling, t.timedOut, t.awaitingTravel = true, true, false - t.mu.Unlock() go func() { spawn := pos.Vec3Middle() @@ -113,7 +116,7 @@ func (t *TravelComputer) Travel(e Traveller, source *world.World, destination *w e.Teleport(spawn) t.mu.Lock() + defer t.mu.Unlock() t.travelling = false - t.mu.Unlock() }() } diff --git a/server/world/portal/nether.go b/server/world/portal/nether.go index 156fec417..85507a19a 100644 --- a/server/world/portal/nether.go +++ b/server/world/portal/nether.go @@ -82,7 +82,8 @@ func FindNetherPortal(w *world.World, pos cube.Pos, radius int) (Nether, bool) { closestPos, closestDist, found := cube.Pos{}, math.MaxFloat64, false for x := pos.X() - radius; x < pos.X()+radius; x++ { for z := pos.Z() - radius; z < pos.Z()+radius; z++ { - for y := w.Dimension().Range().Max(); y >= w.Dimension().Range().Min(); y-- { + r := w.Dimension().Range() + for y := r.Max(); y >= r.Min(); y-- { selectedPos := cube.Pos{x, y, z} if p, ok := w.Block(selectedPos).(portalBlock); ok && p.Portal() == world.Nether { belowPos := selectedPos.Side(cube.FaceDown)