From 5e277defb7bc776a43c32b7318a4a853d2a80d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E5=BF=B5?= <1@72wo.cn> Date: Fri, 15 Aug 2025 22:44:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(map):=20=E5=AE=9E=E7=8E=B0=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E5=8A=A0=E8=BD=BD=E5=92=8C=E7=8E=A9=E5=AE=B6=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E5=9C=B0=E5=9B=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 Player 结构中添加 MapId 字段,用于记录当前所在地图 ID - 新增地图配置解析功能,支持从 XML 文件中读取地图信息 - 实现玩家进入地图的逻辑,包括设置玩家位置和广播通知 - 更新登录逻辑,在玩家登录时自动进入默认地图 - 重构地图相关的数据结构和接口,为后续地图功能扩展做准备 --- common/data/entity/player.go | 1 + common/data/xml/map.go | 122 ++++++++++++++++++ common/data/xml/monster_refresh.go | 26 ---- common/data/xml/monster_refresh_test.go | 25 +--- logic/controller/login.go | 2 + logic/controller/mapin.go | 12 +- logic/controller/mapout.go | 12 ++ logic/controller/walk.go | 9 +- logic/server.go | 4 +- logic/service/mapin/mapin.go | 19 --- .../{mapin/mapout.go => maps/mapin.go} | 32 ++++- logic/service/maps/mapout.go | 12 ++ logic/service/space/space.go | 71 ++++++++++ logic/service/space/walk.go | 76 +++++++++++ logic/service/walk/walk.go | 33 ----- 15 files changed, 348 insertions(+), 108 deletions(-) delete mode 100644 common/data/xml/monster_refresh.go create mode 100644 logic/controller/mapout.go delete mode 100644 logic/service/mapin/mapin.go rename logic/service/{mapin/mapout.go => maps/mapin.go} (82%) create mode 100644 logic/service/maps/mapout.go create mode 100644 logic/service/space/space.go create mode 100644 logic/service/space/walk.go delete mode 100644 logic/service/walk/walk.go diff --git a/common/data/entity/player.go b/common/data/entity/player.go index 438ce15ab..822eb1090 100644 --- a/common/data/entity/player.go +++ b/common/data/entity/player.go @@ -14,6 +14,7 @@ type Player struct { UserID uint32 //用户ID IsLogin bool //是否登录 mu sync.Mutex + MapId uint32 //当前所在的地图ID loginChan chan struct{} // 登录完成通知通道 context.Context diff --git a/common/data/xml/map.go b/common/data/xml/map.go index 9b26c594e..4db176438 100644 --- a/common/data/xml/map.go +++ b/common/data/xml/map.go @@ -1 +1,123 @@ package xml + +import ( + "fmt" + "io" + "log" + "net/http" + "time" + + "github.com/ECUST-XX/xml" +) + +// Maps 根节点,对应标签 +type Maps struct { + XMLName xml.Name `xml:"Maps"` + Maps []Map `xml:"map"` // 包含所有地图配置 +} + +// Map 单张地图配置,对应标签 +type Map struct { + ID int `xml:"id,attr"` // 地图ID + Name string `xml:"name,attr"` // 地图名称 + X int `xml:"x,attr"` // 地图X坐标 + Y int `xml:"y,attr"` // 地图Y坐标 + Des string `xml:"des,attr,omitempty"` // 地图描述(可选) + Super int `xml:"super,attr,omitempty"` // 上级地图ID(可选) + IsFB int `xml:"isFB,attr,omitempty"` // 是否为副本(0/1,可选) + IsLocal int `xml:"isLocal,attr,omitempty"` // 是否为本地地图(0/1,可选) + Sound string `xml:"sound,attr,omitempty"` // 背景音乐(可选) + ReplaceMapID int `xml:"replaceMapId,attr,omitempty"` // 替换地图ID(可选) + FX int `xml:"fx,attr,omitempty"` // 道具地面X坐标(可选) + FY int `xml:"fy,attr,omitempty"` // 道具地面Y坐标(可选) + WX int `xml:"wx,attr,omitempty"` // 道具墙面X坐标(可选) + WY int `xml:"wy,attr,omitempty"` // 道具墙面Y坐标(可选) + HX int `xml:"hx,attr,omitempty"` // 总部主电脑X坐标(可选) + HY int `xml:"hy,attr,omitempty"` // 总部主电脑Y坐标(可选) + + Entries Entries `xml:"Entries,omitempty"` // 进入点配置(可选) + ChangeMapComp ComponentList `xml:"changeMapComp,omitempty"` // 场景切换组件(可选) + FunComp ComponentList `xml:"funComp,omitempty"` // 点击触发组件(可选) + AutoComp ComponentList `xml:"autoComp,omitempty"` // 自动触发组件(可选) +} + +// Entries 进入点集合,对应标签 +type Entries struct { + Entries []Entry `xml:"Entry"` // 多个进入点 +} + +// Entry 单个进入点配置,对应标签 +type Entry struct { + FromMap int `xml:"FromMap,attr"` // 来源地图ID + PosX int `xml:"PosX,attr"` // 进入后X坐标 + PosY int `xml:"PosY,attr"` // 进入后Y坐标 +} + +// ComponentList 组件集合(用于统一管理不同类型的组件列表) +type ComponentList struct { + Components []Component `xml:"component"` // 多个组件 +} + +// Component 单个功能组件配置,对应标签 +type Component struct { + Name string `xml:"name,attr,omitempty"` // 组件名称(可选) + Hit string `xml:"hit,attr,omitempty"` // 碰撞区域标识(可选) + TargetID int `xml:"targetID,attr,omitempty"` // 目标地图ID(可选) + Dir int `xml:"dir,attr,omitempty"` // 滚动方向(1-4,可选) + Fun string `xml:"fun,attr,omitempty"` // 触发函数名(可选) + Des string `xml:"des,attr,omitempty"` // 鼠标提示文本(可选) + IsStop int `xml:"isStop,attr,omitempty"` // 鼠标悬停是否跳帧(0/1,可选) +} + +// ReadHTTPFile 通过HTTP GET请求获取远程文件内容 +// url: 远程文件的URL地址 +// 返回文件内容字节流和可能的错误 +func ReadHTTPFile(url string) ([]byte, error) { + // 创建HTTP客户端并设置超时时间(避免无限等待) + client := &http.Client{ + Timeout: 30 * time.Second, // 30秒超时 + } + + // 发送GET请求 + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + defer resp.Body.Close() // 确保响应体被关闭 + + // 检查响应状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("请求返回非成功状态码: %d", resp.StatusCode) + } + + // 读取响应体内容 + content, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("读取内容失败: %w", err) + } + + return content, nil +} +func getxml() ([]byte, error) { + + // 读取整个文件内容,返回字节切片和错误 + content, err := ReadHTTPFile("http://127.0.0.1:8080/assets/map_info.xml") + if err != nil { + // 处理错误(文件不存在、权限问题等) + log.Fatalf("无法读取文件: %v", err) + } + return content, nil + +} +func getMaps() Maps { + + // 解析XML到结构体 + var maps Maps + t1, _ := getxml() + xml.Unmarshal(t1, &maps) + + return maps +} + +// 全局函数配置 +var MapConfig = getMaps() diff --git a/common/data/xml/monster_refresh.go b/common/data/xml/monster_refresh.go deleted file mode 100644 index 24dd69880..000000000 --- a/common/data/xml/monster_refresh.go +++ /dev/null @@ -1,26 +0,0 @@ -package xml - -import ( - "github.com/ECUST-XX/xml" -) - -type xmls struct { - Text string `xml:",chardata"` - Version string `xml:"comment"` - - SuperMaps SuperMaps `xml:"superMaps"` -} -type SuperMaps struct { - XMLName xml.Name `xml:"superMaps"` - Text string `xml:",comment"` - Maps []Maps `xml:"maps"` -} - -type Maps struct { - Text string `xml:",chardata"` - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - X string `xml:"x,attr"` - Y string `xml:"y,attr"` - // Galaxy string `xml:"galaxy,attr"` -} diff --git a/common/data/xml/monster_refresh_test.go b/common/data/xml/monster_refresh_test.go index 13fec2016..f37f367be 100644 --- a/common/data/xml/monster_refresh_test.go +++ b/common/data/xml/monster_refresh_test.go @@ -48,25 +48,14 @@ var s = ` ` func Test_main(t *testing.T) { - tt := &SuperMaps{} - xml.Unmarshal([]byte(s), tt) - // tt.Maps = append(tt.Maps, Maps{ - // ID: "1", - // Name: "传送舱", - // // Galaxy: "1", - // X: "358", - // Y: "46", - // }) - // tt.Maps = append(tt.Maps, Maps{ - // ID: "4", - // Name: "船长室", - // // Galaxy: "1", - // X: "358", - // Y: "46", - // }) - tf, _ := xml.MarshalIndentShortForm(tt, " ", " ") - fmt.Println(string(tf)) + // 解析XML到结构体 + var maps Maps + t1, _ := getxml() + xml.Unmarshal(t1, &maps) + + //tf, _ := xml.MarshalIndentShortForm(tt, " ", " ") + fmt.Println(maps) } diff --git a/logic/controller/login.go b/logic/controller/login.go index 1f93a9217..d2a08454f 100644 --- a/logic/controller/login.go +++ b/logic/controller/login.go @@ -19,6 +19,8 @@ func (h *Controller) Login(data *login.InInfo, c *entity.Conn) (result *login.Ou share.ShareManager.SetUserOnline(data.Head.UserID, h.Port) //设置用户登录服务器 t.CompleteLogin() //通知客户端登录成功 + t.MapId = 1 + //c.SendPack(data.Def()) result = login.NewOutInfo() //设置登录消息 diff --git a/logic/controller/mapin.go b/logic/controller/mapin.go index dc02da1a5..71731d938 100644 --- a/logic/controller/mapin.go +++ b/logic/controller/mapin.go @@ -3,12 +3,16 @@ package controller import ( "blazing/common/data/entity" "blazing/common/socket/errorcode" - - "blazing/logic/service/mapin" + "blazing/logic/service/maps" + "blazing/logic/service/space" ) -// 处理命令: 1001 -func (h *Controller) InMap(data *mapin.InInfo, c *entity.Conn) (result *mapin.OutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 +func (h *Controller) MapIn(data *maps.InInfo, c *entity.Player) (result *maps.OutInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 + + c.MapId = data.MapId //登录地图 + space.GetPlanet(c.UserID).Set(c.UserID, c) //添加玩家 + result = &maps.OutInfo{UserID: c.UserID} //设置广播信息 + data.Broadcast(c.MapId, *result) //同步广播 return } diff --git a/logic/controller/mapout.go b/logic/controller/mapout.go new file mode 100644 index 000000000..63ad60a7f --- /dev/null +++ b/logic/controller/mapout.go @@ -0,0 +1,12 @@ +package controller + +import ( + "blazing/common/data/entity" + "blazing/common/socket/errorcode" + "blazing/logic/service/maps" +) + +func (h *Controller) MapOut(data *maps.LeaveMapInboundInfo, c *entity.Player) (result *maps.LeaveMapOutboundInfo, err errorcode.ErrorCode) { //这个时候player应该是空的 + result = &maps.LeaveMapOutboundInfo{UserID: c.GetUserID()} + return +} diff --git a/logic/controller/walk.go b/logic/controller/walk.go index 9d59a8373..3c18f9ace 100644 --- a/logic/controller/walk.go +++ b/logic/controller/walk.go @@ -3,14 +3,17 @@ package controller import ( "blazing/common/data/entity" "blazing/common/socket/errorcode" - "blazing/logic/service/walk" + "blazing/logic/service/space" ) -func (h Controller) Walk(data *walk.InInfo, c *entity.Player) (result *walk.OutInfo, err errorcode.ErrorCode) { +func (h Controller) Walk(data *space.InInfo, c *entity.Player) (result *space.OutInfo, err errorcode.ErrorCode) { - result = &walk.OutInfo{Flag: data.Flag, + result = &space.OutInfo{Flag: data.Flag, UserID: c.GetUserID(), Reserve2: data.Reverse2, Point: data.Point} + + data.Broadcast(c.MapId, *result) //走路的广播 + return } diff --git a/logic/server.go b/logic/server.go index 6302c9bdd..75c29a716 100644 --- a/logic/server.go +++ b/logic/server.go @@ -73,11 +73,9 @@ func Start(serverid uint16) { // go func() { t := rpc.StartClient(serverid, uint16(port), new(controller.LogicClient)) - //TODO 待实现掉线重新连接login + controller.Maincontroller.RPCClient = *t //将RPC赋值Start controller.Maincontroller.Port = uint16(port) //赋值服务器ID - //}() - //go rpc.StartClient(uint16(serverid), &controller.Maincontroller) service.NewLoginServiceService().SetServerID(serverid, gconv.Uint16(port), t) socket. diff --git a/logic/service/mapin/mapin.go b/logic/service/mapin/mapin.go deleted file mode 100644 index 88e481e14..000000000 --- a/logic/service/mapin/mapin.go +++ /dev/null @@ -1,19 +0,0 @@ -package mapin - -import ( - "blazing/common/socket/handler" - "blazing/modules/blazing/model" -) - -type InInfo struct { - Head handler.TomeeHeader `cmd:"2001" struc:"[0]pad"` //玩家登录 - // 地图类型 - mapType uint32 - - mapId uint32 - // Point: 直接给坐标x,y - Point model.Pos `fieldDesc:"直接给坐标x,y"` - - // Reverse2: 暂定 占位字符2 - Reverse2 string `struc:"[2]byte"` -} diff --git a/logic/service/mapin/mapout.go b/logic/service/maps/mapin.go similarity index 82% rename from logic/service/mapin/mapout.go rename to logic/service/maps/mapin.go index 6d0d42255..274085748 100644 --- a/logic/service/mapin/mapout.go +++ b/logic/service/maps/mapin.go @@ -1,13 +1,41 @@ -package mapin +package maps import ( + "blazing/common/data/entity" + "blazing/common/socket/handler" + "blazing/logic/service/space" "blazing/modules/blazing/model" ) +type InInfo struct { + Head handler.TomeeHeader `cmd:"2001" struc:"[0]pad"` //切换地图 + // 地图类型 + MapType uint32 + + MapId uint32 + // Point: 直接给坐标x,y + Point model.Pos `fieldDesc:"直接给坐标x,y"` + + // Reverse2: 暂定 占位字符2 + Reverse2 string `struc:"[2]byte"` +} + +func (t *InInfo) Broadcast(mapid uint32, o OutInfo) { + + space.GetPlanet(mapid).Range(func(playerID uint32, player *entity.Player) bool { + + player.SendPack(t.Head.Pack(o)) + return true + }) +} + +// 这里存储星球的map +//var planetmap utils.SyncMap[] //= space.NewSyncMap() + // PeopleInfo 对应Java的PeopleInfo类,实现OutboundMessage接口 type OutInfo struct { // 系统时间 - SystemTime uint64 `struc:"uint64" fieldDesc:"系统时间" json:"system_time"` + SystemTime uint32 `struc:"uint32" fieldDesc:"系统时间" json:"system_time"` // 米米号 UserID uint32 `struc:"uint32" fieldDesc:"米米号" json:"user_id"` diff --git a/logic/service/maps/mapout.go b/logic/service/maps/mapout.go new file mode 100644 index 000000000..f1be5e0c0 --- /dev/null +++ b/logic/service/maps/mapout.go @@ -0,0 +1,12 @@ +package maps + +import "blazing/common/socket/handler" + +type LeaveMapOutboundInfo struct { + // 米米号 + UserID uint32 `struc:"uint32" fieldDesc:"米米号" json:"user_id"` +} + +type LeaveMapInboundInfo struct { + Head handler.TomeeHeader `cmd:"2002" struc:"[0]pad"` //切换地图 +} diff --git a/logic/service/space/space.go b/logic/service/space/space.go new file mode 100644 index 000000000..470a5a072 --- /dev/null +++ b/logic/service/space/space.go @@ -0,0 +1,71 @@ +package space + +import ( + "blazing/common/data/entity" + "blazing/modules/blazing/model" + "sync" +) + +// Space 针对Player的并发安全map,键为uint32类型 +type Space struct { + mu sync.RWMutex // 读写锁,读多写少场景更高效 + data map[uint32]*entity.Player // 存储玩家数据的map,键为玩家ID + CanRefresh bool //是否能够刷怪 + ID uint32 // 地图ID + Name string //地图名称 + DefaultPos model.Pos //默认位置DefaultPos + Positions map[uint32]model.Pos //从上一个地图跳转后默认位置 +} + +// NewSyncMap 创建一个新的玩家同步map +func NewSpace() *Space { + return &Space{ + data: make(map[uint32]*entity.Player), + } +} + +// Get 根据玩家ID获取玩家实例 +// 读操作使用RLock,允许多个goroutine同时读取 +func (m *Space) Get(playerID uint32) (*entity.Player, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + val, exists := m.data[playerID] + return val, exists +} + +// Set 存储玩家实例(按ID) +// 写操作使用Lock,独占锁保证数据一致性 +func (m *Space) Set(playerID uint32, player *entity.Player) { + m.mu.Lock() + defer m.mu.Unlock() + m.data[playerID] = player +} + +// Delete 根据玩家ID删除玩家实例 +// 写操作使用Lock +func (m *Space) Delete(playerID uint32) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.data, playerID) +} + +// Len 获取当前玩家数量 +// 读操作使用RLock +func (m *Space) Len() int { + m.mu.RLock() + defer m.mu.RUnlock() + return len(m.data) +} + +// Range 遍历所有玩家并执行回调函数 +// 读操作使用RLock,遍历过程中不会阻塞其他读操作 +func (m *Space) Range(f func(playerID uint32, player *entity.Player) bool) { + m.mu.RLock() + defer m.mu.RUnlock() + for id, player := range m.data { + // 若回调返回false,则停止遍历 + if !f(id, player) { + break + } + } +} diff --git a/logic/service/space/walk.go b/logic/service/space/walk.go new file mode 100644 index 000000000..89b408b4f --- /dev/null +++ b/logic/service/space/walk.go @@ -0,0 +1,76 @@ +package space + +import ( + "blazing/common/data/entity" + "blazing/common/data/xml" + "blazing/common/socket/handler" + "blazing/common/utils" + "blazing/modules/blazing/model" +) + +// 获取星球 +func GetPlanet(id uint32) *Space { + planet, ok := planetmap.Load(id) + if ok { + return planet + } + + //如果不ok,说明星球未创建,那就新建星球 + + for _, v := range xml.MapConfig.Maps { + if v.ID == int(id) { //找到这个地图 + t := NewSpace() + t.DefaultPos = model.Pos{X: uint32(v.X), Y: uint32(v.Y)} + t.ID = uint32(v.ID) + t.Name = v.Name + t.Positions = make(map[uint32]model.Pos) + for _, v := range v.Entries.Entries { //添加地图入口 + t.Positions[uint32(v.FromMap)] = model.Pos{X: uint32(v.PosX), Y: uint32(v.PosY)} + + } + planetmap.Store(id, t) + return t + + } + + } + + return nil +} + +var planetmap = &utils.SyncMap[uint32, *Space]{} //玩家数据 +type InInfo struct { + Head handler.TomeeHeader `cmd:"2101" struc:"[0]pad"` //玩家登录 + // Flag: 0为走,1为飞行模式,对应Java的@UInt long + Flag uint32 + + // Point: 直接给坐标x,y + Point model.Pos `fieldDesc:"直接给坐标x,y"` + + // Reverse2: 暂定 占位字符2 + Reverse2 string `struc:"[2]byte"` +} + +func (t *InInfo) Broadcast(mapid uint32, o OutInfo) { + + GetPlanet(mapid).Range(func(playerID uint32, player *entity.Player) bool { + + player.SendPack(t.Head.Pack(o)) + return true + }) +} + +// PeopleWalkOutboundInfo 对应Java的PeopleWalkOutboundInfo类,实现OutboundMessage接口 +type OutInfo struct { + // Flag: 0为走,1为飞行模式 + Flag uint32 `fieldDesc:"0为走,1为飞行模式" codec:"uint"` + + // UserID: 走动的人的米米号 + UserID uint32 `fieldDesc:"走动的人的米米号" codec:"uint"` + + // Point: 直接给坐标x,y + Point model.Pos `fieldDesc:"直接给坐标x,y"` + + // Reserve2: 这个字段同C2S_People_Walk中的reserve2 + Reserve2 string `struc:"[2]byte"` +} diff --git a/logic/service/walk/walk.go b/logic/service/walk/walk.go deleted file mode 100644 index 87f2cff83..000000000 --- a/logic/service/walk/walk.go +++ /dev/null @@ -1,33 +0,0 @@ -package walk - -import ( - "blazing/common/socket/handler" - "blazing/modules/blazing/model" -) - -type InInfo struct { - Head handler.TomeeHeader `cmd:"2101" struc:"[0]pad"` //玩家登录 - // Flag: 0为走,1为飞行模式,对应Java的@UInt long - Flag uint32 - - // Point: 直接给坐标x,y - Point model.Pos `fieldDesc:"直接给坐标x,y"` - - // Reverse2: 暂定 占位字符2 - Reverse2 string `struc:"[2]byte"` -} - -// PeopleWalkOutboundInfo 对应Java的PeopleWalkOutboundInfo类,实现OutboundMessage接口 -type OutInfo struct { - // Flag: 0为走,1为飞行模式 - Flag uint32 `fieldDesc:"0为走,1为飞行模式" codec:"uint"` - - // UserID: 走动的人的米米号 - UserID uint32 `fieldDesc:"走动的人的米米号" codec:"uint"` - - // Point: 直接给坐标x,y - Point model.Pos `fieldDesc:"直接给坐标x,y"` - - // Reserve2: 这个字段同C2S_People_Walk中的reserve2 - Reserve2 string `struc:"[2]byte"` -}