From fe47e513511023cb49683808d0504fefce6324b6 Mon Sep 17 00:00:00 2001 From: dk Date: Fri, 26 Jun 2020 13:50:24 -0700 Subject: [PATCH] Refactor d2map (#468) * WIP refactor of d2map stuff * more d2map refactor adding realm init to game client passing map engine from client and server into realm at init change `generate map packet` to have act and level index as data * client explodes, but getting there * realm now initializes, networking works, but map generators dont currently do anything * changed the way that level type records are loaded * fixed funcs for level data lookups * started implementing level generator, currently crashing * client no longer exploding * d2networking refactor put exports into d2client.go and d2server.go kept GameClient and GameServer methods into their respective files made methods for packet handlers instead of the giant switch statements * bugfix: getting first level id by act * minor refactor of gamescreen for readability * towns now generate on server start, create player takes act and level id as args, levels have their own map engine --- d2common/d2data/d2datadict/level_presets.go | 10 +- d2common/d2data/d2datadict/level_sub.go | 1 + d2common/d2data/d2datadict/level_types.go | 87 ++++--- d2common/d2data/d2datadict/levels.go | 195 +++++++++----- d2common/d2enum/level_generation_types.go | 3 +- d2core/d2config/config.go | 1 + d2core/d2config/d2config.go | 1 + d2core/d2map/d2mapengine/act.go | 60 +++++ d2core/d2map/d2mapengine/engine.go | 22 +- d2core/d2map/d2mapengine/generator.go | 6 + d2core/d2map/d2mapengine/generator_maze.go | 15 ++ d2core/d2map/d2mapengine/generator_preset.go | 66 +++++ .../d2map/d2mapengine/generator_wilderness.go | 15 ++ d2core/d2map/d2mapengine/level.go | 79 ++++++ d2core/d2map/d2mapengine/realm.go | 114 ++++++++ d2core/d2map/d2mapentity/player.go | 2 +- d2core/d2map/d2mapgen/act1_overworld.go | 44 ++-- d2core/d2map/d2mapstamp/stamp.go | 14 +- d2game/d2gamescreen/character_select.go | 25 +- d2game/d2gamescreen/game.go | 48 ++-- d2game/d2player/player_state.go | 12 +- d2networking/d2client/d2client.go | 38 +++ d2networking/d2client/game_client.go | 180 ++++++++----- d2networking/d2netpacket/packet_add_player.go | 8 +- .../d2netpacket/packet_generate_map.go | 9 +- d2networking/d2server/connection_manager.go | 21 +- d2networking/d2server/d2server.go | 243 ++++++++++++++++++ d2networking/d2server/game_server.go | 214 +++------------ 28 files changed, 1100 insertions(+), 433 deletions(-) create mode 100644 d2core/d2map/d2mapengine/act.go create mode 100644 d2core/d2map/d2mapengine/generator.go create mode 100644 d2core/d2map/d2mapengine/generator_maze.go create mode 100644 d2core/d2map/d2mapengine/generator_preset.go create mode 100644 d2core/d2map/d2mapengine/generator_wilderness.go create mode 100644 d2core/d2map/d2mapengine/level.go create mode 100644 d2core/d2map/d2mapengine/realm.go create mode 100644 d2networking/d2client/d2client.go create mode 100644 d2networking/d2server/d2server.go diff --git a/d2common/d2data/d2datadict/level_presets.go b/d2common/d2data/d2datadict/level_presets.go index ca33891f..30a2db5e 100644 --- a/d2common/d2data/d2datadict/level_presets.go +++ b/d2common/d2data/d2datadict/level_presets.go @@ -31,13 +31,13 @@ type LevelPresetRecord struct { } // CreateLevelPresetRecord parses a row from lvlprest.txt into a LevelPresetRecord -func createLevelPresetRecord(props []string) LevelPresetRecord { +func createLevelPresetRecord(props []string) *LevelPresetRecord { i := -1 inc := func() int { i++ return i } - result := LevelPresetRecord{ + result := &LevelPresetRecord{ Name: props[inc()], DefinitionId: d2common.StringToInt(props[inc()]), LevelId: d2common.StringToInt(props[inc()]), @@ -69,10 +69,10 @@ func createLevelPresetRecord(props []string) LevelPresetRecord { return result } -var LevelPresets map[int]LevelPresetRecord +var LevelPresets map[int]*LevelPresetRecord func LoadLevelPresets(file []byte) { - LevelPresets = make(map[int]LevelPresetRecord) + LevelPresets = make(map[int]*LevelPresetRecord) data := strings.Split(string(file), "\r\n")[1:] for _, line := range data { if len(line) == 0 { @@ -88,7 +88,7 @@ func LoadLevelPresets(file []byte) { log.Printf("Loaded %d level presets", len(LevelPresets)) } -func LevelPreset(id int) LevelPresetRecord { +func LevelPreset(id int) *LevelPresetRecord { for i := 0; i < len(LevelPresets); i++ { if LevelPresets[i].DefinitionId == id { return LevelPresets[i] diff --git a/d2common/d2data/d2datadict/level_sub.go b/d2common/d2data/d2datadict/level_sub.go index e8e524b4..b880f87d 100644 --- a/d2common/d2data/d2datadict/level_sub.go +++ b/d2common/d2data/d2datadict/level_sub.go @@ -9,6 +9,7 @@ import ( type LevelSubstitutionRecord struct { // Description, reference only. Name string // Name + // This value is used in Levels.txt, in the column 'SubType'. You'll notice // that in LvlSub.txt some rows use the same value, we can say they forms // groups. If you count each row of a group starting from 0, then you'll diff --git a/d2common/d2data/d2datadict/level_types.go b/d2common/d2data/d2datadict/level_types.go index b7a576b8..f3e357c5 100644 --- a/d2common/d2data/d2datadict/level_types.go +++ b/d2common/d2data/d2datadict/level_types.go @@ -2,51 +2,68 @@ package d2datadict import ( "log" - "strings" "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" ) type LevelTypeRecord struct { - Name string - Id int - Files [32]string - Beta bool - Act int - Expansion bool + Name string // Name + Id int // Id + Files []string // File 1 -- File 32 + Beta bool // Beta + Act int // Act + Expansion bool // Expansion } -var LevelTypes []LevelTypeRecord +var LevelTypes map[d2enum.RegionIdType]*LevelTypeRecord func LoadLevelTypes(file []byte) { - data := strings.Split(string(file), "\r\n")[1:] - LevelTypes = make([]LevelTypeRecord, len(data)) - for i, j := 0, 0; i < len(data); i, j = i+1, j+1 { - idx := -1 - inc := func() int { - idx++ - return idx + LevelTypes = make(map[d2enum.RegionIdType]*LevelTypeRecord) + dict := d2common.LoadDataDictionary(string(file)) + for idx := range dict.Data { + record := &LevelTypeRecord{ + Name: dict.GetString("Name", idx), + Id: dict.GetNumber("Id", idx), + Files: []string{ + dict.GetString("File 1", idx), + dict.GetString("File 2", idx), + dict.GetString("File 3", idx), + dict.GetString("File 4", idx), + dict.GetString("File 5", idx), + dict.GetString("File 6", idx), + dict.GetString("File 7", idx), + dict.GetString("File 8", idx), + dict.GetString("File 9", idx), + dict.GetString("File 10", idx), + dict.GetString("File 11", idx), + dict.GetString("File 12", idx), + dict.GetString("File 13", idx), + dict.GetString("File 14", idx), + dict.GetString("File 15", idx), + dict.GetString("File 16", idx), + dict.GetString("File 17", idx), + dict.GetString("File 18", idx), + dict.GetString("File 19", idx), + dict.GetString("File 20", idx), + dict.GetString("File 21", idx), + dict.GetString("File 22", idx), + dict.GetString("File 23", idx), + dict.GetString("File 24", idx), + dict.GetString("File 25", idx), + dict.GetString("File 26", idx), + dict.GetString("File 27", idx), + dict.GetString("File 28", idx), + dict.GetString("File 29", idx), + dict.GetString("File 30", idx), + dict.GetString("File 31", idx), + dict.GetString("File 32", idx), + }, + Beta: dict.GetNumber("Beta", idx) > 0, + Act: dict.GetNumber("Act", idx), + Expansion: dict.GetNumber("Expansion", idx) > 0, } - if len(data[i]) == 0 { - continue - } - parts := strings.Split(data[i], "\t") - if parts[0] == "Expansion" { - j-- - continue - } - LevelTypes[j].Name = parts[inc()] - LevelTypes[j].Id = d2common.StringToInt(parts[inc()]) - for fileIdx := range LevelTypes[i].Files { - LevelTypes[j].Files[fileIdx] = parts[inc()] - if LevelTypes[j].Files[fileIdx] == "0" { - LevelTypes[j].Files[fileIdx] = "" - } - - } - LevelTypes[j].Beta = parts[inc()] != "1" - LevelTypes[j].Act = d2common.StringToInt(parts[inc()]) - LevelTypes[j].Expansion = parts[inc()] != "1" + LevelTypes[d2enum.RegionIdType(record.Id)] = record } log.Printf("Loaded %d LevelType records", len(LevelTypes)) } diff --git a/d2common/d2data/d2datadict/levels.go b/d2common/d2data/d2datadict/levels.go index 280d45e6..f1b937b0 100644 --- a/d2common/d2data/d2datadict/levels.go +++ b/d2common/d2data/d2datadict/levels.go @@ -38,7 +38,7 @@ type LevelDetailsRecord struct { AutomapIndex int // Layer // sizeX - SizeY in each difficuly. If this is a preset area this sets the - // X size for the area. Othervise use the same value here that are used in + // X size for the area. Otherwise use the same value here that are used in // lvlprest.txt to set the size for the .ds1 file. SizeXNormal int // SizeX SizeYNormal int // SizeY @@ -141,28 +141,14 @@ type LevelDetailsRecord struct { // linked with, but the actuall number of Vis ( 0 - 7 ) is determined by // your actual map (the .ds1 fle). // Example: Normally Cave levels are only using vis 0-3 and wilderness areas 4-7 . - LevelLinkId0 int // Vis0 - LevelLinkId1 int // Vis1 - LevelLinkId2 int // Vis2 - LevelLinkId3 int // Vis3 - LevelLinkId4 int // Vis4 - LevelLinkId5 int // Vis5 - LevelLinkId6 int // Vis6 - LevelLinkId7 int // Vis7 + WarpLevelId []int // Vis0 -- Vis7 // This controls the visual graphics then you move the mouse pointer over // an entrance. To show the graphics you use an ID from lvlwarp.txt and the // behavior on the graphics is controlled by lvlwarp.txt. Your Warps must // match your Vis. // Example: If your level uses Vis 3,5,7 then you must also use Warp 3,5,7 . - WarpGraphicsId0 int // Warp0 - WarpGraphicsId1 int // Warp1 - WarpGraphicsId2 int // Warp2 - WarpGraphicsId3 int // Warp3 - WarpGraphicsId4 int // Warp4 - WarpGraphicsId5 int // Warp5 - WarpGraphicsId6 int // Warp6 - WarpGraphicsId7 int // Warp7 + WarpGraphicsId []int // Warp0 -- Warp7 // These settings handle the light intensity as well as its RGB components LightIntensity int // Intensity @@ -377,7 +363,7 @@ type LevelDetailsRecord struct { var LevelDetails map[int]*LevelDetailsRecord -func GetLevelDetails(id int) *LevelDetailsRecord { +func GetLevelDetailsByLevelId(id int) *LevelDetailsRecord { for i := 0; i < len(LevelDetails); i++ { if LevelDetails[i].Id == id { return LevelDetails[i] @@ -387,60 +373,138 @@ func GetLevelDetails(id int) *LevelDetailsRecord { return nil } +func GetLevelDetailsByActId(act int) []*LevelDetailsRecord { + result := make([]*LevelDetailsRecord, 0) + for _, record := range LevelDetails { + if act == record.Act { + result = append(result, record) + } + } + return result +} + +var actIds []int + +func GetNumberOfActs() int { + return len(actIds) +} + +func GetActIds() []int { + return actIds +} + +func GetLevelWarpsByLevelId(id int) []*LevelWarpRecord { + result := make([]*LevelWarpRecord, 0) + level := LevelDetails[id] + for _, warpId := range level.WarpLevelId { + if warpId < 0 { + continue // there are -1 values for empty entries in the table + } + result = append(result, LevelWarps[warpId]) + } + return result +} + +func GetLevelPresetByLevelId(id int) *LevelPresetRecord { + for recordId, record := range LevelPresets { + if id == recordId { + return record + } + } + panic("couldn't find a preset.") +} + +func GetFirstLevelIdByActId(actId int) int { + recordsForAct := GetLevelDetailsByActId(actId) + lowest := -1 + if len(recordsForAct) > 0 { + for _, record := range recordsForAct { + // need to account for level ID 0 which is an empty map in act 1 + if record.Id == 0 { + continue + } + if lowest < 0 { + lowest = record.Id + continue + } + if record.Id < lowest { + lowest = record.Id + } + } + return lowest + } + return 0 +} + +func AppendIfMissing(slice []int, i int) []int { + for _, ele := range slice { + if ele == i { + return slice + } + } + return append(slice, i) +} + func LoadLevelDetails(file []byte) { dict := d2common.LoadDataDictionary(string(file)) numRecords := len(dict.Data) LevelDetails = make(map[int]*LevelDetailsRecord, numRecords) + actIds = make([]int, 0) + for idx := range dict.Data { record := &LevelDetailsRecord{ - Name: dict.GetString("Name ", idx), - Id: dict.GetNumber("Id", idx), - Palette: dict.GetNumber("Pal", idx), - Act: dict.GetNumber("Act", idx), - QuestFlag: dict.GetNumber("QuestFlag", idx), - QuestFlagExpansion: dict.GetNumber("QuestFlagEx", idx), - AutomapIndex: dict.GetNumber("Layer", idx), - SizeXNormal: dict.GetNumber("SizeX", idx), - SizeYNormal: dict.GetNumber("SizeY", idx), - SizeXNightmare: dict.GetNumber("SizeX(N)", idx), - SizeYNightmare: dict.GetNumber("SizeY(N)", idx), - SizeXHell: dict.GetNumber("SizeX(H)", idx), - SizeYHell: dict.GetNumber("SizeY(H)", idx), - WorldOffsetX: dict.GetNumber("OffsetX", idx), - WorldOffsetY: dict.GetNumber("OffsetY", idx), - DependantLevelID: dict.GetNumber("Depend", idx), - TeleportFlag: d2enum.TeleportFlag(dict.GetNumber("Teleport", idx)), - EnableRain: dict.GetNumber("Rain", idx) > 0, - EnableMud: dict.GetNumber("Mud", idx) > 0, - EnablePerspective: dict.GetNumber("NoPer", idx) > 0, - EnableLineOfSightDraw: dict.GetNumber("LOSDraw", idx) > 0, - EnableFloorFliter: dict.GetNumber("FloorFilter", idx) > 0, - EnableBlankScreen: dict.GetNumber("BlankScreen", idx) > 0, - EnableDrawEdges: dict.GetNumber("DrawEdges", idx) > 0, - IsInside: dict.GetNumber("IsInside", idx) > 0, - LevelGenerationType: d2enum.LevelGenerationType(dict.GetNumber("DrlgType", idx)), - LevelType: dict.GetNumber("LevelType", idx), - SubType: dict.GetNumber("SubType", idx), - SubTheme: dict.GetNumber("SubTheme", idx), - SubWaypoint: dict.GetNumber("SubWaypoint", idx), - SubShrine: dict.GetNumber("SubShrine", idx), - LevelLinkId0: dict.GetNumber("Vis0", idx), - LevelLinkId1: dict.GetNumber("Vis1", idx), - LevelLinkId2: dict.GetNumber("Vis2", idx), - LevelLinkId3: dict.GetNumber("Vis3", idx), - LevelLinkId4: dict.GetNumber("Vis4", idx), - LevelLinkId5: dict.GetNumber("Vis5", idx), - LevelLinkId6: dict.GetNumber("Vis6", idx), - LevelLinkId7: dict.GetNumber("Vis7", idx), - WarpGraphicsId0: dict.GetNumber("Warp0", idx), - WarpGraphicsId1: dict.GetNumber("Warp1", idx), - WarpGraphicsId2: dict.GetNumber("Warp2", idx), - WarpGraphicsId3: dict.GetNumber("Warp3", idx), - WarpGraphicsId4: dict.GetNumber("Warp4", idx), - WarpGraphicsId5: dict.GetNumber("Warp5", idx), - WarpGraphicsId6: dict.GetNumber("Warp6", idx), - WarpGraphicsId7: dict.GetNumber("Warp7", idx), + Name: dict.GetString("Name ", idx), + Id: dict.GetNumber("Id", idx), + Palette: dict.GetNumber("Pal", idx), + Act: dict.GetNumber("Act", idx), + QuestFlag: dict.GetNumber("QuestFlag", idx), + QuestFlagExpansion: dict.GetNumber("QuestFlagEx", idx), + AutomapIndex: dict.GetNumber("Layer", idx), + SizeXNormal: dict.GetNumber("SizeX", idx), + SizeYNormal: dict.GetNumber("SizeY", idx), + SizeXNightmare: dict.GetNumber("SizeX(N)", idx), + SizeYNightmare: dict.GetNumber("SizeY(N)", idx), + SizeXHell: dict.GetNumber("SizeX(H)", idx), + SizeYHell: dict.GetNumber("SizeY(H)", idx), + WorldOffsetX: dict.GetNumber("OffsetX", idx), + WorldOffsetY: dict.GetNumber("OffsetY", idx), + DependantLevelID: dict.GetNumber("Depend", idx), + TeleportFlag: d2enum.TeleportFlag(dict.GetNumber("Teleport", idx)), + EnableRain: dict.GetNumber("Rain", idx) > 0, + EnableMud: dict.GetNumber("Mud", idx) > 0, + EnablePerspective: dict.GetNumber("NoPer", idx) > 0, + EnableLineOfSightDraw: dict.GetNumber("LOSDraw", idx) > 0, + EnableFloorFliter: dict.GetNumber("FloorFilter", idx) > 0, + EnableBlankScreen: dict.GetNumber("BlankScreen", idx) > 0, + EnableDrawEdges: dict.GetNumber("DrawEdges", idx) > 0, + IsInside: dict.GetNumber("IsInside", idx) > 0, + LevelGenerationType: d2enum.LevelGenerationType(dict.GetNumber("DrlgType", idx)), + LevelType: dict.GetNumber("LevelType", idx), + SubType: dict.GetNumber("SubType", idx), + SubTheme: dict.GetNumber("SubTheme", idx), + SubWaypoint: dict.GetNumber("SubWaypoint", idx), + SubShrine: dict.GetNumber("SubShrine", idx), + WarpLevelId: []int{ + dict.GetNumber("Vis0", idx), + dict.GetNumber("Vis1", idx), + dict.GetNumber("Vis2", idx), + dict.GetNumber("Vis3", idx), + dict.GetNumber("Vis4", idx), + dict.GetNumber("Vis5", idx), + dict.GetNumber("Vis6", idx), + dict.GetNumber("Vis7", idx), + }, + WarpGraphicsId: []int{ + dict.GetNumber("Vis0", idx), + dict.GetNumber("Vis1", idx), + dict.GetNumber("Vis2", idx), + dict.GetNumber("Vis3", idx), + dict.GetNumber("Vis4", idx), + dict.GetNumber("Vis5", idx), + dict.GetNumber("Vis6", idx), + dict.GetNumber("Vis7", idx), + }, LightIntensity: dict.GetNumber("Intensity", idx), Red: dict.GetNumber("Red", idx), Green: dict.GetNumber("Green", idx), @@ -540,6 +604,7 @@ func LoadLevelDetails(file []byte) { ObjectGroupSpawnChance6: dict.GetNumber("ObjPrb6", idx), ObjectGroupSpawnChance7: dict.GetNumber("ObjPrb7", idx), } + actIds = AppendIfMissing(actIds, record.Act) LevelDetails[idx] = record } log.Printf("Loaded %d LevelDetails records", len(LevelDetails)) diff --git a/d2common/d2enum/level_generation_types.go b/d2common/d2enum/level_generation_types.go index 7f11bee1..b98c4dc2 100644 --- a/d2common/d2enum/level_generation_types.go +++ b/d2common/d2enum/level_generation_types.go @@ -11,7 +11,8 @@ package d2enum type LevelGenerationType int const ( - LevelTypeRandomMaze LevelGenerationType = iota + LevelTypeNone LevelGenerationType = iota + LevelTypeRandomMaze LevelTypePreset LevelTypeWilderness ) diff --git a/d2core/d2config/config.go b/d2core/d2config/config.go index aefdfa80..b7293b27 100644 --- a/d2core/d2config/config.go +++ b/d2core/d2config/config.go @@ -18,6 +18,7 @@ func getDefaultConfig() *Configuration { VsyncEnabled: true, SfxVolume: 1.0, BgmVolume: 0.3, + MaxConnections: 8, MpqPath: "C:/Program Files (x86)/Diablo II", MpqLoadOrder: []string{ "Patch_D2.mpq", diff --git a/d2core/d2config/d2config.go b/d2core/d2config/d2config.go index 665f6ad6..dbf8005d 100644 --- a/d2core/d2config/d2config.go +++ b/d2core/d2config/d2config.go @@ -16,6 +16,7 @@ type Configuration struct { FullScreen bool RunInBackground bool VsyncEnabled bool + MaxConnections int } var singleton = getDefaultConfig() diff --git a/d2core/d2map/d2mapengine/act.go b/d2core/d2map/d2mapengine/act.go new file mode 100644 index 00000000..63ef8fde --- /dev/null +++ b/d2core/d2map/d2mapengine/act.go @@ -0,0 +1,60 @@ +package d2mapengine + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + // "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) + +type MapAct struct { + realm *MapRealm + id int + levels map[int]*MapLevel +} + +func (act *MapAct) isActive() bool { + for _, level := range act.levels { + if level.isActive() { + return true + } + } + return false +} + +func (act *MapAct) Advance(elapsed float64) { + if !act.isActive() { + return + } + for _, level := range act.levels { + level.Advance(elapsed) + } +} + +func (act *MapAct) Init(realm *MapRealm, actIndex int) { + act.realm = realm + act.levels = make(map[int]*MapLevel) + act.id = actIndex + + actLevelRecords := d2datadict.GetLevelDetailsByActId(actIndex) + + log.Printf("Initializing Act %d", actIndex) + for _, record := range actLevelRecords { + level := &MapLevel{} + levelId := record.Id + level.Init(act, levelId) + act.levels[levelId] = level + } + + act.GenerateTown() // ensures that starting point is known for first player +} + +func (act *MapAct) GenerateTown() { + townId := d2datadict.GetFirstLevelIdByActId(act.id) + act.levels[townId].GenerateMap() +} + +func (act *MapAct) GenerateMap(levelId int) { + log.Printf("Generating map in Act %d", act.id) + act.levels[levelId].GenerateMap() +} diff --git a/d2core/d2map/d2mapengine/engine.go b/d2core/d2map/d2mapengine/engine.go index 1fae3662..07d34a4e 100644 --- a/d2core/d2map/d2mapengine/engine.go +++ b/d2core/d2map/d2mapengine/engine.go @@ -20,16 +20,16 @@ import ( // Represents the map data for a specific location type MapEngine struct { - seed int64 // The map seed - entities []d2mapentity.MapEntity // Entities on the map - tiles []d2ds1.TileRecord // The map tiles - size d2common.Size // The size of the map, in tiles - levelType d2datadict.LevelTypeRecord // The level type of this map - dt1TileData []d2dt1.Tile // The DT1 tile data - walkMesh []d2common.PathTile // The walk mesh - startSubTileX int // The starting X position - startSubTileY int // The starting Y position - dt1Files []string // The list of DS1 strings + seed int64 // The map seed + entities []d2mapentity.MapEntity // Entities on the map + tiles []d2ds1.TileRecord // The map tiles + size d2common.Size // The size of the map, in tiles + levelType *d2datadict.LevelTypeRecord // The level type of this map + dt1TileData []d2dt1.Tile // The DT1 tile data + walkMesh []d2common.PathTile // The walk mesh + startSubTileX int // The starting X position + startSubTileY int // The starting Y position + dt1Files []string // The list of DS1 strings } // Creates a new instance of the map engine @@ -114,7 +114,7 @@ func (m *MapEngine) FindTile(style, sequence, tileType int32) d2dt1.Tile { } // Returns the level type of this map -func (m *MapEngine) LevelType() d2datadict.LevelTypeRecord { +func (m *MapEngine) LevelType() *d2datadict.LevelTypeRecord { return m.levelType } diff --git a/d2core/d2map/d2mapengine/generator.go b/d2core/d2map/d2mapengine/generator.go new file mode 100644 index 00000000..ac935565 --- /dev/null +++ b/d2core/d2map/d2mapengine/generator.go @@ -0,0 +1,6 @@ +package d2mapengine + +type MapGenerator interface { + init(seed int64, level *MapLevel, engine *MapEngine) + generate() +} diff --git a/d2core/d2map/d2mapengine/generator_maze.go b/d2core/d2map/d2mapengine/generator_maze.go new file mode 100644 index 00000000..8958a871 --- /dev/null +++ b/d2core/d2map/d2mapengine/generator_maze.go @@ -0,0 +1,15 @@ +package d2mapengine + +type MapGeneratorMaze struct { + seed int64 + level *MapLevel + engine *MapEngine +} + +func (m *MapGeneratorMaze) init(s int64, l *MapLevel, e *MapEngine) { + m.seed = s + m.level = l + m.engine = e +} + +func (m *MapGeneratorMaze) generate() {} diff --git a/d2core/d2map/d2mapengine/generator_preset.go b/d2core/d2map/d2mapengine/generator_preset.go new file mode 100644 index 00000000..9fe3e9a2 --- /dev/null +++ b/d2core/d2map/d2mapengine/generator_preset.go @@ -0,0 +1,66 @@ +package d2mapengine + +import ( + "log" + "math/rand" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp" +) + +type MapGeneratorPreset struct { + seed int64 + level *MapLevel + engine *MapEngine +} + +func (m *MapGeneratorPreset) init(s int64, l *MapLevel, e *MapEngine) { + m.seed = s + m.level = l + m.engine = e +} + +func (m *MapGeneratorPreset) generate() { + rand.Seed(m.seed) + + ////////////////////////////////////////////////////////////////////// FIXME + // TODO: we need to set the difficulty level of the realm in order to pull + // the right data from level details. testing this for now with normal diff + // NOTE: we would be setting difficulty level in the realm when a host + // is connected (the first player) + diffTestKey := "Normal" + m.level.act.realm.difficulty = d2datadict.DifficultyLevels[diffTestKey] // hack + //////////////////////////////////////////////////////////////////////////// + + difficulty := m.level.act.realm.difficulty + details := m.level.details + + tileW, tileH := 0, 0 + switch difficulty.Name { + case "Normal": + tileW = details.SizeXNormal + tileH = details.SizeYNormal + case "Nightmare": + tileW = details.SizeXNightmare + tileH = details.SizeYNightmare + case "Hell": + tileW = details.SizeXHell + tileH = details.SizeYHell + } + + // TODO: we shouldn't need to cast this to a RegionIdType + // In the long run, we aren't going to be using hardcoded enumerations + // we had initially made a list of them for testing, but not necessary now + levelTypeId := d2enum.RegionIdType(m.level.details.LevelType) + levelPresetId := m.level.preset.DefinitionId + + m.engine.ResetMap(levelTypeId, tileW+1, tileH+1) + m.engine.levelType = m.level.types + + stamp := d2mapstamp.LoadStamp(levelTypeId, levelPresetId, -1) + stampRegionPath := stamp.RegionPath() + log.Printf("Region Path: %s", stampRegionPath) + + m.engine.PlaceStamp(stamp, 0, 0) +} diff --git a/d2core/d2map/d2mapengine/generator_wilderness.go b/d2core/d2map/d2mapengine/generator_wilderness.go new file mode 100644 index 00000000..363d40e2 --- /dev/null +++ b/d2core/d2map/d2mapengine/generator_wilderness.go @@ -0,0 +1,15 @@ +package d2mapengine + +type MapGeneratorWilderness struct { + seed int64 + level *MapLevel + engine *MapEngine +} + +func (m *MapGeneratorWilderness) init(s int64, l *MapLevel, e *MapEngine) { + m.seed = s + m.level = l + m.engine = e +} + +func (m *MapGeneratorWilderness) generate() {} diff --git a/d2core/d2map/d2mapengine/level.go b/d2core/d2map/d2mapengine/level.go new file mode 100644 index 00000000..2279a82f --- /dev/null +++ b/d2core/d2map/d2mapengine/level.go @@ -0,0 +1,79 @@ +package d2mapengine + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" +) + +type MapLevel struct { + act *MapAct + details *d2datadict.LevelDetailsRecord + preset *d2datadict.LevelPresetRecord + warps []*d2datadict.LevelWarpRecord + substitutions *d2datadict.LevelSubstitutionRecord + types *d2datadict.LevelTypeRecord + generator MapGenerator + mapEngine *MapEngine + isInit bool + isGenerated bool +} + +func (level *MapLevel) isActive() bool { + // TODO: a level is active only if there is a player in the level + // or in an adjacent level + return true +} + +func (level *MapLevel) Advance(elapsed float64) { + if !level.isActive() { + return + } + level.mapEngine.Advance(elapsed) +} + +func (level *MapLevel) Init(act *MapAct, levelId int) { + if level.isInit { + return + } + if levelId < 1 { + levelId = 1 // there is a Nonetype map at index 0 in levels.txt + } + level.act = act + level.details = d2datadict.GetLevelDetailsByLevelId(levelId) + level.preset = d2datadict.GetLevelPresetByLevelId(levelId) + level.warps = d2datadict.GetLevelWarpsByLevelId(levelId) + level.substitutions = d2datadict.LevelSubstitutions[level.details.SubType] + level.types = d2datadict.LevelTypes[d2enum.RegionIdType(level.details.LevelType)] + level.isInit = true + level.mapEngine = &MapEngine{} + level.mapEngine.seed = level.act.realm.seed + + switch level.details.LevelGenerationType { + case d2enum.LevelTypeNone: + level.generator = nil + case d2enum.LevelTypeRandomMaze: + level.generator = &MapGeneratorMaze{} + case d2enum.LevelTypeWilderness: + level.generator = &MapGeneratorWilderness{} + case d2enum.LevelTypePreset: + level.generator = &MapGeneratorPreset{} + } + + seed := act.realm.seed + if level.generator != nil { + log.Printf("Initializing Level: %s", level.details.Name) + level.generator.init(seed, level, level.mapEngine) + } +} + +func (level *MapLevel) GenerateMap() { + if level.isGenerated { + return + } + log.Printf("Generating Level: %s", level.details.Name) + level.generator.generate() + level.mapEngine.RegenerateWalkPaths() + level.isGenerated = true +} diff --git a/d2core/d2map/d2mapengine/realm.go b/d2core/d2map/d2mapengine/realm.go new file mode 100644 index 00000000..33edc914 --- /dev/null +++ b/d2core/d2map/d2mapengine/realm.go @@ -0,0 +1,114 @@ +package d2mapengine + +import ( + "log" + + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" +) + +/* + A MapRealm represents the state of the maps/levels/quests for a server + + A MapRealm has MapActs + A MapAct has MapLevels + A MapLevel has: + a MapEngine + a MapGenerator for the level + data records from the txt files for the level + + The MapRealm is created by the game server + + The first player to connect to the realm becomes the host + The host determines the difficulty and which quests are completed + + The Realm, Acts, and Levels do not advance unless they are `active` + Nothing happens in a realm unless it is active + Levels do not generate maps until the level becomes `active` + + A Level is active if a player is within it OR in an adjacent level + An Act is active if one of its levels is active + The Realm is active if and only if one of its Acts is active +*/ +type MapRealm struct { + seed int64 + difficulty *d2datadict.DifficultyLevelRecord + acts map[int]*MapAct + players map[string]string + host string +} + +// Checks if the realm is in an active state +func (realm *MapRealm) isActive() bool { + return realm.hasActiveActs() +} + +// Checks if there is an active act +func (realm *MapRealm) hasActiveActs() bool { + for _, act := range realm.acts { + if act.isActive() { + return true + } + } + return false +} + +// Advances the realm, which advances the acts, which advances the levels... +func (realm *MapRealm) Advance(elapsed float64) { + if !realm.isActive() { + return + } + for _, act := range realm.acts { + act.Advance(elapsed) + } +} + +// Sets the host of the realm, which determines quest availability for players +func (realm *MapRealm) SetHost(id string) { + if player, found := realm.players[id]; found { + realm.host = player + log.Printf("Host is now %s", id) + } +} + +// Adds a player to the realm +func (realm *MapRealm) AddPlayer(id string, actId int) { + realm.players[id] = id + if realm.host == "" { + realm.SetHost(id) + } +} + +// Removes a player from the realm +func (realm *MapRealm) RemovePlayer(id string) { + delete(realm.players, id) +} + +// Initialize the realm +func (realm *MapRealm) Init(seed int64) { + // realm.playerStates = make(map[string]*d2mapentitiy.Player) + + log.Printf("Initializing Realm...") + realm.seed = seed + actIds := d2datadict.GetActIds() + realm.acts = make(map[int]*MapAct) + realm.players = make(map[string]string) + + for _, actId := range actIds { + act := &MapAct{} + realm.acts[actId] = act + + act.Init(realm, actId) + } +} + +func (realm *MapRealm) GenerateMap(actId, levelId int) { + realm.acts[actId].GenerateMap(levelId) +} + +func (realm *MapRealm) GetMapEngine(actId, levelId int) *MapEngine { + return realm.acts[actId].levels[levelId].mapEngine +} + +func (realm *MapRealm) GetFirstActLevelId(actId int) int { + return d2datadict.GetFirstLevelIdByActId(actId) +} diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index 38b1ecce..3111765c 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -34,7 +34,7 @@ type Player struct { var baseWalkSpeed = 6.0 var baseRunSpeed = 9.0 -func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) *Player { +func CreatePlayer(id, name string, ActId, LevelId, x, y, direction int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) *Player { object := &d2datadict.ObjectLookupRecord{ Mode: d2enum.AnimationModePlayerTownNeutral.String(), Base: "/data/global/chars", diff --git a/d2core/d2map/d2mapgen/act1_overworld.go b/d2core/d2map/d2mapgen/act1_overworld.go index fd215af6..5c190d4f 100644 --- a/d2core/d2map/d2mapgen/act1_overworld.go +++ b/d2core/d2map/d2mapgen/act1_overworld.go @@ -26,7 +26,7 @@ func loadPreset(mapEngine *d2mapengine.MapEngine, id, index int) *d2mapstamp.Sta func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) { rand.Seed(mapEngine.Seed()) - wilderness1Details := d2datadict.GetLevelDetails(2) + wilderness1Details := d2datadict.GetLevelDetailsByLevelId(2) mapEngine.ResetMap(d2enum.RegionAct1Town, 150, 150) mapWidth := mapEngine.Size().Width mapHeight := mapEngine.Size().Height @@ -59,7 +59,7 @@ func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) { // West Exit mapEngine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height) - generateWilderness1TownWest(mapEngine, mapWidth-townSize.Width - wilderness1Details.SizeXNormal, mapHeight-wilderness1Details.SizeYNormal) + generateWilderness1TownWest(mapEngine, mapWidth-townSize.Width-wilderness1Details.SizeXNormal, mapHeight-wilderness1Details.SizeYNormal) } else { // North Exit mapEngine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height) @@ -69,7 +69,7 @@ func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) { } func generateWilderness1TownEast(mapEngine *d2mapengine.MapEngine, startX, startY int) { - levelDetails := d2datadict.GetLevelDetails(2) + levelDetails := d2datadict.GetLevelDetailsByLevelId(2) fenceNorthStamp := []*d2mapstamp.Stamp{ loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 0), @@ -102,36 +102,36 @@ func generateWilderness1TownEast(mapEngine *d2mapengine.MapEngine, startX, start areaRect := d2common.Rectangle{ Left: startX, - Top: startY+9, + Top: startY + 9, Width: levelDetails.SizeXNormal, - Height: levelDetails.SizeYNormal-3, + Height: levelDetails.SizeYNormal - 3, } generateWilderness1Contents(mapEngine, areaRect) // Draw the north and south fence for i := 0; i < 9; i++ { mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9), startY) - mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9), startY + (levelDetails.SizeYNormal +6)) + mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9), startY+(levelDetails.SizeYNormal+6)) } // West fence for i := 1; i < 6; i++ { - mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+ (levelDetails.SizeYNormal+6) - (i * 9)) + mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(levelDetails.SizeYNormal+6)-(i*9)) } // East Fence for i := 1; i < 10; i++ { - mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX + levelDetails.SizeXNormal, startY+(i*9)) + mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal, startY+(i*9)) } - mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+ levelDetails.SizeYNormal+6) - mapEngine.PlaceStamp(fenceWestEdge, startX, startY+ (levelDetails.SizeYNormal-3) - 45) + mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal+6) + mapEngine.PlaceStamp(fenceWestEdge, startX, startY+(levelDetails.SizeYNormal-3)-45) mapEngine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal, startY) mapEngine.PlaceStamp(fenceSouthEastStamp, startX+levelDetails.SizeXNormal, startY+levelDetails.SizeYNormal+6) } func generateWilderness1TownSouth(mapEngine *d2mapengine.MapEngine, startX, startY int) { - levelDetails := d2datadict.GetLevelDetails(2) + levelDetails := d2datadict.GetLevelDetailsByLevelId(2) fenceNorthStamp := []*d2mapstamp.Stamp{ loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 0), @@ -184,7 +184,7 @@ func generateWilderness1TownSouth(mapEngine *d2mapengine.MapEngine, startX, star } func generateWilderness1TownWest(mapEngine *d2mapengine.MapEngine, startX, startY int) { - levelDetails := d2datadict.GetLevelDetails(2) + levelDetails := d2datadict.GetLevelDetailsByLevelId(2) fenceEastEdge := loadPreset(mapEngine, d2wilderness.TreeBoxSouthWest, 0) fenceNorthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderNorthWest, 0) @@ -218,30 +218,30 @@ func generateWilderness1TownWest(mapEngine *d2mapengine.MapEngine, startX, start // Draw the north and south fences for i := 0; i < 9; i++ { if i > 0 && i < 8 { - mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX + (i*9)-1, startY-15) + mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9)-1, startY-15) } mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9)-1, startY+levelDetails.SizeYNormal-12) } // Draw the east fence for i := 0; i < 6; i++ { - mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX + levelDetails.SizeXNormal-9, startY + (i*9)-6) + mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal-9, startY+(i*9)-6) } // Draw the west fence for i := 0; i < 9; i++ { - mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY + (i*9)-6) + mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(i*9)-6) } // Draw the west fence - mapEngine.PlaceStamp(fenceEastEdge, startX + levelDetails.SizeXNormal-9, startY + 39) + mapEngine.PlaceStamp(fenceEastEdge, startX+levelDetails.SizeXNormal-9, startY+39) mapEngine.PlaceStamp(fenceNorthWestStamp, startX, startY-15) mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal-12) mapEngine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal-9, startY-15) areaRect := d2common.Rectangle{ Left: startX + 9, - Top: startY-10, + Top: startY - 10, Width: levelDetails.SizeXNormal - 9, Height: levelDetails.SizeYNormal - 2, } @@ -250,7 +250,7 @@ func generateWilderness1TownWest(mapEngine *d2mapengine.MapEngine, startX, start } func generateWilderness1Contents(mapEngine *d2mapengine.MapEngine, rect d2common.Rectangle) { - levelDetails := d2datadict.GetLevelDetails(2) + levelDetails := d2datadict.GetLevelDetailsByLevelId(2) denOfEvil := loadPreset(mapEngine, d2wilderness.DenOfEvilEntrance, 0) denOfEvilLoc := d2common.Point{ @@ -295,10 +295,10 @@ func generateWilderness1Contents(mapEngine *d2mapengine.MapEngine, rect d2common for numPlaced < 25 { stamp := stuff[rand.Intn(len(stuff))] - stampRect := d2common.Rectangle { - Left: rect.Left+ rand.Intn(rect.Width) - stamp.Size().Width, - Top: rect.Top+rand.Intn(rect.Height) - stamp.Size().Height, - Width: stamp.Size().Width, + stampRect := d2common.Rectangle{ + Left: rect.Left + rand.Intn(rect.Width) - stamp.Size().Width, + Top: rect.Top + rand.Intn(rect.Height) - stamp.Size().Height, + Width: stamp.Size().Width, Height: stamp.Size().Height, } diff --git a/d2core/d2map/d2mapstamp/stamp.go b/d2core/d2map/d2mapstamp/stamp.go index 0bd4b22b..b97d428a 100644 --- a/d2core/d2map/d2mapstamp/stamp.go +++ b/d2core/d2map/d2mapstamp/stamp.go @@ -18,11 +18,11 @@ import ( // Represents a pre-fabricated map stamp that can be placed on a map type Stamp struct { - regionPath string // The file path of the region - levelType d2datadict.LevelTypeRecord // The level type id for this stamp - levelPreset d2datadict.LevelPresetRecord // The level preset id for this stamp - tiles []d2dt1.Tile // The tiles contained on this stamp - ds1 *d2ds1.DS1 // The backing DS1 file for this stamp + regionPath string // The file path of the region + levelType *d2datadict.LevelTypeRecord // The level type id for this stamp + levelPreset *d2datadict.LevelPresetRecord // The level preset id for this stamp + tiles []d2dt1.Tile // The tiles contained on this stamp + ds1 *d2ds1.DS1 // The backing DS1 file for this stamp } // Loads a stamp based on the supplied parameters @@ -83,12 +83,12 @@ func (mr *Stamp) Size() d2common.Size { } // Gets the level preset id -func (mr *Stamp) LevelPreset() d2datadict.LevelPresetRecord { +func (mr *Stamp) LevelPreset() *d2datadict.LevelPresetRecord { return mr.levelPreset } // Returns the level type id -func (mr *Stamp) LevelType() d2datadict.LevelTypeRecord { +func (mr *Stamp) LevelType() *d2datadict.LevelTypeRecord { return mr.levelType } diff --git a/d2game/d2gamescreen/character_select.go b/d2game/d2gamescreen/character_select.go index 0def4683..4a844a99 100644 --- a/d2game/d2gamescreen/character_select.go +++ b/d2game/d2gamescreen/character_select.go @@ -9,6 +9,7 @@ import ( "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" @@ -162,11 +163,27 @@ func (v *CharacterSelect) updateCharacterBoxes() { v.characterNameLabel[i].SetText(v.gameStates[idx].HeroName) v.characterStatsLabel[i].SetText("Level 1 " + v.gameStates[idx].HeroType.String()) v.characterExpLabel[i].SetText(expText) + + playerId := "" + playerName := "" + actId := d2datadict.GetActIds()[0] + levelId := d2datadict.GetFirstLevelIdByActId(actId) + x, y := 0, 0 + dir := 0 + heroType := v.gameStates[idx].HeroType + heroStats := *v.gameStates[idx].Stats + heroEquipment := d2inventory.HeroObjects[v.gameStates[idx].HeroType] // TODO: Generate or load the object from the actual player data... - v.characterImage[i] = d2mapentity.CreatePlayer("", "", 0, 0, 0, - v.gameStates[idx].HeroType, - *v.gameStates[idx].Stats, - d2inventory.HeroObjects[v.gameStates[idx].HeroType], + v.characterImage[i] = d2mapentity.CreatePlayer( + playerId, + playerName, + actId, + levelId, + x, y, + dir, + heroType, + heroStats, + heroEquipment, ) } } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index f9453db3..f39f31c3 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -20,7 +20,7 @@ import ( type Game struct { gameClient *d2client.GameClient - mapRenderer *d2maprenderer.MapRenderer + MapRenderer *d2maprenderer.MapRenderer gameControls *d2player.GameControls // TODO: Hack localPlayer *d2mapentity.Player lastRegionType d2enum.RegionIdType @@ -35,7 +35,7 @@ func CreateGame(gameClient *d2client.GameClient) *Game { localPlayer: nil, lastRegionType: d2enum.RegionNone, ticksSinceLevelCheck: 0, - mapRenderer: d2maprenderer.CreateMapRenderer(gameClient.MapEngine), + MapRenderer: d2maprenderer.CreateMapRenderer(gameClient.MapEngine), escapeMenu: NewEscapeMenu(), } result.escapeMenu.OnLoad() @@ -56,11 +56,13 @@ func (v *Game) OnUnload() error { func (v *Game) Render(screen d2render.Surface) error { if v.gameClient.RegenMap { v.gameClient.RegenMap = false - v.mapRenderer.RegenerateTileCache() + v.MapRenderer.RegenerateTileCache() } - screen.Clear(color.Black) - v.mapRenderer.Render(screen) + if v.MapRenderer != nil { + screen.Clear(color.Black) + v.MapRenderer.Render(screen) + } if v.gameControls != nil { v.gameControls.Render(screen) @@ -69,7 +71,7 @@ func (v *Game) Render(screen d2render.Surface) error { return nil } -var hideZoneTextAfterSeconds = 2.0 +var zoneTextDuration = 2.0 // seconds func (v *Game) Advance(tickTime float64) error { if (v.escapeMenu != nil && !v.escapeMenu.IsOpen()) || len(v.gameClient.Players) != 1 { @@ -96,11 +98,19 @@ func (v *Game) Advance(tickTime float64) error { } // skip showing zone change text the first time we enter the world - if v.lastRegionType != d2enum.RegionNone && v.lastRegionType != tile.RegionType { - //TODO: Should not be using RegionType as an index - this will return incorrect LevelDetails record for most of the zones. - v.gameControls.SetZoneChangeText(fmt.Sprintf("Entering The %s", d2datadict.LevelDetails[int(tile.RegionType)].LevelDisplayName)) + notNone := v.lastRegionType != d2enum.RegionNone + differentTileType := v.lastRegionType != tile.RegionType + if notNone && differentTileType { + //TODO: Should not be using RegionType as an index - this + // will return incorrect LevelDetails record for most of the + // zones. + levelId := int(tile.RegionType) + levelDetails := d2datadict.LevelDetails[levelId] + str := "Entering The %s" + name := levelDetails.LevelDisplayName + v.gameControls.SetZoneChangeText(fmt.Sprintf(str, name)) v.gameControls.ShowZoneChangeText() - v.gameControls.HideZoneChangeTextAfter(hideZoneTextAfterSeconds) + v.gameControls.HideZoneChangeTextAfter(zoneTextDuration) } v.lastRegionType = tile.RegionType } @@ -114,7 +124,9 @@ func (v *Game) Advance(tickTime float64) error { continue } v.localPlayer = player - v.gameControls = d2player.NewGameControls(player, v.gameClient.MapEngine, v.mapRenderer, v) + engine := v.gameClient.MapEngine + renderer := v.MapRenderer + v.gameControls = d2player.NewGameControls(player, engine, renderer, v) v.gameControls.Load() d2input.BindHandler(v.gameControls) @@ -124,14 +136,16 @@ func (v *Game) Advance(tickTime float64) error { // Update the camera to focus on the player if v.localPlayer != nil && !v.gameControls.FreeCam { - rx, ry := v.mapRenderer.WorldToOrtho(v.localPlayer.LocationX/5, v.localPlayer.LocationY/5) - v.mapRenderer.MoveCameraTo(rx, ry) + wx, wy := v.localPlayer.LocationX/5, v.localPlayer.LocationY/5 + rx, ry := v.MapRenderer.WorldToOrtho(wx, wy) + v.MapRenderer.MoveCameraTo(rx, ry) } return nil } -func (v *Game) OnPlayerMove(x, y float64) { - heroPosX := v.localPlayer.LocationX / 5.0 - heroPosY := v.localPlayer.LocationY / 5.0 - v.gameClient.SendPacketToServer(d2netpacket.CreateMovePlayerPacket(v.gameClient.PlayerId, heroPosX, heroPosY, x, y)) +func (v *Game) OnPlayerMove(x2, y2 float64) { + id := v.gameClient.PlayerId + x1, y1 := v.localPlayer.LocationX/5.0, v.localPlayer.LocationY/5.0 + movePacket := d2netpacket.CreateMovePlayerPacket(id, x1, y1, x2, y2) + v.gameClient.SendPacketToServer(movePacket) } diff --git a/d2game/d2player/player_state.go b/d2game/d2player/player_state.go index 3149746b..68da3e76 100644 --- a/d2game/d2player/player_state.go +++ b/d2game/d2player/player_state.go @@ -20,9 +20,10 @@ type PlayerState struct { HeroType d2enum.Hero `json:"heroType"` HeroLevel int `json:"heroLevel"` Act int `json:"act"` + Level int `json:"actLevel"` FilePath string `json:"-"` Equipment d2inventory.CharacterEquipment `json:"equipment"` - Stats *d2hero.HeroStatsState `json:"stats"` + Stats *d2hero.HeroStatsState `json:"stats"` X float64 `json:"x"` Y float64 `json:"y"` } @@ -45,8 +46,8 @@ func GetAllPlayerStates() []*PlayerState { gameState := LoadPlayerState(path.Join(basePath, file.Name())) if gameState == nil || gameState.HeroType == d2enum.HeroNone { continue - // temporarily loading default class stats if the character was created before saving stats was introduced - // to be removed in the future + // temporarily loading default class stats if the character was created before saving stats was introduced + // to be removed in the future } else if gameState.Stats == nil { gameState.Stats = d2hero.CreateHeroStatsState(gameState.HeroType, *d2datadict.CharStats[gameState.HeroType], 1, 0) gameState.Save() @@ -83,8 +84,9 @@ func CreatePlayerState(heroName string, hero d2enum.Hero, classStats d2datadict. result := &PlayerState{ HeroName: heroName, HeroType: hero, - Act: 1, - Stats: d2hero.CreateHeroStatsState(hero, classStats, 1, 0), + Act: 0, + Level: 1, + Stats: d2hero.CreateHeroStatsState(hero, classStats, 1, 0), Equipment: d2inventory.HeroObjects[hero], FilePath: "", } diff --git a/d2networking/d2client/d2client.go b/d2networking/d2client/d2client.go new file mode 100644 index 00000000..5a9d58b7 --- /dev/null +++ b/d2networking/d2client/d2client.go @@ -0,0 +1,38 @@ +package d2client + +import ( + "fmt" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + d2cct "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2localclient" + "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2remoteclient" +) + +// Creates a connections to the server and returns a game client instance +func Create(connectionType d2cct.ClientConnectionType) (*GameClient, error) { + result := &GameClient{ + // TODO: Mapgen - Needs levels.txt stuff + MapEngine: d2mapengine.CreateMapEngine(), + Players: make(map[string]*d2mapentity.Player), + connectionType: connectionType, + realm: &d2mapengine.MapRealm{}, + } + + switch connectionType { + case d2cct.LANClient: + result.clientConnection = d2remoteclient.Create() + case d2cct.LANServer: + openSocket := true + result.clientConnection = d2localclient.Create(openSocket) + case d2cct.Local: + dontOpenSocket := false + result.clientConnection = d2localclient.Create(dontOpenSocket) + default: + str := "unknown client connection type specified: %d" + return nil, fmt.Errorf(str, connectionType) + } + result.clientConnection.SetClientListener(result) + return result, nil +} diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index cbdaff3b..e87ecd9c 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -1,120 +1,164 @@ package d2client import ( - "fmt" "log" "os" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + // "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2localclient" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2remoteclient" + d2cct "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) type GameClient struct { clientConnection ClientConnection - connectionType d2clientconnectiontype.ClientConnectionType + connectionType d2cct.ClientConnectionType GameState *d2player.PlayerState MapEngine *d2mapengine.MapEngine + MapRenderer *d2maprenderer.MapRenderer + realm *d2mapengine.MapRealm PlayerId string Players map[string]*d2mapentity.Player Seed int64 RegenMap bool } -func Create(connectionType d2clientconnectiontype.ClientConnectionType) (*GameClient, error) { - result := &GameClient{ - MapEngine: d2mapengine.CreateMapEngine(), // TODO: Mapgen - Needs levels.txt stuff - Players: make(map[string]*d2mapentity.Player), - connectionType: connectionType, - } - - switch connectionType { - case d2clientconnectiontype.LANClient: - result.clientConnection = d2remoteclient.Create() - case d2clientconnectiontype.LANServer: - result.clientConnection = d2localclient.Create(true) - case d2clientconnectiontype.Local: - result.clientConnection = d2localclient.Create(false) - default: - return nil, fmt.Errorf("unknown client connection type specified: %d", connectionType) - } - result.clientConnection.SetClientListener(result) - return result, nil -} - +// Using the `clientConnection`, opens a connection and passes the savefile path func (g *GameClient) Open(connectionString string, saveFilePath string) error { return g.clientConnection.Open(connectionString, saveFilePath) } +// Closes the `clientConnection` func (g *GameClient) Close() error { return g.clientConnection.Close() } +// Closes the `clientConnection` func (g *GameClient) Destroy() error { return g.clientConnection.Close() } +// Routes the incoming packets to the packet handlers func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { switch packet.PacketType { - case d2netpackettype.GenerateMap: - mapData := packet.PacketData.(d2netpacket.GenerateMapPacket) - switch mapData.RegionType { - case d2enum.RegionAct1Town: - d2mapgen.GenerateAct1Overworld(g.MapEngine) - } - g.RegenMap = true - case d2netpackettype.UpdateServerInfo: - serverInfo := packet.PacketData.(d2netpacket.UpdateServerInfoPacket) - g.MapEngine.SetSeed(serverInfo.Seed) - g.PlayerId = serverInfo.PlayerId - g.Seed = serverInfo.Seed - log.Printf("Player id set to %s", serverInfo.PlayerId) - case d2netpackettype.AddPlayer: - player := packet.PacketData.(d2netpacket.AddPlayerPacket) - newPlayer := d2mapentity.CreatePlayer(player.Id, player.Name, player.X, player.Y, 0, player.HeroType, player.Stats, player.Equipment) - g.Players[newPlayer.Id] = newPlayer - g.MapEngine.AddEntity(newPlayer) - case d2netpackettype.MovePlayer: - movePlayer := packet.PacketData.(d2netpacket.MovePlayerPacket) - player := g.Players[movePlayer.PlayerId] - path, _, _ := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) - if len(path) > 0 { - player.SetPath(path, func() { - tile := g.MapEngine.TileAt(player.TileX, player.TileY) - if tile == nil { - return - } - regionType := tile.RegionType - if regionType == d2enum.RegionAct1Town { - player.SetIsInTown(true) - } else { - player.SetIsInTown(false) - } - player.SetAnimationMode(player.GetAnimationMode().String()) - }) - } + // UNSURE: should we be bubbling up errors from these handler calls? + case d2netpackettype.UpdateServerInfo: + g.handleUpdateServerInfo(packet) + + case d2netpackettype.AddPlayer: + g.handleAddPlayer(packet) + + case d2netpackettype.GenerateMap: + g.handleGenerateMap(packet) + + case d2netpackettype.MovePlayer: + g.handleMovePlayer(packet) + case d2netpackettype.Ping: - g.clientConnection.SendPacketToServer(d2netpacket.CreatePongPacket(g.PlayerId)) + g.handlePong(packet) + case d2netpackettype.ServerClosed: - // TODO: Need to be tied into a character save and exit - log.Print("Server has been closed") - os.Exit(0) + g.handleServerClosed(packet) + default: log.Fatalf("Invalid packet type: %d", packet.PacketType) } return nil } +// Using the `clientConnection`, sends a packet to the server func (g *GameClient) SendPacketToServer(packet d2netpacket.NetPacket) error { return g.clientConnection.SendPacketToServer(packet) } + +func (g *GameClient) handleUpdateServerInfo(p d2netpacket.NetPacket) { + serverInfo := p.PacketData.(d2netpacket.UpdateServerInfoPacket) + seed := serverInfo.Seed + playerId := serverInfo.PlayerId + + g.Seed = seed + g.realm.Init(seed) + g.PlayerId = playerId + + log.Printf("Player id set to %s", playerId) +} + +func (g *GameClient) handleAddPlayer(p d2netpacket.NetPacket) { + player := p.PacketData.(d2netpacket.AddPlayerPacket) + levelId := g.realm.GetFirstActLevelId(player.Act) + g.MapEngine = g.realm.GetMapEngine(player.Act, levelId) + + pId := player.Id + pName := player.Name + pAct := player.Act + pLvlId := levelId + pX := player.X + pY := player.Y + pDir := 0 + pHero := player.HeroType + pStat := player.Stats + pEquip := player.Equipment + + // UNSURE: maybe we should be passing a struct instead of all the vars? + newPlayer := d2mapentity.CreatePlayer( + pId, pName, pAct, pLvlId, pX, pY, pDir, pHero, pStat, pEquip, + ) + + g.Players[newPlayer.Id] = newPlayer + g.realm.AddPlayer(pId, pAct) + g.MapEngine.AddEntity(newPlayer) +} + +func (g *GameClient) handleGenerateMap(p d2netpacket.NetPacket) { + mapData := p.PacketData.(d2netpacket.GenerateMapPacket) + g.realm.GenerateMap(mapData.ActId, mapData.LevelId) + engine := g.realm.GetMapEngine(mapData.ActId, mapData.LevelId) + g.MapRenderer = d2maprenderer.CreateMapRenderer(engine) + g.RegenMap = true +} + +func (g *GameClient) handleMovePlayer(p d2netpacket.NetPacket) { + movePlayer := p.PacketData.(d2netpacket.MovePlayerPacket) + + player := g.Players[movePlayer.PlayerId] + x1, y1 := movePlayer.StartX, movePlayer.StartY + x2, y2 := movePlayer.DestX, movePlayer.DestY + + path, _, _ := g.MapEngine.PathFind(x1, y1, x2, y2) + + if len(path) > 0 { + player.SetPath(path, func() { + tile := g.MapEngine.TileAt(player.TileX, player.TileY) + if tile == nil { + return + } + + regionType := tile.RegionType + if regionType == d2enum.RegionAct1Town { + player.SetIsInTown(true) + } else { + player.SetIsInTown(false) + } + player.SetAnimationMode(player.GetAnimationMode().String()) + }) + } +} + +func (g *GameClient) handlePong(p d2netpacket.NetPacket) { + pong := d2netpacket.CreatePongPacket(g.PlayerId) + g.clientConnection.SendPacketToServer(pong) +} + +func (g *GameClient) handleServerClosed(p d2netpacket.NetPacket) { + // TODO: Need to be tied into a character save and exit + log.Print("Server has been closed") + os.Exit(0) +} diff --git a/d2networking/d2netpacket/packet_add_player.go b/d2networking/d2netpacket/packet_add_player.go index 1843614d..c9405d6d 100644 --- a/d2networking/d2netpacket/packet_add_player.go +++ b/d2networking/d2netpacket/packet_add_player.go @@ -12,22 +12,24 @@ type AddPlayerPacket struct { Name string `json:"name"` X int `json:"x"` Y int `json:"y"` + Act int `json:"act"` HeroType d2enum.Hero `json:"hero"` Equipment d2inventory.CharacterEquipment `json:"equipment"` - Stats d2hero.HeroStatsState `json:"heroStats"` + Stats d2hero.HeroStatsState `json:"heroStats"` } -func CreateAddPlayerPacket(id, name string, x, y int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) NetPacket { +func CreateAddPlayerPacket(id, name string, act, x, y int, heroType d2enum.Hero, stats d2hero.HeroStatsState, equipment d2inventory.CharacterEquipment) NetPacket { return NetPacket{ PacketType: d2netpackettype.AddPlayer, PacketData: AddPlayerPacket{ Id: id, Name: name, + Act: act, X: x, Y: y, HeroType: heroType, Equipment: equipment, - Stats: stats, + Stats: stats, }, } } diff --git a/d2networking/d2netpacket/packet_generate_map.go b/d2networking/d2netpacket/packet_generate_map.go index 178bd6ff..6a6cb3c3 100644 --- a/d2networking/d2netpacket/packet_generate_map.go +++ b/d2networking/d2netpacket/packet_generate_map.go @@ -1,19 +1,20 @@ package d2netpacket import ( - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" ) type GenerateMapPacket struct { - RegionType d2enum.RegionIdType `json:"regionType"` + ActId int `json:"actId"` + LevelId int `json:"levelId"` } -func CreateGenerateMapPacket(regionType d2enum.RegionIdType) NetPacket { +func CreateGenerateMapPacket(actId, levelId int) NetPacket { return NetPacket{ PacketType: d2netpackettype.GenerateMap, PacketData: GenerateMapPacket{ - RegionType: regionType, + ActId: actId, + LevelId: levelId, }, } diff --git a/d2networking/d2server/connection_manager.go b/d2networking/d2server/connection_manager.go index 1bd7fb63..1a815032 100644 --- a/d2networking/d2server/connection_manager.go +++ b/d2networking/d2server/connection_manager.go @@ -8,13 +8,16 @@ import ( "time" ) -// ConnectionManager is responsible for cleanup up connections accepted by the game server. As the server communicates over -// UDP and is stateless we need to implement some loose state management via a ping/pong system. ConnectionManager also handles +// ConnectionManager is responsible for cleanup up connections accepted by the +// game server. As the server communicates over +// UDP and is stateless we need to implement some loose state management via a +// ping/pong system. ConnectionManager also handles // communication for graceful shutdowns. // // retries: # of attempts before the dropping the client // interval: How long to wait before each ping/pong test -// gameServer: The *GameServer is argument provided for the connection manager to watch over +// gameServer: The *GameServer is argument provided for the connection manager +// to watch over // status: map of inflight ping/pong requests type ConnectionManager struct { sync.RWMutex @@ -50,7 +53,8 @@ func (c *ConnectionManager) Run() { func (c *ConnectionManager) checkPeers() { for id, connection := range c.gameServer.clientConnections { if connection.GetConnectionType() != d2clientconnectiontype.Local { - if err := connection.SendPacketToClient(d2netpacket.CreatePingPacket()); err != nil { + pingPacket := d2netpacket.CreatePingPacket() + if err := connection.SendPacketToClient(pingPacket); err != nil { log.Printf("Cannot ping client id: %s", id) } c.RWMutex.Lock() @@ -66,7 +70,8 @@ func (c *ConnectionManager) checkPeers() { } } -// Recv simply resets the counter, acknowledging we have received a pong from the client. +// Recv simply resets the counter, acknowledging we have received a pong from +// the client. func (c *ConnectionManager) Recv(id string) { c.status[id] = 0 } @@ -81,8 +86,10 @@ func (c *ConnectionManager) Drop(id string) { // Shutdown will notify all of the clients that the server has been shutdown. func (c *ConnectionManager) Shutdown() { - // TODO: Currently this will never actually get called as the go routines are never signaled about the application termination. - // Things can be done more cleanly once we have graceful exits however we still need to account for other OS Signals + // TODO: Currently this will never actually get called as the go routines + // are never signaled about the application termination. + // Things can be done more cleanly once we have graceful exits however we + // still need to account for other OS Signals log.Print("Notifying clients server is shutting down...") for _, connection := range c.gameServer.clientConnections { connection.SendPacketToClient(d2netpacket.CreateServerClosedPacket()) diff --git a/d2networking/d2server/d2server.go b/d2networking/d2server/d2server.go new file mode 100644 index 00000000..d5764933 --- /dev/null +++ b/d2networking/d2server/d2server.go @@ -0,0 +1,243 @@ +package d2server + +import ( + "bytes" + "compress/gzip" + "fmt" + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2config" + "io" + "log" + "net" + "strings" + "time" + + // "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" + + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" + + // "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + packet "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" + packettype "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" + "github.com/OpenDiablo2/OpenDiablo2/d2script" + "github.com/robertkrimen/otto" +) + +var singletonServer *GameServer + +func advance() { + now := d2common.Now() + elapsed := now - singletonServer.lastAdvance + singletonServer.realm.Advance(elapsed) + singletonServer.lastAdvance = now +} + +func Create(openNetworkServer bool) { + log.Print("Creating GameServer") + if singletonServer != nil { + return + } + + config := d2config.Get() + maxConnections := config.MaxConnections + seed := time.Now().UnixNano() + + singletonServer = &GameServer{ + clientConnections: make(map[string]ClientConnection), + realm: &d2mapengine.MapRealm{}, + scriptEngine: d2script.CreateScriptEngine(), + seed: seed, + maxClients: maxConnections, + lastAdvance: d2common.Now(), + } + + singletonServer.realm.Init(seed) + singletonServer.manager = CreateConnectionManager(singletonServer) + + // mapEngine := d2mapengine.CreateMapEngine() + // mapEngine.SetSeed(singletonServer.seed) + // TODO: Mapgen - Needs levels.txt stuff + // mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100) + // d2mapgen.GenerateAct1Overworld(mapEngine) + // singletonServer.mapEngines = append(singletonServer.mapEngines, mapEngine) + + addScriptEngineFunctions() + + if openNetworkServer { + createNetworkServer() + } +} + +func addScriptEngineFunctions() { + singletonServer.scriptEngine.AddFunction("getMapEngines", ottoTestFunc) +} + +func ottoTestFunc(call otto.FunctionCall) otto.Value { + val, err := singletonServer.scriptEngine.ToValue(singletonServer.realm) + if err != nil { + fmt.Print(err.Error()) + } + return val +} + +func createNetworkServer() { + s, err := net.ResolveUDPAddr("udp4", "0.0.0.0:6669") + if err != nil { + panic(err) + } + + singletonServer.udpConnection, err = net.ListenUDP("udp4", s) + if err != nil { + panic(err) + } + singletonServer.udpConnection.SetReadBuffer(4096) +} + +func runNetworkServer() { + buffer := make([]byte, 4096) + srv := singletonServer + for srv.running { + advance() + + _, addr, err := srv.udpConnection.ReadFromUDP(buffer) + if err != nil { + fmt.Printf("Socket error: %s\n", err) + continue + } + buff := bytes.NewBuffer(buffer) + packetTypeId, err := buff.ReadByte() + packetType := packettype.NetPacketType(packetTypeId) + reader, err := gzip.NewReader(buff) + sb := new(strings.Builder) + io.Copy(sb, reader) + stringData := sb.String() + + switch packetType { + case packettype.PlayerConnectionRequest: + srv.handlePlayerConnRequest(addr, stringData) + case packettype.MovePlayer: + srv.handleMovePlayer(addr, stringData) + case packettype.Pong: + srv.handlePong(addr, stringData) + case packettype.ServerClosed: + srv.manager.Shutdown() + case packettype.PlayerDisconnectionNotification: + srv.handlePlayerDisconnectNotification(stringData) + } + } +} + +func Run() { + log.Print("Starting GameServer") + singletonServer.running = true + singletonServer.scriptEngine.RunScript("scripts/server/server.js") + if singletonServer.udpConnection != nil { + go runNetworkServer() + } + log.Print("Network server has been started") +} + +func Stop() { + log.Print("Stopping GameServer") + singletonServer.running = false + if singletonServer.udpConnection != nil { + singletonServer.udpConnection.Close() + } +} + +func Destroy() { + if singletonServer == nil { + return + } + log.Print("Destroying GameServer") + Stop() +} + +func OnClientConnected(client ClientConnection) { + srv := singletonServer + realm := srv.realm + seed := srv.seed + state := client.GetPlayerState() + + actId := state.Act + levelId := d2datadict.GetFirstLevelIdByActId(actId) + engine := realm.GetMapEngine(actId, levelId) + + // params for AddPlayer packet, of new player + id := client.GetUniqueId() + + name := state.HeroName + hero := state.HeroType + stats := *state.Stats + equip := state.Equipment + x, y := engine.GetStartPosition() + state.X = x + state.Y = y + + infoPacket := packet.CreateUpdateServerInfoPacket(seed, id) + mapgenPacket := packet.CreateGenerateMapPacket(actId, levelId) + + // UNSURE: maybe we should pass a struct instead of all of these args + addNew := packet.CreateAddPlayerPacket( + id, name, actId, int(x*5), int(y*5), hero, stats, equip, + ) + + srv.clientConnections[id] = client + + client.SendPacketToClient(infoPacket) + client.SendPacketToClient(mapgenPacket) + + log.Printf("Client connected with an id of %s", id) + realm.AddPlayer(id, state.Act) + + // for each connection, send the AddPlayer packet for the new player + for _, connection := range srv.clientConnections { + conId := connection.GetUniqueId() + connection.SendPacketToClient(addNew) + + if conId == id { + continue + } + + // send an AddPlayer for existing connections to the new player + conState := connection.GetPlayerState() + conActId := conState.Act + conName := conState.HeroName + conHero := conState.HeroType + conEquip := conState.Equipment + conStats := *conState.Stats + conX, conY := 0, 0 + + addExisting := packet.CreateAddPlayerPacket( + conId, conName, conActId, conX, conY, conHero, conStats, conEquip, + ) + + client.SendPacketToClient(addExisting) + } + +} + +func OnClientDisconnected(client ClientConnection) { + log.Printf("Client disconnected with an id of %s", client.GetUniqueId()) + clientId := client.GetUniqueId() + delete(singletonServer.clientConnections, clientId) + singletonServer.realm.RemovePlayer(clientId) +} + +func OnPacketReceived(client ClientConnection, p packet.NetPacket) error { + switch p.PacketType { + case packettype.MovePlayer: + // TODO: This needs to be verified on the server (here) before sending to other clients.... + // TODO: Hacky, this should be updated in realtime ---------------- + // TODO: Verify player id + playerState := singletonServer.clientConnections[client.GetUniqueId()].GetPlayerState() + playerState.X = p.PacketData.(packet.MovePlayerPacket).DestX + playerState.Y = p.PacketData.(packet.MovePlayerPacket).DestY + // ---------------------------------------------------------------- + for _, player := range singletonServer.clientConnections { + player.SendPacketToClient(p) + } + } + return nil +} diff --git a/d2networking/d2server/game_server.go b/d2networking/d2server/game_server.go index d28f13a6..6918349d 100644 --- a/d2networking/d2server/game_server.go +++ b/d2networking/d2server/game_server.go @@ -1,209 +1,67 @@ package d2server import ( - "bytes" - "compress/gzip" "encoding/json" - "fmt" - "io" "log" "net" - "strings" "sync" - "time" - - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine" - - "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" - "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2udpclientconnection" + packet "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket" + packettype "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2netpacket/d2netpackettype" + d2udp "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2server/d2udpclientconnection" "github.com/OpenDiablo2/OpenDiablo2/d2script" - "github.com/robertkrimen/otto" ) type GameServer struct { sync.RWMutex + version string clientConnections map[string]ClientConnection manager *ConnectionManager - mapEngines []*d2mapengine.MapEngine + realm *d2mapengine.MapRealm scriptEngine *d2script.ScriptEngine udpConnection *net.UDPConn seed int64 running bool + maxClients int + lastAdvance float64 } -var singletonServer *GameServer +func (srv *GameServer) handlePlayerConnRequest(addr *net.UDPAddr, data string) { + packetData := &packet.PlayerConnectionRequestPacket{} + json.Unmarshal([]byte(data), packetData) -func Create(openNetworkServer bool) { - log.Print("Creating GameServer") - if singletonServer != nil { - return + srvCon := srv.udpConnection + packetId := packetData.Id + clientCon := d2udp.CreateUDPClientConnection(srvCon, packetId, addr) + + state := packetData.PlayerState + clientCon.SetPlayerState(state) + OnClientConnected(clientCon) +} + +func (srv *GameServer) handleMovePlayer(addr *net.UDPAddr, data string) { + packetData := &packet.MovePlayerPacket{} + json.Unmarshal([]byte(data), packetData) + + netPacket := packet.NetPacket{ + PacketType: packettype.MovePlayer, + PacketData: packetData, } - singletonServer = &GameServer{ - clientConnections: make(map[string]ClientConnection), - mapEngines: make([]*d2mapengine.MapEngine, 0), - scriptEngine: d2script.CreateScriptEngine(), - seed: time.Now().UnixNano(), - } - - singletonServer.manager = CreateConnectionManager(singletonServer) - - mapEngine := d2mapengine.CreateMapEngine() - mapEngine.SetSeed(singletonServer.seed) - mapEngine.ResetMap(d2enum.RegionAct1Town, 100, 100) // TODO: Mapgen - Needs levels.txt stuff - d2mapgen.GenerateAct1Overworld(mapEngine) - singletonServer.mapEngines = append(singletonServer.mapEngines, mapEngine) - - singletonServer.scriptEngine.AddFunction("getMapEngines", func(call otto.FunctionCall) otto.Value { - val, err := singletonServer.scriptEngine.ToValue(singletonServer.mapEngines) - if err != nil { - fmt.Print(err.Error()) - } - return val - }) - - if openNetworkServer { - createNetworkServer() + for _, player := range srv.clientConnections { + player.SendPacketToClient(netPacket) } } -func createNetworkServer() { - s, err := net.ResolveUDPAddr("udp4", "0.0.0.0:6669") - if err != nil { - panic(err) - } - - singletonServer.udpConnection, err = net.ListenUDP("udp4", s) - if err != nil { - panic(err) - } - singletonServer.udpConnection.SetReadBuffer(4096) +func (srv *GameServer) handlePong(addr *net.UDPAddr, data string) { + packetData := packet.PlayerConnectionRequestPacket{} + json.Unmarshal([]byte(data), &packetData) + srv.manager.Recv(packetData.Id) } -func runNetworkServer() { - buffer := make([]byte, 4096) - for singletonServer.running { - _, addr, err := singletonServer.udpConnection.ReadFromUDP(buffer) - if err != nil { - fmt.Printf("Socket error: %s\n", err) - continue - } - buff := bytes.NewBuffer(buffer) - packetTypeId, err := buff.ReadByte() - packetType := d2netpackettype.NetPacketType(packetTypeId) - reader, err := gzip.NewReader(buff) - sb := new(strings.Builder) - io.Copy(sb, reader) - stringData := sb.String() - switch packetType { - case d2netpackettype.PlayerConnectionRequest: - packetData := d2netpacket.PlayerConnectionRequestPacket{} - json.Unmarshal([]byte(stringData), &packetData) - clientConnection := d2udpclientconnection.CreateUDPClientConnection(singletonServer.udpConnection, packetData.Id, addr) - clientConnection.SetPlayerState(packetData.PlayerState) - OnClientConnected(clientConnection) - case d2netpackettype.MovePlayer: - packetData := d2netpacket.MovePlayerPacket{} - json.Unmarshal([]byte(stringData), &packetData) - netPacket := d2netpacket.NetPacket{ - PacketType: packetType, - PacketData: packetData, - } - - for _, player := range singletonServer.clientConnections { - player.SendPacketToClient(netPacket) - } - case d2netpackettype.Pong: - packetData := d2netpacket.PlayerConnectionRequestPacket{} - json.Unmarshal([]byte(stringData), &packetData) - singletonServer.manager.Recv(packetData.Id) - case d2netpackettype.ServerClosed: - singletonServer.manager.Shutdown() - case d2netpackettype.PlayerDisconnectionNotification: - var packet d2netpacket.PlayerDisconnectRequestPacket - json.Unmarshal([]byte(stringData), &packet) - log.Printf("Received disconnect: %s", packet.Id) - } - } -} - -func Run() { - log.Print("Starting GameServer") - singletonServer.running = true - singletonServer.scriptEngine.RunScript("scripts/server/server.js") - if singletonServer.udpConnection != nil { - go runNetworkServer() - } - log.Print("Network server has been started") -} - -func Stop() { - log.Print("Stopping GameServer") - singletonServer.running = false - if singletonServer.udpConnection != nil { - singletonServer.udpConnection.Close() - } -} - -func Destroy() { - if singletonServer == nil { - return - } - log.Print("Destroying GameServer") - Stop() -} - -func OnClientConnected(client ClientConnection) { - // Temporary position hack -------------------------------------------- - sx, sy := singletonServer.mapEngines[0].GetStartPosition() // TODO: Another temporary hack - clientPlayerState := client.GetPlayerState() - clientPlayerState.X = sx - clientPlayerState.Y = sy - // -------------------------------------------------------------------- - - log.Printf("Client connected with an id of %s", client.GetUniqueId()) - singletonServer.clientConnections[client.GetUniqueId()] = client - client.SendPacketToClient(d2netpacket.CreateUpdateServerInfoPacket(singletonServer.seed, client.GetUniqueId())) - client.SendPacketToClient(d2netpacket.CreateGenerateMapPacket(d2enum.RegionAct1Town)) - - playerState := client.GetPlayerState() - createPlayerPacket := d2netpacket.CreateAddPlayerPacket(client.GetUniqueId(), playerState.HeroName, int(sx*5)+3, int(sy*5)+3, - playerState.HeroType, *playerState.Stats, playerState.Equipment) - for _, connection := range singletonServer.clientConnections { - connection.SendPacketToClient(createPlayerPacket) - if connection.GetUniqueId() == client.GetUniqueId() { - continue - } - - conPlayerState := connection.GetPlayerState() - client.SendPacketToClient(d2netpacket.CreateAddPlayerPacket(connection.GetUniqueId(), conPlayerState.HeroName, - int(conPlayerState.X*5)+3, int(conPlayerState.Y*5)+3, conPlayerState.HeroType, *conPlayerState.Stats, conPlayerState.Equipment)) - } - -} - -func OnClientDisconnected(client ClientConnection) { - log.Printf("Client disconnected with an id of %s", client.GetUniqueId()) - delete(singletonServer.clientConnections, client.GetUniqueId()) -} - -func OnPacketReceived(client ClientConnection, packet d2netpacket.NetPacket) error { - switch packet.PacketType { - case d2netpackettype.MovePlayer: - // TODO: This needs to be verified on the server (here) before sending to other clients.... - // TODO: Hacky, this should be updated in realtime ---------------- - // TODO: Verify player id - playerState := singletonServer.clientConnections[client.GetUniqueId()].GetPlayerState() - playerState.X = packet.PacketData.(d2netpacket.MovePlayerPacket).DestX - playerState.Y = packet.PacketData.(d2netpacket.MovePlayerPacket).DestY - // ---------------------------------------------------------------- - for _, player := range singletonServer.clientConnections { - player.SendPacketToClient(packet) - } - } - return nil +func (srv *GameServer) handlePlayerDisconnectNotification(data string) { + var packet packet.PlayerDisconnectRequestPacket + json.Unmarshal([]byte(data), &packet) + log.Printf("Received disconnect: %s", packet.Id) }