package gameserver import ( "log" "math" "strings" "time" "ananta-ps/pkg/protocol" ) const ( CarShopTeleportConfig uint32 = 8 JiguanTeleportConfig uint32 = 2 GarageExitTeleportConfig uint32 = 7 ) type Vec3 struct { X float32 Y float32 Z float32 } type ShopAnchor struct { Entrance Vec3 Interior Vec3 Facing float32 Leave Vec3 LeaveYaw float32 } var defaultShopInteriors = []ShopAnchor{ { Entrance: Vec3{X: 2260.5, Y: 0, Z: 1374.3}, Interior: Vec3{X: 2271.5, Y: 0, Z: 1374.3}, Facing: 90.0, Leave: Vec3{X: 2240.0, Y: 0, Z: 1374.3}, LeaveYaw: 270.0, }, { Entrance: Vec3{X: 2800.0, Y: 0, Z: 1296.0}, Interior: Vec3{X: 2817.3, Y: 0, Z: 1296.7}, Facing: 270.0, Leave: Vec3{X: 2780.0, Y: 0, Z: 1296.0}, LeaveYaw: 90.0, }, } type PreTeleportOption struct { ConfigID uint32 Position Vec3 Facing float32 BeforeResName string AfterResName string BeforePosition Vec3 BeforeRot Vec3 AfterPosition Vec3 AfterRot Vec3 CustomBefore bool CustomAfter bool } func (s *Server) handleAskTeleport(sess *Session, pkt *protocol.RpcPacket) error { r := protocol.NewUxReader(pkt.Body) opt := decodePreTeleportOption(r) log.Printf("[TELEPORT] AskTeleport config=%d before='%s' after='%s' pos=(%.1f,%.1f,%.1f)", opt.ConfigID, opt.BeforeResName, opt.AfterResName, opt.Position.X, opt.Position.Y, opt.Position.Z) now := time.Now().UnixMilli() switch opt.ConfigID { case CarShopTeleportConfig: isExit := strings.Contains(strings.ToLower(opt.BeforeResName), "out") if !isExit && sess.lastShopExitMs != 0 && (now-sess.lastShopExitMs < 4000) { log.Printf("[TELEPORT] Suppressed car shop enter debounce (within 4s of exit)") return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) } shop := findNearestShop(Vec3{X: sess.posX, Y: sess.posY, Z: sess.posZ}) if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 { if isExit { opt.Position = shop.Leave opt.Facing = shop.LeaveYaw opt.AfterPosition = shop.Leave opt.AfterRot = Vec3{X: 0, Y: shop.LeaveYaw, Z: 0} } else { opt.Position = shop.Interior opt.Facing = shop.Facing opt.AfterPosition = shop.Interior opt.AfterRot = Vec3{X: 0, Y: shop.Facing, Z: 0} } } if isExit { sess.lastShopExitMs = now } case JiguanTeleportConfig: // Elevator landing if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 { opt.Position = Vec3{X: sess.posX + 5.0, Y: sess.posY, Z: sess.posZ} opt.AfterPosition = opt.Position opt.CustomAfter = true } case GarageExitTeleportConfig: // Garage exit if opt.Position.X == 0 && opt.Position.Y == 0 && opt.Position.Z == 0 { opt.Position = Vec3{X: 2280.0, Y: 10.0, Z: 1400.0} opt.AfterPosition = opt.Position } } sess.pendingTeleport = opt if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } // Echo SyncPreTeleportOption w := protocol.NewUxWriter() encodePreTeleportOption(w, opt) return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncPreTeleportOption", 68582739), w.Bytes()) } func (s *Server) handleReportPreTeleportFinish(sess *Session, pkt *protocol.RpcPacket) error { log.Printf("[TELEPORT] ReportPreTeleportFinish received, applying teleport destination") if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } opt := sess.pendingTeleport if opt != nil { dest := opt.Position if dest.X != 0 || dest.Y != 0 || dest.Z != 0 { _ = sess.teleport(dest.X, dest.Y, dest.Z, opt.Facing) } } return nil } func (s *Server) handleReportPostTeleportFinish(sess *Session, pkt *protocol.RpcPacket) error { log.Printf("[TELEPORT] ReportPostTeleportFinish completed") sess.pendingTeleport = nil return sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID) } func (s *Server) handleAskEnterRaidByMapEntrance(sess *Session, pkt *protocol.RpcPacket) error { r := protocol.NewUxReader(pkt.Body) entranceID, _ := r.U32() log.Printf("[TELEPORT] AskEnterRaidByMapEntrance entranceID=%d", entranceID) if err := sess.ReturnEmptyOk(pkt.InvokeID, pkt.MethodID); err != nil { return err } // SyncTeleport to entrance target w := protocol.NewUxWriter() w.U64(sess.unitId).U32(entranceID).F32(sess.posX).F32(sess.posY).F32(sess.posZ).F32(sess.facing) return sess.NotifyData(protocol.MethodGetMethodIDDefault("SyncTeleport", 68582740), w.Bytes()) } func findNearestShop(pos Vec3) ShopAnchor { best := defaultShopInteriors[0] bestDist := float64(math.MaxFloat64) for _, shop := range defaultShopInteriors { dx := float64(shop.Entrance.X - pos.X) dz := float64(shop.Entrance.Z - pos.Z) d := dx*dx + dz*dz if d < bestDist { bestDist = d best = shop } } return best } func decodePreTeleportOption(r *protocol.UxReader) *PreTeleportOption { opt := &PreTeleportOption{} if r == nil || r.Remaining() == 0 { return opt } opt.ConfigID, _ = r.U32() opt.Position.X, _ = r.F32() opt.Position.Y, _ = r.F32() opt.Position.Z, _ = r.F32() opt.Facing, _ = r.F32() opt.BeforeResName, _ = r.UxString() opt.AfterResName, _ = r.UxString() opt.BeforePosition.X, _ = r.F32() opt.BeforePosition.Y, _ = r.F32() opt.BeforePosition.Z, _ = r.F32() opt.BeforeRot.X, _ = r.F32() opt.BeforeRot.Y, _ = r.F32() opt.BeforeRot.Z, _ = r.F32() opt.AfterPosition.X, _ = r.F32() opt.AfterPosition.Y, _ = r.F32() opt.AfterPosition.Z, _ = r.F32() opt.AfterRot.X, _ = r.F32() opt.AfterRot.Y, _ = r.F32() opt.AfterRot.Z, _ = r.F32() opt.CustomBefore, _ = r.Bool() opt.CustomAfter, _ = r.Bool() return opt } func encodePreTeleportOption(w *protocol.UxWriter, opt *PreTeleportOption) { if opt == nil { return } w.U32(opt.ConfigID) w.F32(opt.Position.X).F32(opt.Position.Y).F32(opt.Position.Z) w.F32(opt.Facing) w.UxString(opt.BeforeResName) w.UxString(opt.AfterResName) w.F32(opt.BeforePosition.X).F32(opt.BeforePosition.Y).F32(opt.BeforePosition.Z) w.F32(opt.BeforeRot.X).F32(opt.BeforeRot.Y).F32(opt.BeforeRot.Z) w.F32(opt.AfterPosition.X).F32(opt.AfterPosition.Y).F32(opt.AfterPosition.Z) w.F32(opt.AfterRot.X).F32(opt.AfterRot.Y).F32(opt.AfterRot.Z) w.Bool(opt.CustomBefore) w.Bool(opt.CustomAfter) }