601970b028
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>
279 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|