// 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 // (and only escape as HTTP 400 from PUT /api/v1/command). // // 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; on PUT /api/v1/command they // map to HTTP 400. 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) } }