ui/phase-20: ship-group inspector actions

Eight ship-group operations land on the inspector behind a single
inline-form panel: split, send, load, unload, modernize, dismantle,
transfer, join fleet. Each action either appends a typed command to
the local order draft or surfaces a tooltip explaining the
disabled state. Partial-ship operations emit an implicit
breakShipGroup command before the targeted action so the engine
sees a clean (Break, Action) pair on the wire.

`pkg/calc.BlockUpgradeCost` migrates from
`game/internal/controller/ship_group_upgrade.go` so the calc
bridge can wrap a pure pkg/calc formula; the controller now
imports it. The bridge surfaces the function as
`core.blockUpgradeCost`, which the inspector calls once per ship
block to render the modernize cost preview.

`GameReport.otherRaces` is decoded from the report's player block
(non-extinct, ≠ self) and feeds the transfer-to-race picker. The
planet inspector's stationed-ship rows become clickable for own
groups so the actions panel is reachable from the standard click
flow (the renderer continues to hide on-planet groups).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-10 16:27:55 +02:00
parent f7109af55c
commit 3626998a33
36 changed files with 4033 additions and 89 deletions
+68 -1
View File
@@ -24,7 +24,12 @@
import type { Cache } from "../platform/store/index";
import type { GalaxyClient } from "../api/galaxy-client";
import { fetchOrder } from "./order-load";
import type { CommandStatus, OrderCommand } from "./order-types";
import {
isShipGroupCargo,
isShipGroupUpgradeTech,
type CommandStatus,
type OrderCommand,
} from "./order-types";
import { submitOrder } from "./submit";
import { validateEntityName } from "$lib/util/entity-name";
import { validateShipClass } from "$lib/util/ship-class-validation";
@@ -513,6 +518,68 @@ function validateCommand(cmd: OrderCommand): CommandStatus {
// active production / ship groups. Local validation only
// guards the name shape.
return validateEntityName(cmd.name).ok ? "valid" : "invalid";
case "breakShipGroup":
// Engine rule (`controller/ship_group.go.breakGroup`):
// quantity must be at least 1 and strictly less than the
// source group size. We do not know the source size here
// (it lives on the report), so the inspector enforces the
// upper bound before emitting; locally we only refuse the
// degenerate cases — non-positive `quantity`, missing or
// equal UUIDs.
if (cmd.quantity <= 0) return "invalid";
if (!isUuid(cmd.groupId) || !isUuid(cmd.newGroupId)) return "invalid";
if (cmd.groupId === cmd.newGroupId) return "invalid";
return "valid";
case "sendShipGroup":
// Reach is enforced by the picker before the command lands
// in the draft. Locally we only refuse a degenerate
// destination (the engine uses planet number `0` as the
// "no planet" sentinel; FBS encodes as `int64`, so any
// strictly-positive number is wire-valid).
if (cmd.destinationPlanetNumber <= 0) return "invalid";
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "loadShipGroup":
// Cargo type and quantity are pre-checked by the inspector
// against the planet stock and the group's free capacity;
// local validation only guards the wire-valid shape.
if (!isShipGroupCargo(cmd.cargo)) return "invalid";
if (cmd.quantity <= 0) return "invalid";
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "unloadShipGroup":
if (cmd.quantity <= 0) return "invalid";
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "upgradeShipGroup":
// Engine rule
// (`controller/ship_group_upgrade.go.shipGroupUpgrade:56`):
// `tech === "ALL"` requires `level === 0`; per-block tech
// requires a strictly positive level. The inspector also
// caps the level to the player's race tech, but the
// engine re-validates server-side.
if (!isShipGroupUpgradeTech(cmd.tech)) return "invalid";
if (cmd.tech === "ALL") {
if (cmd.level !== 0) return "invalid";
} else if (cmd.level <= 0) {
return "invalid";
}
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "dismantleShipGroup":
return isUuid(cmd.groupId) ? "valid" : "invalid";
case "transferShipGroup":
// `acceptor` is a race name; race names follow the same
// entity-name rules as planet/fleet names. The inspector
// restricts the picker to `GameReport.otherRaces`, so a
// locally-valid name is always a real race.
if (!validateEntityName(cmd.acceptor).ok) return "invalid";
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "joinFleetShipGroup":
if (!validateEntityName(cmd.name).ok) return "invalid";
if (!isUuid(cmd.groupId)) return "invalid";
return "valid";
case "placeholder":
// Phase 12 placeholder entries are content-free and never
// transition out of `draft` — they are not submittable.
+157
View File
@@ -18,8 +18,18 @@ import {
CommandPlanetRouteSet,
CommandShipClassCreate,
CommandShipClassRemove,
CommandShipGroupBreak,
CommandShipGroupDismantle,
CommandShipGroupJoinFleet,
CommandShipGroupLoad,
CommandShipGroupSend,
CommandShipGroupTransfer,
CommandShipGroupUnload,
CommandShipGroupUpgrade,
PlanetProduction,
PlanetRouteLoadType,
ShipGroupCargo,
ShipGroupUpgradeTech,
UserGamesOrderGet,
UserGamesOrderGetResponse,
} from "../proto/galaxy/fbs/order";
@@ -27,6 +37,8 @@ import type {
CargoLoadType,
OrderCommand,
ProductionType,
ShipGroupCargo as ShipGroupCargoLiteral,
ShipGroupUpgradeTech as ShipGroupUpgradeTechLiteral,
} from "./order-types";
const MESSAGE_TYPE = "user.games.order.get";
@@ -222,6 +234,102 @@ function decodeCommand(item: CommandItemView): OrderCommand | null {
name: inner.name() ?? "",
};
}
case CommandPayload.CommandShipGroupBreak: {
const inner = new CommandShipGroupBreak();
item.payload(inner);
return {
kind: "breakShipGroup",
id,
groupId: inner.id() ?? "",
newGroupId: inner.newId() ?? "",
quantity: Number(inner.quantity()),
};
}
case CommandPayload.CommandShipGroupSend: {
const inner = new CommandShipGroupSend();
item.payload(inner);
return {
kind: "sendShipGroup",
id,
groupId: inner.id() ?? "",
destinationPlanetNumber: Number(inner.destination()),
};
}
case CommandPayload.CommandShipGroupLoad: {
const inner = new CommandShipGroupLoad();
item.payload(inner);
const cargo = shipGroupCargoFromFBS(inner.cargo());
if (cargo === null) {
console.warn(
`fetchOrder: skipping CommandShipGroupLoad with unknown cargo enum (${inner.cargo()})`,
);
return null;
}
return {
kind: "loadShipGroup",
id,
groupId: inner.id() ?? "",
cargo,
quantity: inner.quantity(),
};
}
case CommandPayload.CommandShipGroupUnload: {
const inner = new CommandShipGroupUnload();
item.payload(inner);
return {
kind: "unloadShipGroup",
id,
groupId: inner.id() ?? "",
quantity: inner.quantity(),
};
}
case CommandPayload.CommandShipGroupUpgrade: {
const inner = new CommandShipGroupUpgrade();
item.payload(inner);
const tech = shipGroupUpgradeTechFromFBS(inner.tech());
if (tech === null) {
console.warn(
`fetchOrder: skipping CommandShipGroupUpgrade with unknown tech enum (${inner.tech()})`,
);
return null;
}
return {
kind: "upgradeShipGroup",
id,
groupId: inner.id() ?? "",
tech,
level: inner.level(),
};
}
case CommandPayload.CommandShipGroupDismantle: {
const inner = new CommandShipGroupDismantle();
item.payload(inner);
return {
kind: "dismantleShipGroup",
id,
groupId: inner.id() ?? "",
};
}
case CommandPayload.CommandShipGroupTransfer: {
const inner = new CommandShipGroupTransfer();
item.payload(inner);
return {
kind: "transferShipGroup",
id,
groupId: inner.id() ?? "",
acceptor: inner.acceptor() ?? "",
};
}
case CommandPayload.CommandShipGroupJoinFleet: {
const inner = new CommandShipGroupJoinFleet();
item.payload(inner);
return {
kind: "joinFleetShipGroup",
id,
groupId: inner.id() ?? "",
name: inner.name() ?? "",
};
}
default:
console.warn(
`fetchOrder: skipping unknown command kind (payloadType=${payloadType})`,
@@ -288,6 +396,55 @@ export function cargoLoadTypeFromFBS(
}
}
/**
* shipGroupCargoFromFBS reverses `shipGroupCargoToFBS` from
* `submit.ts`. `ShipGroupCargo.UNKNOWN` and any out-of-band value
* yield `null` so the caller drops the entry rather than
* fabricating a synthetic cargo type.
*/
export function shipGroupCargoFromFBS(
value: ShipGroupCargo,
): ShipGroupCargoLiteral | null {
switch (value) {
case ShipGroupCargo.COL:
return "COL";
case ShipGroupCargo.CAP:
return "CAP";
case ShipGroupCargo.MAT:
return "MAT";
case ShipGroupCargo.UNKNOWN:
return null;
default:
return null;
}
}
/**
* shipGroupUpgradeTechFromFBS reverses `shipGroupUpgradeTechToFBS`
* from `submit.ts`. `ShipGroupUpgradeTech.UNKNOWN` and any
* out-of-band value yield `null`.
*/
export function shipGroupUpgradeTechFromFBS(
value: ShipGroupUpgradeTech,
): ShipGroupUpgradeTechLiteral | null {
switch (value) {
case ShipGroupUpgradeTech.ALL:
return "ALL";
case ShipGroupUpgradeTech.DRIVE:
return "DRIVE";
case ShipGroupUpgradeTech.WEAPONS:
return "WEAPONS";
case ShipGroupUpgradeTech.SHIELDS:
return "SHIELDS";
case ShipGroupUpgradeTech.CARGO:
return "CARGO";
case ShipGroupUpgradeTech.UNKNOWN:
return null;
default:
return null;
}
}
function decodeError(
payload: Uint8Array,
resultCode: string,
+212 -1
View File
@@ -166,6 +166,209 @@ export interface RemoveShipClassCommand {
readonly name: string;
}
/**
* ShipGroupCargo mirrors the engine `ShipGroupCargo` enum
* (`pkg/schema/fbs/order.fbs`). Three values: colonists, capital
* (industry crates), and materials. Empty (`EMP`) is a route-level
* concept (`PlanetRouteLoadType`) and is not a valid cargo type for a
* ship-group load command — the FBS enum deliberately omits it.
*/
export type ShipGroupCargo = "COL" | "CAP" | "MAT";
/**
* SHIP_GROUP_CARGO_VALUES is the canonical tuple of `ShipGroupCargo`
* literals. Used by validators and the FBS converters in
* `submit.ts` and `order-load.ts` to narrow incoming strings.
*/
export const SHIP_GROUP_CARGO_VALUES = [
"COL",
"CAP",
"MAT",
] as const satisfies readonly ShipGroupCargo[];
/**
* isShipGroupCargo narrows an arbitrary string to the
* `ShipGroupCargo` union.
*/
export function isShipGroupCargo(value: string): value is ShipGroupCargo {
return (SHIP_GROUP_CARGO_VALUES as readonly string[]).includes(value);
}
/**
* ShipGroupUpgradeTech mirrors the engine `ShipGroupUpgradeTech`
* enum (`pkg/schema/fbs/order.fbs`): `ALL` upgrades every applicable
* block to the player's current race tech (level argument must be 0
* — see `controller/ship_group_upgrade.go:56`); the four per-block
* values upgrade exactly that block to the requested level.
*/
export type ShipGroupUpgradeTech =
| "ALL"
| "DRIVE"
| "WEAPONS"
| "SHIELDS"
| "CARGO";
/**
* SHIP_GROUP_UPGRADE_TECH_VALUES is the canonical tuple of
* `ShipGroupUpgradeTech` literals. The order matches the FBS enum.
*/
export const SHIP_GROUP_UPGRADE_TECH_VALUES = [
"ALL",
"DRIVE",
"WEAPONS",
"SHIELDS",
"CARGO",
] as const satisfies readonly ShipGroupUpgradeTech[];
/**
* isShipGroupUpgradeTech narrows an arbitrary string to the
* `ShipGroupUpgradeTech` union.
*/
export function isShipGroupUpgradeTech(
value: string,
): value is ShipGroupUpgradeTech {
return (SHIP_GROUP_UPGRADE_TECH_VALUES as readonly string[]).includes(value);
}
/**
* BreakShipGroupCommand splits a player-owned ship group into two:
* the original keeps `originalCount - quantity` ships and a new group
* with `newGroupId` carries `quantity`. Used both as a stand-alone
* action and as the implicit prelude to Send / Load / Unload /
* Modernize / Dismantle / Transfer when the player picks fewer than
* all ships. Engine rules (`controller/ship_group.go.breakGroup`):
* source group must be `StateInOrbit`, `quantity` must be in `[1,
* originalCount - 1]` for a real split. The new group carries a
* proportional slice of the cargo and starts unattached to any fleet.
*/
export interface BreakShipGroupCommand {
readonly kind: "breakShipGroup";
readonly id: string;
readonly groupId: string;
readonly newGroupId: string;
readonly quantity: number;
}
/**
* SendShipGroupCommand launches a player-owned ship group toward a
* destination planet. Engine rules
* (`controller/ship_group_send.go.shipGroupSend`): group must be
* `StateInOrbit`; ship class must have a non-zero drive block; the
* destination must be within the player's current
* `FlightDistance() = localPlayerDrive * 40` (torus-aware).
* The picker filters the planet list before emitting, so a draft
* entry that survives validation is always reachable at submit time.
*/
export interface SendShipGroupCommand {
readonly kind: "sendShipGroup";
readonly id: string;
readonly groupId: string;
readonly destinationPlanetNumber: number;
}
/**
* LoadShipGroupCommand loads cargo of one of the three ship-group
* cargo types onto a player-owned group. Engine rules
* (`controller/ship_group.go.shipGroupLoad`): group must be
* `StateInOrbit`; planet must be owned by the player or unowned;
* ship class must have a non-zero cargo block; the existing cargo
* type (if any) must equal `cargo`; `quantity` is bounded by the
* planet's stock and the group's free capacity. The inspector
* pre-checks each of these so a draft entry is always wire-valid.
*/
export interface LoadShipGroupCommand {
readonly kind: "loadShipGroup";
readonly id: string;
readonly groupId: string;
readonly cargo: ShipGroupCargo;
readonly quantity: number;
}
/**
* UnloadShipGroupCommand drops cargo from a player-owned group at
* its current orbit. Engine rules
* (`controller/ship_group.go.shipGroupUnload`): group must be
* `StateInOrbit`; ship class must have a non-zero cargo block; group
* must currently carry cargo. Colonists (`COL`) cannot be unloaded
* over a foreign planet — the inspector disables the action with a
* tooltip in that case. The cargo type is implicit (whatever the
* group is carrying); only `quantity` is sent on the wire.
*/
export interface UnloadShipGroupCommand {
readonly kind: "unloadShipGroup";
readonly id: string;
readonly groupId: string;
readonly quantity: number;
}
/**
* UpgradeShipGroupCommand schedules a tech upgrade for a player-
* owned group at its current orbit. Engine rules
* (`controller/ship_group_upgrade.go.shipGroupUpgrade`): group must
* be `StateInOrbit`; the planet must be owned by the player or
* unowned; for per-block techs the requested `level` must be in
* `(group.tech, race.tech]`; for `tech === "ALL"` the `level` must
* be 0 (the engine fans the upgrade out to every block whose mass is
* non-zero). The inspector renders a live cost preview through
* `core.blockUpgradeCost` to make the production cost visible before
* the player commits.
*/
export interface UpgradeShipGroupCommand {
readonly kind: "upgradeShipGroup";
readonly id: string;
readonly groupId: string;
readonly tech: ShipGroupUpgradeTech;
readonly level: number;
}
/**
* DismantleShipGroupCommand deconstructs a player-owned group at its
* current orbit, returning the empty mass to the planet's materials
* stockpile. Engine rules (`controller/ship_group.go.shipGroupDismantle`):
* group must be `StateInOrbit`; over a foreign planet, colonists
* (`COL`) on board are *lost* — the inspector surfaces an explicit
* two-step confirmation in that case before adding the command to
* the draft.
*/
export interface DismantleShipGroupCommand {
readonly kind: "dismantleShipGroup";
readonly id: string;
readonly groupId: string;
}
/**
* TransferShipGroupCommand hands a player-owned group to another
* race. Engine rules (`controller/ship_group.go.shipGroupTransfer`):
* acceptor must be a different, non-extinct race; group must not
* already be in `StateTransfer`. The inspector restricts the
* acceptor picker to `GameReport.otherRaces` (non-extinct ≠ self),
* so a draft entry always names a real race.
*/
export interface TransferShipGroupCommand {
readonly kind: "transferShipGroup";
readonly id: string;
readonly groupId: string;
readonly acceptor: string;
}
/**
* JoinFleetShipGroupCommand attaches a player-owned group to a fleet
* (creating it on the fly if no fleet by that name exists). Engine
* rules (`controller/fleet.go.ShipGroupJoinFleet`): group must be
* `StateInOrbit`; the target fleet, when it already exists, must
* sit in the same orbit as the group; `name` must pass
* `validateEntityName`. Because the engine handles the whole-group
* attach atomically (no per-ship counter), this command does not
* support implicit-split — the inspector exposes Split as a
* separate explicit action when partial detachment is desired.
*/
export interface JoinFleetShipGroupCommand {
readonly kind: "joinFleetShipGroup";
readonly id: string;
readonly groupId: string;
readonly name: string;
}
/**
* OrderCommand is the discriminated union of every command shape the
* local order draft can hold. The `kind` field is the discriminator;
@@ -179,7 +382,15 @@ export type OrderCommand =
| SetCargoRouteCommand
| RemoveCargoRouteCommand
| CreateShipClassCommand
| RemoveShipClassCommand;
| RemoveShipClassCommand
| BreakShipGroupCommand
| SendShipGroupCommand
| LoadShipGroupCommand
| UnloadShipGroupCommand
| UpgradeShipGroupCommand
| DismantleShipGroupCommand
| TransferShipGroupCommand
| JoinFleetShipGroupCommand;
/**
* PRODUCTION_TYPE_VALUES is the canonical tuple of `ProductionType`
+158
View File
@@ -33,8 +33,18 @@ import {
CommandPlanetRouteSet,
CommandShipClassCreate,
CommandShipClassRemove,
CommandShipGroupBreak,
CommandShipGroupDismantle,
CommandShipGroupJoinFleet,
CommandShipGroupLoad,
CommandShipGroupSend,
CommandShipGroupTransfer,
CommandShipGroupUnload,
CommandShipGroupUpgrade,
PlanetProduction,
PlanetRouteLoadType,
ShipGroupCargo,
ShipGroupUpgradeTech,
UserGamesOrder,
UserGamesOrderResponse,
} from "../proto/galaxy/fbs/order";
@@ -42,6 +52,8 @@ import type {
CargoLoadType,
OrderCommand,
ProductionType,
ShipGroupCargo as ShipGroupCargoLiteral,
ShipGroupUpgradeTech as ShipGroupUpgradeTechLiteral,
} from "./order-types";
const MESSAGE_TYPE = "user.games.order";
@@ -222,6 +234,109 @@ function encodeCommandPayload(
payloadOffset: offset,
};
}
case "breakShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const newIdOffset = builder.createString(cmd.newGroupId);
CommandShipGroupBreak.startCommandShipGroupBreak(builder);
CommandShipGroupBreak.addId(builder, idOffset);
CommandShipGroupBreak.addNewId(builder, newIdOffset);
CommandShipGroupBreak.addQuantity(builder, BigInt(cmd.quantity));
const offset = CommandShipGroupBreak.endCommandShipGroupBreak(builder);
return {
payloadType: CommandPayload.CommandShipGroupBreak,
payloadOffset: offset,
};
}
case "sendShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const offset = CommandShipGroupSend.createCommandShipGroupSend(
builder,
idOffset,
BigInt(cmd.destinationPlanetNumber),
);
return {
payloadType: CommandPayload.CommandShipGroupSend,
payloadOffset: offset,
};
}
case "loadShipGroup": {
const idOffset = builder.createString(cmd.groupId);
CommandShipGroupLoad.startCommandShipGroupLoad(builder);
CommandShipGroupLoad.addId(builder, idOffset);
CommandShipGroupLoad.addCargo(builder, shipGroupCargoToFBS(cmd.cargo));
CommandShipGroupLoad.addQuantity(builder, cmd.quantity);
const offset = CommandShipGroupLoad.endCommandShipGroupLoad(builder);
return {
payloadType: CommandPayload.CommandShipGroupLoad,
payloadOffset: offset,
};
}
case "unloadShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const offset = CommandShipGroupUnload.createCommandShipGroupUnload(
builder,
idOffset,
cmd.quantity,
);
return {
payloadType: CommandPayload.CommandShipGroupUnload,
payloadOffset: offset,
};
}
case "upgradeShipGroup": {
const idOffset = builder.createString(cmd.groupId);
CommandShipGroupUpgrade.startCommandShipGroupUpgrade(builder);
CommandShipGroupUpgrade.addId(builder, idOffset);
CommandShipGroupUpgrade.addTech(
builder,
shipGroupUpgradeTechToFBS(cmd.tech),
);
CommandShipGroupUpgrade.addLevel(builder, cmd.level);
const offset = CommandShipGroupUpgrade.endCommandShipGroupUpgrade(builder);
return {
payloadType: CommandPayload.CommandShipGroupUpgrade,
payloadOffset: offset,
};
}
case "dismantleShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const offset =
CommandShipGroupDismantle.createCommandShipGroupDismantle(
builder,
idOffset,
);
return {
payloadType: CommandPayload.CommandShipGroupDismantle,
payloadOffset: offset,
};
}
case "transferShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const acceptorOffset = builder.createString(cmd.acceptor);
const offset = CommandShipGroupTransfer.createCommandShipGroupTransfer(
builder,
idOffset,
acceptorOffset,
);
return {
payloadType: CommandPayload.CommandShipGroupTransfer,
payloadOffset: offset,
};
}
case "joinFleetShipGroup": {
const idOffset = builder.createString(cmd.groupId);
const nameOffset = builder.createString(cmd.name);
const offset =
CommandShipGroupJoinFleet.createCommandShipGroupJoinFleet(
builder,
idOffset,
nameOffset,
);
return {
payloadType: CommandPayload.CommandShipGroupJoinFleet,
payloadOffset: offset,
};
}
case "placeholder":
throw new SubmitError(
"invalid_request",
@@ -277,6 +392,49 @@ export function cargoLoadTypeToFBS(value: CargoLoadType): PlanetRouteLoadType {
}
}
/**
* shipGroupCargoToFBS converts the wire-stable `ShipGroupCargo`
* literal to the FlatBuffers enum value. Mirrors the engine
* `ShipGroupCargo` enum (`pkg/schema/fbs/order.fbs`). The FBS enum
* carries an `UNKNOWN` zero value as the default; the encoder always
* emits one of the three real values.
*/
export function shipGroupCargoToFBS(
value: ShipGroupCargoLiteral,
): ShipGroupCargo {
switch (value) {
case "COL":
return ShipGroupCargo.COL;
case "CAP":
return ShipGroupCargo.CAP;
case "MAT":
return ShipGroupCargo.MAT;
}
}
/**
* shipGroupUpgradeTechToFBS converts the wire-stable
* `ShipGroupUpgradeTech` literal to the FlatBuffers enum value.
* Mirrors the engine `ShipGroupUpgradeTech` enum
* (`pkg/schema/fbs/order.fbs`).
*/
export function shipGroupUpgradeTechToFBS(
value: ShipGroupUpgradeTechLiteral,
): ShipGroupUpgradeTech {
switch (value) {
case "ALL":
return ShipGroupUpgradeTech.ALL;
case "DRIVE":
return ShipGroupUpgradeTech.DRIVE;
case "WEAPONS":
return ShipGroupUpgradeTech.WEAPONS;
case "SHIELDS":
return ShipGroupUpgradeTech.SHIELDS;
case "CARGO":
return ShipGroupUpgradeTech.CARGO;
}
}
function decodeOrderResponse(
payload: Uint8Array,
commands: OrderCommand[],