Skip to content

Commit

Permalink
Track players in each map region
Browse files Browse the repository at this point in the history
  • Loading branch information
mbpolan committed Oct 29, 2023
1 parent 7cceccb commit 53714aa
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 24 deletions.
51 changes: 27 additions & 24 deletions internal/game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,10 @@ func (g *Game) AddPlayer(p *model.Player, lowMemory bool, writer *network.Protoc
regionOrigin, regionRelative := g.playerRegionPosition(pe)
pe.regionOrigin = regionOrigin

// add the player to the map
regionGlobal := util.RegionOriginToGlobal(g.findEffectiveRegion(pe))
g.mapManager.AddPlayer(pe, regionGlobal)

// plan an initial map region load
region := response.NewLoadRegionResponse(regionOrigin)
pe.Send(region)
Expand Down Expand Up @@ -739,28 +743,6 @@ func (g *Game) broadcastPlayerStatus(pe *playerEntity, targets ...string) {
}
}

// findSpectators returns a slice of players that are within visual distance of a given player.
// Concurrency requirements: (a) game state should be locked and (b) all players should be locked.
func (g *Game) findSpectators(pe *playerEntity) []*playerEntity {
var others []*playerEntity
for _, tpe := range g.players {
// ignore our own player and others players on different z coordinates
if tpe.player.ID == pe.player.ID || tpe.player.GlobalPos.Z != pe.player.GlobalPos.Z {
continue
}

// compute their distance to the player and add them as a spectator if they are within range
// TODO: make this configurable?
dx := util.Abs(tpe.player.GlobalPos.X - pe.player.GlobalPos.X)
dy := util.Abs(tpe.player.GlobalPos.Y - pe.player.GlobalPos.Y)
if dx <= 14 && dy <= 14 {
others = append(others, tpe)
}
}

return others
}

// findPlayerAndLockGame returns the playerEntity for the corresponding player, locking only the game mutex. You must
// call the returned function to properly unlock the mutex.
// Concurrency requirements: (a) game state should NOT be locked and (b) all players should NOT be locked.
Expand Down Expand Up @@ -1550,6 +1532,10 @@ func (g *Game) handleGameUpdate() error {
g.playersOnline.Delete(pe.player.Username)
g.broadcastPlayerStatus(pe)

// remove the player from the map
regionGlobal := util.RegionOriginToGlobal(g.findEffectiveRegion(pe))
g.mapManager.RemovePlayer(pe, regionGlobal)

// flag the player's event loop to stop and disconnect them
pe.mu.Unlock()
pe.Drop()
Expand Down Expand Up @@ -1765,6 +1751,10 @@ func (g *Game) handleGameUpdate() error {
pe.Send(state...)
}

// move the player between regions on the map
g.mapManager.RemovePlayer(pe, util.RegionOriginToGlobal(pe.regionOrigin))
g.mapManager.AddPlayer(pe, util.RegionOriginToGlobal(origin))

// mark this as the current region the player's client has loaded
pe.regionOrigin = origin
changedRegions[pe.player.ID] = true
Expand Down Expand Up @@ -1801,14 +1791,26 @@ func (g *Game) handleGameUpdate() error {
}

// find players within visual distance of this player
others := g.findSpectators(pe)
others := g.mapManager.FindSpectators(pe, regionGlobal)
updatedTracking := map[int]*playerEntity{}

if pe.player.ID == 2 {
var k []int
for _, k1 := range others {
k = append(k, k1.player.ID)
}
logger.Infof("Player 2 others: %+v", k)
}

for _, other := range others {
_, known := pe.tracking[other.player.ID]
// add this player to the new tracking list
updatedTracking[other.player.ID] = other

// determine what to do with this player. there are several possibilities:
// (a) this is the first time we've seen them: send an update including their appearance and location
// (b) we've seen them before, but we don't yet have their update captured
// (c) we've seen them before and know their last update: no action needed
_, known := pe.tracking[other.player.ID]
if !known {
posOffset := other.player.GlobalPos.Sub(pe.player.GlobalPos).To2D()
update.AddToPlayerList(other.index, posOffset, true, true)
Expand Down Expand Up @@ -1853,6 +1855,7 @@ func (g *Game) handleGameUpdate() error {
}
}

pe.tracking = updatedTracking
pe.chatHighWater = time.Now()
}

Expand Down
23 changes: 23 additions & 0 deletions internal/game/map_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,29 @@ func (m *MapManager) State(origin model.Vector3D, trim model.Boundary) []respons
return region.State(trim)
}

// AddPlayer adds a player to the world map at the region whose coordinates correspond to regionGlobal.
func (m *MapManager) AddPlayer(pe *playerEntity, regionGlobal model.Vector3D) {
regions := m.findOverlappingRegions(regionGlobal)
for _, origin := range regions {
region := m.regions[origin]
region.AddPlayer(pe)
}
}

// RemovePlayer removes a player to the world map from the region specified by regionGlobal.
func (m *MapManager) RemovePlayer(pe *playerEntity, regionGlobal model.Vector3D) {
regions := m.findOverlappingRegions(regionGlobal)
for _, origin := range regions {
region := m.regions[origin]
region.RemovePlayer(pe)
}
}

// FindSpectators returns a slice of playerEntity instances that are within viewable distance of the given player.
func (m *MapManager) FindSpectators(pe *playerEntity, regionGlobal model.Vector3D) []*playerEntity {
return m.regions[regionGlobal].FindSpectators(pe)
}

// AddGroundItem adds a ground Item to the top of a tile with an optional timeout (in seconds) when that Item should
// automatically be removed. Stackable items will be added to an existing stackable with the same Item ID, if one
// exists, or they will be placed as new items on the tile.
Expand Down
34 changes: 34 additions & 0 deletions internal/game/region_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type RegionManager struct {
chunkStates map[model.Vector3D]*chunkState
// pendingEvents is a slice of deltas that have occurred to this region's state that need to be reconciled.
pendingEvents []*changeDelta
// players is a map of players IDs to player entities that are in this region.
players map[int]*playerEntity
// mu is a mutex for volatile struct fields.
mu sync.Mutex
}
Expand All @@ -68,6 +70,7 @@ func NewRegionManager(origin model.Vector3D, m *model.Map) *RegionManager {
mgr := &RegionManager{
chunkRelative: map[model.Vector3D]model.Vector2D{},
chunkStates: map[model.Vector3D]*chunkState{},
players: map[int]*playerEntity{},
origin: origin,
clientBaseArea: clientBaseArea,
clientBaseRegion: clientBaseRegion,
Expand Down Expand Up @@ -98,6 +101,37 @@ func (r *RegionManager) State(trim model.Boundary) []response.Response {
return state
}

// AddPlayer adds a player to the region.
func (r *RegionManager) AddPlayer(pe *playerEntity) {
r.players[pe.player.ID] = pe
}

// RemovePlayer removes a player from the region.
func (r *RegionManager) RemovePlayer(pe *playerEntity) {
delete(r.players, pe.player.ID)
}

// FindSpectators returns a slice of players that are within visual distance of a given player.
func (r *RegionManager) FindSpectators(pe *playerEntity) []*playerEntity {
var others []*playerEntity
for _, tpe := range r.players {
// ignore our own player and others players on different z coordinates
if tpe.player.ID == pe.player.ID || tpe.player.GlobalPos.Z != pe.player.GlobalPos.Z {
continue
}

// compute their distance to the player and add them as a spectator if they are within range
// TODO: make this configurable?
dx := util.Abs(tpe.player.GlobalPos.X - pe.player.GlobalPos.X)
dy := util.Abs(tpe.player.GlobalPos.Y - pe.player.GlobalPos.Y)
if dx <= 14 && dy <= 14 {
others = append(others, tpe)
}
}

return others
}

// MarkGroundItemAdded informs the region manager that a ground Item with a stack amount was placed on a tile.
func (r *RegionManager) MarkGroundItemAdded(itemID, amount int, globalPos model.Vector3D) {
// track this change to the region state
Expand Down

0 comments on commit 53714aa

Please sign in to comment.