mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-10 06:16:27 -05:00
88326b5278
* Casting a skill now plays the corresponding overlay(if any). * Prevent a crash caused by nil pointer in HeroSkill deserialization, happening when unmarshalling HeroSkill from packets as a remote client. * Add PlayerAnimationModeNone to handle some of the Skills(e.g. Paladin auras) having "" as animation mode. * Joining a game as remote client now waits for map generation to finish before rendering map or processing map entities. This is temporary hack to prevent the game from crashing due to concurrent map read & write exception. * Send CastSkill packet to other clients. Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
339 lines
10 KiB
Go
339 lines
10 KiB
Go
package d2mapengine
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp"
|
|
)
|
|
|
|
// MapEngine loads the tiles which make up the isometric map and the entities
|
|
type MapEngine struct {
|
|
asset *d2asset.AssetManager
|
|
*d2mapstamp.StampFactory
|
|
*d2mapentity.MapEntityFactory
|
|
seed int64 // The map seed
|
|
entities map[string]d2interface.MapEntity // Entities on the map
|
|
tiles []MapTile
|
|
size d2geom.Size // Size of the map, in tiles
|
|
levelType d2records.LevelTypeRecord // Level type of this map
|
|
dt1TileData []d2dt1.Tile // DT1 tile data
|
|
startSubTileX int // Starting X position
|
|
startSubTileY int // Starting Y position
|
|
dt1Files []string // List of DS1 strings
|
|
// TODO: remove this flag and show loading screen until the initial server packets are handled and the map is generated (only for remote client)
|
|
IsLoading bool // (temp) Whether we have processed the GenerateMapPacket(only for remote client)
|
|
}
|
|
|
|
// CreateMapEngine creates a new instance of the map engine and returns a pointer to it.
|
|
func CreateMapEngine(asset *d2asset.AssetManager) *MapEngine {
|
|
entity, _ := d2mapentity.NewMapEntityFactory(asset)
|
|
stamp := d2mapstamp.NewStampFactory(asset, entity)
|
|
|
|
engine := &MapEngine{
|
|
asset: asset,
|
|
MapEntityFactory: entity,
|
|
StampFactory: stamp,
|
|
// This will be set to true when we are using a remote client connection, and then set to false after we process the GenerateMapPacket
|
|
IsLoading: false,
|
|
}
|
|
|
|
return engine
|
|
}
|
|
|
|
// GetStartingPosition returns the starting position on the map in sub-tiles.
|
|
func (m *MapEngine) GetStartingPosition() (x, y int) {
|
|
return m.startSubTileX, m.startSubTileY
|
|
}
|
|
|
|
// ResetMap clears all map and entity data and reloads it from the cached files.
|
|
func (m *MapEngine) ResetMap(levelType d2enum.RegionIdType, width, height int) {
|
|
m.entities = make(map[string]d2interface.MapEntity)
|
|
m.levelType = *m.asset.Records.Level.Types[levelType]
|
|
m.size = d2geom.Size{Width: width, Height: height}
|
|
m.tiles = make([]MapTile, width*height)
|
|
m.dt1TileData = make([]d2dt1.Tile, 0)
|
|
m.dt1Files = make([]string, 0)
|
|
|
|
for idx := range m.levelType.Files {
|
|
m.addDT1(m.levelType.Files[idx])
|
|
}
|
|
}
|
|
|
|
func (m *MapEngine) addDT1(fileName string) {
|
|
if fileName == "" || fileName == "0" {
|
|
return
|
|
}
|
|
|
|
fileName = strings.ToLower(fileName)
|
|
for i := 0; i < len(m.dt1Files); i++ {
|
|
if m.dt1Files[i] == fileName {
|
|
return
|
|
}
|
|
}
|
|
|
|
fileData, err := m.asset.LoadFile("/data/global/tiles/" + fileName)
|
|
if err != nil {
|
|
log.Printf("Could not load /data/global/tiles/%s", fileName)
|
|
// panic(err)
|
|
return
|
|
}
|
|
|
|
dt1, err := d2dt1.LoadDT1(fileData)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
m.dt1TileData = append(m.dt1TileData, dt1.Tiles...)
|
|
m.dt1Files = append(m.dt1Files, fileName)
|
|
}
|
|
|
|
// AddDS1 loads DT1 files and performs string replacements on them. It
|
|
// appends the tile data and files to MapEngine.dt1TileData and
|
|
// MapEngine.dt1Files.
|
|
func (m *MapEngine) AddDS1(fileName string) {
|
|
if fileName == "" || fileName == "0" {
|
|
return
|
|
}
|
|
|
|
fileData, err := m.asset.LoadFile("/data/global/tiles/" + fileName)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ds1, err := d2ds1.LoadDS1(fileData)
|
|
if err != nil {
|
|
log.Print(err)
|
|
}
|
|
|
|
for idx := range ds1.Files {
|
|
dt1File := ds1.Files[idx]
|
|
dt1File = strings.ToLower(dt1File)
|
|
dt1File = strings.ReplaceAll(dt1File, "c:", "") // Yes they did...
|
|
dt1File = strings.ReplaceAll(dt1File, ".tg1", ".dt1") // Yes they did...
|
|
dt1File = strings.ReplaceAll(dt1File, "\\d2\\data\\global\\tiles\\", "")
|
|
m.addDT1(strings.ReplaceAll(dt1File, "\\", "/"))
|
|
}
|
|
}
|
|
|
|
// LevelType returns the level type of this map.
|
|
func (m *MapEngine) LevelType() d2records.LevelTypeRecord {
|
|
return m.levelType
|
|
}
|
|
|
|
// SetSeed sets the seed of the map for generation.
|
|
func (m *MapEngine) SetSeed(seed int64) {
|
|
log.Printf("Setting map engine seed to %d", seed)
|
|
m.seed = seed
|
|
}
|
|
|
|
// Size returns the size of the map in sub-tiles.
|
|
func (m *MapEngine) Size() d2geom.Size {
|
|
return m.size
|
|
}
|
|
|
|
// Tile returns the TileRecord containing the data
|
|
// for a single map tile.
|
|
func (m *MapEngine) Tile(x, y int) *MapTile {
|
|
return &m.tiles[x+(y*m.size.Width)]
|
|
}
|
|
|
|
// Tiles returns a pointer to a slice contaning all
|
|
// map tile data.
|
|
func (m *MapEngine) Tiles() *[]MapTile {
|
|
return &m.tiles
|
|
}
|
|
|
|
// PlaceStamp places a map stamp at the specified location, creating both entities
|
|
// and tiles. Stamps are pre-defined map areas, see d2mapstamp.
|
|
func (m *MapEngine) PlaceStamp(stamp *d2mapstamp.Stamp, tileOffsetX, tileOffsetY int) {
|
|
stampSize := stamp.Size()
|
|
stampW := stampSize.Width
|
|
stampH := stampSize.Height
|
|
|
|
mapW := m.size.Width
|
|
mapH := m.size.Height
|
|
|
|
xMin := tileOffsetX
|
|
yMin := tileOffsetY
|
|
xMax := xMin + stampSize.Width
|
|
yMax := yMin + stampSize.Height
|
|
|
|
if (xMin < 0) || (yMin < 0) || (xMax > mapW) || (yMax > mapH) {
|
|
panic("Tried placing a stamp outside the bounds of the map")
|
|
}
|
|
|
|
// Copy over the map tile data
|
|
for y := 0; y < stampH; y++ {
|
|
for x := 0; x < stampW; x++ {
|
|
targetTileIndex := m.tileCoordinateToIndex(x+xMin, y+yMin)
|
|
stampTile := *stamp.Tile(x, y)
|
|
m.tiles[targetTileIndex].RegionType = stamp.RegionID()
|
|
m.tiles[targetTileIndex].Components = stampTile
|
|
m.tiles[targetTileIndex].PrepareTile(x, y, m)
|
|
}
|
|
}
|
|
|
|
// Copy over the entities
|
|
stampEntities := stamp.Entities(tileOffsetX, tileOffsetY)
|
|
for idx := range stampEntities {
|
|
e := stampEntities[idx]
|
|
m.entities[e.ID()] = e
|
|
}
|
|
}
|
|
|
|
// converts x,y tile coordinate into index in MapEngine.tiles
|
|
func (m *MapEngine) tileCoordinateToIndex(x, y int) int {
|
|
return x + (y * m.size.Width)
|
|
}
|
|
|
|
// tileIndexToCoordinate converts tile index from MapEngine.tiles to x,y coordinate
|
|
func (m *MapEngine) tileIndexToCoordinate(index int) (x, y int) {
|
|
return index % m.size.Width, index / m.size.Width
|
|
}
|
|
|
|
// SubTileAt gets the flags for the given subtile
|
|
func (m *MapEngine) SubTileAt(subX, subY int) *d2dt1.SubTileFlags {
|
|
tile := m.TileAt(subX/5, subY/5)
|
|
|
|
return tile.GetSubTileFlags(subX%5, subY%5)
|
|
}
|
|
|
|
// TileAt returns a pointer to the data for the map tile at the given
|
|
// x and y index.
|
|
func (m *MapEngine) TileAt(tileX, tileY int) *MapTile {
|
|
idx := m.tileCoordinateToIndex(tileX, tileY)
|
|
if idx < 0 || idx >= len(m.tiles) {
|
|
return nil
|
|
}
|
|
|
|
return &m.tiles[idx]
|
|
}
|
|
|
|
// Entities returns a pointer a slice of all map entities.
|
|
func (m *MapEngine) Entities() map[string]d2interface.MapEntity {
|
|
return m.entities
|
|
}
|
|
|
|
// Seed returns the map generation seed.
|
|
func (m *MapEngine) Seed() int64 {
|
|
return m.seed
|
|
}
|
|
|
|
// AddEntity adds an entity to a slice containing all entities.
|
|
func (m *MapEngine) AddEntity(entity d2interface.MapEntity) {
|
|
m.entities[entity.ID()] = entity
|
|
}
|
|
|
|
// RemoveEntity removes an entity from the map engine
|
|
func (m *MapEngine) RemoveEntity(entity d2interface.MapEntity) {
|
|
if entity == nil {
|
|
return
|
|
}
|
|
|
|
delete(m.entities, entity.ID())
|
|
}
|
|
|
|
// GetTiles returns a slice of all tiles matching the given style,
|
|
// sequence and tileType.
|
|
func (m *MapEngine) GetTiles(style, sequence, tileType int) []d2dt1.Tile {
|
|
tiles := make([]d2dt1.Tile, 0, len(m.dt1TileData))
|
|
|
|
for idx := range m.dt1TileData {
|
|
if m.dt1TileData[idx].Style != int32(style) || m.dt1TileData[idx].Sequence != int32(sequence) ||
|
|
m.dt1TileData[idx].Type != int32(tileType) {
|
|
continue
|
|
}
|
|
|
|
tiles = append(tiles, m.dt1TileData[idx])
|
|
}
|
|
|
|
if len(tiles) == 0 {
|
|
log.Printf("Unknown tile ID [%d %d %d]\n", style, sequence, tileType)
|
|
return nil
|
|
}
|
|
|
|
return tiles
|
|
}
|
|
|
|
// GetStartPosition returns the spawn point on entering the current map.
|
|
func (m *MapEngine) GetStartPosition() (x, y float64) {
|
|
for tileY := 0; tileY < m.size.Height; tileY++ {
|
|
for tileX := 0; tileX < m.size.Width; tileX++ {
|
|
tile := m.tiles[tileX+(tileY*m.size.Width)].Components
|
|
for idx := range tile.Walls {
|
|
if tile.Walls[idx].Type.Special() && tile.Walls[idx].Style == 30 {
|
|
return float64(tileX) + 0.5, float64(tileY) + 0.5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return m.GetCenterPosition()
|
|
}
|
|
|
|
// GetCenterPosition returns the center point of the map.
|
|
func (m *MapEngine) GetCenterPosition() (x, y float64) {
|
|
return float64(m.size.Width) / 2.0, float64(m.size.Height) / 2.0
|
|
}
|
|
|
|
// Advance calls the Advance() method for all entities,
|
|
// processing a single tick.
|
|
func (m *MapEngine) Advance(tickTime float64) {
|
|
// TODO:(temp hack) prevents concurrent map read & write exceptions that occur when we join a TCP game as a remote client
|
|
// due to the engine updating entities before handling the GenerateMapPacket
|
|
if m.IsLoading {
|
|
return
|
|
}
|
|
|
|
for ID := range m.entities {
|
|
m.entities[ID].Advance(tickTime)
|
|
}
|
|
}
|
|
|
|
// TileExists returns true if the tile at the given coordinates exists.
|
|
func (m *MapEngine) TileExists(tileX, tileY int) bool {
|
|
tileIndex := m.tileCoordinateToIndex(tileX, tileY)
|
|
|
|
if valid := (tileIndex >= 0) && (tileIndex <= len(m.tiles)); valid {
|
|
tile := m.tiles[tileIndex].Components
|
|
numFeatures := len(tile.Floors)
|
|
numFeatures += len(tile.Shadows)
|
|
numFeatures += len(tile.Walls)
|
|
numFeatures += len(tile.Substitutions)
|
|
|
|
return numFeatures > 0
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GenerateMap clears the map and places the specified stamp.
|
|
func (m *MapEngine) GenerateMap(regionType d2enum.RegionIdType, levelPreset, fileIndex int) {
|
|
region := m.LoadStamp(regionType, levelPreset, fileIndex)
|
|
regionSize := region.Size()
|
|
m.ResetMap(regionType, regionSize.Width, regionSize.Height)
|
|
m.PlaceStamp(region, 0, 0)
|
|
}
|
|
|
|
// GetTileData returns the tile with the given style, sequence, tileType and index.
|
|
func (m *MapEngine) GetTileData(style, sequence int, tileType d2enum.TileType, index byte) *d2dt1.Tile {
|
|
for idx := range m.dt1TileData {
|
|
if m.dt1TileData[idx].Style == int32(style) && m.dt1TileData[idx].Sequence == int32(sequence) &&
|
|
m.dt1TileData[idx].Type == int32(tileType) && m.dt1TileData[idx].RarityFrameIndex == int32(index) {
|
|
return &m.dt1TileData[idx]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|