Files
galaxy-game/pkg/error/generic.go
T
Ilia Denisov 601970b028
Tests · Go / test (push) Successful in 2m27s
Tests · UI / test (push) Waiting to run
Tests · Integration / integration (pull_request) Successful in 1m45s
Tests · Go / test (pull_request) Successful in 3m13s
Tests · UI / test (pull_request) Successful in 3m8s
refactor(game): lock-free storage, remove /command, flatten engine wrapper
Three-stage refactor of the game-engine plumbing (game logic untouched):

Stage 1 — lock-free persistence + admin serialisation. Remove the file
lock from repo/fs (the .lock file, the Read/Write-vs-*Safe duality and the
dead ReadSafe polling) and replace the two-step rename with a single atomic
rename so concurrent reads are torn-free without a lock. Serialise the
state-mutating admin writers (init/turn/banish) with one shared router
LimitMiddleware, rewritten to block on the request context instead of a
racy shared 100ms timer.

Stage 2 — remove the obsolete immediate-command path end to end. Players
submit through PUT /api/v1/order; the legacy PUT /api/v1/command path is
deleted across game (route, handler, 24 command factories, Ctrl), backend
(Commands handler/route, engineclient.ExecuteCommands), gateway (dispatch +
executeUserGamesCommand + routing entry), the FlatBuffers/model contract
(UserGamesCommand[Response]) and transcoder, plus every affected
OpenAPI/README/FUNCTIONAL/ARCHITECTURE doc. The integration proxy test is
converted to the order path.

Stage 3 — flatten the REST->engine wrapper. Replace the executor adapter,
the controller package functions and RepoController with one concrete
controller.Service; drop the single-implementation Repo and Storage
interfaces (repo.Repo / fs.FS are now concrete). Handlers depend on a thin
handler.Engine seam and own the domain->REST projection; storage is
resolved once at startup instead of per request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:37:07 +02:00

279 lines
9.9 KiB
Go

// Package error defines the engine's error taxonomy used both as a wire
// contract over the engine REST API and as an internal Go error type.
//
// Codes are organised onto three semantic shelves. The high digit
// of each code identifies the shelf; the corresponding HTTP status
// mapping is enforced by router-level handlers (see
// game/internal/router/handler.errorResponse).
//
// Shelf 1xxx (internal / server)
// Engine infrastructure failures: storage, uninitialised game,
// invalid persisted state, missing report. HTTP 500, except
// ErrGameNotInitialized → 501.
//
// Shelf 2xxx (input validation, structural)
// Per-request structural rejections that can be decided without
// inspecting game state: enum mismatches, numeric ranges,
// cross-field shape rules ("ammunition without weapons"), and
// order-level structure ("quit must be the last command"). HTTP
// 400.
//
// Shelf 3xxx (game-state, per-command rejection)
// Game-state runtime rejections that depend on the current state
// snapshot: entity-not-found, not-owned, in-use, ships-busy,
// insufficient resources, send/upgrade/cargo dependencies. These
// surface as per-command `cmdErrorCode` on PUT /api/v1/order.
//
// Code 0 represents "applied without error" and is reserved as the
// successful per-command outcome on CommandMeta.Result. Code -1
// (ErrDummy) is reserved for test fixtures.
package error
import (
"fmt"
)
// Shelf 1xxx — internal / server errors.
const (
ErrStorageFailure int = 1001
ErrGameNotInitialized int = 1002
ErrGameStateInvalid int = 1003
ErrReportNotFound int = 1004
)
// Shelf 2xxx — structural input validation.
const (
ErrInputUnknownRelation int = 2001
ErrInputSameRace int = 2002
ErrInputEntityTypeNameInvalid int = 2003
ErrInputEntityTypeNameEquality int = 2004
ErrInputPlanetNumber int = 2005
ErrInputDriveValue int = 2006
ErrInputWeaponsValue int = 2007
ErrInputShieldsValue int = 2008
ErrInputCargoValue int = 2009
ErrInputShipTypeArmamentValue int = 2010
ErrInputShipTypeWeaponsAndArmamentValue int = 2011
ErrInputShipTypeZeroValues int = 2012
ErrInputScienceSumValues int = 2013
ErrInputProductionInvalid int = 2014
ErrInputCargoTypeInvalid int = 2015
ErrInputBreakGroupIllegalNumber int = 2016
ErrInputTechUnknown int = 2017
ErrInputTechInvalidMixing int = 2018
ErrInputUpgradeParameterNotAllowed int = 2019
ErrInputQuitCommandFollowedByCommand int = 2020
ErrInputUnrecognizedCommand int = 2021
)
// Shelf 3xxx — game-state runtime errors (per-command rejection).
//
// Several constants here retain a historical "Input" prefix in their
// names although the underlying check needs the live game state; the
// shelf is the authoritative classification and supersedes the prefix.
const (
ErrInputUnknownRace int = 3001
ErrInputNewEntityDuplicateIdentifier int = 3002
ErrInputEntityNotExists int = 3003
ErrInputEntityNotOwned int = 3004
ErrEntityInUse int = 3005
ErrRaceExtinct int = 3006
ErrShipsBusy int = 3007
ErrShipsNotOnSamePlanet int = 3008
ErrDeleteShipTypeExistingGroup int = 3009
ErrDeleteShipTypePlanetProduction int = 3010
ErrDeleteSciencePlanetProduction int = 3011
ErrMergeShipTypeNotEqual int = 3012
ErrBreakGroupNumberNotEnough int = 3013
ErrInputCargoLoadNotEnough int = 3014
ErrInputCargoLoadNotEqual int = 3015
ErrInputNoCargoBay int = 3016
ErrInputCargoLoadNoSpaceLeft int = 3017
ErrInputCargoUnloadEmpty int = 3018
ErrInputUpgradeShipTechNotUsed int = 3019
ErrInputUpgradeShipsAlreadyUpToDate int = 3020
ErrUpgradeGroupNumberNotEnough int = 3021
ErrUpgradeInsufficientResources int = 3022
ErrInputUpgradeTechLevelInsufficient int = 3023
ErrSendShipHasNoDrives int = 3024
ErrSendUnreachableDestination int = 3025
ErrSendShipOwnerHasNoPlanets int = 3026
)
// ErrDummy is reserved for test fixtures; production code never uses it.
const ErrDummy int = -1
// IsInternalCode reports whether code belongs to the internal / server
// shelf (1xxx). Internal errors map to HTTP 500 (ErrGameNotInitialized
// is special-cased to 501).
func IsInternalCode(code int) bool { return code >= 1000 && code < 2000 }
// IsInputCode reports whether code belongs to the structural input
// validation shelf (2xxx). Input errors map to HTTP 400.
func IsInputCode(code int) bool { return code >= 2000 && code < 3000 }
// IsGameStateCode reports whether code belongs to the game-state /
// per-command rejection shelf (3xxx). On PUT /api/v1/order these are
// recorded into CommandMeta.CmdErrCode.
func IsGameStateCode(code int) bool { return code >= 3000 && code < 4000 }
func GenericErrorText(code int) string {
switch code {
case ErrDummy:
return "Dummy"
case ErrStorageFailure:
return "Storage failure"
case ErrGameNotInitialized:
return "Game not yet initialized"
case ErrGameStateInvalid:
return "Invalid game state"
case ErrReportNotFound:
return "Report not found"
case ErrInputUnknownRace:
return "Race name is unknown to this game"
case ErrInputUnknownRelation:
return "Unknown relation"
case ErrInputSameRace:
return "Race name must be different from your own"
case ErrInputEntityTypeNameInvalid:
return "Name has invalid length or symbols"
case ErrInputNewEntityDuplicateIdentifier:
return "Entity already exists"
case ErrInputEntityTypeNameEquality:
return "Names should differ"
case ErrInputEntityNotExists:
return "Entity does not exists"
case ErrInputEntityNotOwned:
return "Entity is not owned"
case ErrEntityInUse:
return "Entity currently in use"
case ErrInputPlanetNumber:
return "Invalid Planet number"
case ErrInputDriveValue:
return "Invalid Drive value"
case ErrInputWeaponsValue:
return "Invalid Weapons value"
case ErrInputShieldsValue:
return "Invalid Shields value"
case ErrInputCargoValue:
return "Invalid Cargo value"
case ErrInputShipTypeArmamentValue:
return "Invalid Armament value"
case ErrInputShipTypeWeaponsAndArmamentValue:
return "Invalid Armament or Weapons value"
case ErrInputShipTypeZeroValues:
return "Ship type values cannot be all zeros"
case ErrDeleteShipTypeExistingGroup:
return "Ship type exists in a Group"
case ErrDeleteShipTypePlanetProduction:
return "Ship type in production on the Planet"
case ErrDeleteSciencePlanetProduction:
return "Science in production on the Planet"
case ErrInputScienceSumValues:
return "Science proportions sum should be equal 1"
case ErrInputProductionInvalid:
return "Invalid Production type"
case ErrInputCargoTypeInvalid:
return "Invalid cargo type"
case ErrInputCargoLoadNotEnough:
return "Not enough cargo to load"
case ErrInputCargoLoadNotEqual:
return "Ship(s) already loaded with another cargo"
case ErrInputNoCargoBay:
return "Ship type is not designed to carry cargo"
case ErrInputCargoLoadNoSpaceLeft:
return "No space left on the ships to load cargo"
case ErrInputCargoUnloadEmpty:
return "Ships are not carrying any cargo"
case ErrInputBreakGroupIllegalNumber:
return "Illegal ships number to make new group"
case ErrMergeShipTypeNotEqual:
return "Source and target ship types are not the same"
case ErrBreakGroupNumberNotEnough:
return "Not enough ships in the group to make a separate group"
case ErrShipsBusy:
return "Ship(s) are'n free to use"
case ErrShipsNotOnSamePlanet:
return "Ships not on the same planet"
case ErrInputTechUnknown:
return "Technology name unknown"
case ErrInputTechInvalidMixing:
return "Technologies list must containt only specific values"
case ErrInputUpgradeShipTechNotUsed:
return "Technology is not used with ship class"
case ErrInputUpgradeParameterNotAllowed:
return "Parameter not allowed for upgrade"
case ErrInputUpgradeShipsAlreadyUpToDate:
return "Ships already up to date, nothing to upgrade"
case ErrUpgradeGroupNumberNotEnough:
return "Not enough ships in the group to make an upgrade"
case ErrUpgradeInsufficientResources:
return "Insufficient planet production capacity"
case ErrInputUpgradeTechLevelInsufficient:
return "Insifficient Tech level for requested upgrade"
case ErrInputQuitCommandFollowedByCommand:
return "'Quit' must be the last order's command"
case ErrInputUnrecognizedCommand:
return "Unrecognized command"
case ErrSendShipHasNoDrives:
return "One or more ships are not equipped with hyperdrive and cannot be moved"
case ErrSendUnreachableDestination:
return "Destination planet is too far for current Drive level"
case ErrSendShipOwnerHasNoPlanets:
return "Race is not owning any planet, all flights impossible"
case ErrRaceExtinct:
return "Race is extinct"
default:
return fmt.Sprintf("Undescribed error with code %d", code)
}
}
type GenericError struct {
Code int
subject string
err error
}
func (ge GenericError) Error() string {
msg := GenericErrorText(ge.Code)
if ge.subject != "" {
msg += ": " + ge.subject
}
if ge.err != nil {
msg = fmt.Errorf("%s: %w", msg, ge.err).Error()
}
return msg
}
// Unwrap returns the underlying error wrapped by GenericError, if any.
func (ge GenericError) Unwrap() error {
return ge.err
}
func newGenericError(code int, arg ...any) error {
e := &GenericError{Code: code}
if len(arg) > 0 {
i := 0
switch arg[i].(type) {
case error:
e.err = arg[i].(error)
i += 1
}
if len(arg) >= i+2 {
e.subject = fmt.Sprintf(asString(arg[i]), arg[i+1:]...)
} else if len(arg) == i+1 {
e.subject = asString(arg[i])
}
}
return e
}
func asString(v any) string {
switch s := v.(type) {
case string:
return s
default:
return fmt.Sprint(v)
}
}