2020-02-01 18:55:56 -05:00
|
|
|
package d2map
|
2019-10-31 13:39:05 -04:00
|
|
|
|
|
|
|
import (
|
2019-11-01 22:12:07 -04:00
|
|
|
"image/color"
|
|
|
|
"log"
|
2019-10-31 13:39:05 -04:00
|
|
|
"math"
|
|
|
|
"math/rand"
|
2019-11-01 14:12:23 -04:00
|
|
|
"strconv"
|
|
|
|
|
2020-01-26 00:39:13 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
2020-01-31 23:18:11 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
2020-01-26 00:39:13 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
2020-01-31 23:18:11 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1"
|
2020-01-26 00:39:13 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
2020-02-01 21:06:22 -05:00
|
|
|
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
2020-01-31 23:18:11 -05:00
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
|
2019-10-31 13:39:05 -04:00
|
|
|
)
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
type MapRegion struct {
|
|
|
|
tileRect d2common.Rectangle
|
|
|
|
regionPath string
|
|
|
|
levelType d2datadict.LevelTypeRecord
|
|
|
|
levelPreset d2datadict.LevelPresetRecord
|
|
|
|
tiles []d2dt1.Tile
|
|
|
|
ds1 d2ds1.DS1
|
|
|
|
palette d2datadict.PaletteRec
|
|
|
|
startX float64
|
|
|
|
startY float64
|
2020-02-01 20:39:28 -05:00
|
|
|
imageCacheRecords map[uint32]d2render.Surface
|
2019-11-19 00:38:02 -05:00
|
|
|
seed int64
|
2019-12-28 23:32:24 -05:00
|
|
|
currentFrame int
|
2019-11-24 01:11:32 -05:00
|
|
|
lastFrameTime float64
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
|
2019-12-21 20:53:18 -05:00
|
|
|
func loadRegion(seed int64, tileOffsetX, tileOffsetY int, levelType d2enum.RegionIdType, levelPreset int, fileIndex int) (*MapRegion, []MapEntity) {
|
2019-12-13 00:33:11 -05:00
|
|
|
region := &MapRegion{
|
|
|
|
levelType: d2datadict.LevelTypes[levelType],
|
|
|
|
levelPreset: d2datadict.LevelPresets[levelPreset],
|
2020-02-01 20:39:28 -05:00
|
|
|
imageCacheRecords: map[uint32]d2render.Surface{},
|
2019-11-19 00:38:02 -05:00
|
|
|
seed: seed,
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
2020-02-01 21:51:49 -05:00
|
|
|
region.palette = d2datadict.Palettes[d2enum.PaletteType("act"+strconv.Itoa(region.levelType.Act))]
|
2019-11-23 08:48:33 -05:00
|
|
|
if levelType == d2enum.RegionAct5Lava {
|
2019-12-13 00:33:11 -05:00
|
|
|
region.palette = d2datadict.Palettes[d2enum.PaletteType("act4")]
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, levelTypeDt1 := range region.levelType.Files {
|
|
|
|
if len(levelTypeDt1) != 0 && levelTypeDt1 != "" && levelTypeDt1 != "0" {
|
2020-02-01 18:55:56 -05:00
|
|
|
fileData, err := d2asset.LoadFile("/data/global/tiles/" + levelTypeDt1)
|
2019-12-24 01:48:45 -05:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
dt1 := d2dt1.LoadDT1(fileData)
|
2019-12-13 00:33:11 -05:00
|
|
|
region.tiles = append(region.tiles, dt1.Tiles...)
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
|
|
|
var levelFilesToPick []string
|
|
|
|
for _, fileRecord := range region.levelPreset.Files {
|
|
|
|
if len(fileRecord) != 0 && fileRecord != "" && fileRecord != "0" {
|
|
|
|
levelFilesToPick = append(levelFilesToPick, fileRecord)
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
2019-11-21 19:54:56 -05:00
|
|
|
levelIndex := int(math.Round(float64(len(levelFilesToPick)-1) * rand.Float64()))
|
2019-11-17 00:52:13 -05:00
|
|
|
if fileIndex >= 0 && fileIndex < len(levelFilesToPick) {
|
|
|
|
levelIndex = fileIndex
|
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2020-02-01 21:51:49 -05:00
|
|
|
if levelFilesToPick == nil {
|
|
|
|
panic("no level files to pick from")
|
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
region.regionPath = levelFilesToPick[levelIndex]
|
2020-02-01 18:55:56 -05:00
|
|
|
fileData, err := d2asset.LoadFile("/data/global/tiles/" + region.regionPath)
|
2019-12-24 01:48:45 -05:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
region.ds1 = d2ds1.LoadDS1(fileData)
|
2019-12-13 00:33:11 -05:00
|
|
|
region.tileRect = d2common.Rectangle{
|
|
|
|
Left: tileOffsetX,
|
|
|
|
Top: tileOffsetY,
|
|
|
|
Width: int(region.ds1.Width),
|
|
|
|
Height: int(region.ds1.Height),
|
|
|
|
}
|
|
|
|
|
2019-12-21 20:53:18 -05:00
|
|
|
entities := region.loadEntities()
|
2019-12-13 00:33:11 -05:00
|
|
|
region.loadSpecials()
|
|
|
|
region.generateTileCache()
|
|
|
|
|
|
|
|
return region, entities
|
2019-11-07 23:44:03 -05:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) GetTileRect() d2common.Rectangle {
|
|
|
|
return mr.tileRect
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) GetLevelPreset() d2datadict.LevelPresetRecord {
|
|
|
|
return mr.levelPreset
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) GetLevelType() d2datadict.LevelTypeRecord {
|
|
|
|
return mr.levelType
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) GetPath() string {
|
|
|
|
return mr.regionPath
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) loadSpecials() {
|
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX := range mr.ds1.Tiles[tileY] {
|
|
|
|
for _, wall := range mr.ds1.Tiles[tileY][tileX].Walls {
|
|
|
|
if wall.Type == 10 && wall.Style == 30 && wall.Sequence == 0 {
|
|
|
|
mr.startX, mr.startY = mr.getTileWorldPosition(tileX, tileY)
|
|
|
|
mr.startX += 0.5
|
|
|
|
mr.startY += 0.5
|
|
|
|
return
|
2019-11-17 01:14:58 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
func (mr *MapRegion) GetTile(x, y int) *d2ds1.TileRecord {
|
|
|
|
return &mr.ds1.Tiles[y][x]
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) GetTileData(style int32, sequence int32, tileType d2enum.TileType) *d2dt1.Tile {
|
|
|
|
for _, tile := range mr.tiles {
|
|
|
|
if tile.Style == style && tile.Sequence == sequence && tile.Type == int32(tileType) {
|
|
|
|
return &tile
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) GetTileSize() (int, int) {
|
|
|
|
return mr.tileRect.Width, mr.tileRect.Height
|
|
|
|
}
|
|
|
|
|
2019-12-21 20:53:18 -05:00
|
|
|
func (mr *MapRegion) loadEntities() []MapEntity {
|
2019-12-13 00:33:11 -05:00
|
|
|
var entities []MapEntity
|
|
|
|
|
|
|
|
for _, object := range mr.ds1.Objects {
|
|
|
|
worldX, worldY := mr.getTileWorldPosition(int(object.X), int(object.Y))
|
|
|
|
|
2019-11-17 14:21:48 -05:00
|
|
|
switch object.Lookup.Type {
|
|
|
|
case d2datadict.ObjectTypeCharacter:
|
2019-12-13 00:33:11 -05:00
|
|
|
if object.Lookup.Base != "" && object.Lookup.Token != "" && object.Lookup.TR != "" {
|
2020-02-01 18:55:56 -05:00
|
|
|
npc := CreateNPC(int32(worldX), int32(worldY), object.Lookup, 0)
|
2019-12-13 00:33:11 -05:00
|
|
|
npc.SetPaths(object.Paths)
|
|
|
|
entities = append(entities, npc)
|
2019-11-05 22:23:48 -05:00
|
|
|
}
|
2019-11-17 14:21:48 -05:00
|
|
|
case d2datadict.ObjectTypeItem:
|
2019-12-13 00:33:11 -05:00
|
|
|
if object.ObjectInfo != nil && object.ObjectInfo.Draw && object.Lookup.Base != "" && object.Lookup.Token != "" {
|
2020-02-01 18:55:56 -05:00
|
|
|
entity, err := CreateAnimatedEntity(int32(worldX), int32(worldY), object.Lookup, d2resource.PaletteUnits)
|
2019-12-24 01:48:45 -05:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
entity.SetMode(object.Lookup.Mode, object.Lookup.Class, 0)
|
2019-12-24 01:48:45 -05:00
|
|
|
entities = append(entities, entity)
|
2019-11-17 14:21:48 -05:00
|
|
|
}
|
|
|
|
}
|
2019-11-05 22:23:48 -05:00
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
|
|
|
return entities
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) getStartTilePosition() (float64, float64) {
|
|
|
|
return float64(mr.tileRect.Left) + mr.startX, float64(mr.tileRect.Top) + mr.startY
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) getRandomTile(tiles []d2dt1.Tile, x, y int, seed int64) byte {
|
2019-11-21 19:54:56 -05:00
|
|
|
/* Walker's Alias Method for weighted random selection
|
|
|
|
* with xorshifting for random numbers */
|
|
|
|
|
|
|
|
var tileSeed uint64
|
|
|
|
tileSeed = uint64(seed) + uint64(x)
|
2019-12-13 00:33:11 -05:00
|
|
|
tileSeed *= uint64(y) + uint64(mr.levelType.Id)
|
2019-11-21 19:54:56 -05:00
|
|
|
|
|
|
|
tileSeed ^= tileSeed << 13
|
|
|
|
tileSeed ^= tileSeed >> 17
|
|
|
|
tileSeed ^= tileSeed << 5
|
|
|
|
|
|
|
|
weightSum := 0
|
|
|
|
for _, tile := range tiles {
|
|
|
|
weightSum += int(tile.RarityFrameIndex)
|
|
|
|
}
|
|
|
|
|
|
|
|
if weightSum == 0 {
|
2019-11-24 01:11:32 -05:00
|
|
|
return 0
|
2019-11-21 19:54:56 -05:00
|
|
|
}
|
|
|
|
|
2020-02-01 21:51:49 -05:00
|
|
|
random := tileSeed % uint64(weightSum)
|
2019-11-21 19:54:56 -05:00
|
|
|
|
|
|
|
sum := 0
|
|
|
|
for i, tile := range tiles {
|
|
|
|
sum += int(tile.RarityFrameIndex)
|
|
|
|
if sum >= int(random) {
|
2019-11-24 01:11:32 -05:00
|
|
|
return byte(i)
|
2019-11-21 19:54:56 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This return shouldn't be hit
|
2019-11-24 01:11:32 -05:00
|
|
|
return 0
|
2019-11-17 00:52:13 -05:00
|
|
|
}
|
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
func (mr *MapRegion) getTiles(style, sequence, tileType int32) []d2dt1.Tile {
|
2019-11-19 00:38:02 -05:00
|
|
|
var tiles []d2dt1.Tile
|
2019-12-13 00:33:11 -05:00
|
|
|
for _, tile := range mr.tiles {
|
2019-11-21 20:34:29 -05:00
|
|
|
if tile.Style != style || tile.Sequence != sequence || tile.Type != tileType {
|
2019-10-31 13:39:05 -04:00
|
|
|
continue
|
|
|
|
}
|
2019-11-17 00:52:13 -05:00
|
|
|
tiles = append(tiles, tile)
|
|
|
|
}
|
|
|
|
if len(tiles) == 0 {
|
2019-11-21 20:34:29 -05:00
|
|
|
log.Printf("Unknown tile ID [%d %d %d]\n", style, sequence, tileType)
|
2019-11-24 01:11:32 -05:00
|
|
|
return nil
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
2019-11-24 01:11:32 -05:00
|
|
|
return tiles
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) isVisbile(viewport *Viewport) bool {
|
|
|
|
return viewport.IsTileRectVisible(mr.tileRect)
|
|
|
|
}
|
|
|
|
|
2019-12-28 23:32:24 -05:00
|
|
|
func (mr *MapRegion) advance(elapsed float64) {
|
|
|
|
frameLength := 0.1
|
|
|
|
|
|
|
|
mr.lastFrameTime += elapsed
|
|
|
|
framesAdvanced := int(mr.lastFrameTime / frameLength)
|
|
|
|
mr.lastFrameTime -= float64(framesAdvanced) * frameLength
|
|
|
|
|
|
|
|
mr.currentFrame += framesAdvanced
|
|
|
|
if mr.currentFrame > 9 {
|
|
|
|
mr.currentFrame = 0
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) getTileWorldPosition(tileX, tileY int) (float64, float64) {
|
|
|
|
return float64(tileX + mr.tileRect.Left), float64(tileY + mr.tileRect.Top)
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderPass1(viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX, tile := range mr.ds1.Tiles[tileY] {
|
|
|
|
worldX, worldY := mr.getTileWorldPosition(tileX, tileY)
|
|
|
|
if viewport.IsTileVisible(worldX, worldY) {
|
|
|
|
viewport.PushTranslationWorld(worldX, worldY)
|
|
|
|
mr.renderTilePass1(tile, viewport, target)
|
|
|
|
viewport.PopTranslation()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderPass2(entities []MapEntity, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX, tile := range mr.ds1.Tiles[tileY] {
|
|
|
|
worldX, worldY := mr.getTileWorldPosition(tileX, tileY)
|
|
|
|
if viewport.IsTileVisible(worldX, worldY) {
|
|
|
|
viewport.PushTranslationWorld(worldX, worldY)
|
|
|
|
mr.renderTilePass2(tile, viewport, target)
|
|
|
|
|
|
|
|
for _, entity := range entities {
|
|
|
|
entWorldX, entWorldY := entity.GetPosition()
|
|
|
|
if entWorldX == worldX && entWorldY == worldY {
|
2019-12-28 16:46:08 -05:00
|
|
|
target.PushTranslation(viewport.GetTranslationScreen())
|
|
|
|
entity.Render(target)
|
|
|
|
target.Pop()
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
viewport.PopTranslation()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderPass3(viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX, tile := range mr.ds1.Tiles[tileY] {
|
|
|
|
worldX, worldY := mr.getTileWorldPosition(tileX, tileY)
|
|
|
|
if viewport.IsTileVisible(worldX, worldY) {
|
|
|
|
viewport.PushTranslationWorld(worldX, worldY)
|
|
|
|
mr.renderTilePass3(tile, viewport, target)
|
|
|
|
viewport.PopTranslation()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderTilePass1(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for _, wall := range tile.Walls {
|
|
|
|
if !wall.Hidden && wall.Prop1 != 0 && wall.Type.LowerWall() {
|
|
|
|
mr.renderWall(wall, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, floor := range tile.Floors {
|
|
|
|
if !floor.Hidden && floor.Prop1 != 0 {
|
|
|
|
mr.renderFloor(floor, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, shadow := range tile.Shadows {
|
|
|
|
if !shadow.Hidden && shadow.Prop1 != 0 {
|
|
|
|
mr.renderShadow(shadow, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderTilePass2(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for _, wall := range tile.Walls {
|
|
|
|
if !wall.Hidden && wall.Type.UpperWall() {
|
|
|
|
mr.renderWall(wall, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderTilePass3(tile d2ds1.TileRecord, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for _, wall := range tile.Walls {
|
|
|
|
if wall.Type == d2enum.Roof {
|
|
|
|
mr.renderWall(wall, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderFloor(tile d2ds1.FloorShadowRecord, viewport *Viewport, target d2render.Surface) {
|
|
|
|
var img d2render.Surface
|
2019-11-24 01:11:32 -05:00
|
|
|
if !tile.Animated {
|
2019-12-13 00:33:11 -05:00
|
|
|
img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tile.RandomIndex)
|
2019-11-24 01:11:32 -05:00
|
|
|
} else {
|
2019-12-28 23:32:24 -05:00
|
|
|
img = mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, byte(mr.currentFrame))
|
2019-11-24 01:11:32 -05:00
|
|
|
}
|
2019-11-17 14:21:48 -05:00
|
|
|
if img == nil {
|
2019-11-21 20:34:29 -05:00
|
|
|
log.Printf("Render called on uncached floor {%v,%v}", tile.Style, tile.Sequence)
|
2019-11-21 19:54:56 -05:00
|
|
|
return
|
2019-11-17 14:21:48 -05:00
|
|
|
}
|
2019-12-08 22:18:42 -05:00
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
viewport.PushTranslationOrtho(-80, float64(tile.YAdjust))
|
2019-12-28 16:46:08 -05:00
|
|
|
defer viewport.PopTranslation()
|
|
|
|
|
|
|
|
target.PushTranslation(viewport.GetTranslationScreen())
|
|
|
|
defer target.Pop()
|
|
|
|
|
|
|
|
target.Render(img)
|
2019-11-01 14:12:23 -04:00
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderWall(tile d2ds1.WallRecord, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
img := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tile.RandomIndex)
|
2019-11-17 14:21:48 -05:00
|
|
|
if img == nil {
|
2019-11-21 20:34:29 -05:00
|
|
|
log.Printf("Render called on uncached wall {%v,%v,%v}", tile.Style, tile.Sequence, tile.Type)
|
2019-11-13 14:26:42 -05:00
|
|
|
return
|
|
|
|
}
|
2019-12-08 22:18:42 -05:00
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
viewport.PushTranslationOrtho(-80, float64(tile.YAdjust) - 16)
|
2019-12-28 16:46:08 -05:00
|
|
|
defer viewport.PopTranslation()
|
|
|
|
|
|
|
|
target.PushTranslation(viewport.GetTranslationScreen())
|
|
|
|
defer target.Pop()
|
|
|
|
|
|
|
|
target.Render(img)
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderShadow(tile d2ds1.FloorShadowRecord, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
img := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tile.RandomIndex)
|
2019-11-17 14:21:48 -05:00
|
|
|
if img == nil {
|
2019-11-21 20:34:29 -05:00
|
|
|
log.Printf("Render called on uncached shadow {%v,%v}", tile.Style, tile.Sequence)
|
2019-11-13 14:26:42 -05:00
|
|
|
return
|
|
|
|
}
|
2019-12-08 22:18:42 -05:00
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
viewport.PushTranslationOrtho(-80, float64(tile.YAdjust))
|
2019-12-28 16:46:08 -05:00
|
|
|
defer viewport.PopTranslation()
|
|
|
|
|
|
|
|
target.PushTranslation(viewport.GetTranslationScreen())
|
2020-02-01 21:51:49 -05:00
|
|
|
target.PushColor(color.RGBA{R: 255, G: 255, B: 255, A: 160})
|
2019-12-28 16:46:08 -05:00
|
|
|
defer target.PopN(2)
|
|
|
|
|
|
|
|
target.Render(img)
|
2019-11-01 16:51:50 -04:00
|
|
|
}
|
2019-11-01 14:12:23 -04:00
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderDebug(debugVisLevel int, viewport *Viewport, target d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX := range mr.ds1.Tiles[tileY] {
|
|
|
|
worldX, worldY := mr.getTileWorldPosition(tileX, tileY)
|
|
|
|
if viewport.IsTileVisible(worldX, worldY) {
|
|
|
|
mr.renderTileDebug(int(worldX), int(worldY), debugVisLevel, viewport, target)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) renderTileDebug(x, y int, debugVisLevel int, viewport *Viewport, target d2render.Surface) {
|
2020-02-02 02:57:23 -05:00
|
|
|
ax := x - mr.tileRect.Left
|
|
|
|
ay := y - mr.tileRect.Top
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
if debugVisLevel > 0 {
|
2020-02-02 02:57:23 -05:00
|
|
|
if ay < 0 || ax < 0 || ay >= len(mr.ds1.Tiles) || x >= len(mr.ds1.Tiles[ay]) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
subTileColor := color.RGBA{R: 80, G: 80, B: 255, A: 50}
|
|
|
|
tileColor := color.RGBA{R: 255, G: 255, B: 255, A: 100}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
|
|
|
screenX1, screenY1 := viewport.WorldToScreen(float64(x), float64(y))
|
|
|
|
screenX2, screenY2 := viewport.WorldToScreen(float64(x+1), float64(y))
|
|
|
|
screenX3, screenY3 := viewport.WorldToScreen(float64(x), float64(y+1))
|
|
|
|
|
2019-12-28 16:46:08 -05:00
|
|
|
target.PushTranslation(screenX1, screenY1)
|
|
|
|
defer target.Pop()
|
|
|
|
|
|
|
|
target.DrawLine(screenX2-screenX1, screenY2-screenY1, tileColor)
|
|
|
|
target.DrawLine(screenX3-screenX1, screenY3-screenY1, tileColor)
|
|
|
|
target.PushTranslation(-10, 10)
|
|
|
|
target.DrawText("%v, %v", x, y)
|
|
|
|
target.Pop()
|
2019-12-13 00:33:11 -05:00
|
|
|
|
|
|
|
if debugVisLevel > 1 {
|
|
|
|
for i := 1; i <= 4; i++ {
|
2020-02-02 02:57:23 -05:00
|
|
|
x2 := i * 16
|
|
|
|
y2 := i * 8
|
2019-12-13 00:33:11 -05:00
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
target.PushTranslation(-x2, y2)
|
|
|
|
target.DrawLine(80, 40, subTileColor)
|
2019-12-28 16:46:08 -05:00
|
|
|
target.Pop()
|
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
target.PushTranslation(x2, y2)
|
|
|
|
target.DrawLine(-80, 40, subTileColor)
|
2019-12-28 16:46:08 -05:00
|
|
|
target.Pop()
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
|
|
|
|
2020-02-02 02:57:23 -05:00
|
|
|
tile := mr.ds1.Tiles[ay][ax]
|
2019-12-28 16:46:08 -05:00
|
|
|
for i, floor := range tile.Floors {
|
|
|
|
target.PushTranslation(-20, 10+(i+1)*14)
|
|
|
|
target.DrawText("f: %v-%v", floor.Style, floor.Sequence)
|
|
|
|
target.Pop()
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) generateTileCache() {
|
|
|
|
for tileY := range mr.ds1.Tiles {
|
|
|
|
for tileX := range mr.ds1.Tiles[tileY] {
|
|
|
|
tile := mr.ds1.Tiles[tileY][tileX]
|
|
|
|
|
|
|
|
for i := range tile.Floors {
|
|
|
|
if !tile.Floors[i].Hidden && tile.Floors[i].Prop1 != 0 {
|
|
|
|
mr.generateFloorCache(&tile.Floors[i], tileX, tileY)
|
2019-11-01 14:12:23 -04:00
|
|
|
}
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
|
|
|
|
for i := range tile.Shadows {
|
|
|
|
if !tile.Shadows[i].Hidden && tile.Shadows[i].Prop1 != 0 {
|
|
|
|
mr.generateShadowCache(&tile.Shadows[i], tileX, tileY)
|
2019-11-01 14:12:23 -04:00
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
2019-11-01 22:12:07 -04:00
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
for i := range tile.Walls {
|
|
|
|
if !tile.Walls[i].Hidden && tile.Walls[i].Prop1 != 0 {
|
|
|
|
mr.generateWallCache(&tile.Walls[i], tileX, tileY)
|
2019-11-01 14:12:23 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-01 16:51:50 -04:00
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) getImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte) d2render.Surface {
|
2019-12-13 00:33:11 -05:00
|
|
|
lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex)
|
|
|
|
return mr.imageCacheRecords[lookupIndex]
|
|
|
|
}
|
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
func (mr *MapRegion) setImageCacheRecord(style, sequence byte, tileType d2enum.TileType, randomIndex byte, image d2render.Surface) {
|
2019-12-13 00:33:11 -05:00
|
|
|
lookupIndex := uint32(style)<<24 | uint32(sequence)<<16 | uint32(tileType)<<8 | uint32(randomIndex)
|
|
|
|
mr.imageCacheRecords[lookupIndex] = image
|
|
|
|
}
|
|
|
|
|
|
|
|
func (mr *MapRegion) generateFloorCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) {
|
2020-02-02 02:57:23 -05:00
|
|
|
tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), 0)
|
2019-11-24 01:11:32 -05:00
|
|
|
var tileData []*d2dt1.Tile
|
|
|
|
var tileIndex byte
|
|
|
|
|
|
|
|
if tileOptions == nil {
|
2019-11-21 20:34:29 -05:00
|
|
|
log.Printf("Could not locate tile Style:%d, Seq: %d, Type: %d\n", tile.Style, tile.Sequence, 0)
|
2019-11-24 01:11:32 -05:00
|
|
|
tileData = append(tileData, &d2dt1.Tile{})
|
|
|
|
tileData[0].Width = 10
|
|
|
|
tileData[0].Height = 10
|
|
|
|
} else {
|
2019-12-19 20:01:00 -05:00
|
|
|
if !tileOptions[0].MaterialFlags.Lava {
|
2019-12-13 00:33:11 -05:00
|
|
|
tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed)
|
2019-11-24 01:11:32 -05:00
|
|
|
tileData = append(tileData, &tileOptions[tileIndex])
|
|
|
|
} else {
|
|
|
|
tile.Animated = true
|
|
|
|
for i := range tileOptions {
|
|
|
|
tileData = append(tileData, &tileOptions[i])
|
|
|
|
}
|
|
|
|
}
|
2019-11-17 14:21:48 -05:00
|
|
|
}
|
2019-11-24 01:11:32 -05:00
|
|
|
|
|
|
|
for i := range tileData {
|
2019-12-19 20:01:00 -05:00
|
|
|
if !tileData[i].MaterialFlags.Lava {
|
2019-11-24 01:11:32 -05:00
|
|
|
tile.RandomIndex = tileIndex
|
|
|
|
} else {
|
|
|
|
tileIndex = byte(tileData[i].RarityFrameIndex)
|
|
|
|
}
|
2019-12-13 00:33:11 -05:00
|
|
|
cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex)
|
2019-11-24 01:11:32 -05:00
|
|
|
if cachedImage != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
tileYMinimum := int32(0)
|
|
|
|
for _, block := range tileData[i].Blocks {
|
2020-02-01 21:06:22 -05:00
|
|
|
tileYMinimum = d2common.MinInt32(tileYMinimum, int32(block.Y))
|
2019-11-24 01:11:32 -05:00
|
|
|
}
|
2020-02-01 21:06:22 -05:00
|
|
|
tileYOffset := d2common.AbsInt32(tileYMinimum)
|
|
|
|
tileHeight := d2common.AbsInt32(tileData[i].Height)
|
2020-02-01 20:39:28 -05:00
|
|
|
_, image := d2render.NewSurface(int(tileData[i].Width), int(tileHeight), d2render.FilterNearest)
|
2019-11-24 01:11:32 -05:00
|
|
|
pixels := make([]byte, 4*tileData[i].Width*tileHeight)
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.decodeTileGfxData(tileData[i].Blocks, &pixels, tileYOffset, tileData[i].Width)
|
2019-11-24 01:11:32 -05:00
|
|
|
image.ReplacePixels(pixels)
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.setImageCacheRecord(tile.Style, tile.Sequence, 0, tileIndex, image)
|
2019-11-01 16:51:50 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) generateShadowCache(tile *d2ds1.FloorShadowRecord, tileX, tileY int) {
|
2020-02-02 02:57:23 -05:00
|
|
|
tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), 13)
|
2019-11-24 01:11:32 -05:00
|
|
|
var tileIndex byte
|
|
|
|
var tileData *d2dt1.Tile
|
|
|
|
if tileOptions == nil {
|
|
|
|
return
|
|
|
|
} else {
|
2019-12-13 00:33:11 -05:00
|
|
|
tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed)
|
2019-11-24 01:11:32 -05:00
|
|
|
tileData = &tileOptions[tileIndex]
|
2019-11-01 16:51:50 -04:00
|
|
|
}
|
2019-11-24 01:11:32 -05:00
|
|
|
|
2019-11-19 00:38:02 -05:00
|
|
|
tile.RandomIndex = tileIndex
|
2019-11-01 22:12:07 -04:00
|
|
|
tileMinY := int32(0)
|
|
|
|
tileMaxY := int32(0)
|
2019-11-01 16:51:50 -04:00
|
|
|
for _, block := range tileData.Blocks {
|
2020-02-01 21:06:22 -05:00
|
|
|
tileMinY = d2common.MinInt32(tileMinY, int32(block.Y))
|
|
|
|
tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32))
|
2019-11-01 16:51:50 -04:00
|
|
|
}
|
2019-11-01 22:12:07 -04:00
|
|
|
tileYOffset := -tileMinY
|
|
|
|
tileHeight := int(tileMaxY - tileMinY)
|
2019-11-21 19:54:56 -05:00
|
|
|
tile.YAdjust = int(tileMinY + 80)
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex)
|
2019-11-21 19:54:56 -05:00
|
|
|
if cachedImage != nil {
|
2019-11-24 01:11:32 -05:00
|
|
|
return
|
2019-11-21 19:54:56 -05:00
|
|
|
}
|
|
|
|
|
2020-02-01 21:51:49 -05:00
|
|
|
_, image := d2render.NewSurface(int(tileData.Width), tileHeight, d2render.FilterNearest)
|
2019-11-01 22:12:07 -04:00
|
|
|
pixels := make([]byte, 4*tileData.Width*int32(tileHeight))
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, tileData.Width)
|
2019-11-01 14:12:23 -04:00
|
|
|
image.ReplacePixels(pixels)
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.setImageCacheRecord(tile.Style, tile.Sequence, 13, tileIndex, image)
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) generateWallCache(tile *d2ds1.WallRecord, tileX, tileY int) {
|
2020-02-02 02:57:23 -05:00
|
|
|
tileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), int32(tile.Type))
|
2019-11-24 01:11:32 -05:00
|
|
|
var tileIndex byte
|
|
|
|
var tileData *d2dt1.Tile
|
|
|
|
if tileOptions == nil {
|
|
|
|
return
|
|
|
|
} else {
|
2019-12-13 00:33:11 -05:00
|
|
|
tileIndex = mr.getRandomTile(tileOptions, tileX, tileY, mr.seed)
|
2019-11-24 01:11:32 -05:00
|
|
|
tileData = &tileOptions[tileIndex]
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-19 00:38:02 -05:00
|
|
|
tile.RandomIndex = tileIndex
|
2019-11-10 10:44:13 -05:00
|
|
|
var newTileData *d2dt1.Tile = nil
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-21 20:34:29 -05:00
|
|
|
if tile.Type == 3 {
|
2020-02-02 02:57:23 -05:00
|
|
|
newTileOptions := mr.getTiles(int32(tile.Style), int32(tile.Sequence), int32(4))
|
2019-12-13 00:33:11 -05:00
|
|
|
newTileIndex := mr.getRandomTile(newTileOptions, tileX, tileY, mr.seed)
|
2019-11-24 01:11:32 -05:00
|
|
|
newTileData = &newTileOptions[newTileIndex]
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
tileMinY := int32(0)
|
|
|
|
tileMaxY := int32(0)
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-01 22:12:07 -04:00
|
|
|
target := tileData
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-01 22:12:07 -04:00
|
|
|
if newTileData != nil && newTileData.Height < tileData.Height {
|
|
|
|
target = newTileData
|
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-01 22:12:07 -04:00
|
|
|
for _, block := range target.Blocks {
|
2020-02-01 21:06:22 -05:00
|
|
|
tileMinY = d2common.MinInt32(tileMinY, int32(block.Y))
|
|
|
|
tileMaxY = d2common.MaxInt32(tileMaxY, int32(block.Y+32))
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2020-02-01 21:06:22 -05:00
|
|
|
realHeight := d2common.MaxInt32(d2common.AbsInt32(tileData.Height), tileMaxY-tileMinY)
|
2019-11-01 22:12:07 -04:00
|
|
|
tileYOffset := -tileMinY
|
|
|
|
//tileHeight := int(tileMaxY - tileMinY)
|
2019-11-17 14:21:48 -05:00
|
|
|
|
2019-11-21 20:34:29 -05:00
|
|
|
if tile.Type == 15 {
|
2019-11-21 19:54:56 -05:00
|
|
|
tile.YAdjust = -int(tileData.RoofHeight)
|
|
|
|
} else {
|
|
|
|
tile.YAdjust = int(tileMinY) + 80
|
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
cachedImage := mr.getImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex)
|
2019-11-17 14:21:48 -05:00
|
|
|
if cachedImage != nil {
|
2019-11-24 01:11:32 -05:00
|
|
|
return
|
2019-11-17 14:21:48 -05:00
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-17 16:06:02 -05:00
|
|
|
if realHeight == 0 {
|
|
|
|
log.Printf("Invalid 0 height for wall tile")
|
2019-11-24 01:11:32 -05:00
|
|
|
return
|
2019-11-17 16:06:02 -05:00
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2020-02-01 20:39:28 -05:00
|
|
|
_, image := d2render.NewSurface(160, int(realHeight), d2render.FilterNearest)
|
2019-11-01 22:12:07 -04:00
|
|
|
pixels := make([]byte, 4*160*realHeight)
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.decodeTileGfxData(tileData.Blocks, &pixels, tileYOffset, 160)
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-01 22:12:07 -04:00
|
|
|
if newTileData != nil {
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.decodeTileGfxData(newTileData.Blocks, &pixels, tileYOffset, 160)
|
2019-11-01 22:12:07 -04:00
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-11-11 23:48:55 -05:00
|
|
|
if err := image.ReplacePixels(pixels); err != nil {
|
|
|
|
log.Panicf(err.Error())
|
|
|
|
}
|
2019-11-21 19:54:56 -05:00
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
mr.setImageCacheRecord(tile.Style, tile.Sequence, tile.Type, tileIndex, image)
|
2019-11-17 14:21:48 -05:00
|
|
|
}
|
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
func (mr *MapRegion) decodeTileGfxData(blocks []d2dt1.Block, pixels *[]byte, tileYOffset int32, tileWidth int32) {
|
|
|
|
for _, block := range blocks {
|
|
|
|
if block.Format == d2dt1.BlockFormatIsometric {
|
|
|
|
// 3D isometric decoding
|
|
|
|
xjump := []int32{14, 12, 10, 8, 6, 4, 2, 0, 2, 4, 6, 8, 10, 12, 14}
|
|
|
|
nbpix := []int32{4, 8, 12, 16, 20, 24, 28, 32, 28, 24, 20, 16, 12, 8, 4}
|
|
|
|
blockX := int32(block.X)
|
|
|
|
blockY := int32(block.Y)
|
|
|
|
length := int32(256)
|
|
|
|
x := int32(0)
|
|
|
|
y := int32(0)
|
|
|
|
idx := 0
|
|
|
|
for length > 0 {
|
|
|
|
x = xjump[y]
|
|
|
|
n := nbpix[y]
|
|
|
|
length -= n
|
|
|
|
for n > 0 {
|
|
|
|
colorIndex := block.EncodedData[idx]
|
|
|
|
if colorIndex != 0 {
|
|
|
|
pixelColor := mr.palette.Colors[colorIndex]
|
|
|
|
offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x))
|
|
|
|
(*pixels)[offset] = pixelColor.R
|
|
|
|
(*pixels)[offset+1] = pixelColor.G
|
|
|
|
(*pixels)[offset+2] = pixelColor.B
|
|
|
|
(*pixels)[offset+3] = 255
|
|
|
|
}
|
|
|
|
x++
|
|
|
|
n--
|
|
|
|
idx++
|
|
|
|
}
|
|
|
|
y++
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// RLE Encoding
|
|
|
|
blockX := int32(block.X)
|
|
|
|
blockY := int32(block.Y)
|
|
|
|
x := int32(0)
|
|
|
|
y := int32(0)
|
|
|
|
idx := 0
|
|
|
|
length := block.Length
|
|
|
|
for length > 0 {
|
|
|
|
b1 := block.EncodedData[idx]
|
|
|
|
b2 := block.EncodedData[idx+1]
|
|
|
|
idx += 2
|
|
|
|
length -= 2
|
|
|
|
if (b1 | b2) == 0 {
|
|
|
|
x = 0
|
|
|
|
y++
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
x += int32(b1)
|
|
|
|
length -= int32(b2)
|
|
|
|
for b2 > 0 {
|
|
|
|
colorIndex := block.EncodedData[idx]
|
|
|
|
if colorIndex != 0 {
|
|
|
|
pixelColor := mr.palette.Colors[colorIndex]
|
|
|
|
offset := 4 * (((blockY + y + tileYOffset) * tileWidth) + (blockX + x))
|
|
|
|
(*pixels)[offset] = pixelColor.R
|
|
|
|
(*pixels)[offset+1] = pixelColor.G
|
|
|
|
(*pixels)[offset+2] = pixelColor.B
|
|
|
|
(*pixels)[offset+3] = 255
|
2019-11-17 14:21:48 -05:00
|
|
|
|
2019-12-13 00:33:11 -05:00
|
|
|
}
|
|
|
|
idx++
|
|
|
|
x++
|
|
|
|
b2--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-10-31 13:39:05 -04:00
|
|
|
}
|