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>
277 lines
8.9 KiB
Go
277 lines
8.9 KiB
Go
package order
|
|
|
|
import (
|
|
"encoding/json"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// MessageTypeUserGamesOrder is the authenticated gateway message type
|
|
// used to validate / store a batch of in-game orders through
|
|
// `POST /api/v1/user/games/{game_id}/orders`. The signed payload is a
|
|
// FlatBuffers `order.UserGamesOrder`.
|
|
const MessageTypeUserGamesOrder = "user.games.order"
|
|
|
|
// MessageTypeUserGamesOrderGet is the authenticated gateway message
|
|
// type used to read back the player's stored order for a given turn
|
|
// through `GET /api/v1/user/games/{game_id}/orders?turn=N`. The
|
|
// signed payload is a FlatBuffers `order.UserGamesOrderGet`.
|
|
const MessageTypeUserGamesOrderGet = "user.games.order.get"
|
|
|
|
// UserGamesOrder is the typed payload of MessageTypeUserGamesOrder.
|
|
// `GameID` selects the running engine container; `Commands` is the
|
|
// player order batch; `UpdatedAt` lets the engine reject stale order
|
|
// submissions. The `Actor` field present in the engine's JSON shape is
|
|
// rebuilt by backend from the runtime player mapping — clients never
|
|
// carry it.
|
|
type UserGamesOrder struct {
|
|
// GameID identifies the running game for this batch.
|
|
GameID uuid.UUID `json:"game_id"`
|
|
|
|
// UpdatedAt is the client-side timestamp used for stale-order
|
|
// detection on the engine side.
|
|
UpdatedAt int64 `json:"updatedAt"`
|
|
|
|
// Commands is the player order batch.
|
|
Commands []DecodableCommand `json:"cmd"`
|
|
}
|
|
|
|
func (o UserGamesOrder) MarshalBinary() (data []byte, err error) {
|
|
return json.Marshal(&o)
|
|
}
|
|
|
|
func (o *UserGamesOrder) UnmarshalBinary(data []byte) error {
|
|
return json.Unmarshal(data, o)
|
|
}
|
|
|
|
// UserGamesOrderGet is the typed payload of
|
|
// MessageTypeUserGamesOrderGet. `Turn` is mandatory and must be
|
|
// non-negative; the caller pulls it from the lobby record at game
|
|
// boot. Backend rebinds the player from the runtime player mapping
|
|
// before forwarding to the engine.
|
|
type UserGamesOrderGet struct {
|
|
// GameID identifies the running game whose order is being
|
|
// read back.
|
|
GameID uuid.UUID `json:"game_id"`
|
|
|
|
// Turn selects the turn the stored order belongs to. Negative
|
|
// values are invalid.
|
|
Turn int `json:"turn"`
|
|
}
|
|
|
|
func AsCommand[E DecodableCommand](c DecodableCommand) (result E, ok bool) {
|
|
if v, ok := c.(E); ok {
|
|
return v, true
|
|
}
|
|
return
|
|
}
|
|
|
|
type CommandType string
|
|
|
|
const (
|
|
CommandTypeRaceQuit CommandType = "raceQuit"
|
|
CommandTypeRaceVote CommandType = "raceVote"
|
|
CommandTypeRaceRelation CommandType = "raceRelation"
|
|
CommandTypeShipClassCreate CommandType = "shipClassCreate"
|
|
CommandTypeShipClassMerge CommandType = "shipClassMerge"
|
|
CommandTypeShipClassRemove CommandType = "shipClassRemove"
|
|
CommandTypeShipGroupBreak CommandType = "shipGroupBreak"
|
|
CommandTypeShipGroupLoad CommandType = "shipGroupLoad"
|
|
CommandTypeShipGroupUnload CommandType = "shipGroupUnload"
|
|
CommandTypeShipGroupSend CommandType = "shipGroupSend"
|
|
CommandTypeShipGroupUpgrade CommandType = "shipGroupUpgrade"
|
|
CommandTypeShipGroupMerge CommandType = "shipGroupMerge"
|
|
CommandTypeShipGroupDismantle CommandType = "shipGroupDismantle"
|
|
CommandTypeShipGroupTransfer CommandType = "shipGroupTransfer"
|
|
CommandTypeShipGroupJoinFleet CommandType = "shipGroupJoinFleet"
|
|
CommandTypeFleetMerge CommandType = "fleetMerge"
|
|
CommandTypeFleetSend CommandType = "fleetSend"
|
|
CommandTypeScienceCreate CommandType = "scienceCreate"
|
|
CommandTypeScienceRemove CommandType = "scienceRemove"
|
|
CommandTypePlanetRename CommandType = "planetRename"
|
|
CommandTypePlanetProduce CommandType = "planetProduce"
|
|
CommandTypePlanetRouteSet CommandType = "planetRouteSet"
|
|
CommandTypePlanetRouteRemove CommandType = "planetRouteRemove"
|
|
)
|
|
|
|
func (ct CommandType) String() string {
|
|
return string(ct)
|
|
}
|
|
|
|
type DecodableCommand interface {
|
|
CommandID() string
|
|
CommandType() CommandType
|
|
}
|
|
|
|
type CommandMeta struct {
|
|
CmdType CommandType `json:"@type" binding:"notblank"`
|
|
CmdID string `json:"cmdId" binding:"required,uuid_rfc4122"`
|
|
CmdApplied *bool `json:"cmdApplied,omitempty"`
|
|
CmdErrCode *int `json:"cmdErrorCode,omitempty"`
|
|
CmdErrMsg *string `json:"cmdErrorMessage,omitempty"`
|
|
}
|
|
|
|
func (cm CommandMeta) CommandType() CommandType {
|
|
return cm.CmdType
|
|
}
|
|
|
|
func (cm CommandMeta) CommandID() string {
|
|
return cm.CmdID
|
|
}
|
|
|
|
// Result records the per-command outcome on the meta. errCode == 0 marks
|
|
// the command as applied and clears CmdErrMsg; non-zero records the
|
|
// rejection along with the human-readable engine message so clients can
|
|
// surface the reason without their own code-to-text catalog.
|
|
func (cm *CommandMeta) Result(errCode int, errMsg string) {
|
|
cm.CmdErrCode = &errCode
|
|
cm.CmdApplied = new(bool(errCode == 0))
|
|
if errCode == 0 {
|
|
cm.CmdErrMsg = nil
|
|
return
|
|
}
|
|
cm.CmdErrMsg = &errMsg
|
|
}
|
|
|
|
type CommandRaceQuit struct {
|
|
CommandMeta
|
|
}
|
|
|
|
type CommandRaceVote struct {
|
|
CommandMeta
|
|
Acceptor string `json:"acceptor" binding:"notblank,entity"`
|
|
}
|
|
|
|
type CommandRaceRelation struct {
|
|
CommandMeta
|
|
Acceptor string `json:"acceptor" binding:"notblank,entity"`
|
|
Relation string `json:"relation" binding:"oneof=WAR PEACE"`
|
|
}
|
|
|
|
type CommandShipClassCreate struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"notblank,entity"`
|
|
Drive float64 `json:"drive" binding:"eq=0|gte=1"`
|
|
Armament int `json:"armament" binding:"ammoWeapons=Weapons"`
|
|
Weapons float64 `json:"weapons" binding:"ammoWeapons=Armament"`
|
|
Shields float64 `json:"shields" binding:"eq=0|gte=1"`
|
|
Cargo float64 `json:"cargo" binding:"eq=0|gte=1"`
|
|
}
|
|
|
|
type CommandShipClassMerge struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"notblank,entity,nefield=Target"`
|
|
Target string `json:"target" binding:"notblank,entity,nefield=Name"`
|
|
}
|
|
|
|
type CommandShipClassRemove struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
}
|
|
|
|
type CommandShipGroupLoad struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Cargo string `json:"cargo" binding:"oneof=COL MAT CAP"`
|
|
Quantity float64 `json:"quantity" binding:"gte=0"`
|
|
}
|
|
|
|
type CommandShipGroupUnload struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Quantity float64 `json:"quantity" binding:"gte=0"`
|
|
}
|
|
|
|
type CommandShipGroupSend struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Destination int `json:"planetNumber" binding:"gte=0"`
|
|
}
|
|
|
|
type CommandShipGroupUpgrade struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Tech string `json:"tech" binding:"oneof=ALL DRIVE WEAPONS SHIELDS CARGO"`
|
|
Level float64 `json:"level" binding:"eq=0|gt=1"`
|
|
}
|
|
|
|
type CommandShipGroupMerge struct {
|
|
CommandMeta
|
|
}
|
|
|
|
type CommandShipGroupBreak struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"uuid_rfc4122,nefield=NewID"`
|
|
NewID string `json:"newId" binding:"uuid_rfc4122,nefield=ID"`
|
|
Quantity int `json:"quantity" binding:"gte=0"`
|
|
}
|
|
|
|
type CommandShipGroupDismantle struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
}
|
|
|
|
type CommandShipGroupTransfer struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Acceptor string `json:"acceptor" binding:"required,notblank,entity"`
|
|
}
|
|
|
|
type CommandShipGroupJoinFleet struct {
|
|
CommandMeta
|
|
ID string `json:"id" binding:"required,uuid_rfc4122"`
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
}
|
|
|
|
type CommandFleetMerge struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"required,notblank,entity,nefield=Target"`
|
|
Target string `json:"target" binding:"required,notblank,entity,nefield=Name"`
|
|
}
|
|
|
|
type CommandFleetSend struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
Destination int `json:"planetNumber" binding:"gte=0"`
|
|
}
|
|
|
|
type CommandScienceCreate struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
Drive float64 `json:"drive" binding:"gte=0,lte=1"`
|
|
Weapons float64 `json:"weapons" binding:"gte=0,lte=1"`
|
|
Shields float64 `json:"shields" binding:"gte=0,lte=1"`
|
|
Cargo float64 `json:"cargo" binding:"gte=0,lte=1"`
|
|
}
|
|
|
|
type CommandScienceRemove struct {
|
|
CommandMeta
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
}
|
|
|
|
type CommandPlanetRename struct {
|
|
CommandMeta
|
|
Number int `json:"planetNumber" binding:"gte=0"`
|
|
Name string `json:"name" binding:"required,notblank,entity"`
|
|
}
|
|
|
|
type CommandPlanetProduce struct {
|
|
CommandMeta
|
|
Number int `json:"planetNumber" binding:"gte=0"`
|
|
Production string `json:"production" binding:"oneof=MAT CAP DRIVE WEAPONS SHIELDS CARGO SCIENCE SHIP"`
|
|
Subject string `json:"subject" binding:"subject=Production"`
|
|
}
|
|
|
|
type CommandPlanetRouteSet struct {
|
|
CommandMeta
|
|
Origin int `json:"fromPlanetNumber" binding:"gte=0,nefield=Destination"`
|
|
Destination int `json:"toPlanetNumber" binding:"gte=0,nefield=Origin"`
|
|
LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"`
|
|
}
|
|
|
|
type CommandPlanetRouteRemove struct {
|
|
CommandMeta
|
|
Origin int `json:"fromPlanetNumber" binding:"gte=0"`
|
|
LoadType string `json:"loadType" binding:"oneof=MAT CAP COL EMP"`
|
|
}
|