├── .gitignore ├── tools ├── bazel │ ├── BUILD.bazel │ ├── gopackagesdriver.sh │ ├── gen_build_files.sh │ └── gen_strings.bzl ├── dbc_utils │ ├── dbc_data │ │ ├── BUILD.bazel │ │ ├── ChrStartingLocations.json │ │ ├── ChrStartingStats.json │ │ ├── ChrClasses.json │ │ └── CharBaseInfo.json │ ├── dbc │ │ ├── dbc_golang.go.jinja │ │ ├── BUILD.bazel │ │ ├── dbc_golang_part.go.jinja │ │ ├── record_types.py │ │ └── record_fields.py │ ├── BUILD.bazel │ ├── dbc_generator.py │ ├── mpq_generator.py │ └── golang_generator.py ├── gen_opcode_to_handler │ ├── BUILD.bazel │ └── gen_opcode_to_handler.py └── new_game │ ├── initial_data │ ├── BUILD.bazel │ └── initial_data.go │ ├── BUILD.bazel │ └── new_game.go ├── server ├── world │ ├── data │ │ ├── dynamic │ │ │ ├── messages │ │ │ │ ├── unit.go │ │ │ │ ├── BUILD.bazel │ │ │ │ └── combat.go │ │ │ ├── components │ │ │ │ ├── player.go │ │ │ │ ├── unit.go │ │ │ │ ├── player_features.go │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── health_power.go │ │ │ │ ├── stats.go │ │ │ │ ├── combat.go │ │ │ │ └── movement_info.go │ │ │ ├── interfaces │ │ │ │ ├── unit.go │ │ │ │ ├── item.go │ │ │ │ ├── location.go │ │ │ │ ├── game_object.go │ │ │ │ ├── BUILD.bazel │ │ │ │ ├── movement_info.go │ │ │ │ ├── packet.go │ │ │ │ ├── guid.go │ │ │ │ └── update_fields.go │ │ │ ├── unit_ai.go │ │ │ ├── BUILD.bazel │ │ │ ├── unit_game_logic.go │ │ │ ├── game_object.go │ │ │ ├── container.go │ │ │ ├── player_handlers.go │ │ │ ├── object_utils.go │ │ │ ├── player_game_logic.go │ │ │ └── item.go │ │ └── static │ │ │ ├── BUILD.bazel │ │ │ ├── starting_items.go │ │ │ └── error_codes.go │ ├── game │ │ ├── BUILD.bazel │ │ └── stats.go │ ├── channels │ │ ├── BUILD.bazel │ │ └── channels.go │ ├── packet │ │ ├── handlers │ │ │ ├── client_update_account_data.go │ │ │ ├── client_tutorial_flag.go │ │ │ ├── client_query_time.go │ │ │ ├── client_ping.go │ │ │ ├── client_set_active_mover.go │ │ │ ├── client_stand_state_change.go │ │ │ ├── client_char_delete.go │ │ │ ├── client_attack_stop.go │ │ │ ├── client_move.go │ │ │ ├── client_logout_request.go │ │ │ ├── client_name_query.go │ │ │ ├── client_attack_swing.go │ │ │ ├── client_creature_query.go │ │ │ ├── client_item_query_single.go │ │ │ ├── client_player_login.go │ │ │ ├── BUILD.bazel │ │ │ ├── client_auth_session.go │ │ │ ├── client_char_enum.go │ │ │ └── client_char_create.go │ │ ├── client_query_time.go │ │ ├── client_attack_stop.go │ │ ├── client_char_enum.go │ │ ├── client_logout_request.go │ │ ├── client_update_account_data.go │ │ ├── server_logout_complete.go │ │ ├── client_tutorial_flag.go │ │ ├── server_pong.go │ │ ├── client_ping.go │ │ ├── client_stand_state_change.go │ │ ├── server_char_create.go │ │ ├── server_char_delete.go │ │ ├── client_name_query.go │ │ ├── client_attack_swing.go │ │ ├── client_player_login.go │ │ ├── client_char_delete.go │ │ ├── server_query_time_response.go │ │ ├── client_set_active_mover.go │ │ ├── server_stand_state_update.go │ │ ├── server_account_data_times.go │ │ ├── server_auth_challenge.go │ │ ├── client_creature_query.go │ │ ├── client_item_query_single.go │ │ ├── server_attack_start.go │ │ ├── server_logout_response.go │ │ ├── server_auth_response.go │ │ ├── server_tutorial_flags.go │ │ ├── server_attack_stop.go │ │ ├── server_login_verify_world.go │ │ ├── server_init_world_states.go │ │ ├── server_name_query_response.go │ │ ├── client_auth_session.go │ │ ├── client_move.go │ │ ├── server_creature_query_response.go │ │ ├── BUILD.bazel │ │ ├── client_char_create.go │ │ ├── server_attacker_state_update.go │ │ ├── server_update_object.go │ │ └── server_char_enum.go │ ├── system │ │ ├── state.go │ │ └── BUILD.bazel │ ├── BUILD.bazel │ ├── README.md │ └── world_server.go └── auth │ ├── srp │ ├── BUILD.bazel │ ├── srp_test.go │ └── srp.go │ ├── data │ └── static │ │ ├── op_code.go │ │ ├── BUILD.bazel │ │ └── login_constants.go │ ├── BUILD.bazel │ ├── packet │ ├── BUILD.bazel │ ├── realmlist.go │ ├── login_proof.go │ └── login_challenge.go │ ├── session │ ├── BUILD.bazel │ ├── state.go │ ├── session.go │ └── packets.go │ └── auth_server.go ├── TODO.md ├── lib ├── util │ ├── BUILD.bazel │ └── util.go └── config │ ├── BUILD.bazel │ ├── character.go │ ├── account.go │ └── config.go ├── .github └── workflows │ └── ci-bazel.yaml ├── go.mod ├── README.md ├── mmo_server.go ├── BUILD.bazel ├── .vscode └── settings.json └── WORKSPACE /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* -------------------------------------------------------------------------------- /tools/bazel/BUILD.bazel: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/bazel/gopackagesdriver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | exec bazel run -- @io_bazel_rules_go//go/tools/gopackagesdriver "${@}" -------------------------------------------------------------------------------- /tools/bazel/gen_build_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | bazel run //:gazelle-update-repos && bazel run //:gazelle 6 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_data/BUILD.bazel: -------------------------------------------------------------------------------- 1 | filegroup( 2 | name = "dbc_data", 3 | srcs = glob(["*.json"]), 4 | visibility = ["//visibility:public"], 5 | ) 6 | -------------------------------------------------------------------------------- /server/world/data/dynamic/messages/unit.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | type ( 4 | ModHealth struct { 5 | Amount int 6 | } 7 | 8 | ModPower struct { 9 | Amount int 10 | } 11 | ) 12 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/player.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | type Player struct { 4 | FreeTalentPoints int 5 | DrunkValue int 6 | XP int 7 | Money int 8 | 9 | Tutorials [256]bool 10 | } 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | X Have a well-defined interface which Unit and Player can share (esp. for the in-game logic) 2 | - Make the combat manager defer to the Updater to send combat updates 3 | - Use a Current Health/Max Health model instead of Health Percent/Max Health -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_data/ChrStartingLocations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "race": "Human", 4 | "map": 0, 5 | "zone": 12, 6 | "x": -8949.95, 7 | "y": -132.493, 8 | "z": 83.5312, 9 | "o": 0.0 10 | } 11 | ] -------------------------------------------------------------------------------- /lib/util/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "util", 5 | srcs = ["util.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/lib/util", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/unit.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // AttackInfo encapsulates all information about the result of an attack. 4 | type AttackInfo struct { 5 | // Damage is the amount of health which should be removed from the target. 6 | Damage int 7 | } 8 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc/dbc_golang.go.jinja: -------------------------------------------------------------------------------- 1 | package {{ package }} 2 | 3 | {% for chunk in chunks %} 4 | {{ chunk }} 5 | {% endfor %} 6 | 7 | func init() { 8 | {% for init_function_name in init_function_names %} 9 | {{ init_function_name }}() 10 | {% endfor %} 11 | } 12 | -------------------------------------------------------------------------------- /server/world/game/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "game", 5 | srcs = ["stats.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/game", 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_library") 2 | 3 | py_library( 4 | name = "dbc", 5 | srcs = glob(["*.py"]), 6 | data = glob(["*.jinja"]), 7 | visibility = [ 8 | "//tools/dbc_utils:__subpackages__", 9 | ], 10 | ) 11 | -------------------------------------------------------------------------------- /tools/gen_opcode_to_handler/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_binary") 2 | 3 | py_binary( 4 | name = "gen_opcode_to_handler", 5 | srcs = ["gen_opcode_to_handler.py"], 6 | args = ["--package_path=/home/jeshua/code/wow_server_go/server/world/packet"], 7 | visibility = ["//visibility:public"], 8 | ) 9 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/unit.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 4 | 5 | type Unit struct { 6 | Level int 7 | Race *static.Race 8 | Class *static.Class 9 | Gender static.Gender 10 | Team static.Team 11 | StandState static.StandState 12 | } 13 | -------------------------------------------------------------------------------- /server/world/channels/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "channels", 5 | srcs = ["channels.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/channels", 7 | visibility = ["//visibility:public"], 8 | deps = ["//server/world/data/dynamic/interfaces"], 9 | ) 10 | -------------------------------------------------------------------------------- /.github/workflows/ci-bazel.yaml: -------------------------------------------------------------------------------- 1 | name: Bazel 2 | on: 3 | pull_request: 4 | push: 5 | branches: master 6 | jobs: 7 | Build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: bazel build //... 12 | 13 | Test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - run: bazel test //... 18 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/item.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 4 | 5 | type Item interface { 6 | // GetTemplate retreives the item template this object is based on. 7 | GetTemplate() *static.Item 8 | 9 | // GetContainer retrives the container that the item is within, or nil if it isn't inside 10 | // a container. 11 | GetContainer() GUID 12 | } 13 | -------------------------------------------------------------------------------- /server/world/data/dynamic/messages/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "messages", 5 | srcs = [ 6 | "combat.go", 7 | "unit.go", 8 | ], 9 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages", 10 | visibility = ["//visibility:public"], 11 | deps = ["//server/world/data/dynamic/interfaces"], 12 | ) 13 | -------------------------------------------------------------------------------- /tools/dbc_utils/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_python//python:defs.bzl", "py_binary") 2 | 3 | py_binary( 4 | name = "dbc_generator", 5 | srcs = ["dbc_generator.py"], 6 | deps = ["//tools/dbc_utils/dbc"], 7 | ) 8 | 9 | py_binary( 10 | name = "golang_generator", 11 | srcs = ["golang_generator.py"], 12 | visibility = ["//visibility:public"], 13 | deps = [ 14 | ":dbc_generator", 15 | "//tools/dbc_utils/dbc", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /server/auth/srp/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") 2 | 3 | go_library( 4 | name = "srp", 5 | srcs = ["srp.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/auth/srp", 7 | visibility = ["//visibility:public"], 8 | ) 9 | 10 | go_test( 11 | name = "srp_test", 12 | srcs = ["srp_test.go"], 13 | deps = [ 14 | ":srp", 15 | "@tools_gotest//assert", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/player_features.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | type PlayerFeatures struct { 4 | SkinColor int 5 | Face int 6 | HairStyle int 7 | HairColor int 8 | Feature int 9 | } 10 | 11 | func (pf *PlayerFeatures) Bytes() uint32 { 12 | return uint32(pf.SkinColor) | uint32(pf.Face)<<8 | uint32(pf.HairStyle)<<16 | uint32(pf.HairColor)<<24 13 | } 14 | 15 | func (pf *PlayerFeatures) Bytes2() uint32 { 16 | return uint32(pf.Feature) 17 | } 18 | -------------------------------------------------------------------------------- /server/auth/data/static/op_code.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // OpCode is the opcode type used by the auth server. 4 | type OpCode int 5 | 6 | // OpCodes used by the AuthServer. 7 | // TODO(jeshua): Implement all OpCodes. 8 | const ( 9 | OpCodeLoginChallenge OpCode = 0x00 10 | OpCodeLoginProof OpCode = 0x01 11 | OpCodeRealmlist OpCode = 0x10 12 | ) 13 | 14 | // Int returns an int reprentation of the opcode. 15 | func (oc OpCode) Int() int { 16 | return int(oc) 17 | } 18 | -------------------------------------------------------------------------------- /server/auth/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "auth", 5 | srcs = ["auth_server.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/auth", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//lib/config", 10 | "//server/auth/data/static", 11 | "//server/auth/packet", 12 | "//server/auth/session", 13 | "@com_github_sirupsen_logrus//:logrus", 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /server/world/data/dynamic/messages/combat.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 4 | 5 | type ( 6 | UnitAttack struct { 7 | Target interfaces.GUID 8 | } 9 | 10 | UnitStopAttack struct{} 11 | 12 | UnitRegisterAttack struct { 13 | Attacker interfaces.GUID 14 | } 15 | 16 | UnitDeregisterAttacker struct { 17 | Attacker interfaces.GUID 18 | } 19 | 20 | UnitDied struct { 21 | DeadUnit interfaces.GUID 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /lib/config/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "config", 5 | srcs = [ 6 | "account.go", 7 | "character.go", 8 | "config.go", 9 | ], 10 | importpath = "github.com/jeshuamorrissey/wow_server_go/lib/config", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//server/auth/srp", 14 | "//server/world/data/dynamic", 15 | "//server/world/data/dynamic/interfaces", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /server/world/data/dynamic/unit_ai.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 6 | ) 7 | 8 | func (u *Unit) HandleAttack(attacker interfaces.GUID) { 9 | // TODO(jeshua): more complex AI. 10 | u.SendUpdates([]interface{}{ 11 | &messages.UnitAttack{Target: attacker}, 12 | }) 13 | } 14 | 15 | func (u *Unit) HandleAttackStop(attacker interfaces.GUID) { 16 | } 17 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/location.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "math" 4 | 5 | // Location represents an objects location and orientation within the world. 6 | type Location struct { 7 | X, Y, Z, O float32 8 | } 9 | 10 | // Distance calculates the distance between two locations. 11 | func (loc *Location) Distance(other *Location) float64 { 12 | return math.Sqrt( 13 | math.Pow(float64(loc.X-other.X), 2) + 14 | math.Pow(float64(loc.X-other.X), 2) + 15 | math.Pow(float64(loc.X-other.X), 2)) 16 | } 17 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_data/ChrStartingStats.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "class": "Warrior", 4 | "race": "Human", 5 | "strength": 4, 6 | "agility": 4, 7 | "stamina": 4, 8 | "intellect": 4, 9 | "spirit": 4, 10 | "base_health": 50 11 | }, 12 | { 13 | "class": "Paladin", 14 | "race": "Human", 15 | "strength": 22, 16 | "agility": 20, 17 | "stamina": 22, 18 | "intellect": 20, 19 | "spirit": 21, 20 | "base_health": 50 21 | } 22 | ] -------------------------------------------------------------------------------- /server/auth/packet/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "packet", 5 | srcs = [ 6 | "login_challenge.go", 7 | "login_proof.go", 8 | "realmlist.go", 9 | ], 10 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/auth/packet", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//lib/util", 14 | "//server/auth/data/static", 15 | "//server/auth/session", 16 | "//server/auth/srp", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /server/auth/session/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "session", 5 | srcs = [ 6 | "packets.go", 7 | "session.go", 8 | "state.go", 9 | ], 10 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/auth/session", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//lib/config", 14 | "//lib/util", 15 | "//server/auth/data/static", 16 | "@com_github_sirupsen_logrus//:logrus", 17 | ], 18 | ) 19 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_update_account_data.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientUpdateAccountData(pkt *packet.ClientUpdateAccountData, state *system.State) ([]interfaces.ServerPacket, error) { 11 | // Not implemented. 12 | return nil, nil 13 | } 14 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_tutorial_flag.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientTutorialFlag(pkt *packet.ClientTutorialFlag, state *system.State) ([]interfaces.ServerPacket, error) { 11 | state.Character.Tutorials[pkt.Flag] = true 12 | return nil, nil 13 | } 14 | -------------------------------------------------------------------------------- /server/world/packet/client_query_time.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ClientQueryTime is sent from the client periodically. 10 | type ClientQueryTime struct{} 11 | 12 | // FromBytes reads packet data from the given buffer. 13 | func (pkt *ClientQueryTime) FromBytes(buffer io.Reader) error { 14 | return nil 15 | } 16 | 17 | // OpCode gets the opcode of the packet. 18 | func (*ClientQueryTime) OpCode() static.OpCode { 19 | return static.OpCodeClientQueryTime 20 | } 21 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_query_time.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientQueryTime(pkt *packet.ClientQueryTime, state *system.State) ([]interfaces.ServerPacket, error) { 11 | return []interfaces.ServerPacket{new(packet.ServerQueryTimeResponse)}, nil 12 | } 13 | -------------------------------------------------------------------------------- /server/world/system/state.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic" 8 | ) 9 | 10 | // State contains useful state information passed to all packet 11 | // methods. 12 | type State struct { 13 | Session *Session 14 | 15 | Log *logrus.Entry 16 | 17 | Config *config.Config 18 | OM *dynamic.ObjectManager 19 | Updater *Updater 20 | 21 | Account *config.Account 22 | Character *dynamic.Player 23 | } 24 | -------------------------------------------------------------------------------- /tools/new_game/initial_data/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "initial_data", 5 | srcs = ["initial_data.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/tools/new_game/initial_data", 7 | visibility = ["//visibility:public"], 8 | deps = [ 9 | "//lib/config", 10 | "//server/world/data/dynamic", 11 | "//server/world/data/dynamic/components", 12 | "//server/world/data/dynamic/interfaces", 13 | "//server/world/data/static", 14 | ], 15 | ) 16 | -------------------------------------------------------------------------------- /server/world/packet/client_attack_stop.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ClientAttackStop is sent from the client periodically. 10 | type ClientAttackStop struct{} 11 | 12 | // FromBytes reads packet data from the given buffer. 13 | func (pkt *ClientAttackStop) FromBytes(buffer io.Reader) error { 14 | return nil 15 | } 16 | 17 | // OpCode gets the opcode of the packet. 18 | func (*ClientAttackStop) OpCode() static.OpCode { 19 | return static.OpCodeClientAttackstop 20 | } 21 | -------------------------------------------------------------------------------- /server/world/packet/client_char_enum.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ClientCharEnum is sent from the client when first connecting. 10 | type ClientCharEnum struct{} 11 | 12 | // FromBytes reads packet data from the given buffer. 13 | func (pkt *ClientCharEnum) FromBytes(buffer io.Reader) error { 14 | return nil 15 | } 16 | 17 | // OpCode returns the opcode for this packet. 18 | func (pkt *ClientCharEnum) OpCode() static.OpCode { 19 | return static.OpCodeClientCharEnum 20 | } 21 | -------------------------------------------------------------------------------- /tools/new_game/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "new_game_lib", 5 | srcs = ["new_game.go"], 6 | importpath = "github.com/jeshuamorrissey/wow_server_go/tools/new_game", 7 | visibility = ["//visibility:private"], 8 | deps = [ 9 | "//lib/config", 10 | "//server/world/data/static", 11 | "//tools/new_game/initial_data", 12 | ], 13 | ) 14 | 15 | go_binary( 16 | name = "new_game", 17 | embed = [":new_game_lib"], 18 | visibility = ["//visibility:public"], 19 | ) 20 | -------------------------------------------------------------------------------- /server/world/packet/client_logout_request.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ClientLogoutRequest is sent from the client periodically. 10 | type ClientLogoutRequest struct{} 11 | 12 | // FromBytes reads packet data from the given buffer. 13 | func (pkt *ClientLogoutRequest) FromBytes(buffer io.Reader) error { 14 | return nil 15 | } 16 | 17 | // OpCode gets the opcode of the packet. 18 | func (*ClientLogoutRequest) OpCode() static.OpCode { 19 | return static.OpCodeClientLogoutRequest 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jeshuamorrissey/wow_server_go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/jinzhu/gorm v1.9.16 7 | github.com/sirupsen/logrus v1.8.1 8 | gotest.tools v2.2.0+incompatible 9 | ) 10 | 11 | require ( 12 | github.com/google/go-cmp v0.5.6 // indirect 13 | github.com/jinzhu/inflection v1.0.0 // indirect 14 | github.com/pkg/errors v0.9.1 // indirect 15 | golang.org/x/mod v0.5.1 // indirect 16 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect 17 | golang.org/x/tools v0.1.8 // indirect 18 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_ping.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientPing(pkt *packet.ClientPing, state *system.State) ([]interfaces.ServerPacket, error) { 11 | response := new(packet.ServerPong) 12 | response.Pong = pkt.Ping 13 | 14 | return []interfaces.ServerPacket{response}, nil 15 | } 16 | -------------------------------------------------------------------------------- /server/world/packet/client_update_account_data.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ClientUpdateAccountData is sent from the client periodically. 10 | type ClientUpdateAccountData struct{} 11 | 12 | // FromBytes reads packet data from the given buffer. 13 | func (pkt *ClientUpdateAccountData) FromBytes(buffer io.Reader) error { 14 | return nil 15 | } 16 | 17 | // OpCode gets the opcode of the packet. 18 | func (*ClientUpdateAccountData) OpCode() static.OpCode { 19 | return static.OpCodeClientUpdateAccountData 20 | } 21 | -------------------------------------------------------------------------------- /server/auth/data/static/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | load("//tools/bazel:gen_strings.bzl", "gen_strings") 3 | 4 | gen_strings( 5 | name = "enum_strings", 6 | srcs = glob(["*.go"]), 7 | types = [ 8 | "LoginErrorCode", 9 | "OpCode", 10 | ], 11 | ) 12 | 13 | go_library( 14 | name = "static", 15 | srcs = [ 16 | "login_constants.go", 17 | "op_code.go", 18 | ":enum_strings", # keep 19 | ], 20 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static", 21 | visibility = ["//visibility:public"], 22 | ) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wow-server-go 2 | 3 | This project aims to create golang-based server for World of Warcraft 1.12.1. This is for education purposes only. 4 | 5 | ## Running the server 6 | 7 | The server is built/run using [bazel](https://bazel.build). 8 | 9 | 1. `$ bazel run //:wow_server_go` 10 | 2. Change the `realmlist.wtf` file in your game client to: 11 | 12 | ``` 13 | set realmlist 127.0.0.1:5000 14 | set patchlist 127.0.0.1:5000 15 | ``` 16 | 17 | 3. Launch the game and enter the account name `test` and password `test`. 18 | 19 | ## Running the tests 20 | 21 | Tests are also managed using bazel. 22 | 23 | - `$ bazel test //...` 24 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/game_object.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Object represents a generic object within the game. All objects 4 | // should implement this interface. 5 | type Object interface { 6 | // GUID should return the full GUID of the object. 7 | GUID() GUID 8 | 9 | // SetGUID should set the GUID of the object. 10 | SetGUID(GUID) 11 | 12 | // GetLocation should return the location of the object. 13 | GetLocation() *Location 14 | 15 | // UpdateFields should return the update fields for the object. 16 | UpdateFields() UpdateFieldsMap 17 | 18 | StartUpdateLoop() 19 | SendUpdates([]interface{}) 20 | } 21 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "interfaces", 5 | srcs = [ 6 | "game_object.go", 7 | "guid.go", 8 | "item.go", 9 | "location.go", 10 | "movement_info.go", 11 | "packet.go", 12 | "unit.go", 13 | "update_fields.go", 14 | ], 15 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces", 16 | visibility = ["//visibility:public"], 17 | deps = [ 18 | "//lib/util", 19 | "//server/world/data/static", 20 | ], 21 | ) 22 | -------------------------------------------------------------------------------- /server/world/packet/server_logout_complete.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ServerLogoutComplete is sent back in response to ClientPing. 10 | type ServerLogoutComplete struct{} 11 | 12 | // ToBytes writes out the packet to an array of bytes. 13 | func (pkt *ServerLogoutComplete) ToBytes() ([]byte, error) { 14 | buffer := bytes.NewBufferString("") 15 | return buffer.Bytes(), nil 16 | } 17 | 18 | // OpCode gets the opcode of the packet. 19 | func (*ServerLogoutComplete) OpCode() static.OpCode { 20 | return static.OpCodeServerLogoutComplete 21 | } 22 | -------------------------------------------------------------------------------- /server/world/packet/client_tutorial_flag.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ClientTutorialFlag is sent from the client periodically. 11 | type ClientTutorialFlag struct { 12 | Flag uint32 13 | } 14 | 15 | // FromBytes reads packet data from the given buffer. 16 | func (pkt *ClientTutorialFlag) FromBytes(buffer io.Reader) error { 17 | binary.Read(buffer, binary.LittleEndian, &pkt.Flag) 18 | return nil 19 | } 20 | 21 | // OpCode gets the opcode of the packet. 22 | func (*ClientTutorialFlag) OpCode() static.OpCode { 23 | return static.OpCodeClientTutorialFlag 24 | } 25 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_set_active_mover.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientSetActiveMover(pkt *packet.ClientSetActiveMover, state *system.State) ([]interfaces.ServerPacket, error) { 11 | if pkt.GUID != state.Character.GUID() { 12 | state.Log.Errorf("Incorrect mover GUID: it is %v, but should be %v", pkt.GUID, state.Character.GUID()) 13 | } 14 | 15 | return nil, nil 16 | } 17 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_stand_state_change.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientStandStateChange(pkt *packet.ClientStandStateChange, state *system.State) ([]interfaces.ServerPacket, error) { 11 | state.Character.StandState = pkt.State 12 | 13 | response := new(packet.ServerStandStateUpdate) 14 | response.State = pkt.State 15 | 16 | return []interfaces.ServerPacket{response}, nil 17 | } 18 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/movement_info.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 4 | 5 | // MovementInfo records movement information for a unit. 6 | type MovementInfo struct { 7 | MoveFlags static.MovementFlag 8 | Time uint32 9 | Location Location 10 | 11 | Transport struct { 12 | GUID GUID 13 | Location Location 14 | Time uint32 15 | } 16 | 17 | Pitch float32 // Swimming pitch. 18 | FallTime uint32 // Last time the unit fell. 19 | Jump struct { // Information about the character's jump. 20 | Velocity, SinAngle, CosAngle, XYSpeed float32 21 | } 22 | 23 | // Spline related? 24 | Unk1 float32 25 | } 26 | -------------------------------------------------------------------------------- /server/world/packet/server_pong.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerPong is sent back in response to ClientPing. 11 | type ServerPong struct { 12 | Pong uint32 13 | } 14 | 15 | // ToBytes writes out the packet to an array of bytes. 16 | func (pkt *ServerPong) ToBytes() ([]byte, error) { 17 | buffer := bytes.NewBufferString("") 18 | 19 | binary.Write(buffer, binary.LittleEndian, pkt.Pong) 20 | 21 | return buffer.Bytes(), nil 22 | } 23 | 24 | // OpCode gets the opcode of the packet. 25 | func (*ServerPong) OpCode() static.OpCode { 26 | return static.OpCodeServerPong 27 | } 28 | -------------------------------------------------------------------------------- /server/world/channels/channels.go: -------------------------------------------------------------------------------- 1 | package channels 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | ) 6 | 7 | type CombatUpdate struct { 8 | Attacker interfaces.Object 9 | Target interfaces.Object 10 | AttackInfo *interfaces.AttackInfo 11 | } 12 | 13 | type PacketUpdate struct { 14 | SendTo interfaces.GUID // can be set to 0 to mean "broadcast" 15 | Packet interfaces.ServerPacket 16 | Location *interfaces.Location 17 | } 18 | 19 | var ( 20 | CombatUpdates chan *CombatUpdate = make(chan *CombatUpdate) 21 | ObjectUpdates chan interfaces.GUID = make(chan interfaces.GUID) 22 | PacketUpdates chan *PacketUpdate = make(chan *PacketUpdate) 23 | ) 24 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_char_delete.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | // Handle will ensure that the given account exists. 11 | func HandleClientCharDelete(pkt *packet.ClientCharDelete, state *system.State) ([]interfaces.ServerPacket, error) { 12 | response := new(packet.ServerCharDelete) 13 | response.Error = static.CharErrorCodeDeleteFailed 14 | return []interfaces.ServerPacket{response}, nil 15 | } 16 | -------------------------------------------------------------------------------- /server/world/system/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "system", 5 | srcs = [ 6 | "session.go", 7 | "state.go", 8 | "updater.go", 9 | ], 10 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/system", 11 | visibility = ["//visibility:public"], 12 | deps = [ 13 | "//lib/config", 14 | "//lib/util", 15 | "//server/world/channels", 16 | "//server/world/data/dynamic", 17 | "//server/world/data/dynamic/interfaces", 18 | "//server/world/data/static", 19 | "//server/world/packet", 20 | "@com_github_sirupsen_logrus//:logrus", 21 | ], 22 | ) 23 | -------------------------------------------------------------------------------- /server/world/packet/client_ping.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ClientPing is sent from the client periodically. 11 | type ClientPing struct { 12 | Ping uint32 13 | Latency uint32 14 | } 15 | 16 | // FromBytes reads packet data from the given buffer. 17 | func (pkt *ClientPing) FromBytes(buffer io.Reader) error { 18 | binary.Read(buffer, binary.LittleEndian, &pkt.Ping) 19 | binary.Read(buffer, binary.LittleEndian, &pkt.Latency) 20 | return nil 21 | } 22 | 23 | // OpCode gets the opcode of the packet. 24 | func (*ClientPing) OpCode() static.OpCode { 25 | return static.OpCodeClientPing 26 | } 27 | -------------------------------------------------------------------------------- /server/world/packet/client_stand_state_change.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ClientStandStateChange is sent from the client periodically. 11 | type ClientStandStateChange struct { 12 | State static.StandState 13 | } 14 | 15 | // FromBytes reads packet data from the given buffer. 16 | func (pkt *ClientStandStateChange) FromBytes(buffer io.Reader) error { 17 | binary.Read(buffer, binary.LittleEndian, &pkt.State) 18 | return nil 19 | } 20 | 21 | // OpCode gets the opcode of the packet. 22 | func (*ClientStandStateChange) OpCode() static.OpCode { 23 | return static.OpCodeClientStandstatechange 24 | } 25 | -------------------------------------------------------------------------------- /server/world/packet/server_char_create.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ServerCharCreate is sent from the client when making a character. 10 | type ServerCharCreate struct { 11 | Error static.CharErrorCode 12 | } 13 | 14 | // ToBytes writes out the packet to an array of bytes. 15 | func (pkt *ServerCharCreate) ToBytes() ([]byte, error) { 16 | buffer := bytes.NewBufferString("") 17 | 18 | buffer.WriteByte(uint8(pkt.Error)) 19 | 20 | return buffer.Bytes(), nil 21 | } 22 | 23 | // OpCode gets the opcode of the packet. 24 | func (*ServerCharCreate) OpCode() static.OpCode { 25 | return static.OpCodeServerCharCreate 26 | } 27 | -------------------------------------------------------------------------------- /server/world/packet/server_char_delete.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // ServerCharDelete is sent from the client when making a character. 10 | type ServerCharDelete struct { 11 | Error static.CharErrorCode 12 | } 13 | 14 | // ToBytes writes out the packet to an array of bytes. 15 | func (pkt *ServerCharDelete) ToBytes() ([]byte, error) { 16 | buffer := bytes.NewBufferString("") 17 | 18 | buffer.WriteByte(uint8(pkt.Error)) 19 | 20 | return buffer.Bytes(), nil 21 | } 22 | 23 | // OpCode gets the opcode of the packet. 24 | func (*ServerCharDelete) OpCode() static.OpCode { 25 | return static.OpCodeServerCharDelete 26 | } 27 | -------------------------------------------------------------------------------- /server/world/packet/client_name_query.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientNameQuery is sent from the client periodically. 12 | type ClientNameQuery struct { 13 | GUID interfaces.GUID 14 | } 15 | 16 | // FromBytes reads packet data from the given buffer. 17 | func (pkt *ClientNameQuery) FromBytes(buffer io.Reader) error { 18 | return binary.Read(buffer, binary.LittleEndian, &pkt.GUID) 19 | } 20 | 21 | // OpCode gets the opcode of the packet. 22 | func (*ClientNameQuery) OpCode() static.OpCode { 23 | return static.OpCodeClientNameQuery 24 | } 25 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_data/ChrClasses.json: -------------------------------------------------------------------------------- 1 | [{"id": 1, "primary_stat": 0, "power_type": 1, "pet_type": "PET", "name": "Warrior"}, {"id": 2, "primary_stat": 0, "power_type": 0, "pet_type": "PET", "name": "Paladin"}, {"id": 3, "primary_stat": 1, "power_type": 0, "pet_type": "PET", "name": "Hunter"}, {"id": 4, "primary_stat": 1, "power_type": 3, "pet_type": "PET", "name": "Rogue"}, {"id": 5, "primary_stat": 0, "power_type": 0, "pet_type": "PET", "name": "Priest"}, {"id": 7, "primary_stat": 0, "power_type": 0, "pet_type": "PET", "name": "Shaman"}, {"id": 8, "primary_stat": 0, "power_type": 0, "pet_type": "PET", "name": "Mage"}, {"id": 9, "primary_stat": 0, "power_type": 0, "pet_type": "DEMON", "name": "Warlock"}, {"id": 11, "primary_stat": 0, "power_type": 0, "pet_type": "PET", "name": "Druid"}] -------------------------------------------------------------------------------- /server/world/packet/client_attack_swing.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientAttackSwing is sent from the client periodically. 12 | type ClientAttackSwing struct { 13 | Target interfaces.GUID 14 | } 15 | 16 | // FromBytes reads packet data from the given buffer. 17 | func (pkt *ClientAttackSwing) FromBytes(buffer io.Reader) error { 18 | binary.Read(buffer, binary.LittleEndian, &pkt.Target) 19 | return nil 20 | } 21 | 22 | // OpCode gets the opcode of the packet. 23 | func (*ClientAttackSwing) OpCode() static.OpCode { 24 | return static.OpCodeClientAttackswing 25 | } 26 | -------------------------------------------------------------------------------- /server/world/packet/client_player_login.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientPlayerLogin is sent from the client periodically. 12 | type ClientPlayerLogin struct { 13 | GUID interfaces.GUID 14 | } 15 | 16 | // FromBytes reads packet data from the given buffer. 17 | func (pkt *ClientPlayerLogin) FromBytes(buffer io.Reader) error { 18 | binary.Read(buffer, binary.LittleEndian, &pkt.GUID) 19 | return nil 20 | } 21 | 22 | // OpCode gets the opcode of the packet. 23 | func (*ClientPlayerLogin) OpCode() static.OpCode { 24 | return static.OpCodeClientPlayerLogin 25 | } 26 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/packet.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // Packet is a generic packet. 10 | type Packet interface { 11 | // OpCode returns the opcode for the given packet as an int. 12 | OpCode() static.OpCode 13 | } 14 | 15 | // ServerPacket is a packet sent from this server to a client. 16 | type ServerPacket interface { 17 | Packet 18 | 19 | // ToBytes writes the packet out to an array of bytes. 20 | ToBytes() ([]byte, error) 21 | } 22 | 23 | // ClientPacket is a packet sent from the client to this server. 24 | type ClientPacket interface { 25 | Packet 26 | 27 | // FromBytes reads the packet from a generic reader. 28 | FromBytes(io.Reader) error 29 | } 30 | -------------------------------------------------------------------------------- /server/world/packet/client_char_delete.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ClientCharDelete is sent from the client when deleting a character. 11 | type ClientCharDelete struct { 12 | HighGUID static.HighGUID 13 | ID uint32 14 | } 15 | 16 | // FromBytes loads the packet from the given data. 17 | func (pkt *ClientCharDelete) FromBytes(buffer io.Reader) error { 18 | binary.Read(buffer, binary.LittleEndian, &pkt.HighGUID) 19 | binary.Read(buffer, binary.LittleEndian, &pkt.ID) 20 | return nil 21 | } 22 | 23 | // OpCode returns the opcode for this packet. 24 | func (pkt *ClientCharDelete) OpCode() static.OpCode { 25 | return static.OpCodeClientCharDelete 26 | } 27 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_attack_stop.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | func HandleClientAttackStop(pkt *packet.ClientAttackStop, state *system.State) ([]interfaces.ServerPacket, error) { 11 | state.Character.SendUpdates([]interface{}{ 12 | &messages.UnitStopAttack{}, 13 | }) 14 | 15 | return []interfaces.ServerPacket{ 16 | &packet.ServerAttackStop{ 17 | Attacker: state.Character.GUID(), 18 | Target: state.Character.Target, 19 | }, 20 | }, nil 21 | } 22 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_move.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientMove(pkt *packet.ClientMove, state *system.State) ([]interfaces.ServerPacket, error) { 11 | state.Character.MovementInfo = pkt.MovementInfo 12 | 13 | location := state.Character.GetLocation() 14 | location.X = pkt.MovementInfo.Location.X 15 | location.Y = pkt.MovementInfo.Location.Y 16 | location.Z = pkt.MovementInfo.Location.Z 17 | location.O = pkt.MovementInfo.Location.O 18 | 19 | return nil, nil 20 | } 21 | -------------------------------------------------------------------------------- /server/world/packet/server_query_time_response.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "time" 7 | 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerQueryTimeResponse is sent back in response to ClientPing. 12 | type ServerQueryTimeResponse struct{} 13 | 14 | // ToBytes writes out the packet to an array of bytes. 15 | func (pkt *ServerQueryTimeResponse) ToBytes() ([]byte, error) { 16 | buffer := bytes.NewBufferString("") 17 | 18 | binary.Write(buffer, binary.BigEndian, uint32(time.Now().Unix())) 19 | 20 | return buffer.Bytes(), nil 21 | } 22 | 23 | // OpCode gets the opcode of the packet. 24 | func (*ServerQueryTimeResponse) OpCode() static.OpCode { 25 | return static.OpCodeServerQueryTimeResponse 26 | } 27 | -------------------------------------------------------------------------------- /server/world/packet/client_set_active_mover.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientSetActiveMover is sent from the client periodically. 12 | type ClientSetActiveMover struct { 13 | GUID interfaces.GUID 14 | } 15 | 16 | // FromBytes reads packet data from the given buffer. 17 | func (pkt *ClientSetActiveMover) FromBytes(buffer io.Reader) error { 18 | binary.Read(buffer, binary.LittleEndian, &pkt.GUID) 19 | return nil 20 | } 21 | 22 | // OpCode gets the opcode of the packet. 23 | func (*ClientSetActiveMover) OpCode() static.OpCode { 24 | return static.OpCodeClientSetActiveMover 25 | } 26 | -------------------------------------------------------------------------------- /server/world/packet/server_stand_state_update.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerStandStateUpdate is sent back in response to ClientPing. 11 | type ServerStandStateUpdate struct { 12 | State static.StandState 13 | } 14 | 15 | // ToBytes writes out the packet to an array of bytes. 16 | func (pkt *ServerStandStateUpdate) ToBytes() ([]byte, error) { 17 | buffer := bytes.NewBufferString("") 18 | 19 | binary.Write(buffer, binary.LittleEndian, pkt.State) 20 | 21 | return buffer.Bytes(), nil 22 | } 23 | 24 | // OpCode gets the opcode of the packet. 25 | func (*ServerStandStateUpdate) OpCode() static.OpCode { 26 | return static.OpCodeServerStandstateUpdate 27 | } 28 | -------------------------------------------------------------------------------- /server/world/packet/server_account_data_times.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerAccountDataTimes is sent back in response to ClientPing. 11 | type ServerAccountDataTimes struct{} 12 | 13 | // ToBytes writes out the packet to an array of bytes. 14 | func (pkt *ServerAccountDataTimes) ToBytes() ([]byte, error) { 15 | buffer := bytes.NewBufferString("") 16 | 17 | for i := 0; i < 32; i++ { 18 | binary.Write(buffer, binary.LittleEndian, uint32(0)) 19 | } 20 | 21 | return buffer.Bytes(), nil 22 | } 23 | 24 | // OpCode gets the opcode of the packet. 25 | func (*ServerAccountDataTimes) OpCode() static.OpCode { 26 | return static.OpCodeServerAccountDataTimes 27 | } 28 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "components", 5 | srcs = [ 6 | "combat.go", 7 | "health_power.go", 8 | "movement_info.go", 9 | "player.go", 10 | "player_features.go", 11 | "stats.go", 12 | "unit.go", 13 | ], 14 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/components", 15 | visibility = ["//visibility:public"], 16 | deps = [ 17 | "//lib/util", 18 | "//server/world/channels", 19 | "//server/world/data/dynamic/interfaces", 20 | "//server/world/data/dynamic/messages", 21 | "//server/world/data/static", 22 | "//server/world/packet", 23 | ], 24 | ) 25 | -------------------------------------------------------------------------------- /server/world/packet/server_auth_challenge.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerAuthChallenge is the initial message sent from the server 11 | // to the client to start authorization. 12 | type ServerAuthChallenge struct { 13 | Seed uint32 14 | } 15 | 16 | // ToBytes writes out the packet to an array of bytes. 17 | func (pkt *ServerAuthChallenge) ToBytes() ([]byte, error) { 18 | buffer := bytes.NewBufferString("") 19 | 20 | binary.Write(buffer, binary.BigEndian, pkt.Seed) 21 | 22 | return buffer.Bytes(), nil 23 | } 24 | 25 | // OpCode gets the opcode of the packet. 26 | func (*ServerAuthChallenge) OpCode() static.OpCode { 27 | return static.OpCodeServerAuthChallenge 28 | } 29 | -------------------------------------------------------------------------------- /mmo_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 9 | "github.com/jeshuamorrissey/wow_server_go/server/auth" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world" 11 | ) 12 | 13 | func main() { 14 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true}) 15 | logrus.SetLevel(logrus.DebugLevel) 16 | 17 | // Load the world config and object manager. 18 | // TODO(jeshua): make this load from a command-line flag maybe? 19 | config := config.NewConfigFromJSON("world.json") 20 | 21 | var wg sync.WaitGroup 22 | wg.Add(2) 23 | go func() { 24 | defer wg.Done() 25 | auth.RunAuthServer(5000, config) 26 | }() 27 | 28 | go func() { 29 | defer wg.Done() 30 | world.RunWorldServer("Sydney", 5001, config) 31 | }() 32 | 33 | wg.Wait() 34 | } 35 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_logout_request.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientLogoutRequest(pkt *packet.ClientLogoutRequest, state *system.State) ([]interfaces.ServerPacket, error) { 11 | response := new(packet.ServerLogoutResponse) 12 | 13 | // TODO: Actually implement this! 14 | response.Reason = 0 15 | response.InstantLogout = true 16 | 17 | if state.Character != nil { 18 | state.Updater.Logout(state.Character.GUID()) 19 | } 20 | 21 | return []interfaces.ServerPacket{ 22 | response, new(packet.ServerLogoutComplete), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /server/world/packet/client_creature_query.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientCreatureQuery is sent from the client periodically. 12 | type ClientCreatureQuery struct { 13 | Entry uint32 14 | GUID interfaces.GUID 15 | } 16 | 17 | // FromBytes reads packet data from the given buffer. 18 | func (pkt *ClientCreatureQuery) FromBytes(buffer io.Reader) error { 19 | binary.Read(buffer, binary.LittleEndian, &pkt.Entry) 20 | binary.Read(buffer, binary.LittleEndian, &pkt.GUID) 21 | return nil 22 | } 23 | 24 | // OpCode gets the opcode of the packet. 25 | func (*ClientCreatureQuery) OpCode() static.OpCode { 26 | return static.OpCodeClientCreatureQuery 27 | } 28 | -------------------------------------------------------------------------------- /server/world/packet/client_item_query_single.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientItemQuerySingle is sent from the client periodically. 12 | type ClientItemQuerySingle struct { 13 | Entry uint32 14 | GUID interfaces.GUID 15 | } 16 | 17 | // FromBytes reads packet data from the given buffer. 18 | func (pkt *ClientItemQuerySingle) FromBytes(buffer io.Reader) error { 19 | binary.Read(buffer, binary.LittleEndian, &pkt.Entry) 20 | binary.Read(buffer, binary.LittleEndian, &pkt.GUID) 21 | return nil 22 | } 23 | 24 | // OpCode gets the opcode of the packet. 25 | func (*ClientItemQuerySingle) OpCode() static.OpCode { 26 | return static.OpCodeClientItemQuerySingle 27 | } 28 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_name_query.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientNameQuery(pkt *packet.ClientNameQuery, state *system.State) ([]interfaces.ServerPacket, error) { 11 | if !state.OM.Exists(pkt.GUID) { 12 | return nil, nil 13 | } 14 | 15 | return []interfaces.ServerPacket{&packet.ServerNameQueryResponse{ 16 | RealmName: state.Config.Name, 17 | CharacterName: state.Account.Character.Name, 18 | PlayerGUID: state.Character.GUID(), 19 | PlayerRace: state.Character.Race, 20 | PlayerGender: state.Character.Gender, 21 | PlayerClass: state.Character.Class, 22 | }}, nil 23 | } 24 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_attack_swing.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | func HandleClientAttackSwing(pkt *packet.ClientAttackSwing, state *system.State) ([]interfaces.ServerPacket, error) { 11 | target := state.OM.Get(pkt.Target) 12 | if target == nil { 13 | state.Log.Warnf("Received CLIENT_ATTACK_SWING with non-existant target %v (%v)", pkt.Target.Low(), pkt.Target.High()) 14 | return []interfaces.ServerPacket{}, nil 15 | } 16 | 17 | state.Character.SendUpdates([]interface{}{ 18 | &messages.UnitAttack{Target: target.GUID()}, 19 | }) 20 | 21 | return []interfaces.ServerPacket{}, nil 22 | } 23 | -------------------------------------------------------------------------------- /server/world/packet/server_attack_start.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerAttackStart is sent back in response to ClientPing. 12 | type ServerAttackStart struct { 13 | Attacker interfaces.GUID 14 | Target interfaces.GUID 15 | } 16 | 17 | // ToBytes writes out the packet to an array of bytes. 18 | func (pkt *ServerAttackStart) ToBytes() ([]byte, error) { 19 | buffer := bytes.NewBufferString("") 20 | 21 | binary.Write(buffer, binary.LittleEndian, pkt.Attacker) 22 | binary.Write(buffer, binary.LittleEndian, pkt.Target) 23 | 24 | return buffer.Bytes(), nil 25 | } 26 | 27 | // OpCode gets the opcode of the packet. 28 | func (*ServerAttackStart) OpCode() static.OpCode { 29 | return static.OpCodeServerAttackstart 30 | } 31 | -------------------------------------------------------------------------------- /server/world/packet/server_logout_response.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerLogoutResponse is sent back in response to ClientPing. 11 | type ServerLogoutResponse struct { 12 | Reason uint32 13 | InstantLogout bool 14 | } 15 | 16 | // ToBytes writes out the packet to an array of bytes. 17 | func (pkt *ServerLogoutResponse) ToBytes() ([]byte, error) { 18 | buffer := bytes.NewBufferString("") 19 | 20 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Reason)) 21 | if pkt.InstantLogout { 22 | binary.Write(buffer, binary.LittleEndian, uint8(1)) 23 | } else { 24 | binary.Write(buffer, binary.LittleEndian, uint8(0)) 25 | } 26 | 27 | return buffer.Bytes(), nil 28 | } 29 | 30 | // OpCode gets the opcode of the packet. 31 | func (*ServerLogoutResponse) OpCode() static.OpCode { 32 | return static.OpCodeServerLogoutResponse 33 | } 34 | -------------------------------------------------------------------------------- /server/world/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | genrule( 4 | name = "opcode_to_handler", 5 | srcs = ["//server/world/packet:packet_srcs"], 6 | outs = ["opcode_to_handler.go"], 7 | cmd = "$(location //tools/gen_opcode_to_handler) --package_path=./server/world/packet > $@", 8 | tools = ["//tools/gen_opcode_to_handler"], 9 | ) 10 | 11 | go_library( 12 | name = "world", 13 | srcs = [ 14 | "opcode_to_handler.go", 15 | "world_server.go", 16 | ], 17 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world", 18 | visibility = ["//visibility:public"], 19 | deps = [ 20 | "//lib/config", 21 | "//server/world/data/dynamic/interfaces", 22 | "//server/world/data/static", 23 | "//server/world/packet", 24 | "//server/world/packet/handlers", # keep 25 | "//server/world/system", 26 | "@com_github_sirupsen_logrus//:logrus", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /server/world/data/dynamic/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "dynamic", 5 | srcs = [ 6 | "container.go", 7 | "game_object.go", 8 | "item.go", 9 | "object_manager.go", 10 | "object_utils.go", 11 | "player.go", 12 | "player_game_logic.go", 13 | "player_handlers.go", 14 | "unit.go", 15 | "unit_ai.go", 16 | "unit_game_logic.go", 17 | ], 18 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic", 19 | visibility = ["//visibility:public"], 20 | deps = [ 21 | "//server/world/channels", 22 | "//server/world/data/dynamic/components", 23 | "//server/world/data/dynamic/interfaces", 24 | "//server/world/data/dynamic/messages", 25 | "//server/world/data/static", 26 | "//server/world/game", 27 | "@com_github_sirupsen_logrus//:logrus", 28 | ], 29 | ) 30 | -------------------------------------------------------------------------------- /lib/config/character.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 4 | 5 | type CharacterSettings struct { 6 | HideHelm bool `json:"hide_helm"` 7 | HideCloak bool `json:"hide_cloak"` 8 | } 9 | 10 | type Character struct { 11 | Name string `json:"name"` 12 | GUID interfaces.GUID `json:"guid"` 13 | HasLoggedIn bool `json:"has_logged_in"` 14 | Settings CharacterSettings `json:"settings"` 15 | } 16 | 17 | // Flags returns an set of flags based on the character's state. 18 | func (char *Character) Flags() uint32 { 19 | var flags uint32 20 | // if char.Settings.HideHelm { 21 | // flags |= uint32(static.CharacterFlagHideHelm) 22 | // } 23 | 24 | // if char.Settings.HideCloak { 25 | // flags |= uint32(static.CharacterFlagHideCloak) 26 | // } 27 | 28 | // if char.Settings.IsGhost { 29 | // flags |= uint32(static.CharacterFlagGhost) 30 | // } 31 | 32 | return flags 33 | } 34 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_creature_query.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | // Handle will ensure that the given account exists. 11 | func HandleClientCreatureQuery(pkt *packet.ClientCreatureQuery, state *system.State) ([]interfaces.ServerPacket, error) { 12 | response := new(packet.ServerCreatureQueryResponse) 13 | 14 | response.Unit = nil 15 | if unit, ok := static.Units[int(pkt.Entry)]; ok { 16 | response.Unit = unit 17 | response.Entry = pkt.Entry 18 | } else if pkt.GUID != 0 && state.OM.Exists(pkt.GUID) { 19 | response.Unit = state.OM.GetUnit(pkt.GUID).Template() 20 | response.Entry = uint32(response.Unit.Entry) 21 | } 22 | 23 | return []interfaces.ServerPacket{response}, nil 24 | } 25 | -------------------------------------------------------------------------------- /server/world/packet/server_auth_response.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerAuthResponse is the initial message sent from the server 11 | // to the client to start authorization. 12 | type ServerAuthResponse struct { 13 | Error static.AuthErrorCode 14 | } 15 | 16 | // ToBytes writes out the packet to an array of bytes. 17 | func (pkt *ServerAuthResponse) ToBytes() ([]byte, error) { 18 | buffer := bytes.NewBufferString("") 19 | 20 | binary.Write(buffer, binary.LittleEndian, pkt.Error) 21 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // unk 22 | binary.Write(buffer, binary.LittleEndian, uint8(0)) // unk 23 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // unk 24 | 25 | return buffer.Bytes(), nil 26 | } 27 | 28 | // OpCode gets the opcode of the packet. 29 | func (*ServerAuthResponse) OpCode() static.OpCode { 30 | return static.OpCodeServerAuthResponse 31 | } 32 | -------------------------------------------------------------------------------- /server/world/packet/server_tutorial_flags.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "math/big" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerTutorialFlags is sent back in response to ClientPing. 12 | type ServerTutorialFlags struct { 13 | Tutorials [256]bool 14 | } 15 | 16 | // ToBytes writes out the packet to an array of bytes. 17 | func (pkt *ServerTutorialFlags) ToBytes() ([]byte, error) { 18 | buffer := bytes.NewBufferString("") 19 | 20 | // Convert the binary array to a bitmask. 21 | mask := big.NewInt(0) 22 | for i, isDone := range pkt.Tutorials { 23 | if isDone { 24 | mask.SetBit(mask, i, 1) 25 | } 26 | } 27 | 28 | buffer.Write(util.PadBigIntBytes(util.ReverseBytes(mask.Bytes()), 8)) 29 | 30 | return buffer.Bytes(), nil 31 | } 32 | 33 | // OpCode gets the opcode of the packet. 34 | func (*ServerTutorialFlags) OpCode() static.OpCode { 35 | return static.OpCodeServerTutorialFlags 36 | } 37 | -------------------------------------------------------------------------------- /server/world/packet/server_attack_stop.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerAttackStop is sent back in response to ClientPing. 12 | type ServerAttackStop struct { 13 | Attacker interfaces.GUID 14 | Target interfaces.GUID 15 | } 16 | 17 | // ToBytes writes out the packet to an array of bytes. 18 | func (pkt *ServerAttackStop) ToBytes() ([]byte, error) { 19 | buffer := bytes.NewBufferString("") 20 | 21 | binary.Write(buffer, binary.LittleEndian, pkt.Attacker.Pack()) 22 | if pkt.Target > 0 { 23 | binary.Write(buffer, binary.LittleEndian, pkt.Target.Pack()) 24 | } 25 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // unk 26 | 27 | return buffer.Bytes(), nil 28 | } 29 | 30 | // OpCode gets the opcode of the packet. 31 | func (*ServerAttackStop) OpCode() static.OpCode { 32 | return static.OpCodeServerAttackstop 33 | } 34 | -------------------------------------------------------------------------------- /tools/bazel/gen_strings.bzl: -------------------------------------------------------------------------------- 1 | """Utility macro for runner `stringer` across a set of files.""" 2 | 3 | def gen_strings(name, srcs, types): 4 | """Generate a string representation of the types using "stringer". 5 | 6 | Args: 7 | name: The name of the rule. 8 | srcs: Golang source files to include. 9 | types: A list of types to generate strings for. 10 | """ 11 | all_files = [] 12 | for type in types: 13 | rule_name = "{}.{}".format(name, type) 14 | filename = "{}.go".format(rule_name) 15 | all_files.append(filename) 16 | 17 | native.genrule( 18 | name = rule_name, 19 | srcs = srcs, 20 | outs = [filename], 21 | cmd = "HOME=$$(pwd) $(location @org_golang_x_tools//cmd/stringer) -output $@ -trimprefix {} -type {} {}/*".format( 22 | type, 23 | type, 24 | native.package_name(), 25 | ), 26 | tools = ["@org_golang_x_tools//cmd/stringer"], 27 | ) 28 | 29 | native.filegroup( 30 | name = name, 31 | srcs = all_files, 32 | ) 33 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_item_query_single.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | // Handle will ensure that the given account exists. 11 | func HandleClientItemQuerySingle(pkt *packet.ClientItemQuerySingle, state *system.State) ([]interfaces.ServerPacket, error) { 12 | response := new(packet.ServerItemQuerySingleResponse) 13 | 14 | response.Entry = pkt.Entry 15 | response.Item = nil 16 | if item, ok := static.Items[int(pkt.Entry)]; ok { 17 | response.Item = item 18 | } else if pkt.GUID != 0 && state.OM.Exists(pkt.GUID) { 19 | if item := state.OM.GetItem(pkt.GUID); item != nil { 20 | response.Item = item.GetTemplate() 21 | } else if container := state.OM.GetContainer(pkt.GUID); container != nil { 22 | response.Item = container.GetTemplate() 23 | } 24 | } 25 | 26 | return []interfaces.ServerPacket{response}, nil 27 | } 28 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/health_power.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 5 | ) 6 | 7 | const ( 8 | HealthPerStamina = 10 9 | ManaPerIntellect = 20 10 | 11 | PlayerRegenPercentBasePerSecond float64 = 0.05 // 5% mana regen per second (20 seconds to fully recover mana) 12 | PlayerRegenPercentPerSpiritPerSecond float64 = 0.05 / 100.0 // 5% mana regen per 100 spirit (~100 at level 60) 13 | PlayerRegenCombatPenaltyPercent float64 = 0.80 // 80% reduction in regen when in combat 14 | 15 | UnitRegentPercentPerSecond float64 = 0.3 // 30% regen per second 16 | UnitRegentCombatPenaltyPercent float64 = 1.0 // 100% reduction in regen when in combat 17 | ) 18 | 19 | // HealthPower tracks 20 | type HealthPower struct { 21 | CurrentHealth int 22 | CurrentPower int 23 | } 24 | 25 | func (hp *HealthPower) ModHealth(healthMod int, maxHealth int) { 26 | hp.CurrentHealth = util.Clamp(0, hp.CurrentHealth+healthMod, maxHealth) 27 | } 28 | 29 | func (hp *HealthPower) ModPower(powerMod int, maxPower int) { 30 | hp.CurrentPower = util.Clamp(0, hp.CurrentPower+powerMod, maxPower) 31 | } 32 | -------------------------------------------------------------------------------- /server/world/packet/server_login_verify_world.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerLoginVerifyWorld is sent back in response to ClientPing. 12 | type ServerLoginVerifyWorld struct { 13 | MapID int 14 | Location interfaces.Location 15 | } 16 | 17 | // ToBytes writes out the packet to an array of bytes. 18 | func (pkt *ServerLoginVerifyWorld) ToBytes() ([]byte, error) { 19 | buffer := bytes.NewBufferString("") 20 | 21 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.MapID)) 22 | binary.Write(buffer, binary.LittleEndian, float32(pkt.Location.X)) 23 | binary.Write(buffer, binary.LittleEndian, float32(pkt.Location.Y)) 24 | binary.Write(buffer, binary.LittleEndian, float32(pkt.Location.Z)) 25 | binary.Write(buffer, binary.LittleEndian, float32(pkt.Location.O)) 26 | 27 | return buffer.Bytes(), nil 28 | } 29 | 30 | // OpCode gets the opcode of the packet. 31 | func (*ServerLoginVerifyWorld) OpCode() static.OpCode { 32 | return static.OpCodeServerLoginVerifyWorld 33 | } 34 | -------------------------------------------------------------------------------- /server/world/data/dynamic/unit_game_logic.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/game" 9 | ) 10 | 11 | // Unit interface methods (game-logic). 12 | func (u *Unit) Initialize() { 13 | go u.regenHealthAndPower() 14 | } 15 | 16 | // Utility methods. 17 | func (u *Unit) regenHealthAndPower() { 18 | for range time.Tick(static.RegenTimeout) { 19 | if u.CurrentHealth == 0 { 20 | return 21 | } 22 | 23 | secondsInTimeout := static.RegenTimeout / time.Second 24 | healthMod := game.UnitRegenPerSecond(u.maxHealth(), u.IsInCombat()) * int(secondsInTimeout) 25 | powerMod := game.UnitRegenPerSecond(u.maxPower(), u.IsInCombat()) * int(secondsInTimeout) 26 | 27 | u.UpdateChannel() <- []interface{}{ 28 | &messages.ModHealth{Amount: healthMod}, 29 | &messages.ModPower{Amount: powerMod}, 30 | } 31 | } 32 | } 33 | 34 | func (u *Unit) maxHealth() int { 35 | return u.Template().MaxHealth 36 | } 37 | 38 | func (u *Unit) maxPower() int { 39 | return u.Template().MaxPower 40 | } 41 | -------------------------------------------------------------------------------- /lib/config/account.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "math/big" 4 | 5 | type Account struct { 6 | Name string `json:"name"` 7 | Character *Character `json:"character"` 8 | 9 | // The following fields are required for "authentication". 10 | SaltStr string `json:"srp_salt"` 11 | salt *big.Int 12 | VerifierStr string `json:"srp_verifier"` 13 | verifier *big.Int 14 | SessionKeyStr string `json:"srp_session_key"` 15 | sessionKey *big.Int 16 | } 17 | 18 | // Verifier gets a big.Int version of the account verifier. 19 | func (a *Account) Verifier() *big.Int { 20 | if a.verifier == nil { 21 | a.verifier, _ = new(big.Int).SetString(a.VerifierStr, 16) 22 | } 23 | 24 | return a.verifier 25 | } 26 | 27 | // Salt gets a big.Int version of the account salt. 28 | func (a *Account) Salt() *big.Int { 29 | if a.salt == nil { 30 | a.salt, _ = new(big.Int).SetString(a.SaltStr, 16) 31 | } 32 | 33 | return a.salt 34 | } 35 | 36 | // SessionKey gets a big.Int version of the account session key. 37 | func (a *Account) SessionKey() *big.Int { 38 | if a.sessionKey == nil { 39 | a.sessionKey, _ = new(big.Int).SetString(a.SessionKeyStr, 16) 40 | } 41 | 42 | return a.sessionKey 43 | } 44 | -------------------------------------------------------------------------------- /BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | load("@bazel_gazelle//:def.bzl", "gazelle") 3 | 4 | # gazelle:prefix github.com/jeshuamorrissey/wow_server_go 5 | gazelle(name = "gazelle") 6 | 7 | gazelle( 8 | name = "gazelle-update-repos", 9 | args = [ 10 | "-from_file=go.mod", 11 | "-to_macro=tools/bazel/deps.bzl%go_dependencies", 12 | "-prune", 13 | ], 14 | command = "update-repos", 15 | ) 16 | 17 | genrule( 18 | name = "example_save", 19 | srcs = [], 20 | outs = ["world.json"], 21 | cmd = "$(location //tools/new_game) -name test > $@", 22 | tools = ["//tools/new_game"], 23 | ) 24 | 25 | go_library( 26 | name = "wow_server_go_lib", 27 | srcs = ["mmo_server.go"], 28 | importpath = "github.com/jeshuamorrissey/wow_server_go", 29 | visibility = ["//visibility:private"], 30 | deps = [ 31 | "//lib/config", 32 | "//server/auth", 33 | "//server/world", 34 | "@com_github_sirupsen_logrus//:logrus", 35 | ], 36 | ) 37 | 38 | go_binary( 39 | name = "wow_server_go", 40 | data = [":example_save"], 41 | embed = [":wow_server_go_lib"], 42 | visibility = ["//visibility:public"], 43 | ) 44 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.goroot": "${workspaceFolder}/bazel-wow_server_go/external/go_sdk", 3 | "go.toolsEnvVars": { 4 | "GOPACKAGESDRIVER": "${workspaceFolder}/tools/bazel/gopackagesdriver.sh" 5 | }, 6 | "go.enableCodeLens": { 7 | "references": false, 8 | "runtest": false 9 | }, 10 | "gopls": { 11 | "build.directoryFilters": [ 12 | "-bazel-bin", 13 | "-bazel-out", 14 | "-bazel-testlogs", 15 | "-bazel-wow_server_go", 16 | ], 17 | "formatting.gofumpt": true, 18 | "formatting.local": "github.com/jeshuamorrissey/wow_server_go", 19 | "ui.completion.usePlaceholders": true, 20 | "ui.semanticTokens": true, 21 | "ui.codelenses": { 22 | "gc_details": false, 23 | "regenerate_cgo": false, 24 | "generate": false, 25 | "test": false, 26 | "tidy": false, 27 | "upgrade_dependency": false, 28 | "vendor": false 29 | }, 30 | }, 31 | "go.useLanguageServer": true, 32 | "go.buildOnSave": "off", 33 | "go.lintOnSave": "off", 34 | "go.vetOnSave": "off", 35 | "python.formatting.provider": "yapf", 36 | } -------------------------------------------------------------------------------- /server/world/packet/handlers/client_player_login.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 7 | ) 8 | 9 | // Handle will ensure that the given account exists. 10 | func HandleClientPlayerLogin(pkt *packet.ClientPlayerLogin, state *system.State) ([]interfaces.ServerPacket, error) { 11 | state.Log.Infof("%v %v", pkt, state.OM.Players) 12 | if !state.OM.Exists(pkt.GUID) { 13 | state.Log.Errorf("Attempt to log in with unknown GUID %v!", pkt.GUID) 14 | return []interfaces.ServerPacket{}, nil 15 | } 16 | 17 | player := state.OM.GetPlayer(pkt.GUID) 18 | state.Updater.Login(player.GUID(), state.Session) 19 | state.Character = player 20 | 21 | return []interfaces.ServerPacket{ 22 | &packet.ServerLoginVerifyWorld{ 23 | MapID: player.MapID, 24 | Location: player.Location, 25 | }, 26 | &packet.ServerAccountDataTimes{}, 27 | &packet.ServerTutorialFlags{ 28 | Tutorials: player.Tutorials, 29 | }, 30 | &packet.ServerInitWorldStates{ 31 | Map: uint32(player.MapID), 32 | Zone: uint32(player.ZoneID), 33 | }, 34 | }, nil 35 | } 36 | -------------------------------------------------------------------------------- /tools/new_game/new_game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 11 | "github.com/jeshuamorrissey/wow_server_go/tools/new_game/initial_data" 12 | ) 13 | 14 | func main() { 15 | namePtr := flag.String("name", "", "The name of the save file.") 16 | 17 | flag.Parse() 18 | 19 | name := *namePtr 20 | if name == "" { 21 | log.Fatal("Missing required flag -name") 22 | return 23 | } 24 | 25 | // Make an empty configuration file. 26 | config := config.NewConfig(name) 27 | 28 | // Make a character. 29 | var err error 30 | config.Accounts[0].Character, err = initial_data.NewCharacter( 31 | config, 32 | "Leeroy Jenkins", 33 | static.RaceHuman, static.ClassPaladin, static.GenderMale, 34 | 1, 1, 1, 1, 1) 35 | if err != nil { 36 | log.Fatalf("Failed to create character: %v", err) 37 | return 38 | } 39 | 40 | // Populate the world. 41 | initial_data.PopulateWorld(config) 42 | 43 | jsonContent, err := json.Marshal(&config) 44 | if err != nil { 45 | log.Fatalf("Failed to save config to JSON: %v", err) 46 | return 47 | } 48 | 49 | fmt.Print(string(jsonContent)) 50 | } 51 | -------------------------------------------------------------------------------- /server/world/packet/handlers/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | go_library( 4 | name = "handlers", 5 | srcs = [ 6 | "client_attack_stop.go", 7 | "client_attack_swing.go", 8 | "client_auth_session.go", 9 | "client_char_create.go", 10 | "client_char_delete.go", 11 | "client_char_enum.go", 12 | "client_creature_query.go", 13 | "client_item_query_single.go", 14 | "client_logout_request.go", 15 | "client_move.go", 16 | "client_name_query.go", 17 | "client_ping.go", 18 | "client_player_login.go", 19 | "client_query_time.go", 20 | "client_set_active_mover.go", 21 | "client_stand_state_change.go", 22 | "client_tutorial_flag.go", 23 | "client_update_account_data.go", 24 | ], 25 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/packet/handlers", 26 | visibility = ["//visibility:public"], 27 | deps = [ 28 | "//server/world/data/dynamic/interfaces", 29 | "//server/world/data/dynamic/messages", 30 | "//server/world/data/static", 31 | "//server/world/packet", 32 | "//server/world/system", 33 | "//tools/new_game/initial_data", 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /server/world/packet/server_init_world_states.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerInitWorldStates is sent back in response to ClientPing. 11 | type ServerInitWorldStates struct { 12 | Map uint32 13 | Zone uint32 14 | Blocks []WorldStateBlock 15 | } 16 | 17 | // WorldStateBlock represents some world state to initialize. 18 | type WorldStateBlock struct { 19 | State uint32 20 | Value uint32 21 | } 22 | 23 | // ToBytes writes out the packet to an array of bytes. 24 | func (pkt *ServerInitWorldStates) ToBytes() ([]byte, error) { 25 | buffer := bytes.NewBufferString("") 26 | 27 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Map)) 28 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Zone)) 29 | binary.Write(buffer, binary.LittleEndian, uint16(len(pkt.Blocks))) 30 | for _, block := range pkt.Blocks { 31 | binary.Write(buffer, binary.LittleEndian, uint32(block.State)) 32 | binary.Write(buffer, binary.LittleEndian, uint32(block.Value)) 33 | } 34 | 35 | return buffer.Bytes(), nil 36 | } 37 | 38 | // OpCode gets the opcode of the packet. 39 | func (*ServerInitWorldStates) OpCode() static.OpCode { 40 | return static.OpCodeServerInitWorldStates 41 | } 42 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/guid.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "encoding/binary" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // GUID is a globally-unique identifier used to identify objects within the game. 10 | type GUID uint64 11 | 12 | // MakeGUID constructs a new GUID from a low/high pair. 13 | func MakeGUID(low uint32, high static.HighGUID) GUID { 14 | return GUID(uint64(high)<<32 | uint64(low)) 15 | } 16 | 17 | // High returns the high-part of the GUID. This correspondings to a HighGUID value. 18 | func (guid GUID) High() static.HighGUID { 19 | return static.HighGUID(guid >> 32) 20 | } 21 | 22 | // Low returns the low-part of the GUID. This is the part that can be modified by the server. 23 | func (guid GUID) Low() uint32 { 24 | return uint32(guid) 25 | } 26 | 27 | // Pack returns a minimal version of the GUID as an array of bytes. 28 | func (guid GUID) Pack() []byte { 29 | guidBytes := make([]byte, 8) 30 | binary.LittleEndian.PutUint64(guidBytes, uint64(guid)) 31 | 32 | mask := uint8(0) 33 | packedGUID := make([]byte, 0) 34 | for i, b := range guidBytes { 35 | if b != 0 { 36 | mask |= (1 << uint(i)) 37 | packedGUID = append(packedGUID, b) 38 | } 39 | } 40 | 41 | return append([]byte{mask}, packedGUID...) 42 | } 43 | -------------------------------------------------------------------------------- /server/world/game/stats.go: -------------------------------------------------------------------------------- 1 | package game 2 | 3 | const ( 4 | HealthPerStamina = 10 5 | 6 | PlayerRegenPercentBasePerSecond float64 = 0.05 // 5% mana regen per second (20 seconds to fully recover mana) 7 | PlayerRegenPercentPerSpiritPerSecond float64 = 0.05 / 100.0 // 5% mana regen per 100 spirit (~100 at level 60) 8 | PlayerRegenCombatPenaltyPercent float64 = 0.80 // 80% reduction in regen when in combat 9 | 10 | UnitRegentPercentPerSecond float64 = 0.3 // 30% regen per second 11 | UnitRegentCombatPenaltyPercent float64 = 1.0 // 100% reduction in regen when in combat 12 | ) 13 | 14 | func HealthFromStamina(stamina int) int { 15 | return stamina * HealthPerStamina 16 | } 17 | 18 | func PlayerRegenPerSecond(max int, spirit int, inCombat bool) int { 19 | combatPenalty := 1.0 20 | if inCombat { 21 | combatPenalty = 1.0 - PlayerRegenCombatPenaltyPercent 22 | } 23 | 24 | healthPerSecond := int(combatPenalty * PlayerRegenPercentBasePerSecond * float64(max)) 25 | healthPerSecondFromSpirit := int(combatPenalty * PlayerRegenPercentPerSpiritPerSecond * float64(spirit)) 26 | 27 | return healthPerSecond + healthPerSecondFromSpirit 28 | } 29 | 30 | func UnitRegenPerSecond(max int, inCombat bool) int { 31 | combatPenalty := 1.0 32 | if inCombat { 33 | combatPenalty = 1.0 - UnitRegentCombatPenaltyPercent 34 | } 35 | 36 | return int(combatPenalty * UnitRegentPercentPerSecond * float64(max)) 37 | } 38 | -------------------------------------------------------------------------------- /server/world/packet/server_name_query_response.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerNameQueryResponse is sent back in response to ClientPing. 12 | type ServerNameQueryResponse struct { 13 | RealmName string 14 | CharacterName string 15 | PlayerGUID interfaces.GUID 16 | PlayerRace *static.Race 17 | PlayerGender static.Gender 18 | PlayerClass *static.Class 19 | } 20 | 21 | // ToBytes writes out the packet to an array of bytes. 22 | func (pkt *ServerNameQueryResponse) ToBytes() ([]byte, error) { 23 | buffer := bytes.NewBufferString("") 24 | 25 | binary.Write(buffer, binary.LittleEndian, uint64(pkt.PlayerGUID)) 26 | 27 | buffer.WriteString(pkt.CharacterName) 28 | buffer.WriteByte('\x00') 29 | 30 | buffer.WriteString(pkt.RealmName) 31 | buffer.WriteByte('\x00') 32 | 33 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.PlayerRace.ID)) 34 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.PlayerGender)) 35 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.PlayerClass.ID)) 36 | 37 | return buffer.Bytes(), nil 38 | } 39 | 40 | // OpCode gets the opcode of the packet. 41 | func (*ServerNameQueryResponse) OpCode() static.OpCode { 42 | return static.OpCodeServerNameQueryResponse 43 | } 44 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_auth_session.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 10 | ) 11 | 12 | // Handle will ensure that the given account exists. 13 | func HandleClientAuthSession(pkt *packet.ClientAuthSession, state *system.State) ([]interfaces.ServerPacket, error) { 14 | response := new(packet.ServerAuthResponse) 15 | response.Error = static.AuthOK 16 | 17 | for _, account := range state.Config.Accounts { 18 | if strings.ToUpper(account.Name) == strings.ToUpper(string(pkt.AccountName)) { 19 | state.Account = account 20 | } 21 | } 22 | 23 | if state.Account == nil { 24 | response.Error = static.AuthUnknownAccount 25 | } 26 | 27 | // TODO(jeshua): validate the information sent by the client. 28 | // If there is no session key, account is invalid. 29 | if state.Account != nil && state.Account.SessionKey() == nil { 30 | response.Error = static.AuthBadServerProof 31 | } 32 | 33 | if response.Error == static.AuthOK { 34 | state.Log = state.Log.WithField("account", state.Account.Name) 35 | state.Log.Infof("Account %v authenticated!", state.Account.Name) 36 | } 37 | 38 | return []interfaces.ServerPacket{response}, nil 39 | } 40 | -------------------------------------------------------------------------------- /server/auth/session/state.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "math/big" 5 | 6 | "github.com/sirupsen/logrus" 7 | 8 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 9 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 10 | ) 11 | 12 | // State represents all information required by the AuthServer. 13 | type State struct { 14 | log *logrus.Entry 15 | session *Session 16 | 17 | // Mapping from OpCode --> Packet generation function. 18 | opCodeToPacket map[static.OpCode]func() ClientPacket 19 | 20 | // The global configuration. 21 | Config *config.Config 22 | 23 | // The following are SRP specific numbers which are generated as part 24 | // of the authentication flow. 25 | PublicEphemeral *big.Int 26 | PrivateEphemeral *big.Int 27 | 28 | // The user's account is fetched after the first packet is processed. 29 | Account *config.Account 30 | } 31 | 32 | // NewState creates a new state based on the given DB connection. 33 | func NewState(config *config.Config, log *logrus.Entry, opCodeToPacket map[static.OpCode]func() ClientPacket) *State { 34 | return &State{ 35 | Config: config, 36 | log: log, 37 | opCodeToPacket: opCodeToPacket, 38 | PublicEphemeral: new(big.Int), 39 | PrivateEphemeral: new(big.Int), 40 | } 41 | } 42 | 43 | // AddLogField will add a new field to the logger for this session. 44 | func (s *State) AddLogField(key string, value interface{}) { 45 | s.log = s.log.WithField(key, value) 46 | } 47 | -------------------------------------------------------------------------------- /server/world/packet/client_auth_session.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ClientAuthSession is the initial message sent from the server 11 | // to the client to start authorization. 12 | type ClientAuthSession struct { 13 | BuildNumber uint32 14 | AccountName []byte 15 | ClientSeed uint32 16 | ClientProof [20]byte 17 | AddonSize uint32 18 | AddonsCompressed []byte 19 | } 20 | 21 | // FromBytes reads a ClientAuthSession pcket from the byter buffer. 22 | func (pkt *ClientAuthSession) FromBytes(buffer io.Reader) error { 23 | var unk uint32 24 | 25 | binary.Read(buffer, binary.LittleEndian, &pkt.BuildNumber) 26 | binary.Read(buffer, binary.LittleEndian, &unk) 27 | 28 | // Null-terminated account name 29 | var b byte 30 | binary.Read(buffer, binary.LittleEndian, &b) 31 | for b != '\x00' { 32 | pkt.AccountName = append(pkt.AccountName, b) 33 | binary.Read(buffer, binary.LittleEndian, &b) 34 | } 35 | 36 | binary.Read(buffer, binary.LittleEndian, &pkt.ClientSeed) 37 | binary.Read(buffer, binary.LittleEndian, &pkt.ClientProof) 38 | binary.Read(buffer, binary.LittleEndian, &pkt.AddonSize) 39 | 40 | pkt.AddonsCompressed = make([]byte, pkt.AddonSize) 41 | buffer.Read(pkt.AddonsCompressed) 42 | return nil 43 | } 44 | 45 | // OpCode returns the opcode for this packet. 46 | func (pkt *ClientAuthSession) OpCode() static.OpCode { 47 | return static.OpCodeClientAuthSession 48 | } 49 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | http_archive( 4 | name = "io_bazel_rules_go", 5 | sha256 = "2b1641428dff9018f9e85c0384f03ec6c10660d935b750e3fa1492a281a53b0f", 6 | urls = [ 7 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 8 | "https://github.com/bazelbuild/rules_go/releases/download/v0.29.0/rules_go-v0.29.0.zip", 9 | ], 10 | ) 11 | 12 | http_archive( 13 | name = "bazel_gazelle", 14 | sha256 = "de69a09dc70417580aabf20a28619bb3ef60d038470c7cf8442fafcf627c21cb", 15 | urls = [ 16 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 17 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.24.0/bazel-gazelle-v0.24.0.tar.gz", 18 | ], 19 | ) 20 | 21 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 22 | 23 | go_rules_dependencies() 24 | 25 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") 26 | load("//tools/bazel:deps.bzl", "go_dependencies") 27 | 28 | # gazelle:repository_macro tools/bazel/deps.bzl%go_dependencies 29 | go_dependencies() 30 | 31 | go_register_toolchains(version = "1.17.5") 32 | 33 | gazelle_dependencies() 34 | 35 | ## Python rules 36 | http_archive( 37 | name = "rules_python", 38 | sha256 = "cd6730ed53a002c56ce4e2f396ba3b3be262fd7cb68339f0377a45e8227fe332", 39 | url = "https://github.com/bazelbuild/rules_python/releases/download/0.5.0/rules_python-0.5.0.tar.gz", 40 | ) 41 | -------------------------------------------------------------------------------- /server/world/data/dynamic/interfaces/update_fields.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "math/big" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 11 | ) 12 | 13 | // UpdateFieldsMap is a mapping from an UpdateField constant to some value. 14 | // Values stored can be either floating point numbers of unsigned integers (both 32-bit). 15 | type UpdateFieldsMap map[static.UpdateField]interface{} 16 | 17 | // ToBytes converts an update fields map to bytes. 18 | func (m UpdateFieldsMap) ToBytes(nFields int) []byte { 19 | mask := big.NewInt(0) 20 | fields := bytes.NewBufferString("") 21 | 22 | for i := 0; i < nFields; i++ { 23 | field := static.UpdateField(i) 24 | valueGeneric, ok := m[field] 25 | if !ok { 26 | continue 27 | } 28 | 29 | switch value := valueGeneric.(type) { 30 | case float32: 31 | binary.Write(fields, binary.LittleEndian, value) 32 | mask.SetBit(mask, i, 1) 33 | case uint32: 34 | binary.Write(fields, binary.LittleEndian, value) 35 | mask.SetBit(mask, i, 1) 36 | default: 37 | panic(fmt.Sprintf("Unknown field type %T in update fields (%v)", value, field)) 38 | } 39 | } 40 | 41 | nBlocks := (nFields + 32 - 1) / 32 42 | nBytes := (nBlocks * 32) / 8 43 | 44 | fieldBytes := make([]byte, 0) 45 | fieldBytes = append(fieldBytes, uint8(nBlocks)) 46 | fieldBytes = append(fieldBytes, util.PadBigIntBytes(util.ReverseBytes(mask.Bytes()), nBytes)...) 47 | fieldBytes = append(fieldBytes, fields.Bytes()...) 48 | return fieldBytes 49 | } 50 | -------------------------------------------------------------------------------- /lib/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // ReverseBytes takes as input a byte array and returns a reversed version 9 | // of it. 10 | func ReverseBytes(data []byte) []byte { 11 | for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { 12 | data[i], data[j] = data[j], data[i] 13 | } 14 | 15 | return data 16 | } 17 | 18 | // PadBigIntBytes takes as input an array of bytes and a size and ensures that the 19 | // byte array is at least nBytes in length. \x00 bytes will be added to the end 20 | // until the desired length is reached. 21 | func PadBigIntBytes(data []byte, nBytes int) []byte { 22 | if len(data) > nBytes { 23 | return data[:nBytes] 24 | } 25 | 26 | currSize := len(data) 27 | for i := 0; i < nBytes-currSize; i++ { 28 | data = append(data, '\x00') 29 | } 30 | 31 | return data 32 | } 33 | 34 | // ReadBytes will read a specified number of bytes from a given buffer. If not all 35 | // of the data is read (or there was an error), an error will be returned. 36 | func ReadBytes(buffer io.Reader, length int) ([]byte, error) { 37 | data := make([]byte, length) 38 | 39 | if length > 0 { 40 | n, err := buffer.Read(data) 41 | if err != nil { 42 | return nil, fmt.Errorf("error while reading bytes: %v", err) 43 | } 44 | 45 | if n != length { 46 | return nil, fmt.Errorf("short read: wanted %v bytes, got %v", length, n) 47 | } 48 | } 49 | 50 | return data, nil 51 | } 52 | 53 | // Clamp takes a min, max and value and will return the value unless it is out of bounds, 54 | // in which case it will return the bound. 55 | func Clamp(min, value, max int) int { 56 | if value < min { 57 | return min 58 | } else if value > max { 59 | return max 60 | } 61 | 62 | return value 63 | } 64 | -------------------------------------------------------------------------------- /server/auth/auth_server.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | 7 | "github.com/sirupsen/logrus" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 10 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 11 | "github.com/jeshuamorrissey/wow_server_go/server/auth/packet" 12 | "github.com/jeshuamorrissey/wow_server_go/server/auth/session" 13 | ) 14 | 15 | var serverName = "LOGIN" 16 | 17 | var opCodeToPacket = map[static.OpCode]func() session.ClientPacket{ 18 | static.OpCodeLoginChallenge: func() session.ClientPacket { return new(packet.ClientLoginChallenge) }, 19 | static.OpCodeLoginProof: func() session.ClientPacket { return new(packet.ClientLoginProof) }, 20 | static.OpCodeRealmlist: func() session.ClientPacket { return new(packet.ClientRealmlist) }, 21 | } 22 | 23 | // RunAuthServer starts the authentication server, the first point of contact for the client. 24 | func RunAuthServer(port int, config *config.Config) { 25 | log := logrus.WithFields(logrus.Fields{"server": serverName, "port": port}) 26 | 27 | listener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port)) 28 | if err != nil { 29 | log.Fatalf("Error while opening port: %v\n", err) 30 | } 31 | 32 | log.Infof("Listening for %v connections on :%v...", serverName, listener.Addr().String()) 33 | 34 | for { 35 | conn, err := listener.Accept() 36 | if err != nil { 37 | log.Fatalf("Error while receiving client connection: %v\n", err) 38 | } 39 | 40 | log.Printf("Receiving %v connection from %v\n", serverName, conn.RemoteAddr()) 41 | go session.NewSession( 42 | logrus.WithFields(logrus.Fields{"server": serverName, "account": "???"}), 43 | conn, 44 | conn, 45 | session.NewState(config, log, opCodeToPacket), 46 | ).Run() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/world/data/dynamic/game_object.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/channels" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // GameObject represents a generic game object. 10 | type GameObject struct { 11 | ID interfaces.GUID 12 | Entry uint32 13 | ScaleX float32 14 | 15 | // The channel to receive updates on. 16 | update chan []interface{} 17 | } 18 | 19 | // Object interface methods. 20 | func (o *GameObject) GUID() interfaces.GUID { return o.ID } 21 | func (o *GameObject) SetGUID(guid interfaces.GUID) { o.ID = guid } 22 | func (o *GameObject) GetLocation() *interfaces.Location { return nil } 23 | 24 | func (o *GameObject) UpdateFields() interfaces.UpdateFieldsMap { 25 | return interfaces.UpdateFieldsMap{ 26 | static.UpdateFieldGUIDLow: uint32(o.GUID().Low()), 27 | static.UpdateFieldGUIDHigh: uint32(o.GUID().High()), 28 | static.UpdateFieldEntry: uint32(o.Entry), 29 | static.UpdateFieldScaleX: float32(o.ScaleX), 30 | } 31 | } 32 | 33 | func (o *GameObject) UpdateChannel() chan []interface{} { 34 | return o.update 35 | } 36 | 37 | func (o *GameObject) SendUpdates(updates []interface{}) { 38 | o.UpdateChannel() <- updates 39 | } 40 | 41 | func (o *GameObject) CreateUpdateChannel() { 42 | if o.update == nil { 43 | o.update = make(chan []interface{}, 100) 44 | } 45 | } 46 | 47 | func (o *GameObject) StartUpdateLoop() { 48 | if o.UpdateChannel() != nil { 49 | return 50 | } 51 | 52 | o.CreateUpdateChannel() 53 | go func() { 54 | for { 55 | for _, update := range <-o.UpdateChannel() { 56 | switch update.(type) { 57 | default: 58 | } 59 | } 60 | 61 | channels.ObjectUpdates <- o.GUID() 62 | } 63 | }() 64 | } 65 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_generator.py: -------------------------------------------------------------------------------- 1 | """A utility which generates DBC files. 2 | 3 | It can read files in the following formats: 4 | 1. Binary 5 | 2. JSON 6 | 7 | ... and produce files in the following formats: 8 | 1. Binary 9 | 2. JSON 10 | 3. Go (useful for generating Enums). 11 | """ 12 | import argparse 13 | import os 14 | 15 | from dbc import record_types, dbc 16 | 17 | FILENAME_TO_RECORD_TYPE = dict( 18 | ChrClasses=record_types.ChrClasses, 19 | ChrRaces=record_types.ChrRaces, 20 | ChrStartingLocations=record_types.ChrStartingLocations, 21 | ChrStartingStats=record_types.ChrStartingStats, 22 | CharBaseInfo=record_types.CharBaseInfo, 23 | ) 24 | 25 | 26 | def main(args: argparse.Namespace): 27 | record_type = FILENAME_TO_RECORD_TYPE[os.path.splitext( 28 | os.path.basename(args.src))[0]] 29 | 30 | if args.src.endswith('.dbc'): 31 | with open(args.src, 'rb') as f: 32 | table = dbc.Table.FromBinary(f.read(), record_type) 33 | elif args.src.endswith('.json'): 34 | with open(args.src, 'r') as f: 35 | table = dbc.Table.FromJSON(f.read(), record_type) 36 | else: 37 | raise ValueError('Unknown input file type') 38 | 39 | if args.dst.endswith('.dbc'): 40 | output = table.ToBinary() 41 | elif args.dst.endswith('.json'): 42 | output = table.ToJSON() 43 | else: 44 | raise ValueError('Unknown output file type') 45 | 46 | with open(args.dst, 'w') as f: 47 | f.write(output) 48 | 49 | 50 | if __name__ == '__main__': 51 | parser = argparse.ArgumentParser(description='Manage DBC files.') 52 | parser.add_argument( 53 | 'src', type=str, help='The source file to use for the conversion.') 54 | parser.add_argument( 55 | 'dst', type=str, help='The destination file for the conversion.') 56 | args = parser.parse_args() 57 | main(args) 58 | -------------------------------------------------------------------------------- /server/auth/data/static/login_constants.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // LoginErrorCode is a common type for all error codes returned 4 | // during the login process. 5 | type LoginErrorCode uint8 6 | 7 | // ErrorCodes used the login process. 8 | const ( 9 | LoginOK LoginErrorCode = 0x00 10 | LoginFailed LoginErrorCode = 0x01 // "unable to connect" 11 | LoginFailed2 LoginErrorCode = 0x02 // "unable to connect" 12 | LoginBanned LoginErrorCode = 0x03 // "this account has been closed" 13 | LoginUnknownAccount LoginErrorCode = 0x04 // "information is not valid" 14 | LoginUnknownAccount3 LoginErrorCode = 0x05 // "information is not valid" 15 | LoginAlreadyOnline LoginErrorCode = 0x06 // "this account is already logged in" 16 | LoginNoTime LoginErrorCode = 0x07 // "you have no time left on this account" 17 | LoginDBBusy LoginErrorCode = 0x08 // "could not log in at this time, try again later" 18 | LoginBadVersion LoginErrorCode = 0x09 // "unable to validate game version" 19 | LoginDownloadFile LoginErrorCode = 0x0A 20 | LoginFailed3 LoginErrorCode = 0x0B // "unable to connect" 21 | LoginSuspended LoginErrorCode = 0x0C // "this account has been temporarily suspended" 22 | LoginFailed4 LoginErrorCode = 0x0D // "unable to connect" 23 | LoginConnected LoginErrorCode = 0x0E 24 | LoginParentalControl LoginErrorCode = 0x0F // "blocked by parental controls" 25 | LoginLockedEnforced LoginErrorCode = 0x10 // "disconnected from server" 26 | ) 27 | 28 | // Game version information which must be present in the 29 | // ClientLoingChallenge packet for a connection to be established. 30 | const ( 31 | SupportedGameName = "WoW" 32 | SupportedGameBuild = 5875 33 | ) 34 | 35 | // Game version information (which has to be variable because it is a slice). 36 | var ( 37 | SupportedGameVersion = [3]uint8{1, 12, 1} 38 | ) 39 | -------------------------------------------------------------------------------- /server/world/packet/client_move.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/binary" 5 | "io" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ClientMove is sent from the client periodically. 12 | type ClientMove struct { 13 | MoveOpCode static.OpCode 14 | MovementInfo interfaces.MovementInfo 15 | } 16 | 17 | // NewClientMovePacket constructs a new movement packet and returns it. 18 | func NewClientMovePacket(opCode static.OpCode) *ClientMove { 19 | return &ClientMove{ 20 | MoveOpCode: opCode, 21 | } 22 | } 23 | 24 | // FromBytes reads packet data from the given buffer. 25 | func (pkt *ClientMove) FromBytes(buffer io.Reader) error { 26 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.MoveFlags) 27 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Time) 28 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Location) 29 | if pkt.MovementInfo.MoveFlags|static.MovementFlagOnTransport != 0 { 30 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Transport) 31 | } 32 | 33 | if pkt.MovementInfo.MoveFlags|static.MovementFlagSwimming != 0 { 34 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Pitch) 35 | } 36 | 37 | if pkt.MovementInfo.MoveFlags|static.MovementFlagOnTransport == 0 { 38 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.FallTime) 39 | } 40 | 41 | if pkt.MovementInfo.MoveFlags|static.MovementFlagFalling != 0 { 42 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Jump) 43 | } 44 | 45 | if pkt.MovementInfo.MoveFlags|static.MovementFlagSplineElevation != 0 { 46 | binary.Read(buffer, binary.LittleEndian, &pkt.MovementInfo.Unk1) 47 | } 48 | 49 | return nil 50 | } 51 | 52 | // OpCode gets the opcode of the packet. 53 | func (pkt *ClientMove) OpCode() static.OpCode { 54 | return pkt.MoveOpCode 55 | } 56 | -------------------------------------------------------------------------------- /server/world/README.md: -------------------------------------------------------------------------------- 1 | # Code Structure 2 | 3 | ## Files 4 | - `server.go`: this contains the main server/session information 5 | 6 | ## Directories 7 | - `data`: this is all static or dynamic data within the world. 8 | - `data/static`: this is data which does not change over the course of gameplay 9 | - `data/dyanmic`: this is data which changes over the course of gameplay 10 | - `data/config`: this is data which is provided once at start, but then doesn't change again 11 | 12 | - `packet`: this contains all packet decoding/handling logic. The logic in here should be minimal 13 | and palm off actual work to the systems. 14 | 15 | - `system`: this contains the various in-game systems. These are often long-running processes that 16 | are updated at regular intervals. 17 | 18 | - `game`: this contains in-game logic (such as resolving combat between two different units). 19 | 20 | ### `data/dynamic` 21 | 22 | This contains all information about in-game objects. There are a number of types: 23 | 24 | Implemented (partially): 25 | - `Container`: containers (i.e. item with slots). 26 | - `GameObject`: super-class for all other objects. 27 | - `Item`: items (e.g. sword/shield). This refers to specific instances of items. 28 | - `Player`: human-controlled characters. This is a superset of `Unit`. 29 | - `Unit`: computer-controlled characters (e.g. monsters, enemies, trainers, quest givers, ...). 30 | 31 | Ones left to implement: 32 | - `Corpse`: a `Player` or `Unit` corpse. 33 | - `DynamicObject`: interactive, non-living objects (e.g. chests). 34 | - `Pet`: computer-controlled `Player` companions (e.g. hunter pets, warlock summons). 35 | - `Transport`: transport-only units (e.g. griffons for flight paths). 36 | 37 | These all inherit from each other, in the following tree (partially complete): 38 | 39 | ``` 40 | GameObject 41 | | 42 | |---> Item ---> Container 43 | | 44 | |---> Unit ---> Player 45 | | 46 | |---> DynamicObject 47 | ``` 48 | -------------------------------------------------------------------------------- /server/world/data/static/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | load("//tools/bazel:gen_strings.bzl", "gen_strings") 3 | 4 | genrule( 5 | name = "dbc_data", 6 | srcs = ["//tools/dbc_utils/dbc_data"], 7 | outs = ["dbc_data.go"], 8 | cmd = "$(location //tools/dbc_utils:golang_generator) --output_file=$@ $(locations //tools/dbc_utils/dbc_data)", 9 | tools = ["//tools/dbc_utils:golang_generator"], 10 | ) 11 | 12 | gen_strings( 13 | name = "enum_strings", 14 | srcs = glob(["*.go"]), 15 | types = [ 16 | "AttackTargetState", 17 | "AuthErrorCode", 18 | "BagFamily", 19 | "Bonding", 20 | "Byte1Flags", 21 | "Byte2Flags", 22 | "CharacterFlag", 23 | "CharErrorCode", 24 | "DisplayID", 25 | "EquipmentSlot", 26 | "FoodType", 27 | "Gender", 28 | "HighGUID", 29 | "HitInfo", 30 | "InventoryType", 31 | "ItemClass", 32 | "ItemFlag", 33 | "ItemPrototypeFlag", 34 | "ItemQuality", 35 | "ItemSubClass", 36 | "Language", 37 | "MovementFlag", 38 | "OpCode", 39 | "PlayerBytes", 40 | "PlayerFlag", 41 | "Power", 42 | "SheathType", 43 | "SpellCategory", 44 | "SpellSchool", 45 | "StandState", 46 | "Stat", 47 | "Team", 48 | "TypeID", 49 | "TypeMask", 50 | "UpdateField", 51 | "UpdateFlags", 52 | "UpdateType", 53 | ], 54 | ) 55 | 56 | go_library( 57 | name = "static", 58 | srcs = [ 59 | "constants.go", 60 | "dbc_data.go", 61 | "error_codes.go", 62 | "item.go", 63 | "op_code.go", 64 | "starting_items.go", 65 | "unit.go", 66 | ":enum_strings", # keep 67 | ], 68 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/data/static", 69 | visibility = ["//visibility:public"], 70 | ) 71 | -------------------------------------------------------------------------------- /server/world/data/dynamic/container.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/channels" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | // Container represents an instance of an in-game container. 10 | type Container struct { 11 | Item 12 | 13 | NumSlots int 14 | Items map[int]interfaces.GUID 15 | } 16 | 17 | // Object interface methods. 18 | func (cn *Container) GUID() interfaces.GUID { return cn.Item.GUID() } 19 | func (cn *Container) SetGUID(guid interfaces.GUID) { cn.Item.SetGUID(guid) } 20 | func (cn *Container) GetLocation() *interfaces.Location { return cn.Item.GetLocation() } 21 | 22 | func (cn *Container) UpdateFields() interfaces.UpdateFieldsMap { 23 | fields := interfaces.UpdateFieldsMap{ 24 | static.UpdateFieldContainerNumSlots: uint32(cn.NumSlots), 25 | } 26 | 27 | for slot, itemGUID := range cn.Items { 28 | fieldStart := static.UpdateField(int(static.UpdateFieldContainerSlot1) + (slot * 2)) 29 | fields[fieldStart] = uint32(itemGUID.Low()) 30 | fields[fieldStart+1] = uint32(itemGUID.High()) 31 | } 32 | 33 | mergedFields := cn.Item.UpdateFields() 34 | for k, v := range fields { 35 | mergedFields[k] = v 36 | } 37 | 38 | mergedFields[static.UpdateFieldType] = uint32(TypeMask(cn)) 39 | 40 | return mergedFields 41 | } 42 | 43 | func (cn *Container) StartUpdateLoop() { 44 | if cn.UpdateChannel() != nil { 45 | return 46 | } 47 | 48 | cn.CreateUpdateChannel() 49 | go func() { 50 | for { 51 | for _, update := range <-cn.UpdateChannel() { 52 | switch update.(type) { 53 | default: 54 | } 55 | } 56 | 57 | channels.ObjectUpdates <- cn.GUID() 58 | } 59 | }() 60 | } 61 | 62 | // Item interface methods. 63 | func (cn *Container) GetTemplate() *static.Item { return cn.Item.GetTemplate() } 64 | func (cn *Container) GetContainer() interfaces.GUID { return cn.Item.GetContainer() } 65 | -------------------------------------------------------------------------------- /server/world/packet/server_creature_query_response.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // ServerCreatureQueryResponse is sent back in response to ClientPing. 11 | type ServerCreatureQueryResponse struct { 12 | Entry uint32 13 | Unit *static.Unit 14 | } 15 | 16 | // ToBytes writes out the packet to an array of bytes. 17 | func (pkt *ServerCreatureQueryResponse) ToBytes() ([]byte, error) { 18 | buffer := bytes.NewBufferString("") 19 | 20 | if pkt.Unit == nil { 21 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Entry|0x80000000)) 22 | return buffer.Bytes(), nil 23 | } 24 | 25 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Entry)) 26 | buffer.WriteString(pkt.Unit.Name) 27 | buffer.WriteByte('\x00') 28 | buffer.WriteByte('\x00') // name2 29 | buffer.WriteByte('\x00') // name3 30 | buffer.WriteByte('\x00') // name4 31 | buffer.WriteString(pkt.Unit.SubName) 32 | buffer.WriteByte('\x00') 33 | 34 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // CreatureTypeFlags 35 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // CreatureType 36 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // CreatureFamily 37 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // CreatureRank 38 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // unk 39 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // PetSpellDataID 40 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Unit.DisplayID)) // DisplayID 41 | binary.Write(buffer, binary.LittleEndian, uint8(0)) // IsCivilian 42 | binary.Write(buffer, binary.LittleEndian, uint8(0)) // IsRacialLeader 43 | 44 | return buffer.Bytes(), nil 45 | } 46 | 47 | // OpCode gets the opcode of the packet. 48 | func (*ServerCreatureQueryResponse) OpCode() static.OpCode { 49 | return static.OpCodeServerCreatureQueryResponse 50 | } 51 | -------------------------------------------------------------------------------- /server/world/packet/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | filegroup( 4 | name = "packet_srcs", 5 | srcs = glob(["*.go"]), 6 | visibility = ["//visibility:public"], 7 | ) 8 | 9 | go_library( 10 | name = "packet", 11 | srcs = [ 12 | "client_attack_stop.go", 13 | "client_attack_swing.go", 14 | "client_auth_session.go", 15 | "client_char_create.go", 16 | "client_char_delete.go", 17 | "client_char_enum.go", 18 | "client_creature_query.go", 19 | "client_item_query_single.go", 20 | "client_logout_request.go", 21 | "client_move.go", 22 | "client_name_query.go", 23 | "client_ping.go", 24 | "client_player_login.go", 25 | "client_query_time.go", 26 | "client_set_active_mover.go", 27 | "client_stand_state_change.go", 28 | "client_tutorial_flag.go", 29 | "client_update_account_data.go", 30 | "server_account_data_times.go", 31 | "server_attack_start.go", 32 | "server_attack_stop.go", 33 | "server_attacker_state_update.go", 34 | "server_auth_challenge.go", 35 | "server_auth_response.go", 36 | "server_char_create.go", 37 | "server_char_delete.go", 38 | "server_char_enum.go", 39 | "server_creature_query_response.go", 40 | "server_init_world_states.go", 41 | "server_item_query_single_response.go", 42 | "server_login_verify_world.go", 43 | "server_logout_complete.go", 44 | "server_logout_response.go", 45 | "server_name_query_response.go", 46 | "server_pong.go", 47 | "server_query_time_response.go", 48 | "server_stand_state_update.go", 49 | "server_tutorial_flags.go", 50 | "server_update_object.go", 51 | ], 52 | importpath = "github.com/jeshuamorrissey/wow_server_go/server/world/packet", 53 | visibility = ["//visibility:public"], 54 | deps = [ 55 | "//lib/util", 56 | "//server/world/data/dynamic/interfaces", 57 | "//server/world/data/static", 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_char_enum.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 8 | ) 9 | 10 | // Handle will ensure that the given account exists. 11 | func HandleClientCharEnum(pkt *packet.ClientCharEnum, state *system.State) ([]interfaces.ServerPacket, error) { 12 | charObj := state.OM.GetPlayer(state.Account.Character.GUID) 13 | equipment := make(map[static.EquipmentSlot]*packet.ItemSummary) 14 | for slot, itemGUID := range charObj.Equipment { 15 | item := state.OM.GetItem(itemGUID) 16 | equipment[slot] = &packet.ItemSummary{ 17 | DisplayID: item.GetTemplate().DisplayID, 18 | InventoryType: item.GetTemplate().InventoryType, 19 | } 20 | } 21 | 22 | var firstBagSummary *packet.ItemSummary = nil 23 | if charObj.FirstBag() != nil { 24 | firstBagSummary = &packet.ItemSummary{ 25 | DisplayID: charObj.FirstBag().GetTemplate().DisplayID, 26 | InventoryType: charObj.FirstBag().GetTemplate().InventoryType, 27 | } 28 | } 29 | 30 | return []interfaces.ServerPacket{&packet.ServerCharEnum{ 31 | Characters: []*packet.CharacterSummary{ 32 | { 33 | Name: state.Account.Character.Name, 34 | GUID: state.Account.Character.GUID, 35 | Race: charObj.Race, 36 | Class: charObj.Class, 37 | Gender: charObj.Gender, 38 | SkinColor: charObj.SkinColor, 39 | Face: charObj.Face, 40 | HairStyle: charObj.HairStyle, 41 | HairColor: charObj.HairColor, 42 | Feature: charObj.Feature, 43 | Level: charObj.Level, 44 | ZoneID: charObj.ZoneID, 45 | MapID: charObj.MapID, 46 | Location: charObj.Location, 47 | HasLoggedIn: state.Account.Character.HasLoggedIn, 48 | Flags: state.Account.Character.Flags(), 49 | Equipment: equipment, 50 | FirstBag: firstBagSummary, 51 | }, 52 | }, 53 | }}, nil 54 | } 55 | -------------------------------------------------------------------------------- /server/world/packet/client_char_create.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "io" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 11 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 12 | ) 13 | 14 | func normalizeCharacterName(name string) string { 15 | re, _ := regexp.Compile("[^a-zA-Z]+") 16 | name = re.ReplaceAllString(name, "") 17 | return strings.Title(name) 18 | } 19 | 20 | func validateCharacterName(name string) static.CharErrorCode { 21 | if len(name) == 0 { 22 | return static.CharErrorCodeNameNoName 23 | } else if len(name) > static.MaxPlayerNameLength { 24 | return static.CharErrorCodeNameTooLong 25 | } else if len(name) < static.MinPlayerNameLength { 26 | return static.CharErrorCodeNameTooShort 27 | } 28 | 29 | return static.CharErrorCodeCreateSuccess 30 | } 31 | 32 | // ClientCharCreate is sent from the client when making a character. 33 | type ClientCharCreate struct { 34 | Name string 35 | Race *static.Race 36 | Class *static.Class 37 | Gender static.Gender 38 | SkinColor uint8 39 | Face uint8 40 | HairStyle uint8 41 | HairColor uint8 42 | Feature uint8 43 | } 44 | 45 | // FromBytes loads the packet from the given data. 46 | func (pkt *ClientCharCreate) FromBytes(bufferBase io.Reader) error { 47 | buffer := bufio.NewReader(bufferBase) 48 | pkt.Name, _ = buffer.ReadString('\x00') 49 | pkt.Name = normalizeCharacterName(pkt.Name) 50 | binary.Read(buffer, binary.LittleEndian, &pkt.Race) 51 | binary.Read(buffer, binary.LittleEndian, &pkt.Class) 52 | binary.Read(buffer, binary.LittleEndian, &pkt.Gender) 53 | binary.Read(buffer, binary.LittleEndian, &pkt.SkinColor) 54 | binary.Read(buffer, binary.LittleEndian, &pkt.Face) 55 | binary.Read(buffer, binary.LittleEndian, &pkt.HairStyle) 56 | binary.Read(buffer, binary.LittleEndian, &pkt.HairColor) 57 | binary.Read(buffer, binary.LittleEndian, &pkt.Feature) 58 | util.ReadBytes(buffer, 1) // OutfitID 59 | return nil 60 | } 61 | 62 | // OpCode returns the opcode for this packet. 63 | func (pkt *ClientCharCreate) OpCode() static.OpCode { 64 | return static.OpCodeClientCharCreate 65 | } 66 | -------------------------------------------------------------------------------- /server/world/packet/handlers/client_char_create.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 11 | "github.com/jeshuamorrissey/wow_server_go/tools/new_game/initial_data" 12 | ) 13 | 14 | func normalizeCharacterName(name string) string { 15 | re, _ := regexp.Compile("[^a-zA-Z]+") 16 | name = re.ReplaceAllString(name, "") 17 | return strings.Title(name) 18 | } 19 | 20 | func validateCharacterName(name string) static.CharErrorCode { 21 | if len(name) == 0 { 22 | return static.CharErrorCodeNameNoName 23 | } else if len(name) > static.MaxPlayerNameLength { 24 | return static.CharErrorCodeNameTooLong 25 | } else if len(name) < static.MinPlayerNameLength { 26 | return static.CharErrorCodeNameTooShort 27 | } 28 | 29 | return static.CharErrorCodeCreateSuccess 30 | } 31 | 32 | // Handle will ensure that the given account exists. 33 | func HandleClientCharCreate(pkt *packet.ClientCharCreate, state *system.State) ([]interfaces.ServerPacket, error) { 34 | response := new(packet.ServerCharCreate) 35 | response.Error = static.CharErrorCodeCreateSuccess 36 | 37 | // Check for invalid names. 38 | response.Error = validateCharacterName(pkt.Name) 39 | if response.Error != static.CharErrorCodeCreateSuccess { 40 | return []interfaces.ServerPacket{response}, nil 41 | } 42 | 43 | // If the character already exists, fail. 44 | if state.Account.Character != nil { 45 | response.Error = static.CharErrorCodeCreateFailed 46 | return []interfaces.ServerPacket{response}, nil 47 | } 48 | 49 | // Make the character. 50 | charObj, err := initial_data.NewCharacter( 51 | state.Config, 52 | pkt.Name, 53 | pkt.Race, pkt.Class, pkt.Gender, 54 | pkt.SkinColor, pkt.Face, pkt.HairStyle, pkt.HairColor, pkt.Feature) 55 | if err != nil { 56 | response.Error = static.CharErrorCodeCreateError 57 | return []interfaces.ServerPacket{response}, nil 58 | } 59 | 60 | state.Account.Character = charObj 61 | 62 | return []interfaces.ServerPacket{response}, nil 63 | } 64 | -------------------------------------------------------------------------------- /tools/dbc_utils/mpq_generator.py: -------------------------------------------------------------------------------- 1 | """A utility which generate a MPQ patch file. 2 | 3 | It reads JSON DBC files and produces a single MPQ in the requested location. 4 | 5 | It also takes any data within "other_data" and copies it into the MPQ. 6 | """ 7 | import argparse 8 | import os 9 | import tempfile 10 | import subprocess 11 | 12 | from dbc import dbc 13 | import dbc_generator 14 | 15 | 16 | def main(args: argparse.Namespace): 17 | my_dir = os.path.dirname(os.path.realpath(__file__)) 18 | 19 | # Build script using MPQEditor. 20 | script = [ 21 | 'new "{}"'.format(args.out), 22 | 'add "{}" "{}" /r'.format(args.out, 23 | os.path.join(my_dir, 'other_data')), 24 | ] 25 | 26 | # Make DBC files in a temporary directory. 27 | with tempfile.TemporaryDirectory() as d: 28 | os.mkdir(os.path.join(d, 'DBFilesClient')) 29 | 30 | for filename, record_type in dbc_generator.FILENAME_TO_RECORD_TYPE.items(): 31 | with open('dbc_data/{}.json'.format(filename)) as f: 32 | table = dbc.Table.FromJSON(f.read(), record_type) 33 | 34 | output_filename = os.path.join( 35 | d, 'DBFilesClient', '{}.dbc'.format(filename)) 36 | with open(output_filename, 'wb') as f: 37 | f.write(table.ToBinary()) 38 | 39 | script.append('add "{}" "{}" /r'.format(args.out, d)) 40 | 41 | with tempfile.NamedTemporaryFile(delete=False) as script_file: 42 | script_file.write('\n'.join(script).encode()) 43 | script_file.close() 44 | 45 | if os.path.exists(args.out): 46 | os.remove(args.out) 47 | 48 | subprocess.check_output( 49 | (args.mpqeditor_bin, '-console', script_file.name)) 50 | 51 | os.remove(script_file.name) 52 | 53 | 54 | if __name__ == '__main__': 55 | parser = argparse.ArgumentParser(description='Manage DBC files.') 56 | parser.add_argument( 57 | '--mpqeditor_bin', 58 | type=str, 59 | help='The location of the MPQEditor.exe binary.', 60 | default='D:\\Games\\World of Warcraft (Vanilla) - Tools\\MPQEditor\\MPQEditor.exe') 61 | parser.add_argument( 62 | 'out', type=str, help='The output MPQ filepath.') 63 | args = parser.parse_args() 64 | main(args) 65 | -------------------------------------------------------------------------------- /server/world/packet/server_attacker_state_update.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // ServerAttackerStateUpdate is sent back in response to ClientPing. 12 | type ServerAttackerStateUpdate struct { 13 | HitInfo static.HitInfo 14 | Attacker interfaces.GUID 15 | Target interfaces.GUID 16 | Damage int32 17 | OriginalDamage int32 18 | OverDamage int32 19 | TargetState static.AttackTargetState 20 | AttackerState uint32 21 | MeleeSpellID uint32 22 | BlockAmount uint32 23 | RageGained uint32 24 | Absorb uint32 25 | } 26 | 27 | // ToBytes writes out the packet to an array of bytes. 28 | func (pkt *ServerAttackerStateUpdate) ToBytes() ([]byte, error) { 29 | buffer := bytes.NewBufferString("") 30 | 31 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.HitInfo)) 32 | buffer.Write(pkt.Attacker.Pack()) 33 | buffer.Write(pkt.Target.Pack()) 34 | binary.Write(buffer, binary.LittleEndian, int32(pkt.Damage)) 35 | 36 | is_sub_damage := 1 37 | binary.Write(buffer, binary.LittleEndian, uint8(is_sub_damage)) // TODO: SubDamage 38 | if is_sub_damage != 0 { 39 | binary.Write(buffer, binary.LittleEndian, uint32(static.SpellSchoolPhysical)) // Damage school mask 40 | binary.Write(buffer, binary.LittleEndian, float32(pkt.Damage)) // sub damage 41 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.Damage)) // sub damage 42 | binary.Write(buffer, binary.LittleEndian, int32(pkt.Absorb)) // absorbed 43 | binary.Write(buffer, binary.LittleEndian, int32(0)) // reissted 44 | } 45 | 46 | binary.Write(buffer, binary.LittleEndian, uint8(pkt.TargetState)) 47 | if pkt.Absorb == 0 { 48 | binary.Write(buffer, binary.LittleEndian, uint32(0)) 49 | } else { 50 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // should be -1 by unsigned?? 51 | } 52 | 53 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.MeleeSpellID)) 54 | binary.Write(buffer, binary.LittleEndian, uint32(pkt.BlockAmount)) 55 | 56 | return buffer.Bytes(), nil 57 | } 58 | 59 | // OpCode gets the opcode of the packet. 60 | func (*ServerAttackerStateUpdate) OpCode() static.OpCode { 61 | return static.OpCodeServerAttackerstateupdate 62 | } 63 | -------------------------------------------------------------------------------- /server/auth/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Session represents a single user's connection to the server, 11 | type Session struct { 12 | // A logger to write to. 13 | log *logrus.Entry 14 | 15 | // The I/O for this session. Usually will be socket conns. 16 | input io.Reader 17 | output io.Writer 18 | 19 | // The state with some dynamic information. 20 | state *State 21 | } 22 | 23 | // NewSession makes a new session and returns it. 24 | func NewSession(log *logrus.Entry, input io.Reader, output io.Writer, state *State) *Session { 25 | return &Session{ 26 | log: log, 27 | input: input, 28 | output: output, 29 | state: state, 30 | } 31 | } 32 | 33 | // SendPacket will send a packet back to the given output. 34 | func (s *Session) SendPacket(pkt ServerPacket) error { 35 | s.log.Tracef("--> %v", pkt.OpCode()) 36 | 37 | // Write the header. 38 | pktData, err := pkt.ToBytes(s.state) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | header, err := writeHeader(s.state, len(pktData), pkt.OpCode()) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Send the data. 49 | toSend := append(header, pktData...) 50 | n, err := s.output.Write(toSend) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | if n != len(toSend) { 56 | return fmt.Errorf("expected %v bytes to send, only sent %v", len(pktData), n) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | // Run takes as input a data source (input) and data destination (output) and 63 | // manages incoming packets, routing them to the handler and then sending the 64 | // output to the appropriate place. 65 | func (s *Session) Run() { 66 | for { 67 | packet, opCode, err := readPacket(s.state, s.input) 68 | if err != nil { 69 | // This usually happens because of an EOF, so just terminate. 70 | s.log.Warnf("Terminating connection: %v\n", err) 71 | return 72 | } 73 | 74 | // If the packet is nil, we didn't know how to read, so just ignore it. 75 | if packet == nil { 76 | s.log.Warnf("<-- %v [UNHANDLED]", opCode) 77 | continue 78 | } 79 | 80 | s.log.Tracef("<-- %v", opCode) 81 | 82 | // Load and then handle the packet. 83 | response, err := packet.Handle(s.state) 84 | if err != nil { 85 | s.log.Warnf("Error while handling packet: %v\n", err) 86 | return 87 | } 88 | 89 | for _, pkt := range response { 90 | s.SendPacket(pkt) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/stats.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 7 | ) 8 | 9 | type BasicStats struct { 10 | Strength int 11 | Agility int 12 | Stamina int 13 | Intellect int 14 | Spirit int 15 | } 16 | 17 | func (bs *BasicStats) regenForTimePeriod(max int, timePassed time.Duration, isInCombat bool) int { 18 | combatPenalty := 1.0 19 | if isInCombat { 20 | combatPenalty = 1.0 - PlayerRegenCombatPenaltyPercent 21 | } 22 | 23 | statPerSecondFromSpirit := int(combatPenalty * PlayerRegenPercentPerSpiritPerSecond * float64(bs.Spirit)) 24 | statPerSecond := int(combatPenalty*PlayerRegenPercentBasePerSecond*float64(max)) + statPerSecondFromSpirit 25 | 26 | return int(float64(statPerSecond) * (float64(timePassed) / float64(time.Second))) 27 | } 28 | 29 | // HealthRegen returns the amount of health generated in a particular time period. 30 | func (bs *BasicStats) HealthRegen(timePassed time.Duration, isInCombat bool) int { 31 | return bs.regenForTimePeriod(bs.MaxHealth(), timePassed, isInCombat) 32 | } 33 | 34 | // PowerRegen returns the amount of health generated in a particular time period. 35 | func (bs *BasicStats) PowerRegen(timePassed time.Duration, isInCombat bool) int { 36 | return bs.regenForTimePeriod(bs.MaxPower(), timePassed, isInCombat) 37 | } 38 | 39 | // MaxHealth returns the maximum health given the current stats. 40 | func (bs *BasicStats) MaxHealth() int { 41 | return HealthPerStamina * bs.Stamina 42 | } 43 | 44 | // MaxPower returns the maximum power given the current stats. 45 | func (bs *BasicStats) MaxPower() int { 46 | return ManaPerIntellect * bs.Intellect 47 | } 48 | 49 | // MeleeAttackPower returns the melee attack power contribution from stats. 50 | func (bs *BasicStats) MeleeAttackPower(class *static.Class) int { 51 | switch class { 52 | case static.ClassHunter: 53 | case static.ClassRogue: 54 | return bs.Strength*1 + bs.Agility*1 55 | case static.ClassPriest: 56 | case static.ClassMage: 57 | case static.ClassWarlock: 58 | return bs.Strength * 1 59 | } 60 | 61 | return bs.Strength * 2 62 | } 63 | 64 | // RangedAttackPower returns the melee attack power contribution from stats. 65 | func (bs *BasicStats) RangedAttackPower(class *static.Class) int { 66 | switch class { 67 | case static.ClassWarrior: 68 | case static.ClassHunter: 69 | case static.ClassRogue: 70 | return bs.Agility * 2 71 | } 72 | 73 | return bs.Agility * 1 74 | } 75 | -------------------------------------------------------------------------------- /server/world/data/dynamic/player_handlers.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/components" 5 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 6 | ) 7 | 8 | // HandleModHealth is the handler for the ModHealth message. 9 | // 10 | // This handler will change the health by the requested amount. 11 | func (p *Player) HandleModHealth(msg *messages.ModHealth) { 12 | p.ModHealth(msg.Amount, p.MaxHealth()) 13 | } 14 | 15 | // HandleModPower is the handler for the ModPower message. 16 | // 17 | // This handler will change the power by the requested amount. 18 | func (p *Player) HandleModPower(msg *messages.ModPower) { 19 | p.ModPower(msg.Amount, p.MaxPower()) 20 | } 21 | 22 | // HandleAttack is the handler for the UnitAttack message. 23 | // 24 | // This handler will notify the target that they are being attacked, and then proceed 25 | // to attack them with each weapon they have equipped. 26 | func (p *Player) HandleAttack(msg *messages.UnitAttack) { 27 | target := GetObjectManager().Get(msg.Target) 28 | if target == nil { 29 | return 30 | } 31 | 32 | // First, we want to attack the target; so tell them we wish to attack them. 33 | target.SendUpdates([]interface{}{ 34 | &messages.UnitRegisterAttack{Attacker: p.GUID()}, 35 | }) 36 | 37 | // Now, we have to attack with our weapons. We make one timer for each weapon (melee and ranged) 38 | // with the assumption that only one will be within range at a time (and only that one will do 39 | // damage). 40 | for _, weapon := range p.weapons() { 41 | p.Attack(p, target, weapon.GetTemplate().AttackRate, func() *components.Damage { 42 | return &components.Damage{ 43 | Base: weapon.CalculateDamage(), 44 | } 45 | }) 46 | } 47 | } 48 | 49 | func (p *Player) HandleRegisterAttacker(msg *messages.UnitRegisterAttack) { 50 | p.RegisterAttacker(msg.Attacker) 51 | } 52 | 53 | func (p *Player) HandleDeregisterAttacker(msg *messages.UnitDeregisterAttacker) { 54 | p.DeregisterAttacker(msg.Attacker) 55 | } 56 | 57 | func (p *Player) HandleAttackStop(msg *messages.UnitStopAttack) { 58 | // First, if we have a target, tell them we aren't attacking them. 59 | if p.Target > 0 { 60 | if target := GetObjectManager().Get(p.Target); target != nil { 61 | target.SendUpdates([]interface{}{ 62 | &messages.UnitDeregisterAttacker{Attacker: p.GUID()}, 63 | }) 64 | } 65 | } 66 | 67 | // Now, we can stop attacking and clean up our attack timers. 68 | p.StopAttack() 69 | } 70 | -------------------------------------------------------------------------------- /tools/dbc_utils/golang_generator.py: -------------------------------------------------------------------------------- 1 | """A utility which generates DBC files. 2 | 3 | It can read files in the following formats: 4 | 1. Binary 5 | 2. JSON 6 | 7 | ... and produce files in the following formats: 8 | 1. Binary 9 | 2. JSON 10 | 3. Go (useful for generating Enums). 11 | """ 12 | from typing import List 13 | from dbc_generator import FILENAME_TO_RECORD_TYPE 14 | 15 | import argparse 16 | import os 17 | import subprocess 18 | import jinja2 19 | 20 | from dbc import dbc 21 | 22 | def gen_golang_file(chunks: List[bytes], init_function_names: List[str]) -> bytes: 23 | """Convert the table to a Golang file.""" 24 | args = { 25 | 'package': 'static', 26 | 'chunks': chunks, 27 | 'init_function_names': init_function_names, 28 | } 29 | 30 | template_env = jinja2.Environment( 31 | loader=jinja2.FileSystemLoader(searchpath=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'dbc'))) 32 | template_env.trim_blocks = True 33 | template_env.lstrip_blocks = True 34 | return template_env.get_template('dbc_golang.go.jinja').render(**args) 35 | 36 | 37 | def main(args: argparse.Namespace): 38 | golang_chunks = [] 39 | init_function_names = [] 40 | 41 | for filename in args.DATA_FILES: 42 | dbc_filename = os.path.splitext(os.path.basename(filename))[0] 43 | if dbc_filename not in FILENAME_TO_RECORD_TYPE: 44 | print('Unknown DBC {}'.format(dbc_filename)) 45 | continue 46 | 47 | record_type = FILENAME_TO_RECORD_TYPE[dbc_filename] 48 | 49 | with open(filename, 'r') as f: 50 | if filename.endswith('.json'): 51 | table = dbc.Table.FromJSON(f.read(), record_type) 52 | elif filename.endswith('.dbc'): 53 | table = dbc.Table.FromBinary(f.read(), record_type) 54 | 55 | 56 | golang_chunk, init_function_name = table.ToGolangPart() 57 | 58 | golang_chunks.append(golang_chunk) 59 | init_function_names.append(init_function_name) 60 | 61 | with open(args.output_file, 'w') as f: 62 | f.write(gen_golang_file(golang_chunks, init_function_names)) 63 | 64 | subprocess.run(['gofmt', '-w', args.output_file]) 65 | 66 | 67 | if __name__ == '__main__': 68 | parser = argparse.ArgumentParser(description='Manage DBC files.') 69 | parser.add_argument( 70 | 'DATA_FILES', type=str, nargs='+', help='The directory containing the DBC input files.') 71 | parser.add_argument( 72 | '--output_file', type=str, help='The destination output go file.') 73 | args = parser.parse_args() 74 | main(args) 75 | -------------------------------------------------------------------------------- /server/auth/srp/srp_test.go: -------------------------------------------------------------------------------- 1 | package srp_test 2 | 3 | import ( 4 | "math/big" 5 | "testing" 6 | 7 | "gotest.tools/assert" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/server/auth/srp" 10 | ) 11 | 12 | func TestGenerateEphemeralPair(t *testing.T) { 13 | var v, expectedb, expectedB big.Int 14 | 15 | v.SetString("37510828772889775988011936555774753323884663064643682200527651910267083044538", 10) 16 | expectedb.SetString("3679141816495610969398422835318306156547245306", 10) 17 | expectedB.SetString("16630279820182697578309394812726193457375869535456855997552735653810818403718", 10) 18 | 19 | b, B := srp.GenerateEphemeralPair(&v) 20 | assert.Equal(t, b.Cmp(&expectedb), 0) 21 | assert.Equal(t, B.Cmp(&expectedB), 0) 22 | } 23 | 24 | func TestGenerateVerifier(t *testing.T) { 25 | var s, expectedV big.Int 26 | 27 | s.SetString("66759882342950727220130969932663635787137805713109467932708165413389947953699", 10) 28 | expectedV.SetString("37510828772889775988011936555774753323884663064643682200527651910267083044538", 10) 29 | 30 | v := srp.GenerateVerifier("JESHUA", "JESHUA", &s) 31 | assert.Equal(t, v.Cmp(&expectedV), 0) 32 | } 33 | 34 | func TestCalculateSessionKey(t *testing.T) { 35 | var A, B, b, v, s, expectedK, expectedM big.Int 36 | 37 | A.SetString("1234344069974946706941181551060269688256096998192437644043961152849307948728", 10) 38 | B.SetString("16630279820182697578309394812726193457375869535456855997552735653810818403718", 10) 39 | b.SetString("3679141816495610969398422835318306156547245306", 10) 40 | v.SetString("37510828772889775988011936555774753323884663064643682200527651910267083044538", 10) 41 | s.SetString("66759882342950727220130969932663635787137805713109467932708165413389947953699", 10) 42 | 43 | expectedK.SetString("1223778727786691224255566132121120158338166041153346746306820190174949498228440143950889596323712", 10) 44 | expectedM.SetString("1278405643266187066239549723718271591736372958987", 10) 45 | 46 | K, M := srp.CalculateSessionKey(&A, &B, &b, &v, &s, "JESHUA") 47 | 48 | assert.Equal(t, K.Cmp(&expectedK), 0) 49 | assert.Equal(t, M.Cmp(&expectedM), 0) 50 | } 51 | 52 | func TestCalculateServerProof(t *testing.T) { 53 | var A, M, K, expectedProof big.Int 54 | 55 | A.SetString("1234344069974946706941181551060269688256096998192437644043961152849307948728", 10) 56 | M.SetString("1278405643266187066239549723718271591736372958987", 10) 57 | K.SetString("1223778727786691224255566132121120158338166041153346746306820190174949498228440143950889596323712", 10) 58 | expectedProof.SetString("1284245613498486112994244042115912960631626548879", 10) 59 | 60 | proof := srp.CalculateServerProof(&A, &M, &K) 61 | assert.Equal(t, proof.Cmp(&expectedProof), 0) 62 | } 63 | -------------------------------------------------------------------------------- /server/auth/session/packets.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 11 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 12 | ) 13 | 14 | // Packet is a generic packet. 15 | type Packet interface { 16 | // OpCode returns the opcode for the given packet as an int. 17 | OpCode() static.OpCode 18 | } 19 | 20 | // ServerPacket is a packet sent from this server to a client. 21 | type ServerPacket interface { 22 | Packet 23 | 24 | // ToBytes writes the packet out to an array of bytes. 25 | ToBytes(*State) ([]byte, error) 26 | } 27 | 28 | // ClientPacket is a packet sent from the client to this server. 29 | type ClientPacket interface { 30 | Packet 31 | 32 | // FromBytes reads the packet from a generic reader. 33 | FromBytes(*State, io.Reader) error 34 | 35 | // Handle the packet and return a list of server packets to send back 36 | // to the client. 37 | Handle(*State) ([]ServerPacket, error) 38 | } 39 | 40 | func readPacket(state *State, buffer io.Reader) (ClientPacket, static.OpCode, error) { 41 | opCode, length, err := readHeader(state, buffer) 42 | if err != nil { 43 | return nil, 0, err 44 | } 45 | 46 | data, err := util.ReadBytes(buffer, length) 47 | if err != nil { 48 | return nil, 0, err 49 | } 50 | 51 | builder, ok := state.opCodeToPacket[opCode] 52 | if !ok { 53 | return nil, opCode, nil 54 | } 55 | 56 | pkt := builder() 57 | pkt.FromBytes(state, bytes.NewReader(data)) 58 | 59 | return pkt, opCode, nil 60 | } 61 | 62 | func readHeader(state *State, buffer io.Reader) (static.OpCode, int, error) { 63 | opCodeData := make([]byte, 1) 64 | n, err := buffer.Read(opCodeData) 65 | if err != nil { 66 | return static.OpCode(0), 0, fmt.Errorf("erorr while reading opcode: %v", err) 67 | } 68 | 69 | if n != 1 { 70 | return static.OpCode(0), 0, errors.New("short read when reading opcode data") 71 | } 72 | 73 | // In the auth server, the length is based on the packet type. 74 | opCode := static.OpCode(opCodeData[0]) 75 | length := 0 76 | if opCode == static.OpCodeLoginChallenge { 77 | lenData, err := util.ReadBytes(buffer, 3) 78 | if err != nil { 79 | return static.OpCode(0), 0, fmt.Errorf("error while reading header length: %v", err) 80 | } 81 | 82 | length = int(binary.LittleEndian.Uint16(lenData[1:])) 83 | } else if opCode == static.OpCodeLoginProof { 84 | length = 74 85 | } else if opCode == static.OpCodeRealmlist { 86 | length = 4 87 | } 88 | 89 | return opCode, length, nil 90 | } 91 | 92 | func writeHeader(state *State, packetLen int, opCode static.OpCode) ([]byte, error) { 93 | return []byte{uint8(opCode.Int())}, nil 94 | } 95 | -------------------------------------------------------------------------------- /server/world/data/dynamic/object_utils.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // HighGUID returns the high GUID for a given type of object. Will 11 | // panic if an unknown type is passed. 12 | func HighGUID(obj interfaces.Object) static.HighGUID { 13 | switch o := obj.(type) { 14 | case *Item: 15 | return static.HighGUIDItem 16 | case *Container: 17 | return static.HighGUIDContainer 18 | case *Unit: 19 | return static.HighGUIDUnit 20 | case *Player: 21 | return static.HighGUIDPlayer 22 | default: 23 | panic(fmt.Sprintf("HighGUID: Unknown object type %T!", o)) 24 | } 25 | } 26 | 27 | // TypeID returns the type ID for a given game object. 28 | func TypeID(obj interfaces.Object) static.TypeID { 29 | switch o := obj.(type) { 30 | case *Item: 31 | return static.TypeIDItem 32 | case *Container: 33 | return static.TypeIDContainer 34 | case *Unit: 35 | return static.TypeIDUnit 36 | case *Player: 37 | return static.TypeIDPlayer 38 | default: 39 | panic(fmt.Sprintf("TypeID: Unknown object type %T!", o)) 40 | } 41 | } 42 | 43 | // TypeMask returns the type mask for a given game object. 44 | func TypeMask(obj interfaces.Object) static.TypeMask { 45 | switch o := obj.(type) { 46 | case *Item: 47 | return static.TypeMaskObject | static.TypeMaskItem 48 | case *Container: 49 | return static.TypeMaskObject | static.TypeMaskItem | static.TypeMaskContainer 50 | case *Unit: 51 | return static.TypeMaskObject | static.TypeMaskUnit 52 | case *Player: 53 | return static.TypeMaskObject | static.TypeMaskUnit | static.TypeMaskPlayer 54 | default: 55 | panic(fmt.Sprintf("TypeMask: Unknown object type %T!", o)) 56 | } 57 | } 58 | 59 | // UpdateFlags returns the update flags for a given game object. 60 | func UpdateFlags(obj interfaces.Object) static.UpdateFlags { 61 | switch o := obj.(type) { 62 | case *Item: 63 | return static.UpdateFlagsAll 64 | case *Container: 65 | return static.UpdateFlagsAll 66 | case *Unit: 67 | return static.UpdateFlagsAll | static.UpdateFlagsLiving | static.UpdateFlagsHasPosition 68 | case *Player: 69 | return static.UpdateFlagsAll | static.UpdateFlagsLiving | static.UpdateFlagsHasPosition 70 | default: 71 | panic(fmt.Sprintf("UpdateFlags: Unknown object type %T!", o)) 72 | } 73 | } 74 | 75 | // NumUpdateFields returns the number of bytes in the mask for an object type. 76 | func NumUpdateFields(obj interfaces.Object) int { 77 | switch o := obj.(type) { 78 | case *Item: 79 | return 48 80 | case *Container: 81 | return 106 82 | case *Unit: 83 | return 188 84 | case *Player: 85 | return 1282 86 | default: 87 | panic(fmt.Sprintf("UpdateFlags: Unknown object type %T!", o)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/server/auth/srp" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic" 11 | ) 12 | 13 | type Config struct { 14 | Name string `json:"name"` 15 | Accounts []*Account `json:"accounts"` 16 | ObjectManager *dynamic.ObjectManager `json:"objects"` 17 | } 18 | 19 | // NewConfig creates a new config object from scratch with the given account name. 20 | func NewConfig(accountName string) *Config { 21 | config := Config{Name: accountName, ObjectManager: dynamic.GetObjectManager()} 22 | 23 | salt := srp.GenerateSalt() 24 | verifier := srp.GenerateVerifier(strings.ToUpper(accountName), strings.ToUpper(accountName), salt) 25 | config.Accounts = append(config.Accounts, &Account{ 26 | Name: strings.ToUpper(accountName), 27 | SaltStr: salt.Text(16), 28 | VerifierStr: verifier.Text(16), 29 | }) 30 | 31 | return &config 32 | } 33 | 34 | // NewConfigFromJSON creates a config object and populates with data from a JSON file. 35 | func NewConfigFromJSON(jsonFilepath string) *Config { 36 | config := &Config{ 37 | Accounts: make([]*Account, 0), 38 | ObjectManager: dynamic.GetObjectManager(), 39 | } 40 | 41 | config.LoadFromJSON(jsonFilepath) 42 | return config 43 | } 44 | 45 | // LoadFromJSON overwrites the current config file from a JSON source. 46 | func (wc *Config) LoadFromJSON(jsonFilepath string) error { 47 | file, err := os.OpenFile(jsonFilepath, os.O_RDONLY, 0555) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | data, err := ioutil.ReadAll(file) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | err = json.Unmarshal(data, wc) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | // Start the update loop on all objects. 63 | for guid := range wc.ObjectManager.ActiveIDs { 64 | wc.ObjectManager.Get(guid).StartUpdateLoop() 65 | } 66 | 67 | // Initialize all units and players. 68 | for _, player := range wc.ObjectManager.Players { 69 | player.Initialize() 70 | } 71 | 72 | for _, unit := range wc.ObjectManager.Units { 73 | unit.Initialize() 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // SaveToJSON saves the current config state to a JSON file. 80 | func (wc *Config) SaveToJSON(filepath string) error { 81 | data, err := json.Marshal(wc) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | file, err := os.OpenFile(filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0555) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | defer file.Close() 92 | 93 | _, err = file.Write(data) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /server/world/data/static/starting_items.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | var startingItems = map[string]map[string][]string{ 4 | "Warrior": { 5 | "Human": { 6 | "Recruit's Boots", 7 | "Recruit's Pants", 8 | "Recruit's Shirt", 9 | "Worn Shortsword", 10 | "Worn Wooden Shield", 11 | }, 12 | }, 13 | "Paladin": { 14 | "Human": { 15 | "Recruit's Boots", 16 | "Recruit's Pants", 17 | "Recruit's Shirt", 18 | "Worn Shortsword", 19 | "Worn Wooden Shield", 20 | }, 21 | }, 22 | } 23 | 24 | // GetStartingItems is a utility which will return a mapping of equipment slot 25 | // to the item that should be in that slot. 26 | func GetStartingItems(class *Class, race *Race) (map[EquipmentSlot]*Item, []*Item) { 27 | // items := startingItems[fmt.Sprintf("%d:%d", class, race)] 28 | items := startingItems[class.Name][race.Name] 29 | equipment := make(map[EquipmentSlot]*Item) 30 | nonEquipment := make([]*Item, 0) 31 | 32 | for _, itemID := range items { 33 | item := ItemsByName[itemID] 34 | 35 | if item.InventoryType == InventoryTypeHead { 36 | equipment[EquipmentSlotHead] = item 37 | } else if item.InventoryType == InventoryTypeShoulders { 38 | equipment[EquipmentSlotShoulders] = item 39 | } else if item.InventoryType == InventoryTypeBody { 40 | equipment[EquipmentSlotBody] = item 41 | } else if item.InventoryType == InventoryTypeChest || item.InventoryType == InventoryTypeRobe { 42 | equipment[EquipmentSlotChest] = item 43 | } else if item.InventoryType == InventoryTypeWaist { 44 | equipment[EquipmentSlotWaist] = item 45 | } else if item.InventoryType == InventoryTypeLegs { 46 | equipment[EquipmentSlotLegs] = item 47 | } else if item.InventoryType == InventoryTypeFeet { 48 | equipment[EquipmentSlotFeet] = item 49 | } else if item.InventoryType == InventoryTypeWrists { 50 | equipment[EquipmentSlotWrists] = item 51 | } else if item.InventoryType == InventoryTypeHands { 52 | equipment[EquipmentSlotHands] = item 53 | } else if item.InventoryType == InventoryTypeWeapon || item.InventoryType == InventoryType2HWeapon || item.InventoryType == InventoryTypeWeaponMainHand { 54 | equipment[EquipmentSlotMainHand] = item 55 | } else if item.InventoryType == InventoryTypeShield || item.InventoryType == InventoryTypeWeaponOffHand { 56 | equipment[EquipmentSlotOffHand] = item 57 | } else if item.InventoryType == InventoryTypeThrown || item.InventoryType == InventoryTypeRanged || item.InventoryType == InventoryTypeRangedRight { 58 | equipment[EquipmentSlotRanged] = item 59 | } else if item.InventoryType == InventoryTypeTabard { 60 | equipment[EquipmentSlotTabard] = item 61 | } else if item.InventoryType == InventoryTypeCloak { 62 | equipment[EquipmentSlotBack] = item 63 | } else { 64 | nonEquipment = append(nonEquipment, item) 65 | } 66 | } 67 | 68 | return equipment, nonEquipment 69 | } 70 | -------------------------------------------------------------------------------- /server/world/data/dynamic/player_game_logic.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 8 | ) 9 | 10 | // Unit interface methods (game-logic). 11 | func (p *Player) Initialize() { 12 | go p.restoreHealthPower() 13 | } 14 | 15 | // Utility methods. 16 | func (p *Player) restoreHealthPower() { 17 | for range time.Tick(static.RegenTimeout) { 18 | if p.IsLoggedIn { 19 | p.SendUpdates([]interface{}{ 20 | &messages.ModHealth{Amount: p.HealthRegen(static.RegenTimeout, p.IsInCombat())}, 21 | &messages.ModPower{Amount: p.PowerRegen(static.RegenTimeout, p.IsInCombat())}, 22 | }) 23 | } 24 | } 25 | } 26 | 27 | // meleeAttackRate calculates the attack rate for a given equipment slot. 28 | func (p *Player) meleeAttackRate(slot static.EquipmentSlot) time.Duration { 29 | weapon := GetObjectManager().GetItem(p.Equipment[slot]) 30 | if weapon == nil { 31 | if slot == static.EquipmentSlotMainHand { 32 | return time.Duration(2000) * time.Millisecond 33 | } else { 34 | return time.Duration(0) 35 | } 36 | } 37 | 38 | return weapon.GetTemplate().AttackRate 39 | } 40 | 41 | func (p *Player) resistances() map[static.SpellSchool]int { 42 | resistances := map[static.SpellSchool]int{ 43 | static.SpellSchoolPhysical: 0, 44 | static.SpellSchoolHoly: 0, 45 | static.SpellSchoolFire: 0, 46 | static.SpellSchoolNature: 0, 47 | static.SpellSchoolFrost: 0, 48 | static.SpellSchoolShadow: 0, 49 | static.SpellSchoolArcane: 0, 50 | } 51 | 52 | /// Add modifications based on items. 53 | for _, itemGUID := range p.Equipment { 54 | item := GetObjectManager().GetItem(itemGUID) 55 | 56 | for k, v := range item.GetTemplate().Resistances { 57 | resistances[k] += v 58 | } 59 | } 60 | 61 | /// Add modifications based on stats. 62 | // Each point in agility gives 2 armor. 63 | resistances[static.SpellSchoolPhysical] += p.Agility * 2 64 | 65 | // Each point in spirit increases resistances by 0.05. 66 | spiritBonus := int(0.05 * float32(p.Spirit)) 67 | resistances[static.SpellSchoolHoly] += spiritBonus 68 | resistances[static.SpellSchoolFire] += spiritBonus 69 | resistances[static.SpellSchoolNature] += spiritBonus 70 | resistances[static.SpellSchoolFrost] += spiritBonus 71 | resistances[static.SpellSchoolShadow] += spiritBonus 72 | resistances[static.SpellSchoolArcane] += spiritBonus 73 | 74 | return resistances 75 | } 76 | 77 | func (p *Player) weapons() []*Item { 78 | weapons := make([]*Item, 0) 79 | for _, slot := range []static.EquipmentSlot{static.EquipmentSlotMainHand, static.EquipmentSlotOffHand, static.EquipmentSlotRanged} { 80 | weaponGUID, ok := p.Equipment[slot] 81 | if ok { 82 | if weapon := GetObjectManager().GetItem(weaponGUID); weapon != nil { 83 | if weapon.GetTemplate().AttackRate > 0 { 84 | weapons = append(weapons, weapon) 85 | } 86 | } 87 | } 88 | } 89 | 90 | return weapons 91 | } 92 | -------------------------------------------------------------------------------- /server/auth/packet/realmlist.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | 8 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 9 | "github.com/jeshuamorrissey/wow_server_go/server/auth/session" 10 | ) 11 | 12 | // ClientRealmlist packet contains no fields. 13 | type ClientRealmlist struct{} 14 | 15 | func (pkt *ClientRealmlist) FromBytes(state *session.State, buffer io.Reader) error { 16 | var unk uint32 17 | return binary.Read(buffer, binary.LittleEndian, &unk) 18 | } 19 | 20 | // OpCode gets the opcode of the packet. 21 | func (*ClientRealmlist) OpCode() static.OpCode { 22 | return static.OpCodeRealmlist 23 | } 24 | 25 | // Realm is information required to send as part of the realmlist. 26 | type Realm struct { 27 | Icon uint32 28 | Flags uint8 29 | Name string 30 | Address string 31 | Population float32 32 | NumCharacters uint8 33 | Timezone uint8 34 | } 35 | 36 | // ServerRealmlist is made up of a list of realms. 37 | type ServerRealmlist struct { 38 | Realms []Realm 39 | } 40 | 41 | // Bytes converts the ServerRealmlist packet to an array of bytes. 42 | func (pkt *ServerRealmlist) ToBytes(state *session.State) ([]byte, error) { 43 | realmsBuffer := bytes.NewBufferString("") 44 | 45 | binary.Write(realmsBuffer, binary.LittleEndian, uint32(0)) // unk 46 | realmsBuffer.WriteByte(uint8(len(pkt.Realms))) 47 | 48 | for _, realm := range pkt.Realms { 49 | binary.Write(realmsBuffer, binary.LittleEndian, realm.Icon) 50 | binary.Write(realmsBuffer, binary.LittleEndian, realm.Flags) 51 | realmsBuffer.WriteString(realm.Name + "\x00") 52 | realmsBuffer.WriteString(realm.Address + "\x00") 53 | binary.Write(realmsBuffer, binary.LittleEndian, realm.Population) 54 | binary.Write(realmsBuffer, binary.LittleEndian, realm.NumCharacters) 55 | binary.Write(realmsBuffer, binary.LittleEndian, realm.Timezone) 56 | binary.Write(realmsBuffer, binary.LittleEndian, uint8(0)) // unk 57 | } 58 | 59 | binary.Write(realmsBuffer, binary.LittleEndian, uint16(2)) // unk 60 | 61 | // Make the real buffer, which has the length at the start. 62 | buffer := bytes.NewBufferString("") 63 | binary.Write(buffer, binary.LittleEndian, uint16(realmsBuffer.Len())) 64 | buffer.Write(realmsBuffer.Bytes()) 65 | 66 | return buffer.Bytes(), nil 67 | } 68 | 69 | // OpCode returns ServerRealmlistOpCode. 70 | func (*ServerRealmlist) OpCode() static.OpCode { 71 | return static.OpCodeRealmlist 72 | } 73 | 74 | // Handle will check the database for the account and send an appropriate response. 75 | func (pkt *ClientRealmlist) Handle(state *session.State) ([]session.ServerPacket, error) { 76 | response := new(ServerRealmlist) 77 | 78 | response.Realms = append(response.Realms, Realm{ 79 | Icon: 0, 80 | Flags: 0, 81 | Name: state.Config.Name, 82 | Address: "localhost:5001", 83 | Population: 0, 84 | NumCharacters: 0, 85 | Timezone: 0, 86 | }) 87 | 88 | return []session.ServerPacket{response}, nil 89 | } 90 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc_data/CharBaseInfo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "race": 1, 4 | "class": 1 5 | }, 6 | { 7 | "race": 1, 8 | "class": 2 9 | }, 10 | { 11 | "race": 1, 12 | "class": 4 13 | }, 14 | { 15 | "race": 1, 16 | "class": 5 17 | }, 18 | { 19 | "race": 1, 20 | "class": 8 21 | }, 22 | { 23 | "race": 1, 24 | "class": 9 25 | }, 26 | { 27 | "race": 2, 28 | "class": 1 29 | }, 30 | { 31 | "race": 2, 32 | "class": 3 33 | }, 34 | { 35 | "race": 2, 36 | "class": 4 37 | }, 38 | { 39 | "race": 2, 40 | "class": 7 41 | }, 42 | { 43 | "race": 2, 44 | "class": 9 45 | }, 46 | { 47 | "race": 3, 48 | "class": 1 49 | }, 50 | { 51 | "race": 3, 52 | "class": 2 53 | }, 54 | { 55 | "race": 3, 56 | "class": 3 57 | }, 58 | { 59 | "race": 3, 60 | "class": 4 61 | }, 62 | { 63 | "race": 3, 64 | "class": 5 65 | }, 66 | { 67 | "race": 3, 68 | "class": 8 69 | }, 70 | { 71 | "race": 4, 72 | "class": 1 73 | }, 74 | { 75 | "race": 4, 76 | "class": 3 77 | }, 78 | { 79 | "race": 4, 80 | "class": 4 81 | }, 82 | { 83 | "race": 4, 84 | "class": 5 85 | }, 86 | { 87 | "race": 4, 88 | "class": 11 89 | }, 90 | { 91 | "race": 5, 92 | "class": 1 93 | }, 94 | { 95 | "race": 5, 96 | "class": 4 97 | }, 98 | { 99 | "race": 5, 100 | "class": 5 101 | }, 102 | { 103 | "race": 5, 104 | "class": 8 105 | }, 106 | { 107 | "race": 5, 108 | "class": 9 109 | }, 110 | { 111 | "race": 6, 112 | "class": 1 113 | }, 114 | { 115 | "race": 6, 116 | "class": 3 117 | }, 118 | { 119 | "race": 6, 120 | "class": 7 121 | }, 122 | { 123 | "race": 6, 124 | "class": 11 125 | }, 126 | { 127 | "race": 7, 128 | "class": 1 129 | }, 130 | { 131 | "race": 7, 132 | "class": 4 133 | }, 134 | { 135 | "race": 7, 136 | "class": 8 137 | }, 138 | { 139 | "race": 7, 140 | "class": 9 141 | }, 142 | { 143 | "race": 8, 144 | "class": 1 145 | }, 146 | { 147 | "race": 8, 148 | "class": 3 149 | }, 150 | { 151 | "race": 8, 152 | "class": 4 153 | }, 154 | { 155 | "race": 8, 156 | "class": 5 157 | }, 158 | { 159 | "race": 8, 160 | "class": 7 161 | }, 162 | { 163 | "race": 8, 164 | "class": 8 165 | } 166 | ] -------------------------------------------------------------------------------- /server/auth/packet/login_proof.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "math/big" 8 | 9 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 10 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 11 | "github.com/jeshuamorrissey/wow_server_go/server/auth/session" 12 | "github.com/jeshuamorrissey/wow_server_go/server/auth/srp" 13 | ) 14 | 15 | // ClientLoginProof encodes proof that the client has the correct information. 16 | type ClientLoginProof struct { 17 | A big.Int 18 | M big.Int 19 | CRCHash big.Int 20 | NumberOfKeys uint8 21 | SecurityFlags uint8 22 | } 23 | 24 | // Read will load a ClientLoginProof packet from a buffer. 25 | // An error will be returned if at least one of the fields didn't load correctly. 26 | func (pkt *ClientLoginProof) FromBytes(state *session.State, buffer io.Reader) error { 27 | aBuffer := make([]byte, 32) 28 | buffer.Read(aBuffer) 29 | pkt.A.SetBytes(util.ReverseBytes(aBuffer)) 30 | 31 | mBuffer := make([]byte, 20) 32 | buffer.Read(mBuffer) 33 | pkt.M.SetBytes(util.ReverseBytes(mBuffer)) 34 | 35 | crcHashBuffer := make([]byte, 20) 36 | buffer.Read(crcHashBuffer) 37 | pkt.CRCHash.SetBytes(util.ReverseBytes(crcHashBuffer)) 38 | 39 | binary.Read(buffer, binary.LittleEndian, &pkt.NumberOfKeys) 40 | return binary.Read(buffer, binary.LittleEndian, &pkt.SecurityFlags) 41 | } 42 | 43 | // OpCode gets the opcode of the packet. 44 | func (*ClientLoginProof) OpCode() static.OpCode { 45 | return static.OpCodeLoginProof 46 | } 47 | 48 | // ServerLoginProof is the server's response to a client's challenge. It contains 49 | // some SRP information used for handshaking. 50 | type ServerLoginProof struct { 51 | Error static.LoginErrorCode 52 | Proof big.Int 53 | } 54 | 55 | // Bytes writes out the packet to an array of bytes. 56 | func (pkt *ServerLoginProof) ToBytes(state *session.State) ([]byte, error) { 57 | buffer := bytes.NewBufferString("") 58 | 59 | buffer.WriteByte(uint8(pkt.Error)) 60 | 61 | if pkt.Error == 0 { 62 | buffer.Write(util.PadBigIntBytes(util.ReverseBytes(pkt.Proof.Bytes()), 20)) 63 | buffer.Write([]byte("\x00\x00\x00\x00")) // unk1 64 | } 65 | 66 | return buffer.Bytes(), nil 67 | } 68 | 69 | // OpCode gets the opcode of the packet. 70 | func (*ServerLoginProof) OpCode() static.OpCode { 71 | return static.OpCodeLoginProof 72 | } 73 | 74 | // Handle will check the database for the account and send an appropriate response. 75 | func (pkt *ClientLoginProof) Handle(state *session.State) ([]session.ServerPacket, error) { 76 | response := new(ServerLoginProof) 77 | 78 | K, M := srp.CalculateSessionKey( 79 | &pkt.A, 80 | state.PublicEphemeral, 81 | state.PrivateEphemeral, 82 | state.Account.Verifier(), 83 | state.Account.Salt(), 84 | state.Account.Name) 85 | 86 | if M.Cmp(&pkt.M) != 0 { 87 | response.Error = 4 // TODO(jeshua): make these constants 88 | } else { 89 | response.Error = 0 90 | response.Proof.Set(srp.CalculateServerProof(&pkt.A, M, K)) 91 | 92 | state.AddLogField("account", state.Account.Name) 93 | 94 | state.Account.SessionKeyStr = K.Text(16) 95 | } 96 | 97 | return []session.ServerPacket{response}, nil 98 | } 99 | -------------------------------------------------------------------------------- /server/world/packet/server_update_object.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | // OutOfRangeUpdate represents a list of GUIDs which are no longer in range. 12 | type OutOfRangeUpdate struct { 13 | GUIDS []interfaces.GUID 14 | } 15 | 16 | // ObjectUpdate represents an update to an individual object. 17 | type ObjectUpdate struct { 18 | GUID interfaces.GUID 19 | UpdateType static.UpdateType 20 | UpdateFlags static.UpdateFlags 21 | IsSelf bool 22 | TypeID static.TypeID 23 | 24 | MovementUpdate []byte 25 | NumUpdateFields int 26 | UpdateFields interfaces.UpdateFieldsMap 27 | 28 | VictimGUID interfaces.GUID 29 | WorldTime uint32 30 | } 31 | 32 | // ServerUpdateObject is the UPDATE_OBJECT packet. 33 | type ServerUpdateObject struct { 34 | OutOfRangeUpdates OutOfRangeUpdate 35 | ObjectUpdates []ObjectUpdate 36 | } 37 | 38 | // ToBytes converts the packet into an array of bytes. 39 | func (pkt *ServerUpdateObject) ToBytes() ([]byte, error) { 40 | buffer := bytes.NewBufferString("") 41 | 42 | nUpdates := len(pkt.ObjectUpdates) 43 | if len(pkt.OutOfRangeUpdates.GUIDS) > 0 { 44 | nUpdates++ 45 | } 46 | 47 | binary.Write(buffer, binary.LittleEndian, uint32(nUpdates)) 48 | binary.Write(buffer, binary.LittleEndian, uint8(0)) // hasTransportUpdate 49 | 50 | if len(pkt.OutOfRangeUpdates.GUIDS) > 0 { 51 | buffer.WriteByte(uint8(static.UpdateTypeOutOfRangeObjects)) 52 | binary.Write(buffer, binary.LittleEndian, uint32(len(pkt.OutOfRangeUpdates.GUIDS))) 53 | for _, guid := range pkt.OutOfRangeUpdates.GUIDS { 54 | buffer.Write(guid.Pack()) 55 | } 56 | } 57 | 58 | for _, update := range pkt.ObjectUpdates { 59 | buffer.WriteByte(uint8(update.UpdateType)) 60 | buffer.Write(update.GUID.Pack()) 61 | 62 | if update.UpdateType != static.UpdateTypeValues { 63 | updateFlags := update.UpdateFlags 64 | 65 | if update.IsSelf { 66 | updateFlags |= static.UpdateFlagsSelf 67 | } 68 | 69 | buffer.WriteByte(uint8(update.TypeID)) 70 | buffer.WriteByte(uint8(updateFlags)) 71 | 72 | if update.MovementUpdate != nil { 73 | buffer.Write(update.MovementUpdate) 74 | } 75 | 76 | if updateFlags&static.UpdateFlagsHighGUID != 0 { 77 | binary.Write(buffer, binary.LittleEndian, uint32(update.GUID.High())) 78 | } 79 | 80 | if updateFlags&static.UpdateFlagsAll != 0 { 81 | binary.Write(buffer, binary.LittleEndian, uint32(1)) 82 | } 83 | 84 | if updateFlags&static.UpdateFlagsFullGUID != 0 && update.VictimGUID != 0 { 85 | buffer.Write(update.VictimGUID.Pack()) 86 | } 87 | 88 | if updateFlags&static.UpdateFlagsTransport != 0 { 89 | binary.Write(buffer, binary.LittleEndian, uint32(update.WorldTime)) 90 | } 91 | } 92 | 93 | buffer.Write(update.UpdateFields.ToBytes(update.NumUpdateFields)) 94 | } 95 | 96 | return buffer.Bytes(), nil 97 | } 98 | 99 | // OpCode returns the OpCode of the packet. 100 | func (pkt *ServerUpdateObject) OpCode() static.OpCode { 101 | return static.OpCodeServerUpdateObject 102 | } 103 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc/dbc_golang_part.go.jinja: -------------------------------------------------------------------------------- 1 | // {{ type_name }} represents data within the {{ dbc_name }}.dbc file. 2 | type {{ type_name }} struct { 3 | {% for field in fields %} 4 | {{ field.GoName() }} {{ field.GoType() }} 5 | {% endfor %} 6 | } 7 | 8 | var ( 9 | // {{ type_name }}ByID is the primary source of truth, storing data for for this DBC. 10 | {{ type_name }}ByID map[int]*{{ type_name }} 11 | ) 12 | 13 | // Indexes for this DBC file, to make querying easier. 14 | var ( 15 | {% if num_indexed_fields > 0 %} 16 | {# If we have many indexed fields, then generate a ByIndex object. #} 17 | {{ type_name }}ByIndex {{ index_map_type }} 18 | 19 | {# If we only have a single indexed field, just generate separate variables. #} 20 | {% if num_indexed_fields == 1 %} 21 | {% for record in records %} 22 | {% set indexed_field = record.IndexedFields()[0] %} 23 | {{ type_name }}{{ record[indexed_field.name].title().replace(' ', '') }} *{{ type_name }} 24 | {% endfor %} 25 | {% endif %} 26 | {% endif %} 27 | ) 28 | 29 | func {{ init_function_name }}() { 30 | // Set the source of truth. 31 | {{ type_name }}ByID = map[int]*{{ type_name }}{ 32 | {% for record in records %} 33 | {{ record.id if record.id else record._id }}: &{{ type_name }}{ 34 | {% for field in fields %} 35 | {% if not field.foreign_key_field %} 36 | {{ field.GoName() }}: {{ record.GoValue(field.name) }}, 37 | {% else %} 38 | {{ field.GoName() }}: {{ field.foreign_key_type }}{{ record[field.name] }}, 39 | {% endif %} 40 | {% endfor %} 41 | }, 42 | {% endfor %} 43 | } 44 | 45 | {% if num_indexed_fields > 0 %} 46 | // Set the index. 47 | {{ type_name }}ByIndex = make({{ index_map_type }}) 48 | 49 | {% if num_indexed_fields > 1 %} 50 | // Initialize sub-maps for each indexed field. 51 | {% for record in records %} 52 | {% for _ in record.IndexedFields()[:-1] %} 53 | {{ type_name }}ByIndex 54 | {%- for index_field in record.IndexedFields()[:loop.index] -%} 55 | {%- if index_field.GoType() == 'string' -%} 56 | ["{{ record[index_field.name].title().replace(' ', '') }}"] 57 | {%- else -%} 58 | [{{ index_field.GoName() }}{{ record[index_field.name].title().replace(' ', '') }}] 59 | {%- endif -%} 60 | {%- endfor -%} 61 | = make( 62 | {%- for index_field in record.IndexedFields()[loop.index:] -%} 63 | map[*{{ index_field.GoName() }}] 64 | {%- endfor -%} 65 | *{{ type_name }}) 66 | {% endfor %} 67 | {% endfor %} 68 | {% endif %} 69 | 70 | // Set the index values. 71 | {% for record in records %} 72 | {{ type_name }}ByIndex 73 | {%- for index_field in record.IndexedFields() -%} 74 | {%- if index_field.GoType() == 'string' -%} 75 | ["{{ record[index_field.name].title().replace(' ', '') }}"] 76 | {%- else -%} 77 | [{{ index_field.GoName() }}{{ record[index_field.name].title().replace(' ', '') }}] 78 | {%- endif -%} 79 | {%- endfor -%} 80 | = {{ type_name }}ByID[{{ record.id if record.id else record._id }}] 81 | {% endfor %} 82 | 83 | {% if num_indexed_fields == 1 %} 84 | // As there is only a single index, add some special convenience values. 85 | {% for record in records %} 86 | {% set indexed_field = record.IndexedFields()[0] %} 87 | {{ type_name }}{{ record[indexed_field.name].title().replace(' ', '') }} = {{ type_name }}ByID[{{ record.id if record.id else record._id }}] 88 | {% endfor %} 89 | {% endif %} 90 | {% endif %} 91 | } 92 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc/record_types.py: -------------------------------------------------------------------------------- 1 | from dbc import dbc 2 | from dbc.record_fields import Int, ID, String, LocalizedString, Float, Byte 3 | 4 | 5 | class ChrClasses(dbc.Record): 6 | @classmethod 7 | def GoTypeName(cls): 8 | return "Class" 9 | 10 | @classmethod 11 | def Fields(cls): 12 | return [ 13 | Int(name='id'), 14 | Int(default=1), 15 | Int(name='primary_stat'), 16 | Int(name='power_type'), 17 | String(name='pet_type'), 18 | LocalizedString(name='name', indexed=True), 19 | String(default=lambda r: r.name.upper()), 20 | Int(default=0), 21 | Int(default=0), 22 | ] 23 | 24 | 25 | class ChrRaces(dbc.Record): 26 | @classmethod 27 | def GoTypeName(cls): 28 | return 'Race' 29 | 30 | @classmethod 31 | def Fields(cls): 32 | return [ 33 | Int(name='id'), 34 | Int(name='flags'), 35 | Int(name='faction_id'), # faction_id 36 | Int(name='unk'), # unk 37 | Int(name='male_display_id'), 38 | Int(name='female_display_id'), 39 | String(name='client_prefix'), 40 | Float(name='mount_scale'), 41 | Int(name='base_language'), 42 | Int(name='creature_type'), 43 | Int(name='login_effect_spell_id'), 44 | Int(name='combat_stun_spell_id'), 45 | Int(name='res_sickness_spell_id'), 46 | Int(name='splash_sound_id'), 47 | Int(name='starting_taxi_nodes'), 48 | String(name='client_file_string'), 49 | Int(name='cinematic_sequence_id'), 50 | LocalizedString(name='name', indexed=True), 51 | String(name='male_feature_name'), 52 | String(name='female_feature_name'), 53 | String(name='hair_customization_name'), 54 | ] 55 | 56 | 57 | class ChrStartingStats(dbc.Record): 58 | @classmethod 59 | def GoTypeName(cls): 60 | return 'StartingStats' 61 | 62 | @classmethod 63 | def Fields(cls): 64 | return [ 65 | ID(), 66 | String(name='class', indexed=True, 67 | foreign_key=(ChrClasses, 'name')), 68 | String(name='race', indexed=True, foreign_key=(ChrRaces, 'name')), 69 | Int(name='strength'), 70 | Int(name='agility'), 71 | Int(name='stamina'), 72 | Int(name='intellect'), 73 | Int(name='spirit'), 74 | Int(name='base_health'), 75 | ] 76 | 77 | 78 | class ChrStartingLocations(dbc.Record): 79 | @classmethod 80 | def GoTypeName(cls): 81 | return 'StartingLocations' 82 | 83 | @classmethod 84 | def Fields(cls): 85 | return [ 86 | ID(), 87 | String(name='race', indexed=True, foreign_key=(ChrRaces, 'name')), 88 | Int(name='map'), 89 | Int(name='zone'), 90 | Float(name='x'), 91 | Float(name='y'), 92 | Float(name='z'), 93 | Float(name='o'), 94 | ] 95 | 96 | 97 | class CharBaseInfo(dbc.Record): 98 | @classmethod 99 | def GoTypeName(cls): 100 | return 'CharBaseInfo' 101 | 102 | @classmethod 103 | def Fields(cls): 104 | return [ 105 | Byte(name='race'), 106 | Byte(name='class'), 107 | ] 108 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/combat.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/server/world/channels" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/messages" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 11 | ) 12 | 13 | type Damage struct { 14 | Base map[static.SpellSchool]int 15 | } 16 | 17 | type combatHook func(*Damage) 18 | 19 | type autoAttackTimer struct { 20 | done chan bool 21 | ticker *time.Ticker 22 | } 23 | 24 | type Combat struct { 25 | Target interfaces.GUID 26 | Attackers map[interfaces.GUID]bool 27 | 28 | OutgoingDamageMods map[string]func(*Damage) 29 | IncomingDamageMods map[string]func(*Damage) 30 | 31 | autoAttackTimers []*autoAttackTimer 32 | } 33 | 34 | // IsInCombat will return true iff we are in combat. 35 | func (c *Combat) IsInCombat() bool { 36 | return c.Target != 0 || len(c.Attackers) > 0 37 | } 38 | 39 | func (c *Combat) StopAttack() { 40 | for _, timer := range c.autoAttackTimers { 41 | timer.done <- true 42 | } 43 | 44 | c.autoAttackTimers = make([]*autoAttackTimer, 0) 45 | c.Target = 0 46 | } 47 | 48 | // RegisterAttacker will note that the given object is attacking us. 49 | func (c *Combat) RegisterAttacker(attacker interfaces.GUID) { 50 | if c.Attackers == nil { 51 | c.Attackers = make(map[interfaces.GUID]bool) 52 | } 53 | 54 | c.Attackers[attacker] = true 55 | } 56 | 57 | func (c *Combat) DeregisterAttacker(attacker interfaces.GUID) { 58 | delete(c.Attackers, attacker) 59 | } 60 | 61 | func (c *Combat) resolveSingleAttack(attacker interfaces.Object, target interfaces.Object, damage *Damage) { 62 | for _, damageMod := range c.OutgoingDamageMods { 63 | damageMod(damage) 64 | } 65 | 66 | finalDamage := 0 67 | for _, damage := range damage.Base { 68 | finalDamage += damage 69 | } 70 | 71 | channels.CombatUpdates <- &channels.CombatUpdate{ 72 | Attacker: attacker, 73 | Target: target, 74 | AttackInfo: &interfaces.AttackInfo{ 75 | Damage: finalDamage, 76 | }, 77 | } 78 | 79 | target.SendUpdates([]interface{}{ 80 | &messages.ModHealth{Amount: -finalDamage}, 81 | }) 82 | } 83 | 84 | // Attack will start a goroutine which will manage attacking. 85 | func (c *Combat) Attack(attacker interfaces.Object, target interfaces.Object, attackRate time.Duration, calculateBaseDamage func() *Damage) { 86 | c.Target = target.GUID() 87 | 88 | // Send a packet saying who is attacking who. 89 | channels.PacketUpdates <- &channels.PacketUpdate{ 90 | Packet: &packet.ServerAttackStart{ 91 | Attacker: attacker.GUID(), 92 | Target: target.GUID(), 93 | }, 94 | 95 | Location: attacker.GetLocation(), 96 | } 97 | 98 | if c.autoAttackTimers == nil { 99 | c.autoAttackTimers = make([]*autoAttackTimer, 0) 100 | } 101 | 102 | autoAttackTimer := &autoAttackTimer{ 103 | done: make(chan bool), 104 | ticker: time.NewTicker(attackRate), 105 | } 106 | 107 | c.autoAttackTimers = append(c.autoAttackTimers, autoAttackTimer) 108 | 109 | c.resolveSingleAttack(attacker, target, calculateBaseDamage()) 110 | go func() { 111 | for { 112 | select { 113 | case <-autoAttackTimer.ticker.C: 114 | c.resolveSingleAttack(attacker, target, calculateBaseDamage()) 115 | case <-autoAttackTimer.done: 116 | autoAttackTimer.ticker.Stop() 117 | return 118 | } 119 | } 120 | }() 121 | } 122 | -------------------------------------------------------------------------------- /server/world/data/static/error_codes.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | // CharErrorCode is a common type for all error codes returned 4 | // during the character creation/deletion process. 5 | type CharErrorCode uint8 6 | 7 | // ErrorCodes used the character creation/deletion process. 8 | const ( 9 | CharErrorCodeCreateAccountLimit CharErrorCode = 0x35 10 | CharErrorCodeCreateDisabled CharErrorCode = 0x32 11 | CharErrorCodeCreateError CharErrorCode = 0x2F 12 | CharErrorCodeCreateFailed CharErrorCode = 0x30 13 | CharErrorCodeCreateInProgress CharErrorCode = 0x2D 14 | CharErrorCodeCreateNameInUse CharErrorCode = 0x31 15 | CharErrorCodeCreateOnlyExisting CharErrorCode = 0x37 16 | CharErrorCodeCreatePvpTeamsViolation CharErrorCode = 0x33 17 | CharErrorCodeCreateServerLimit CharErrorCode = 0x34 18 | CharErrorCodeCreateServerQueue CharErrorCode = 0x36 19 | CharErrorCodeCreateSuccess CharErrorCode = 0x2E 20 | CharErrorCodeDeleteFailed CharErrorCode = 0x3A 21 | CharErrorCodeDeleteFailedLockedForTransfer CharErrorCode = 0x3B 22 | CharErrorCodeDeleteInProgress CharErrorCode = 0x38 23 | CharErrorCodeDeleteSuccess CharErrorCode = 0x39 24 | CharErrorCodeListFailed CharErrorCode = 0x2C 25 | CharErrorCodeListRetrieved CharErrorCode = 0x2B 26 | CharErrorCodeListRetrieving CharErrorCode = 0x2A 27 | CharErrorCodeNameConsecutiveSpaces CharErrorCode = 0x50 28 | CharErrorCodeNameFailure CharErrorCode = 0x51 29 | CharErrorCodeNameInvalidApostrophe CharErrorCode = 0x4C 30 | CharErrorCodeNameInvalidCharacter CharErrorCode = 0x48 31 | CharErrorCodeNameInvalidSpace CharErrorCode = 0x4F 32 | CharErrorCodeNameMixedLanguages CharErrorCode = 0x49 33 | CharErrorCodeNameMultipleApostrophes CharErrorCode = 0x4D 34 | CharErrorCodeNameNoName CharErrorCode = 0x45 35 | CharErrorCodeNameProfane CharErrorCode = 0x4A 36 | CharErrorCodeNameReserved CharErrorCode = 0x4B 37 | CharErrorCodeNameSuccess CharErrorCode = 0x52 38 | CharErrorCodeNameThreeConsecutive CharErrorCode = 0x4E 39 | CharErrorCodeNameTooLong CharErrorCode = 0x47 40 | CharErrorCodeNameTooShort CharErrorCode = 0x46 41 | ) 42 | 43 | // AuthErrorCode is a common type for all error codes returned 44 | // during the login process. 45 | type AuthErrorCode uint8 46 | 47 | // ErrorCodes used the login process. 48 | const ( 49 | AuthOK AuthErrorCode = 0x0C 50 | AuthFailed AuthErrorCode = 0x0D 51 | AuthReject AuthErrorCode = 0x0E 52 | AuthBadServerProof AuthErrorCode = 0x0F 53 | AuthUnavailable AuthErrorCode = 0x10 54 | AuthSystemError AuthErrorCode = 0x11 55 | AuthBillingError AuthErrorCode = 0x12 56 | AuthBillingExpired AuthErrorCode = 0x13 57 | AuthVersionMismatch AuthErrorCode = 0x14 58 | AuthUnknownAccount AuthErrorCode = 0x15 59 | AuthIncorrectPassword AuthErrorCode = 0x16 60 | AuthSessionExpired AuthErrorCode = 0x17 61 | AuthServerShuttingDown AuthErrorCode = 0x18 62 | AuthAlreadyLoggingIn AuthErrorCode = 0x19 63 | AuthLoginServerNotFound AuthErrorCode = 0x1A 64 | AuthWaitQueue AuthErrorCode = 0x1B 65 | AuthBanned AuthErrorCode = 0x1C 66 | AuthAlreadyOnline AuthErrorCode = 0x1D 67 | AuthNoTime AuthErrorCode = 0x1E 68 | AuthDbBusy AuthErrorCode = 0x1F 69 | AuthSuspended AuthErrorCode = 0x20 70 | AuthParentalControl AuthErrorCode = 0x21 71 | ) 72 | -------------------------------------------------------------------------------- /server/world/packet/server_char_enum.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 9 | ) 10 | 11 | type ItemSummary struct { 12 | DisplayID static.DisplayID 13 | InventoryType static.InventoryType 14 | } 15 | 16 | type CharacterSummary struct { 17 | Name string 18 | GUID interfaces.GUID 19 | Race *static.Race 20 | Class *static.Class 21 | Gender static.Gender 22 | SkinColor int 23 | Face int 24 | HairStyle int 25 | HairColor int 26 | Feature int 27 | Level int 28 | ZoneID int 29 | MapID int 30 | Location interfaces.Location 31 | HasLoggedIn bool 32 | Flags uint32 33 | Equipment map[static.EquipmentSlot]*ItemSummary 34 | FirstBag *ItemSummary 35 | } 36 | 37 | // ServerCharEnum is sent back in response to ClientPing. 38 | type ServerCharEnum struct { 39 | Characters []*CharacterSummary 40 | } 41 | 42 | // ToBytes writes out the packet to an array of bytes. 43 | func (pkt *ServerCharEnum) ToBytes() ([]byte, error) { 44 | buffer := bytes.NewBufferString("") 45 | 46 | buffer.WriteByte(uint8(len(pkt.Characters))) // number of characters 47 | 48 | for _, char := range pkt.Characters { 49 | binary.Write(buffer, binary.LittleEndian, char.GUID.Low()) 50 | binary.Write(buffer, binary.LittleEndian, char.GUID.High()) 51 | buffer.WriteString(char.Name) 52 | buffer.WriteByte(0) 53 | buffer.WriteByte(uint8(char.Race.ID)) 54 | buffer.WriteByte(uint8(char.Class.ID)) 55 | buffer.WriteByte(uint8(char.Gender)) 56 | buffer.WriteByte(uint8(char.SkinColor)) 57 | buffer.WriteByte(uint8(char.Face)) 58 | buffer.WriteByte(uint8(char.HairStyle)) 59 | buffer.WriteByte(uint8(char.HairColor)) 60 | buffer.WriteByte(uint8(char.Feature)) 61 | buffer.WriteByte(uint8(char.Level)) 62 | binary.Write(buffer, binary.LittleEndian, uint32(char.ZoneID)) 63 | binary.Write(buffer, binary.LittleEndian, uint32(char.MapID)) 64 | binary.Write(buffer, binary.LittleEndian, float32(char.Location.X)) 65 | binary.Write(buffer, binary.LittleEndian, float32(char.Location.Y)) 66 | binary.Write(buffer, binary.LittleEndian, float32(char.Location.Z)) 67 | 68 | // TODO(jeshua): implement the following fields with comments. 69 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // GuildID 70 | binary.Write(buffer, binary.LittleEndian, char.Flags) 71 | 72 | if !char.HasLoggedIn { 73 | buffer.WriteByte(1) 74 | } else { 75 | buffer.WriteByte(0) 76 | } 77 | 78 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // PetID 79 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // PetLevel 80 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // PetFamily 81 | 82 | for slot := static.EquipmentSlotHead; slot <= static.EquipmentSlotTabard; slot++ { 83 | if itemSummary, ok := char.Equipment[slot]; ok { 84 | binary.Write(buffer, binary.LittleEndian, uint32(itemSummary.DisplayID)) 85 | binary.Write(buffer, binary.LittleEndian, uint8(itemSummary.InventoryType)) 86 | } else { 87 | binary.Write(buffer, binary.LittleEndian, uint32(0)) 88 | binary.Write(buffer, binary.LittleEndian, uint8(0)) 89 | } 90 | } 91 | 92 | if char.FirstBag != nil { 93 | binary.Write(buffer, binary.LittleEndian, uint32(char.FirstBag.DisplayID)) 94 | binary.Write(buffer, binary.LittleEndian, uint8(char.FirstBag.InventoryType)) 95 | } else { 96 | binary.Write(buffer, binary.LittleEndian, uint32(0)) 97 | binary.Write(buffer, binary.LittleEndian, uint8(0)) 98 | 99 | } 100 | 101 | } 102 | 103 | return buffer.Bytes(), nil 104 | } 105 | 106 | // OpCode gets the opcode of the packet. 107 | func (*ServerCharEnum) OpCode() static.OpCode { 108 | return static.OpCodeServerCharEnum 109 | } 110 | -------------------------------------------------------------------------------- /tools/gen_opcode_to_handler/gen_opcode_to_handler.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import re 4 | import sys 5 | from typing import Dict, List 6 | 7 | _TYPE_REGEXP = re.compile(r'type (Client[a-zA-Z]+) struct') 8 | _OPCODE_METHOD_REGEXP = 'func \(.*\*{}\) OpCode.*return static\.([a-zA-Z]+)' 9 | 10 | GOLANG_FILE_TEMPLATE = ''' 11 | package world 12 | 13 | import ( 14 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 15 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 16 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 17 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet/handlers" 18 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 19 | ) 20 | 21 | 22 | var opCodeToHandler = map[static.OpCode]func(interfaces.ClientPacket, *system.State) ([]interfaces.ServerPacket, error){ 23 | %(generated_functions)s 24 | } 25 | ''' 26 | 27 | GOLANG_GENERATED_FUNCTION_TEMPLATE = ''' static.%(op_code)s: func(pkt interfaces.ClientPacket, state *system.State) ([]interfaces.ServerPacket, error) { 28 | return handlers.Handle%(packet_name)s(pkt.(*packet.%(packet_name)s), state) 29 | },''' 30 | 31 | MOVE_OPCODES = [ 32 | 'OpCodeClientMoveHeartbeat', 33 | 'OpCodeClientMoveSetFacing', 34 | 'OpCodeClientMoveStartBackward', 35 | 'OpCodeClientMoveStartForward', 36 | 'OpCodeClientMoveStartStrafeLeft', 37 | 'OpCodeClientMoveStartStrafeRight', 38 | 'OpCodeClientMoveStartTurnLeft', 39 | 'OpCodeClientMoveStartTurnRight', 40 | 'OpCodeClientMoveStop', 41 | 'OpCodeClientMoveStopStrafe', 42 | 'OpCodeClientMoveStopTurn', 43 | ] 44 | 45 | 46 | def find_packet_types(file_content: str) -> List[str]: 47 | return _TYPE_REGEXP.findall(file_content) 48 | 49 | 50 | def find_opcode_names(file_content: str, 51 | packet_types: List[str]) -> Dict[str, str]: 52 | packet_to_opcode = {} 53 | for packet_type in packet_types: 54 | # Special case: ClientMove can handle multiple opcodes. 55 | if packet_type == 'ClientMove': 56 | packet_to_opcode[packet_type] = MOVE_OPCODES 57 | continue 58 | 59 | regexp = re.compile(_OPCODE_METHOD_REGEXP.format(packet_type), 60 | re.MULTILINE | re.DOTALL) 61 | opcodes = regexp.findall(file_content) 62 | if not opcodes: 63 | print(f'Could not find opcode for {packet_type}', file=sys.stderr) 64 | continue 65 | 66 | packet_to_opcode[packet_type] = opcodes[0] 67 | 68 | return packet_to_opcode 69 | 70 | 71 | def main(package_path: str): 72 | packet_to_opcode = {} 73 | for file in sorted(os.listdir(package_path)): 74 | filepath = os.path.join(package_path, file) 75 | if os.path.splitext(filepath)[1] == '.go': 76 | with open(filepath) as f: 77 | file_content = f.read() 78 | packet_types = find_packet_types(file_content) 79 | packet_to_opcode.update( 80 | find_opcode_names(file_content, packet_types)) 81 | 82 | generated_functions = [] 83 | for packet_type_name, opcode_name in sorted(packet_to_opcode.items()): 84 | if isinstance(opcode_name, list): 85 | for op in opcode_name: 86 | generated_functions.append( 87 | GOLANG_GENERATED_FUNCTION_TEMPLATE % 88 | dict(op_code=op, packet_name=packet_type_name)) 89 | else: 90 | generated_functions.append( 91 | GOLANG_GENERATED_FUNCTION_TEMPLATE % 92 | dict(op_code=opcode_name, packet_name=packet_type_name)) 93 | 94 | print(GOLANG_FILE_TEMPLATE % 95 | dict(generated_functions='\n'.join(generated_functions))) 96 | 97 | 98 | if __name__ == '__main__': 99 | argument_parser = argparse.ArgumentParser() 100 | argument_parser.add_argument( 101 | '--package_path', 102 | type=str, 103 | required=True, 104 | help='The path to the golang package which contains the packet types.') 105 | main(**argument_parser.parse_args().__dict__) 106 | -------------------------------------------------------------------------------- /server/auth/srp/srp.go: -------------------------------------------------------------------------------- 1 | package srp 2 | 3 | import ( 4 | "crypto/sha1" 5 | "math/big" 6 | ) 7 | 8 | const ( 9 | // G is the SRP Generator; the base of many mathematical expressions. 10 | G uint8 = 7 11 | 12 | // K is the SRP Verifier Scale Factor; used to scale the verifier which 13 | // is stored in the database. 14 | K uint8 = 3 15 | ) 16 | 17 | // N is the SRP Modulus; all operations are performed in base N. 18 | func N() *big.Int { 19 | n := big.NewInt(0) 20 | n.SetString("62100066509156017342069496140902949863249758336000796928566441170293728648119", 10) 21 | return n 22 | } 23 | 24 | // GenerateSalt generates a random salt. 25 | func GenerateSalt() *big.Int { 26 | // TODO(jeshua): Make this a random number. 27 | s := big.NewInt(0) 28 | s.SetString("66759882342950727220130969932663635787137805713109467932708165413389947953699", 10) 29 | return s 30 | } 31 | 32 | func _H(parts ...[]byte) []byte { 33 | hash := sha1.New() 34 | for _, part := range parts { 35 | hash.Write(reverse(part)) 36 | } 37 | 38 | return reverse(hash.Sum(nil)) 39 | } 40 | 41 | // GenerateVerifier will generate a hash of the account name, password and salt 42 | // which can be used as the SRP verifier. 43 | func GenerateVerifier(accountName, password string, salt *big.Int) *big.Int { 44 | x := big.NewInt(0) 45 | x.SetBytes(_H(salt.Bytes(), _H(reverse([]byte(accountName)), []byte(":"), reverse([]byte(password))))) 46 | 47 | g := big.NewInt(int64(G)) 48 | return g.Exp(g, x, N()) 49 | } 50 | 51 | // GeneratePublicEphemeral calculaes the B value given b & v. 52 | func GeneratePublicEphemeral(v *big.Int, b *big.Int) *big.Int { 53 | g := big.NewInt(int64(G)) 54 | 55 | B := big.NewInt(0) 56 | B.Mul(v, big.NewInt(3)) 57 | B.Add(B, g.Exp(g, b, N())) 58 | B.Mod(B, N()) 59 | return B 60 | } 61 | 62 | // GenerateEphemeralPair generates a (private, public) ephemeral pair given a user's 63 | // verifier. 64 | func GenerateEphemeralPair(v *big.Int) (*big.Int, *big.Int) { 65 | // TODO(jeshua): Make this a random number. 66 | b := big.NewInt(0) 67 | b.SetString("3679141816495610969398422835318306156547245306", 10) 68 | 69 | return b, GeneratePublicEphemeral(v, b) 70 | } 71 | 72 | func padBigIntBytes(data []byte, nBytes int) []byte { 73 | if len(data) > nBytes { 74 | return data[:nBytes] 75 | } 76 | 77 | currSize := len(data) 78 | for i := 0; i < nBytes-currSize; i++ { 79 | data = append([]byte{'\x00'}, data...) 80 | } 81 | 82 | return data 83 | } 84 | 85 | func interleave(S *big.Int) *big.Int { 86 | T := padBigIntBytes(reverse(S.Bytes()), 32) 87 | 88 | G := make([]byte, 16) 89 | H := make([]byte, 16) 90 | for i := 0; i < 16; i++ { 91 | G[i] = T[i*2] 92 | H[i] = T[i*2+1] 93 | } 94 | 95 | G = reverse(_H(reverse(G))) 96 | H = reverse(_H(reverse(H))) 97 | 98 | K := make([]byte, 0) 99 | for i := 0; i < 20; i++ { 100 | K = append(K, G[i], H[i]) 101 | } 102 | 103 | KInt := big.NewInt(0) 104 | KInt.SetBytes(reverse(K)) 105 | return KInt 106 | } 107 | 108 | func reverse(data []byte) []byte { 109 | for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { 110 | data[i], data[j] = data[j], data[i] 111 | } 112 | 113 | return data 114 | } 115 | 116 | // CalculateSessionKey takes as input the client's proof and calculates the 117 | // persistent session key. 118 | func CalculateSessionKey(A, B, b, v, s *big.Int, accountName string) (*big.Int, *big.Int) { 119 | u := big.NewInt(0) 120 | u.SetBytes(_H(A.Bytes(), B.Bytes())) 121 | 122 | S := big.NewInt(0) 123 | S.Exp(v, u, N()) 124 | S.Mul(S, A) 125 | S.Exp(S, b, N()) 126 | 127 | K := interleave(S) 128 | 129 | NHash := big.NewInt(0) 130 | NHash.SetBytes(_H(N().Bytes())) 131 | 132 | gHash := big.NewInt(0) 133 | gHash.SetBytes(_H(big.NewInt(int64(G)).Bytes())) 134 | gHash.Xor(gHash, NHash) 135 | 136 | M := big.NewInt(0) 137 | M.SetBytes(_H(gHash.Bytes(), _H(reverse([]byte(accountName))), s.Bytes(), A.Bytes(), B.Bytes(), K.Bytes())) 138 | return K, M 139 | } 140 | 141 | // CalculateServerProof will calculate a proof to send back to the client so they 142 | // know we are a legit server. 143 | func CalculateServerProof(A, M, K *big.Int) *big.Int { 144 | proof := big.NewInt(0) 145 | proof.SetBytes(_H(A.Bytes(), M.Bytes(), K.Bytes())) 146 | return proof 147 | } 148 | -------------------------------------------------------------------------------- /server/auth/packet/login_challenge.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "io" 7 | "math/big" 8 | "strings" 9 | 10 | "github.com/jeshuamorrissey/wow_server_go/lib/util" 11 | "github.com/jeshuamorrissey/wow_server_go/server/auth/data/static" 12 | "github.com/jeshuamorrissey/wow_server_go/server/auth/session" 13 | "github.com/jeshuamorrissey/wow_server_go/server/auth/srp" 14 | ) 15 | 16 | // ClientLoginChallenge encodes information about a new connection to the 17 | // login server. 18 | type ClientLoginChallenge struct { 19 | GameName [4]byte 20 | Version [3]uint8 21 | Build uint16 22 | Platform [4]byte 23 | OS [4]byte 24 | Locale [4]byte 25 | TimezoneOffset uint32 26 | IPAddress uint32 27 | AccountName []byte 28 | } 29 | 30 | // Read will load a ClientLoginChallenge packet from a buffer. 31 | // An error will be returned if at least one of the fields didn't load correctly. 32 | func (pkt *ClientLoginChallenge) FromBytes(state *session.State, buffer io.Reader) error { 33 | binary.Read(buffer, binary.LittleEndian, &pkt.GameName) 34 | binary.Read(buffer, binary.LittleEndian, &pkt.Version) 35 | binary.Read(buffer, binary.LittleEndian, &pkt.Build) 36 | binary.Read(buffer, binary.LittleEndian, &pkt.Platform) 37 | binary.Read(buffer, binary.LittleEndian, &pkt.OS) 38 | binary.Read(buffer, binary.LittleEndian, &pkt.Locale) 39 | binary.Read(buffer, binary.LittleEndian, &pkt.TimezoneOffset) 40 | binary.Read(buffer, binary.BigEndian, &pkt.IPAddress) 41 | 42 | var accountNameLen uint8 43 | binary.Read(buffer, binary.LittleEndian, &accountNameLen) 44 | 45 | pkt.AccountName = make([]byte, accountNameLen) 46 | return binary.Read(buffer, binary.LittleEndian, &pkt.AccountName) 47 | } 48 | 49 | // OpCode gets the opcode of the packet. 50 | func (*ClientLoginChallenge) OpCode() static.OpCode { 51 | return static.OpCodeLoginChallenge 52 | } 53 | 54 | // ServerLoginChallenge is the server's response to a client's challenge. It contains 55 | // some SRP information used for handshaking. 56 | type ServerLoginChallenge struct { 57 | Error static.LoginErrorCode 58 | B big.Int 59 | Salt big.Int 60 | SaltCRC big.Int 61 | } 62 | 63 | // Bytes writes out the packet to an array of bytes. 64 | func (pkt *ServerLoginChallenge) ToBytes(state *session.State) ([]byte, error) { 65 | buffer := bytes.NewBufferString("") 66 | 67 | buffer.WriteByte(0) // unk1 68 | buffer.WriteByte(uint8(pkt.Error)) 69 | 70 | if pkt.Error == 0 { 71 | buffer.Write(util.PadBigIntBytes(util.ReverseBytes(pkt.B.Bytes()), 32)) 72 | buffer.WriteByte(1) 73 | buffer.WriteByte(srp.G) 74 | buffer.WriteByte(32) 75 | buffer.Write(util.ReverseBytes(srp.N().Bytes())) 76 | buffer.Write(util.PadBigIntBytes(util.ReverseBytes(pkt.Salt.Bytes()), 32)) 77 | buffer.Write(util.PadBigIntBytes(util.ReverseBytes(pkt.SaltCRC.Bytes()), 16)) 78 | buffer.WriteByte(0) // unk2 79 | } 80 | 81 | return buffer.Bytes(), nil 82 | } 83 | 84 | // OpCode gets the opcode of the packet. 85 | func (*ServerLoginChallenge) OpCode() static.OpCode { 86 | return static.OpCodeLoginChallenge 87 | } 88 | 89 | // Handle will check the database for the account and send an appropriate response. 90 | func (pkt *ClientLoginChallenge) Handle(state *session.State) ([]session.ServerPacket, error) { 91 | response := new(ServerLoginChallenge) 92 | response.Error = static.LoginOK 93 | 94 | // Validate the packet. 95 | gameName := strings.TrimRight(string(pkt.GameName[:]), "\x00") 96 | if gameName != static.SupportedGameName { 97 | response.Error = static.LoginFailed 98 | } else if pkt.Version != static.SupportedGameVersion || pkt.Build != static.SupportedGameBuild { 99 | response.Error = static.LoginBadVersion 100 | } else { 101 | for _, account := range state.Config.Accounts { 102 | if strings.ToLower(account.Name) == strings.ToLower(string(pkt.AccountName)) { 103 | state.Account = account 104 | break 105 | } 106 | } 107 | 108 | if state.Account == nil { 109 | response.Error = static.LoginUnknownAccount 110 | } 111 | } 112 | 113 | if response.Error == static.LoginOK { 114 | b, B := srp.GenerateEphemeralPair(state.Account.Verifier()) 115 | state.PrivateEphemeral.Set(b) 116 | state.PublicEphemeral.Set(B) 117 | 118 | response.B.Set(B) 119 | response.Salt.Set(state.Account.Salt()) 120 | response.SaltCRC.SetInt64(0) 121 | } 122 | 123 | return []session.ServerPacket{response}, nil 124 | } 125 | -------------------------------------------------------------------------------- /server/world/data/dynamic/item.go: -------------------------------------------------------------------------------- 1 | package dynamic 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/channels" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 10 | ) 11 | 12 | // Item represents an instance of an in-game item. 13 | type Item struct { 14 | GameObject 15 | 16 | // Basic information. 17 | Durability int 18 | StackCount int 19 | DurationRemaining *time.Time 20 | ChargesRemaining int 21 | 22 | // Flags. 23 | IsBound bool 24 | IsUnlocked bool 25 | IsWrapped bool 26 | IsReadable bool 27 | 28 | // Relationships. 29 | Owner interfaces.GUID 30 | Container interfaces.GUID 31 | Creator interfaces.GUID 32 | GiftCreator interfaces.GUID 33 | } 34 | 35 | // Object interface methods. 36 | func (i *Item) GUID() interfaces.GUID { return i.GameObject.GUID() } 37 | func (i *Item) SetGUID(guid interfaces.GUID) { i.GameObject.SetGUID(guid) } 38 | 39 | func (i *Item) GetLocation() *interfaces.Location { 40 | if container := GetObjectManager().Get(i.Container); container != nil { 41 | return container.GetLocation() 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (i *Item) StartUpdateLoop() { 48 | if i.UpdateChannel() != nil { 49 | return 50 | } 51 | 52 | i.CreateUpdateChannel() 53 | go func() { 54 | for { 55 | for _, update := range <-i.UpdateChannel() { 56 | switch update.(type) { 57 | default: 58 | } 59 | } 60 | 61 | channels.ObjectUpdates <- i.GUID() 62 | } 63 | }() 64 | } 65 | 66 | func (i *Item) UpdateFields() interfaces.UpdateFieldsMap { 67 | fields := interfaces.UpdateFieldsMap{ 68 | static.UpdateFieldItemOwner: uint32(i.Owner.Low()), 69 | static.UpdateFieldItemOwner + 1: uint32(i.Owner.High()), 70 | static.UpdateFieldItemContained: uint32(i.Container.Low()), 71 | static.UpdateFieldItemContained + 1: uint32(i.Container.High()), 72 | static.UpdateFieldItemCreator: uint32(i.Creator.Low()), 73 | static.UpdateFieldItemCreator + 1: uint32(i.Creator.High()), 74 | static.UpdateFieldItemGiftCreator: uint32(i.GiftCreator.Low()), 75 | static.UpdateFieldItemGiftCreator + 1: uint32(i.GiftCreator.High()), 76 | static.UpdateFieldItemStackCount: uint32(i.StackCount), 77 | static.UpdateFieldItemSpellCharges: uint32(i.ChargesRemaining), 78 | static.UpdateFieldItemFlags: uint32(i.flags()), 79 | // static.UpdateFieldItemEnchantmentID: uint32(0), // TODO 80 | // static.UpdateFieldItemEnchantmentDuration: uint32(0), // TODO 81 | // static.UpdateFieldItemEnchantmentCharges: uint32(0), // TODO 82 | // static.UpdateFieldItemPropertySeed: uint32(0), // TODO 83 | // static.UpdateFieldItemRandomPropertiesID: uint32(0), // TODO 84 | // static.UpdateFieldItemItemTextID: uint32(0), // TODO 85 | static.UpdateFieldItemDurability: uint32(i.Durability), 86 | static.UpdateFieldItemMaxDurability: uint32(i.GetTemplate().MaxDurability), 87 | } 88 | 89 | if i.DurationRemaining == nil { 90 | fields[static.UpdateFieldItemDuration] = uint32(0) 91 | } else { 92 | fields[static.UpdateFieldItemDuration] = uint32(i.DurationRemaining.Second()) 93 | } 94 | 95 | mergedFields := i.GameObject.UpdateFields() 96 | for k, v := range fields { 97 | mergedFields[k] = v 98 | } 99 | 100 | mergedFields[static.UpdateFieldType] = uint32(TypeMask(i)) 101 | 102 | return mergedFields 103 | } 104 | 105 | // Item interface methods. 106 | func (i *Item) GetTemplate() *static.Item { return static.Items[int(i.Entry)] } 107 | func (i *Item) GetContainer() interfaces.GUID { return i.Container } 108 | 109 | // Utility methods. 110 | // CalculateDamage determines the damage this item will do and returns a map. This includes 111 | // some randomness if the item has a damage range. 112 | func (i *Item) CalculateDamage() map[static.SpellSchool]int { 113 | damage := make(map[static.SpellSchool]int) 114 | for school, damages := range i.GetTemplate().Damages { 115 | damage[school] = damages.Min + rand.Intn(damages.Min+damages.Max+1) 116 | } 117 | 118 | return damage 119 | } 120 | 121 | func (i *Item) flags() int { 122 | var flags int 123 | if i.IsBound { 124 | flags |= int(static.ItemFlagBound) 125 | } 126 | 127 | if i.IsUnlocked { 128 | flags |= int(static.ItemFlagUnlocked) 129 | } 130 | 131 | if i.IsWrapped { 132 | flags |= int(static.ItemFlagWrapped) 133 | } 134 | 135 | if i.IsReadable { 136 | flags |= int(static.ItemFlagReadable) 137 | } 138 | 139 | return flags 140 | } 141 | -------------------------------------------------------------------------------- /server/world/data/dynamic/components/movement_info.go: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 8 | ) 9 | 10 | type Movement struct { 11 | interfaces.MovementInfo 12 | 13 | SpeedWalk float32 14 | SpeedRun float32 15 | SpeedRunBackward float32 16 | SpeedSwim float32 17 | SpeedSwimBackward float32 18 | SpeedTurn float32 19 | } 20 | 21 | // MovementUpdate returns a buffer which represents the movement update object for this unit. 22 | func (m *Movement) MovementUpdate() []byte { 23 | buffer := bytes.NewBufferString("") 24 | 25 | binary.Write(buffer, binary.LittleEndian, uint32(m.movementFlags())) 26 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // Time 27 | 28 | binary.Write(buffer, binary.LittleEndian, float32(m.Location.X)) 29 | binary.Write(buffer, binary.LittleEndian, float32(m.Location.Y)) 30 | binary.Write(buffer, binary.LittleEndian, float32(m.Location.Z)) 31 | binary.Write(buffer, binary.LittleEndian, float32(m.Location.O)) 32 | 33 | // TODO(jeshua): transport. 34 | // if GetObjectManager().Exists(m.Transport) { 35 | // transportObj := GetObjectManager().GetTransport(m.Transport) 36 | // binary.Write(buffer, binary.LittleEndian, uint64(m.Transport)) 37 | // binary.Write(buffer, binary.LittleEndian, float32(transportObj.Location.X)) 38 | // binary.Write(buffer, binary.LittleEndian, float32(transportObj.Location.Y)) 39 | // binary.Write(buffer, binary.LittleEndian, float32(transportObj.Location.Z)) 40 | // binary.Write(buffer, binary.LittleEndian, float32(transportObj.Location.O)) 41 | // binary.Write(buffer, binary.LittleEndian, uint32(0)) // Time 42 | // } 43 | 44 | // if m.IsSwimming { 45 | // binary.Write(buffer, binary.LittleEndian, float32(m.MovementInfo.Pitch)) 46 | // } 47 | 48 | // if !GetObjectManager().Exists(m.Transport) { 49 | binary.Write(buffer, binary.LittleEndian, uint32(0)) // LastFallTime 50 | // } 51 | 52 | // if m.IsFalling { 53 | // binary.Write(buffer, binary.LittleEndian, float32(m.MovementInfo.Jump.Velocity)) 54 | // binary.Write(buffer, binary.LittleEndian, float32(m.MovementInfo.Jump.SinAngle)) 55 | // binary.Write(buffer, binary.LittleEndian, float32(m.MovementInfo.Jump.CosAngle)) 56 | // binary.Write(buffer, binary.LittleEndian, float32(m.MovementInfo.Jump.XYSpeed)) 57 | // } 58 | 59 | // SplineElevation update goes HERE. 60 | 61 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedWalk)) 62 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedRun)) 63 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedRunBackward)) 64 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedSwim)) 65 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedSwimBackward)) 66 | binary.Write(buffer, binary.LittleEndian, float32(m.SpeedTurn)) 67 | 68 | // Spline update goes HERE. 69 | 70 | return buffer.Bytes() 71 | } 72 | 73 | func (m *Movement) movementFlags() uint32 { 74 | movementFlags := uint32(0) 75 | // if u.Is { 76 | // movementFlags |= uint32(static.MovementFlagForward) 77 | // } 78 | // if u.Is { 79 | // movementFlags |= uint32(static.MovementFlagBackward) 80 | // } 81 | // if u.Is { 82 | // movementFlags |= uint32(static.MovementFlagStrafeLeft) 83 | // } 84 | // if u.Is { 85 | // movementFlags |= uint32(static.MovementFlagStrafeRight) 86 | // } 87 | // if u.Is { 88 | // movementFlags |= uint32(static.MovementFlagTurnLeft) 89 | // } 90 | // if u.Is { 91 | // movementFlags |= uint32(static.MovementFlagTurnRight) 92 | // } 93 | // if u.Is { 94 | // movementFlags |= uint32(static.MovementFlagPitchUp) 95 | // } 96 | // if u.Is { 97 | // movementFlags |= uint32(static.MovementFlagPitchDown) 98 | // } 99 | // if u.Is { 100 | // movementFlags |= uint32(static.MovementFlagWalkMode) 101 | // } 102 | // if u.Is { 103 | // movementFlags |= uint32(static.MovementFlagLevitating) 104 | // } 105 | // if u.Is { 106 | // movementFlags |= uint32(static.MovementFlagFlying) 107 | // } 108 | // if u.IsFalling { 109 | // movementFlags |= uint32(static.MovementFlagFalling) 110 | // } 111 | // if u.Is { 112 | // movementFlags |= uint32(static.MovementFlagFallingFar) 113 | // } 114 | // if u.IsSwimming { 115 | // movementFlags |= uint32(static.MovementFlagSwimming) 116 | // } 117 | // if u.Is { 118 | // movementFlags |= uint32(static.MovementFlagSplineEnabled) 119 | // } 120 | // if u.Is { 121 | // movementFlags |= uint32(static.MovementFlagCanFly) 122 | // } 123 | // if u.Is { 124 | // movementFlags |= uint32(static.MovementFlagFlyingOld) 125 | // } 126 | // if u.Is { 127 | // movementFlags |= uint32(static.MovementFlagOnTransport) 128 | // } 129 | // if u.Is { 130 | // movementFlags |= uint32(static.MovementFlagSplineElevation) 131 | // } 132 | // if u.Is { 133 | // movementFlags |= uint32(static.MovementFlagRoot) 134 | // } 135 | // if u.Is { 136 | // movementFlags |= uint32(static.MovementFlagWaterWalking) 137 | // } 138 | // if u.Is { 139 | // movementFlags |= uint32(static.MovementFlagSafeFall) 140 | // } 141 | // if u.Is { 142 | // movementFlags |= uint32(static.MovementFlagHover) 143 | // } 144 | return movementFlags 145 | } 146 | -------------------------------------------------------------------------------- /tools/dbc_utils/dbc/record_fields.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Iterable, Text, Optional, Type, Tuple 2 | 3 | 4 | class Field: 5 | def __init__(self, name: Text = None, default: Optional[Any] = None, indexed: bool = False, foreign_key: Optional[Tuple[Type, Text]] = None): 6 | self.name = name 7 | self.default = default 8 | self.indexed = indexed 9 | 10 | self.foreign_key_field = None 11 | self.foreign_key_type = None 12 | if foreign_key: 13 | record_type, field_name = foreign_key 14 | for field in record_type.Fields(): 15 | if field.name == field_name: 16 | self.foreign_key_field = field 17 | self.foreign_key_type = record_type.GoTypeName() 18 | 19 | def Value(self, string_block, record): 20 | raise NotImplementedError() 21 | 22 | def _GetValue(self, record): 23 | if self.name is not None and hasattr(record, self.name): 24 | return getattr(record, self.name) 25 | if self.default is not None: 26 | if callable(self.default): 27 | return self.default(record) 28 | return self.default 29 | raise ValueError('Field {} in {} should have a name or a default value.'.format( 30 | self, type(record))) 31 | 32 | def GoName(self) -> Text: 33 | if not self.name: 34 | return None 35 | 36 | name = ''.join(x.title() for x in self.name.split('_')) 37 | name = name.replace('Id', 'ID') 38 | name = name.replace('_ID', 'ID') 39 | return name 40 | 41 | def GoType(self) -> Text: 42 | if self.foreign_key_type is not None: 43 | return '*{}'.format(self.foreign_key_type) 44 | 45 | return None 46 | 47 | @classmethod 48 | def Format(cls) -> Text: 49 | raise NotImplementedError() 50 | 51 | @classmethod 52 | def Load(cls, string_block, args: Iterable[Any]): 53 | raise NotImplementedError() 54 | 55 | 56 | class Byte(Field): 57 | def Value(self, string_block, record): 58 | return [super(Byte, self)._GetValue(record)] 59 | 60 | def GoType(self) -> Text: 61 | go_type = super(Byte, self).GoType() 62 | if go_type: 63 | return go_type 64 | 65 | return 'uint8' 66 | 67 | @classmethod 68 | def Format(cls) -> Text: 69 | return 'B' 70 | 71 | @classmethod 72 | def Load(cls, string_block, args: Iterable[Any]): 73 | return next(args) 74 | 75 | 76 | class Int(Field): 77 | def Value(self, string_block, record): 78 | return [super(Int, self)._GetValue(record)] 79 | 80 | def GoType(self) -> Text: 81 | go_type = super(Int, self).GoType() 82 | if go_type: 83 | return go_type 84 | 85 | return 'int' 86 | 87 | @classmethod 88 | def Format(cls) -> Text: 89 | return 'I' 90 | 91 | @classmethod 92 | def Load(cls, string_block, args: Iterable[Any]): 93 | return next(args) 94 | 95 | 96 | class ID(Int): 97 | def __init__(self, name: Text = None, default: Optional[Any] = None, indexed: bool = False): 98 | super(ID, self).__init__(name='_id', default=default, indexed=indexed) 99 | 100 | 101 | class Float(Field): 102 | def GoType(self) -> Text: 103 | go_type = super(Float, self).GoType() 104 | if go_type: 105 | return go_type 106 | 107 | return 'float32' 108 | 109 | def Value(self, string_block, record): 110 | return [super(Float, self)._GetValue(record)] 111 | 112 | @classmethod 113 | def Format(cls) -> Text: 114 | return 'f' 115 | 116 | @classmethod 117 | def Load(cls, string_block, args: Iterable[Any]): 118 | return next(args) 119 | 120 | 121 | class String(Field): 122 | def Value(self, string_block, record): 123 | return [string_block.OffsetFor(self._GetValue(record))] 124 | 125 | def GoType(self) -> Text: 126 | go_type = super(String, self).GoType() 127 | if go_type: 128 | return go_type 129 | 130 | return 'string' 131 | 132 | @classmethod 133 | def Format(cls) -> Text: 134 | return 'I' 135 | 136 | @classmethod 137 | def Load(cls, string_block, args: Iterable[Any]): 138 | return string_block[next(args)] 139 | 140 | 141 | class LocalizedString(Field): 142 | def Value(self, string_block, record): 143 | return [ 144 | string_block.OffsetFor(self._GetValue(record)), # enUS 145 | 0, 0, 0, 0, 0, 0, 0, # other locales, unused 146 | 0, # flags, unused 147 | ] 148 | 149 | def GoType(self) -> Text: 150 | go_type = super(LocalizedString, self).GoType() 151 | if go_type: 152 | return go_type 153 | 154 | return 'string' 155 | 156 | @classmethod 157 | def Format(cls) -> Text: 158 | return 'IIIIIIIII' 159 | 160 | @classmethod 161 | def Load(cls, string_block, args: Iterable[Any]): 162 | locales = dict( 163 | enUS=next(args), 164 | koKR=next(args), # unused 165 | frFR=next(args), # unused 166 | deDE=next(args), # unused 167 | enCN=next(args), # unused 168 | enTW=next(args), # unused 169 | esES=next(args), # unused 170 | esMX=next(args), # unused 171 | ) 172 | 173 | next(args) # flags, unused 174 | 175 | return string_block[locales['enUS']] 176 | -------------------------------------------------------------------------------- /server/world/world_server.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | 7 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/system" 11 | 12 | "github.com/sirupsen/logrus" 13 | 14 | "github.com/jeshuamorrissey/wow_server_go/server/world/packet" 15 | ) 16 | 17 | var opCodeToPacket = map[static.OpCode]func() interfaces.ClientPacket{ 18 | static.OpCodeClientAuthSession: func() interfaces.ClientPacket { return new(packet.ClientAuthSession) }, 19 | static.OpCodeClientCharCreate: func() interfaces.ClientPacket { return new(packet.ClientCharCreate) }, 20 | static.OpCodeClientCharDelete: func() interfaces.ClientPacket { return new(packet.ClientCharDelete) }, 21 | static.OpCodeClientCharEnum: func() interfaces.ClientPacket { return new(packet.ClientCharEnum) }, 22 | static.OpCodeClientCreatureQuery: func() interfaces.ClientPacket { return new(packet.ClientCreatureQuery) }, 23 | static.OpCodeClientItemQuerySingle: func() interfaces.ClientPacket { return new(packet.ClientItemQuerySingle) }, 24 | static.OpCodeClientLogoutRequest: func() interfaces.ClientPacket { return new(packet.ClientLogoutRequest) }, 25 | static.OpCodeClientNameQuery: func() interfaces.ClientPacket { return new(packet.ClientNameQuery) }, 26 | static.OpCodeClientPing: func() interfaces.ClientPacket { return new(packet.ClientPing) }, 27 | static.OpCodeClientPlayerLogin: func() interfaces.ClientPacket { return new(packet.ClientPlayerLogin) }, 28 | static.OpCodeClientQueryTime: func() interfaces.ClientPacket { return new(packet.ClientQueryTime) }, 29 | static.OpCodeClientSetActiveMover: func() interfaces.ClientPacket { return new(packet.ClientSetActiveMover) }, 30 | static.OpCodeClientStandstatechange: func() interfaces.ClientPacket { return new(packet.ClientStandStateChange) }, 31 | static.OpCodeClientTutorialFlag: func() interfaces.ClientPacket { return new(packet.ClientTutorialFlag) }, 32 | static.OpCodeClientUpdateAccountData: func() interfaces.ClientPacket { return new(packet.ClientUpdateAccountData) }, 33 | static.OpCodeClientAttackswing: func() interfaces.ClientPacket { return new(packet.ClientAttackSwing) }, 34 | static.OpCodeClientAttackstop: func() interfaces.ClientPacket { return new(packet.ClientAttackStop) }, 35 | 36 | // Movement packets have the same receiver. 37 | static.OpCodeClientMoveHeartbeat: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveHeartbeat) }, 38 | static.OpCodeClientMoveSetFacing: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveSetFacing) }, 39 | static.OpCodeClientMoveStartBackward: func() interfaces.ClientPacket { 40 | return packet.NewClientMovePacket(static.OpCodeClientMoveStartBackward) 41 | }, 42 | static.OpCodeClientMoveStartForward: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveStartForward) }, 43 | static.OpCodeClientMoveStartStrafeLeft: func() interfaces.ClientPacket { 44 | return packet.NewClientMovePacket(static.OpCodeClientMoveStartStrafeLeft) 45 | }, 46 | static.OpCodeClientMoveStartStrafeRight: func() interfaces.ClientPacket { 47 | return packet.NewClientMovePacket(static.OpCodeClientMoveStartStrafeRight) 48 | }, 49 | static.OpCodeClientMoveStartTurnLeft: func() interfaces.ClientPacket { 50 | return packet.NewClientMovePacket(static.OpCodeClientMoveStartTurnLeft) 51 | }, 52 | static.OpCodeClientMoveStartTurnRight: func() interfaces.ClientPacket { 53 | return packet.NewClientMovePacket(static.OpCodeClientMoveStartTurnRight) 54 | }, 55 | static.OpCodeClientMoveStop: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveStop) }, 56 | static.OpCodeClientMoveStopStrafe: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveStopStrafe) }, 57 | static.OpCodeClientMoveStopTurn: func() interfaces.ClientPacket { return packet.NewClientMovePacket(static.OpCodeClientMoveStopTurn) }, 58 | } 59 | 60 | func setupSession(sess *system.Session) { 61 | pkt := packet.ServerAuthChallenge{Seed: 0} 62 | sess.Send(&pkt) 63 | } 64 | 65 | // RunWorldServer takes as input a database and runs an world server referencing 66 | // it. 67 | func RunWorldServer(realmName string, port int, config *config.Config) { 68 | log := logrus.WithFields(logrus.Fields{"server": "WORLD", "port": port}) 69 | log.Logger.SetLevel(logrus.TraceLevel) 70 | 71 | // Start updater. 72 | updater := system.NewUpdater(log, config.ObjectManager) 73 | go updater.Run() 74 | 75 | // Start session handler. 76 | listener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(port)) 77 | if err != nil { 78 | log.Fatalf("Error while opening port: %v\n", err) 79 | } 80 | 81 | log.Infof("Listening for WORLD connections on :%v...", listener.Addr().String()) 82 | 83 | for { 84 | conn, err := listener.Accept() 85 | if err != nil { 86 | log.Fatalf("Error while receiving client connection: %v\n", err) 87 | } 88 | 89 | log.Printf("Receiving WORLD connection from %v\n", conn.RemoteAddr()) 90 | sess := system.NewSession( 91 | conn, 92 | conn, 93 | opCodeToPacket, 94 | opCodeToHandler, 95 | config, 96 | logrus.WithFields(logrus.Fields{"server": "WORLD", "account": "???"}), 97 | updater, 98 | ) 99 | setupSession(sess) 100 | go sess.Run() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tools/new_game/initial_data/initial_data.go: -------------------------------------------------------------------------------- 1 | package initial_data 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jeshuamorrissey/wow_server_go/lib/config" 7 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic" 8 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/components" 9 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/dynamic/interfaces" 10 | "github.com/jeshuamorrissey/wow_server_go/server/world/data/static" 11 | ) 12 | 13 | // NewCharacter creates a new character entry in the database and 14 | // returns a pointer to it. 15 | func NewCharacter( 16 | cfg *config.Config, 17 | name string, 18 | race *static.Race, class *static.Class, gender static.Gender, 19 | skinColor, face, hairStyle, hairColor, feature uint8) (*config.Character, error) { 20 | startingEquipment, startingItems := static.GetStartingItems(class, race) 21 | 22 | equipment := make(map[static.EquipmentSlot]interfaces.GUID) 23 | for slot, item := range startingEquipment { 24 | itemObj := &dynamic.Item{ 25 | GameObject: dynamic.GameObject{ 26 | Entry: uint32(item.Entry), 27 | ScaleX: 1.0, 28 | }, 29 | 30 | Durability: item.MaxDurability, 31 | } 32 | 33 | cfg.ObjectManager.Add(itemObj) 34 | equipment[slot] = itemObj.GUID() 35 | } 36 | 37 | inventory := make(map[int]interfaces.GUID) 38 | for i, item := range startingItems { 39 | itemObj := &dynamic.Item{ 40 | GameObject: dynamic.GameObject{ 41 | Entry: uint32(item.Entry), 42 | ScaleX: 1.0, 43 | }, 44 | 45 | Durability: item.MaxDurability, 46 | } 47 | 48 | cfg.ObjectManager.Add(itemObj) 49 | inventory[i] = itemObj.GUID() 50 | } 51 | 52 | startingLocation := static.StartingLocationsByIndex[race] 53 | startingStats := static.StartingStatsByIndex[class][race] 54 | 55 | charObj := dynamic.InitializePlayer(&dynamic.Player{ 56 | GameObject: dynamic.GameObject{ 57 | Entry: 0, 58 | ScaleX: static.GetPlayerScale(race, gender), 59 | }, 60 | 61 | Movement: components.Movement{ 62 | MovementInfo: interfaces.MovementInfo{ 63 | Location: interfaces.Location{ 64 | X: startingLocation.X, 65 | Y: startingLocation.Y, 66 | Z: startingLocation.Z, 67 | O: startingLocation.O, 68 | }, 69 | }, 70 | 71 | SpeedWalk: 2.5, 72 | SpeedRun: 7.0, 73 | SpeedRunBackward: 4.5, 74 | SpeedSwim: 4.72, 75 | SpeedSwimBackward: 2.5, 76 | SpeedTurn: 3.14159, 77 | }, 78 | 79 | Unit: components.Unit{ 80 | Level: 1, 81 | Race: race, 82 | Class: class, 83 | Gender: gender, 84 | }, 85 | 86 | BasicStats: components.BasicStats{ 87 | Strength: startingStats.Strength, 88 | Agility: startingStats.Agility, 89 | Stamina: startingStats.Stamina, 90 | Intellect: startingStats.Intellect, 91 | Spirit: startingStats.Spirit, 92 | }, 93 | 94 | PlayerFeatures: components.PlayerFeatures{ 95 | SkinColor: int(skinColor), 96 | Face: int(face), 97 | HairStyle: int(hairStyle), 98 | HairColor: int(hairColor), 99 | Feature: int(feature), 100 | }, 101 | 102 | Player: components.Player{ 103 | Money: 10000, 104 | }, 105 | 106 | ZoneID: startingLocation.Zone, 107 | MapID: startingLocation.Map, 108 | 109 | Equipment: equipment, 110 | Inventory: inventory, 111 | }) 112 | 113 | cfg.ObjectManager.Add(charObj) 114 | for _, guid := range equipment { 115 | if cfg.ObjectManager.Exists(guid) { 116 | cfg.ObjectManager.GetItem(guid).Owner = charObj.GUID() 117 | cfg.ObjectManager.GetItem(guid).Container = charObj.GUID() 118 | } 119 | } 120 | 121 | for _, guid := range inventory { 122 | if cfg.ObjectManager.Exists(guid) { 123 | cfg.ObjectManager.GetItem(guid).Owner = charObj.GUID() 124 | cfg.ObjectManager.GetItem(guid).Container = charObj.GUID() 125 | } 126 | } 127 | 128 | return &config.Character{ 129 | Name: name, 130 | GUID: charObj.GUID(), 131 | }, nil 132 | } 133 | 134 | func PopulateWorld(cfg *config.Config) error { 135 | cfg.ObjectManager.Add(dynamic.InitializeUnit(&dynamic.Unit{ 136 | GameObject: dynamic.GameObject{ 137 | Entry: uint32(static.UnitsByName["The Man"].Entry), 138 | ScaleX: 1.0, 139 | }, 140 | 141 | Unit: components.Unit{ 142 | Level: 1, 143 | Race: static.RaceHuman, 144 | Class: static.ClassRogue, 145 | Gender: static.GenderMale, 146 | }, 147 | 148 | Movement: components.Movement{ 149 | MovementInfo: interfaces.MovementInfo{ 150 | Location: interfaces.Location{ 151 | X: -8945.95, 152 | Y: -132.493, 153 | Z: 83.5312, 154 | O: 180.0, 155 | }, 156 | }, 157 | 158 | SpeedWalk: 2.5, 159 | SpeedRun: 7.0, 160 | SpeedRunBackward: 4.5, 161 | SpeedSwim: 4.72, 162 | SpeedSwimBackward: 2.5, 163 | SpeedTurn: 3.14159, 164 | }, 165 | 166 | RespawnTimeMS: 1000 * time.Millisecond, 167 | })) 168 | 169 | // cfg.ObjectManager.Add(dynamic.InitializeUnit(&dynamic.Unit{ 170 | // GameObject: dynamic.GameObject{ 171 | // Entry: uint32(static.UnitsByName["The Man"].Entry), 172 | // ScaleX: 1.0, 173 | // }, 174 | 175 | // Unit: components.Unit{ 176 | // Level: 1, 177 | // Race: static.RaceHuman, 178 | // Class: static.ClassRogue, 179 | // Gender: static.GenderMale, 180 | // }, 181 | 182 | // Movement: components.Movement{ 183 | // MovementInfo: interfaces.MovementInfo{ 184 | // Location: interfaces.Location{ 185 | // X: -8942.95, 186 | // Y: -132.493, 187 | // Z: 83.5312, 188 | // O: 180.0, 189 | // }, 190 | // }, 191 | 192 | // SpeedWalk: 2.5, 193 | // SpeedRun: 7.0, 194 | // SpeedRunBackward: 4.5, 195 | // SpeedSwim: 4.72, 196 | // SpeedSwimBackward: 2.5, 197 | // SpeedTurn: 3.14159, 198 | // }, 199 | // })) 200 | 201 | return nil 202 | } 203 | --------------------------------------------------------------------------------