DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,699 @@
package worldmap
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
)
var (
ErrRunTopologyRequired = errors.New("dnf dungeon run topology is required")
ErrRunStartMissing = errors.New("dnf dungeon run start room is missing")
ErrRunNotActive = errors.New("dnf dungeon run is not active")
ErrRunCurrentRoomNotCleared = errors.New("dnf dungeon run current room is not cleared")
ErrRunTargetVisitChanged = errors.New("dnf dungeon run target visit state changed")
ErrRoomCoordinateMalformed = errors.New("dnf dungeon room coordinate is malformed")
ErrRoomNotFound = errors.New("dnf dungeon room is not in topology")
ErrRoomNotAdjacent = errors.New("dnf dungeon room is not adjacent")
ErrRoomMapUnresolved = errors.New("dnf dungeon room map is unresolved")
ErrLayeredCoordinateMismatch = errors.New("dnf dungeon layered map coordinate does not match the current room")
ErrLayeredTransitionChanged = errors.New("dnf dungeon layered map transition changed")
ErrLayeredBaseMismatch = errors.New("dnf dungeon layered base map does not match the cached room")
ErrStoryStageTransitionChanged = errors.New("dnf dungeon story stage transition changed")
ErrMapOverrideTransitionChanged = errors.New("dnf dungeon map-override transition changed")
)
type DungeonRunStatus string
const (
DungeonRunActive DungeonRunStatus = "active"
DungeonRunCompleted DungeonRunStatus = "completed"
DungeonRunAbandoned DungeonRunStatus = "abandoned"
)
type DungeonRunSnapshot struct {
DungeonID int64 `json:"dungeon_id"`
MazeIndex int `json:"maze_index"`
Status DungeonRunStatus `json:"status"`
Current RoomCoordinate `json:"current"`
Visited []RoomCoordinate `json:"visited,omitempty"`
Cleared []RoomCoordinate `json:"cleared,omitempty"`
}
type DungeonRun struct {
mu sync.RWMutex
topology *DungeonTopology
current coordinateKey
visited map[coordinateKey]struct{}
cleared map[coordinateKey]struct{}
status DungeonRunStatus
}
func ParseRoomCoordinate(value string) (RoomCoordinate, error) {
value = strings.TrimSpace(value)
xText, yText, ok := strings.Cut(value, ":")
if !ok || xText == "" || yText == "" || strings.Contains(yText, ":") {
return RoomCoordinate{}, fmt.Errorf("%w: %q", ErrRoomCoordinateMalformed, value)
}
x, err := strconv.ParseInt(xText, 10, 64)
if err != nil {
return RoomCoordinate{}, fmt.Errorf("%w: %q", ErrRoomCoordinateMalformed, value)
}
y, err := strconv.ParseInt(yText, 10, 64)
if err != nil {
return RoomCoordinate{}, fmt.Errorf("%w: %q", ErrRoomCoordinateMalformed, value)
}
return RoomCoordinate{X: x, Y: y}, nil
}
func NewDungeonRun(topology *DungeonTopology) (*DungeonRun, error) {
if topology == nil {
return nil, ErrRunTopologyRequired
}
if topology.Start == nil {
return nil, fmt.Errorf("%w: dungeon=%d maze=%d", ErrRunStartMissing, topology.DungeonID, topology.MazeIndex)
}
start := *topology.Start
room, ok := topology.Room(start)
if !ok {
return nil, fmt.Errorf("%w: dungeon=%d maze=%d room=%s", ErrRoomNotFound, topology.DungeonID, topology.MazeIndex, start)
}
if room.Map == nil {
return nil, roomMapError(topology, start)
}
key := coordinateKey{x: start.X, y: start.Y}
return &DungeonRun{
topology: topology,
current: key,
visited: map[coordinateKey]struct{}{key: {}},
cleared: make(map[coordinateKey]struct{}),
status: DungeonRunActive,
}, nil
}
func (r *DungeonRun) CurrentRoom() (DungeonRoom, bool) {
if r == nil {
return DungeonRoom{}, false
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.topology.Room(RoomCoordinate{X: r.current.x, Y: r.current.y})
}
// Rooms returns the resolved immutable room layout selected for this run.
// Callers receive cloned room values and cannot mutate the run topology.
func (r *DungeonRun) Rooms() []DungeonRoom {
if r == nil {
return nil
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.topology.Rooms()
}
func (r *DungeonRun) MoveByte(nextX, nextY byte) (DungeonRoom, error) {
return r.MoveTo(RoomCoordinate{X: int64(nextX), Y: int64(nextY)})
}
// PreviewMoveByte validates a room transition without changing the run.
func (r *DungeonRun) PreviewMoveByte(nextX, nextY byte) (DungeonRoom, error) {
return r.PreviewMoveTo(RoomCoordinate{X: int64(nextX), Y: int64(nextY)})
}
func (r *DungeonRun) MoveRoomID(roomID string) (DungeonRoom, error) {
coordinate, err := ParseRoomCoordinate(roomID)
if err != nil {
return DungeonRoom{}, err
}
return r.MoveTo(coordinate)
}
// PreviewMoveTo validates a room transition without changing current, visited,
// cleared, or completion state.
func (r *DungeonRun) PreviewMoveTo(target RoomCoordinate) (DungeonRoom, error) {
room, _, _, err := r.previewMoveToVisit(target)
return room, err
}
// previewStoryStage validates one PVF-declared activity-story stage as
// adjacent room travel, then replaces the topology's base map descriptor with
// the exact ordered story candidate. A previously visited coordinate remains
// a fresh story scene; the revisit bit is used only to select client payload
// mode 2.
func (r *DungeonRun) previewStoryStage(stageIndex int) (DungeonRoom, bool, error) {
if r == nil {
return DungeonRoom{}, false, ErrRunTopologyRequired
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.resolveStoryStageLocked(stageIndex)
}
func (r *DungeonRun) commitStoryStage(
stageIndex int,
expectedCoordinate RoomCoordinate,
expectedMapID int64,
expectedRevisit bool,
targetCleared bool,
) (DungeonRoom, error) {
if r == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
room, revisit, err := r.resolveStoryStageLocked(stageIndex)
if err != nil {
return DungeonRoom{}, err
}
actualMapID := int64(0)
if room.Map != nil {
actualMapID = room.Map.Map.ID
}
if room.Coordinate != expectedCoordinate || actualMapID != expectedMapID || revisit != expectedRevisit {
return DungeonRoom{}, fmt.Errorf(
"%w: stage=%d expected=%s/%d revisit=%t actual=%s/%d revisit=%t",
ErrStoryStageTransitionChanged,
stageIndex,
expectedCoordinate,
expectedMapID,
expectedRevisit,
room.Coordinate,
actualMapID,
revisit,
)
}
targetKey := coordinateKey{x: room.Coordinate.X, y: room.Coordinate.Y}
r.current = targetKey
r.visited[targetKey] = struct{}{}
delete(r.cleared, targetKey)
if targetCleared {
r.cleared[targetKey] = struct{}{}
}
return room, nil
}
func (r *DungeonRun) resolveStoryStageLocked(stageIndex int) (DungeonRoom, bool, error) {
if r.topology == nil || r.topology.resolver == nil {
return DungeonRoom{}, false, ErrResolverTableRequired
}
if r.status != DungeonRunActive {
return DungeonRoom{}, false, fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
resolved, stage, err := r.topology.resolver.ResolveStoryStage(
r.topology.DungeonID,
r.topology.MazeIndex,
stageIndex,
)
if err != nil {
return DungeonRoom{}, false, err
}
targetKey := coordinateKey{x: stage.Coordinate.X, y: stage.Coordinate.Y}
room, ok := r.topology.rooms[targetKey]
if !ok {
return DungeonRoom{}, false, fmt.Errorf("%w: room=%s", ErrRoomNotFound, stage.Coordinate)
}
if abs64(stage.Coordinate.X-r.current.x)+abs64(stage.Coordinate.Y-r.current.y) != 1 {
return DungeonRoom{}, false, fmt.Errorf(
"%w: current=%s target=%s story_stage=%d",
ErrRoomNotAdjacent,
RoomCoordinate{X: r.current.x, Y: r.current.y},
stage.Coordinate,
stageIndex,
)
}
if !r.topology.AllowMoveBeforeClear {
if _, ok := r.cleared[r.current]; !ok {
return DungeonRoom{}, false, fmt.Errorf(
"%w: room=%s story_stage=%d",
ErrRunCurrentRoomNotCleared,
RoomCoordinate{X: r.current.x, Y: r.current.y},
stageIndex,
)
}
}
_, revisit := r.visited[targetKey]
room.Map = &resolved
return r.topology.prepareRoom(room), revisit, nil
}
// previewCurrentLayered resolves a PVF-declared layer for the current room
// without requiring the source room to be clear. Current clients request this
// same-coordinate transition from cinematic [CHANGE MAP] with MOVE_MAP kind 1.
func (r *DungeonRun) previewCurrentLayered(coordinate RoomCoordinate, layerIndex int) (DungeonRoom, error) {
if r == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.resolveCurrentLayeredLocked(coordinate, layerIndex)
}
// commitCurrentLayered revalidates the exact previewed map and replaces the
// coordinate's prior clear bit with the fresh layer's state. Visited/current
// coordinates do not change because a layered transition is not room travel.
func (r *DungeonRun) commitCurrentLayered(
coordinate RoomCoordinate,
layerIndex int,
expectedMapID int64,
targetCleared bool,
) (DungeonRoom, error) {
if r == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
room, err := r.resolveCurrentLayeredLocked(coordinate, layerIndex)
if err != nil {
return DungeonRoom{}, err
}
if room.Map == nil || room.Map.Map.ID != expectedMapID {
actualMapID := int64(0)
if room.Map != nil {
actualMapID = room.Map.Map.ID
}
return DungeonRoom{}, fmt.Errorf(
"%w: room=%s layer=%d expected_map=%d actual_map=%d",
ErrLayeredTransitionChanged,
coordinate,
layerIndex,
expectedMapID,
actualMapID,
)
}
delete(r.cleared, r.current)
if targetCleared {
r.cleared[r.current] = struct{}{}
}
return room, nil
}
func (r *DungeonRun) resolveCurrentLayeredLocked(
coordinate RoomCoordinate,
layerIndex int,
) (DungeonRoom, error) {
if r.topology == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
if r.status != DungeonRunActive {
return DungeonRoom{}, fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
current := RoomCoordinate{X: r.current.x, Y: r.current.y}
if coordinate != current {
return DungeonRoom{}, fmt.Errorf(
"%w: current=%s requested=%s",
ErrLayeredCoordinateMismatch,
current,
coordinate,
)
}
room, ok := r.topology.rooms[r.current]
if !ok {
return DungeonRoom{}, fmt.Errorf("%w: room=%s", ErrRoomNotFound, current)
}
if r.topology.resolver == nil {
return DungeonRoom{}, ErrResolverTableRequired
}
resolved, err := r.topology.resolver.ResolveLayered(
r.topology.DungeonID,
r.topology.MazeIndex,
coordinate,
layerIndex,
)
if err != nil {
return DungeonRoom{}, err
}
room.Map = &resolved
return r.topology.prepareRoom(room), nil
}
// previewCurrentLayeredBase resolves the topology-owned base room for the
// current coordinate without mutating the run. A layered transition never
// changes the current coordinate, so the topology entry remains the base room
// that the explicit PVF layer list temporarily replaced. The coordinate's
// current clear bit is reported for diagnostics only: after a layered commit
// it describes the layer, not the base room.
func (r *DungeonRun) previewCurrentLayeredBase(coordinate RoomCoordinate) (DungeonRoom, bool, error) {
if r == nil {
return DungeonRoom{}, false, ErrRunTopologyRequired
}
r.mu.RLock()
defer r.mu.RUnlock()
room, err := r.resolveCurrentLayeredBaseLocked(coordinate)
if err != nil {
return DungeonRoom{}, false, err
}
_, cleared := r.cleared[r.current]
return room, cleared, nil
}
// commitCurrentLayeredBase revalidates the exact topology-owned base map and
// replaces the coordinate's clear bit with the restored base room's state.
// Visited/current coordinates do not change because a layered base return is
// not room travel.
func (r *DungeonRun) commitCurrentLayeredBase(
coordinate RoomCoordinate,
expectedMapID int64,
targetCleared bool,
) (DungeonRoom, error) {
if r == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
room, err := r.resolveCurrentLayeredBaseLocked(coordinate)
if err != nil {
return DungeonRoom{}, err
}
if room.Map == nil || room.Map.Map.ID != expectedMapID {
actualMapID := int64(0)
if room.Map != nil {
actualMapID = room.Map.Map.ID
}
return DungeonRoom{}, fmt.Errorf(
"%w: room=%s expected_base_map=%d actual_base_map=%d",
ErrLayeredBaseMismatch,
coordinate,
expectedMapID,
actualMapID,
)
}
delete(r.cleared, r.current)
if targetCleared {
r.cleared[r.current] = struct{}{}
}
return room, nil
}
func (r *DungeonRun) resolveCurrentLayeredBaseLocked(coordinate RoomCoordinate) (DungeonRoom, error) {
if r.topology == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
if r.status != DungeonRunActive && r.status != DungeonRunCompleted {
return DungeonRoom{}, fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
current := RoomCoordinate{X: r.current.x, Y: r.current.y}
if coordinate != current {
return DungeonRoom{}, fmt.Errorf(
"%w: current=%s requested=%s",
ErrLayeredCoordinateMismatch,
current,
coordinate,
)
}
room, ok := r.topology.rooms[r.current]
if !ok {
return DungeonRoom{}, fmt.Errorf("%w: room=%s", ErrRoomNotFound, current)
}
if room.Map == nil {
return DungeonRoom{}, roomMapError(r.topology, current)
}
return r.topology.prepareRoom(room), nil
}
// previewMoveToVisit validates a transition and reports the persistent run
// state for its target under the same read lock. DungeonSession uses this to
// distinguish a first visit from a revisit without treating rebuilt PVF scene
// data as prior runtime state.
func (r *DungeonRun) previewMoveToVisit(target RoomCoordinate) (DungeonRoom, bool, bool, error) {
if r == nil {
return DungeonRoom{}, false, false, ErrRunTopologyRequired
}
r.mu.RLock()
defer r.mu.RUnlock()
room, err := r.validateMoveLocked(target)
if err != nil {
return DungeonRoom{}, false, false, err
}
targetKey := coordinateKey{x: target.X, y: target.Y}
_, visited := r.visited[targetKey]
_, cleared := r.cleared[targetKey]
return room, visited, cleared, nil
}
// previewMoveMapOverride validates ordinary adjacent room travel while
// replacing only the target room's map template. The target coordinate remains
// the client-requested logical room; mechanisms such as Gent Infiltrate use a
// hidden PVF coordinate solely as the source of the replacement map.
func (r *DungeonRun) previewMoveMapOverride(
target RoomCoordinate,
resolved ResolvedMap,
) (DungeonRoom, bool, bool, error) {
if r == nil {
return DungeonRoom{}, false, false, ErrRunTopologyRequired
}
r.mu.RLock()
defer r.mu.RUnlock()
return r.resolveMoveMapOverrideLocked(target, resolved)
}
func (r *DungeonRun) commitMoveMapOverride(
target RoomCoordinate,
resolved ResolvedMap,
expectedMapID int64,
expectedRevisit bool,
targetCleared bool,
) (DungeonRoom, error) {
if r == nil {
return DungeonRoom{}, ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
room, revisit, _, err := r.resolveMoveMapOverrideLocked(target, resolved)
if err != nil {
return DungeonRoom{}, err
}
actualMapID := int64(0)
if room.Map != nil {
actualMapID = room.Map.Map.ID
}
if room.Coordinate != target || actualMapID != expectedMapID || revisit != expectedRevisit {
return DungeonRoom{}, fmt.Errorf(
"%w: expected=%s/%d revisit=%t actual=%s/%d revisit=%t",
ErrMapOverrideTransitionChanged,
target,
expectedMapID,
expectedRevisit,
room.Coordinate,
actualMapID,
revisit,
)
}
targetKey := coordinateKey{x: target.X, y: target.Y}
r.current = targetKey
r.visited[targetKey] = struct{}{}
delete(r.cleared, targetKey)
if targetCleared {
r.cleared[targetKey] = struct{}{}
}
return room, nil
}
func (r *DungeonRun) resolveMoveMapOverrideLocked(
target RoomCoordinate,
resolved ResolvedMap,
) (DungeonRoom, bool, bool, error) {
if resolved.Map.ID <= 0 {
return DungeonRoom{}, false, false, fmt.Errorf(
"%w: room=%s map=%d",
ErrMapReferenceMissing,
target,
resolved.Map.ID,
)
}
room, err := r.validateMoveLocked(target)
if err != nil {
return DungeonRoom{}, false, false, err
}
copy := resolved
copy.Map = cloneMap(resolved.Map)
room.Map = &copy
targetKey := coordinateKey{x: target.X, y: target.Y}
_, revisit := r.visited[targetKey]
_, cleared := r.cleared[targetKey]
return room, revisit, cleared, nil
}
func (r *DungeonRun) MoveTo(target RoomCoordinate) (DungeonRoom, error) {
return r.moveTo(target, false)
}
func (r *DungeonRun) moveTo(target RoomCoordinate, clearTarget bool) (DungeonRoom, error) {
room, _, err := r.moveToVisit(target, clearTarget, nil)
return room, err
}
// moveToVisit commits a transition and can require the target's first/revisit
// classification to match a prior preview. The optional expectation keeps a
// cached-room restore fail-closed if another owner mutated DungeonRun directly
// between preview and commit.
func (r *DungeonRun) moveToVisit(target RoomCoordinate, clearTarget bool, expectedRevisit *bool) (DungeonRoom, bool, error) {
if r == nil {
return DungeonRoom{}, false, ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
room, err := r.validateMoveLocked(target)
if err != nil {
return DungeonRoom{}, false, err
}
targetKey := coordinateKey{x: target.X, y: target.Y}
_, revisit := r.visited[targetKey]
if expectedRevisit != nil && revisit != *expectedRevisit {
return DungeonRoom{}, revisit, fmt.Errorf(
"%w: room=%s expected_revisit=%t actual_revisit=%t",
ErrRunTargetVisitChanged,
target,
*expectedRevisit,
revisit,
)
}
r.current = targetKey
r.visited[targetKey] = struct{}{}
if clearTarget {
r.cleared[targetKey] = struct{}{}
}
return room, revisit, nil
}
func (r *DungeonRun) validateMoveLocked(target RoomCoordinate) (DungeonRoom, error) {
if r.status != DungeonRunActive {
return DungeonRoom{}, fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
targetKey := coordinateKey{x: target.X, y: target.Y}
room, ok := r.topology.rooms[targetKey]
if !ok {
return DungeonRoom{}, fmt.Errorf("%w: room=%s", ErrRoomNotFound, target)
}
adjacent := abs64(target.X-r.current.x)+abs64(target.Y-r.current.y) == 1
if !adjacent && !r.topology.allowsWarpTransition(r.current, targetKey) {
return DungeonRoom{}, fmt.Errorf(
"%w: current=%s target=%s",
ErrRoomNotAdjacent, RoomCoordinate{X: r.current.x, Y: r.current.y}, target,
)
}
if !r.topology.AllowMoveBeforeClear {
if _, ok := r.cleared[r.current]; !ok {
return DungeonRoom{}, fmt.Errorf(
"%w: room=%s",
ErrRunCurrentRoomNotCleared, RoomCoordinate{X: r.current.x, Y: r.current.y},
)
}
}
if room.Map == nil {
return DungeonRoom{}, roomMapError(r.topology, target)
}
return r.topology.prepareRoom(room), nil
}
func (r *DungeonRun) MarkCurrentRoomCleared() error {
if r == nil {
return ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
if r.status != DungeonRunActive {
return fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
r.cleared[r.current] = struct{}{}
return nil
}
func (r *DungeonRun) Complete() error {
if r == nil {
return ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
if r.status != DungeonRunActive {
return fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
if _, ok := r.cleared[r.current]; !ok {
return fmt.Errorf(
"%w: room=%s",
ErrRunCurrentRoomNotCleared, RoomCoordinate{X: r.current.x, Y: r.current.y},
)
}
r.status = DungeonRunCompleted
return nil
}
// CompleteCurrentRoom records an authoritative scripted completion event for
// the active room. Unlike Complete, it does not require every blocking actor to
// have reported a death: some PVF tutorial finals end by escape/cinematic while
// ordinary actors are still present. It marks only the room/run lifecycle and
// never invents defeated runtime objects.
func (r *DungeonRun) CompleteCurrentRoom() error {
if r == nil {
return ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
if r.status != DungeonRunActive {
return fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
r.cleared[r.current] = struct{}{}
r.status = DungeonRunCompleted
return nil
}
func (r *DungeonRun) Abandon() error {
if r == nil {
return ErrRunTopologyRequired
}
r.mu.Lock()
defer r.mu.Unlock()
if r.status != DungeonRunActive {
return fmt.Errorf("%w: status=%s", ErrRunNotActive, r.status)
}
r.status = DungeonRunAbandoned
return nil
}
func (r *DungeonRun) Snapshot() DungeonRunSnapshot {
if r == nil {
return DungeonRunSnapshot{}
}
r.mu.RLock()
defer r.mu.RUnlock()
snapshot := DungeonRunSnapshot{
DungeonID: r.topology.DungeonID,
MazeIndex: r.topology.MazeIndex,
Status: r.status,
Current: RoomCoordinate{X: r.current.x, Y: r.current.y},
Visited: sortedCoordinates(r.visited),
Cleared: sortedCoordinates(r.cleared),
}
return snapshot
}
func roomMapError(topology *DungeonTopology, coordinate RoomCoordinate) error {
key := coordinateKey{x: coordinate.X, y: coordinate.Y}
if cause := topology.resolutionErrors[key]; cause != nil {
return fmt.Errorf("%w: room=%s: %w", ErrRoomMapUnresolved, coordinate, cause)
}
return fmt.Errorf("%w: room=%s", ErrRoomMapUnresolved, coordinate)
}
func sortedCoordinates(values map[coordinateKey]struct{}) []RoomCoordinate {
coordinates := make([]RoomCoordinate, 0, len(values))
for key := range values {
coordinates = append(coordinates, RoomCoordinate{X: key.x, Y: key.y})
}
sort.Slice(coordinates, func(i, j int) bool {
if coordinates[i].Y != coordinates[j].Y {
return coordinates[i].Y < coordinates[j].Y
}
return coordinates[i].X < coordinates[j].X
})
return coordinates
}
func abs64(value int64) int64 {
if value < 0 {
return -value
}
return value
}