editor

package
v1.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 16, 2026 License: GPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package editor implements an in-memory inventory edit session — a read-only workspace built from a parsed SaveSlot that mirrors the inventory and storage views.

Phase 1 is intentionally non-mutating: the workspace exists only to support future reorder/add/transfer/weapon-edit flows which will rebuild the slot in a single Save step. Nothing in this package writes to slot.Data or to the in-memory slot structs.

Index

Constants

View Source
const (
	SeverityError   = "error"
	SeverityWarning = "warning"
)

Severity constants.

View Source
const (
	CodeDuplicateUID             = "duplicate_uid"
	CodeDuplicateHandle          = "duplicate_handle"
	CodeUnknownItemID            = "unknown_item_id"
	CodeQuantityZero             = "quantity_zero"
	CodeUpgradeOutOfRange        = "upgrade_out_of_range"
	CodeCategoryUnsupported      = "category_unsupported"
	CodePassThroughRecords       = "pass_through_records"
	CodeSharedAoWConflict        = "shared_aow_conflict"
	CodeSharedAoWNotChecked      = "shared_aow_not_checked"
	CodePendingAoWUnknown        = "pending_aow_unknown"
	CodePendingAoWConflict       = "pending_aow_conflict"
	CodeCurrentAoWMissing        = "current_aow_missing"
	CodeCurrentAoWShared         = "current_aow_shared"
	CodeCurrentAoWNonAoWCategory = "current_aow_non_aow_category"
)

Validation issue codes. Frontend / tests use these as stable keys.

View Source
const (
	ReasonUnsupportedCategory  = "unsupported_category"
	ReasonUnknownItem          = "unknown_item"
	ReasonTechnicalPlaceholder = "technical_placeholder"
	ReasonDuplicateHandle      = "duplicate_handle"
)

Stable Reason codes attached to RawInventoryRecord. Tests assert against these so they must be kept stable.

View Source
const (
	AoWStatusNone    = "none"    // weapon has no custom AoW (sentinel handle)
	AoWStatusCustom  = "custom"  // weapon has a custom AoW that resolves to a known ashes_of_war DB entry
	AoWStatusMissing = "missing" // weapon's AoW handle is non-sentinel but cannot be resolved (orphan / dangling)
	AoWStatusShared  = "shared"  // weapon's AoW handle is referenced by another weapon (save corruption)
)

CurrentAoWStatus values surface read-side AoW state on editable weapons. Empty means the field is not relevant (non-weapon or never populated). All others apply only to weapon-editable items.

Variables

View Source
var ErrSessionClosed = errors.New("inventory edit session is closed")

ErrSessionClosed is returned by InventoryEditSession.Acquire when the session has been discarded (Close) before the caller could lock it. Callers map this to the same wire-level message as a missing session so the frontend's existing self-heal path treats both identically.

View Source
var SupportedCategories = map[string]bool{
	"melee_armaments":      true,
	"ranged_and_catalysts": true,
	"shields":              true,
	"talismans":            true,
	"head":                 true,
	"chest":                true,
	"arms":                 true,
	"legs":                 true,
}

SupportedCategories is the conservative Phase 1 allow-list. Everything outside this set is preserved as a RawInventoryRecord (pass-through) and cannot be edited.

Functions

func AddItem

func AddItem(snap *InventoryWorkspaceSnapshot, spec AddItemSpec, targetContainer ContainerKind, targetPosition int) error

AddItem inserts a new editable item at targetPosition in targetContainer.

Errors:

  • nil snapshot
  • targetContainer not "inventory"/"storage"
  • spec missing both ItemID and BaseItemID
  • itemID unknown in DB
  • category outside SupportedCategories (Phase 1 allow-list)

targetPosition is clamped — negative → 0, past length → append. Positions are recomputed for both editable slices. Pass-through records are never touched. The snapshot is marked Dirty and Validate is re-run.

func AutoRepairWorkspaceItem added in v1.1.0

func AutoRepairWorkspaceItem(snap *InventoryWorkspaceSnapshot, uid, code string) error

AutoRepairWorkspaceItem applies the appropriate automatic fix for code to the item identified by uid. Delegates to UpdateWeapon so the snapshot is re-validated and marked Dirty on success. Returns an error for non-repairable codes or unknown UIDs.

func CanRepairCode added in v1.1.0

func CanRepairCode(code string) bool

CanRepairCode reports whether code can be auto-repaired without binary patching (i.e. via AutoRepairWorkspaceItem).

func ClampUpgrade

func ClampUpgrade(requested, max int) int

ClampUpgrade bounds a requested upgrade level to [0, max]. Used by the add path so the encoded item ID can never carry a level above the item's real MaxUpgrade (which would produce a permanently out-of-range item). Relocated from app.go so backend packages (e.g. the templates plan layer planned in spec/56 §14.4) can reuse it without pulling in the main package.

func ComputeBaseRevision

func ComputeBaseRevision(slot *core.SaveSlot) string

ComputeBaseRevision derives a content marker from a few stable slot fields. It exists so the frontend / save path can detect if the underlying slot has changed between Start and a future Save (e.g. the user reloaded the save file). It is not a security hash.

func MoveItem

func MoveItem(snap *InventoryWorkspaceSnapshot, uid string, targetContainer ContainerKind, targetPosition int) error

MoveItem relocates an editable item inside the workspace.

The item is located by UID across both InventoryItems and StorageItems, removed from its current slice, has its Container field updated, and is re-inserted into the target slice at the requested position. Pass-through (unsupported) records are never touched.

targetPosition is clamped — values below 0 land at 0, values past the (post-removal) target length append at the end. Positions are recomputed for both editable slices after the move so the snapshot remains consistent. The snapshot is marked Dirty and Validate is re-run; the resulting report is stored on snap.Validation.

Returns an error if the snapshot is nil, the target container is not "inventory"/"storage", or the UID does not match any editable item.

func NewSessionID

func NewSessionID() string

NewSessionID returns a short random hex token used as session identifier.

8 random bytes give a 128-bit space — far more than enough since App limits one active session per character (≤ 10 concurrent sessions). The crypto/rand failure path falls back to a nanosecond timestamp so startup never blocks on a missing entropy source.

func RemoveItem

func RemoveItem(snap *InventoryWorkspaceSnapshot, uid string) error

RemoveItem deletes an editable item from the workspace by UID.

Pass-through records are never affected. Positions are recomputed for both editable slices after the removal. Dirty is set and Validate is re-run. Returns an error if the snapshot is nil or the UID is unknown.

func ReorderItems added in v1.3.0

func ReorderItems(snap *InventoryWorkspaceSnapshot, inventoryUIDs, storageUIDs []string) error

ReorderItems atomically replaces the ordering of BOTH editable containers from full desired UID lists.

Each list must be an exact permutation of the editable items currently in that container: same length, no duplicates, no missing UID, no foreign UID, and no UID belonging to the other container. Both lists are validated in full before anything is mutated, so a rejected request leaves both containers byte-for-byte unchanged. Pass-through (unsupported) records, quantities and item data are never touched.

On success the editable Inventory and Storage slices are rebuilt in the requested order, positions are recomputed, Dirty is set, and Validate runs exactly once.

func RepairDescForIssue added in v1.1.0

func RepairDescForIssue(snap *InventoryWorkspaceSnapshot, iss WorkspaceValidationIssue) string

RepairDescForIssue returns a human-readable description of what AutoRepairWorkspaceItem will do for iss. Returns "" for non-repairable codes or when the item is not found in snap.

func UpdateWeapon

func UpdateWeapon(snap *InventoryWorkspaceSnapshot, uid string, patch WeaponPatch) error

UpdateWeapon applies a WeaponPatch to the editable item identified by uid. It mutates the EditableItem in place, marks the snapshot Dirty, and re-runs Validate.

Errors:

  • nil snapshot
  • unknown UID
  • item is not weapon-editable (must satisfy IsWeapon)
  • SetUpgrade with value outside [0, MaxUpgrade]
  • SetInfusionName with an infusion name not in db.InfuseTypes
  • SetAoWItemID with a non-zero ID that is not a known ashes_of_war DB entry

On error nothing in the snapshot is mutated; the caller's prior state is preserved. (We validate up front before touching the item.)

Types

type AddItemSpec

type AddItemSpec struct {
	ItemID       uint32 `json:"itemID"`
	BaseItemID   uint32 `json:"baseItemID"`
	Quantity     uint32 `json:"quantity"`
	Upgrade      int    `json:"upgrade"`
	InfusionName string `json:"infusionName"`
	AoWItemID    uint32 `json:"aowItemID"`
}

AddItemSpec describes a request to add a new editable item to the workspace.

Resolution rules:

  • ItemID, if non-zero, is treated as the effective (already-encoded) item ID; CurrentUpgrade/InfusionName are decoded from it via the same helper used for existing items.
  • ItemID == 0 falls back to BaseItemID. No upgrade/infusion encoding happens in Phase 1.6.
  • Both zero is an error.
  • Quantity == 0 normalizes to 1 (user-friendly: a new item must hold at least one unit; the workspace has no concept of "zero stack").

Phase 1.6 limitations:

  • spec.Upgrade / spec.InfusionName / spec.AoWItemID are reserved for Phase 4 weapon-edit encoding and are NOT consulted here. Frontend wishing to add a +N weapon must encode the effective ItemID itself.
  • No real handle is allocated — OriginalHandle stays 0, HasGaItem is false. The eventual Save step will mint a real handle.

type ContainerKind

type ContainerKind string

ContainerKind identifies which container an item lives in.

const (
	ContainerInventory ContainerKind = "inventory"
	ContainerStorage   ContainerKind = "storage"
)

type EditableItem

type EditableItem struct {
	UID            string        `json:"uid"`
	Source         ItemSource    `json:"source"`
	Container      ContainerKind `json:"container"`
	Position       int           `json:"position"`
	OriginalHandle uint32        `json:"originalHandle"`
	ItemID         uint32        `json:"itemID"`
	BaseItemID     uint32        `json:"baseItemID"`
	Name           string        `json:"name"`
	Category       string        `json:"category"`
	// SubCategory is the canonical in-game weapon-class grouping (e.g.
	// "Daggers", "Small Shields", "Bows"), sourced 1:1 from the DB
	// ItemData.SubCategory. Empty for categories without sub-grouping. The
	// Sort Order "Default" sort uses it to reproduce the in-game section
	// hierarchy; it is never inferred from item names in the frontend.
	SubCategory      string  `json:"subCategory,omitempty"`
	Quantity         uint32  `json:"quantity"`
	AcquisitionIndex uint32  `json:"acquisitionIndex"`
	Weight           float64 `json:"weight,omitempty"`
	SortID           uint32  `json:"sortId,omitempty"`
	SortGroupID      uint8   `json:"sortGroupId,omitempty"`
	CurrentUpgrade   int     `json:"currentUpgrade"`
	MaxUpgrade       int     `json:"maxUpgrade"`
	InfusionName     string  `json:"infusionName,omitempty"`
	IconPath         string  `json:"iconPath,omitempty"`
	HasGaItem        bool    `json:"hasGaItem"`
	IsWeapon         bool    `json:"isWeapon"`
	IsArmor          bool    `json:"isArmor"`
	IsTalisman       bool    `json:"isTalisman"`
	// AoW mounting compatibility metadata, populated for weapon-editable
	// items. WepType / CanMountAoW mirror the DB lookups done by
	// vm.MapParsedSlotToVM so the WeaponEditModal can resolve AoW
	// compatibility directly from the workspace item without falling
	// back to GetCharacter (which can desync for newly-added items, or
	// when the handle has been re-allocated by a prior Save).
	WepType               uint16 `json:"wepType,omitempty"`
	CanMountAoW           bool   `json:"canMountAoW,omitempty"`
	DefaultAoWID          int32  `json:"defaultAoWID,omitempty"`
	DefaultAoWName        string `json:"defaultAoWName,omitempty"`
	CurrentAoWHandle      uint32 `json:"currentAoWHandle,omitempty"`
	CurrentAoWItemID      uint32 `json:"currentAoWItemID,omitempty"`
	CurrentAoWName        string `json:"currentAoWName,omitempty"`
	HasCurrentAoW         bool   `json:"hasCurrentAoW,omitempty"`
	CurrentAoWShared      bool   `json:"currentAoWShared,omitempty"`
	CurrentAoWStatus      string `json:"currentAoWStatus,omitempty"`
	PendingAoWItemID      uint32 `json:"pendingAoWItemID,omitempty"`
	PendingAoWName        string `json:"pendingAoWName,omitempty"`
	PendingAoWClear       bool   `json:"pendingAoWClear,omitempty"`
	HasPendingWeaponPatch bool   `json:"hasPendingWeaponPatch,omitempty"`

	// OriginalSlotIndex is the physical CommonItems slot this item was parsed
	// from (-1 for items added in the workspace). The save path replays an
	// unchanged container to these exact slots so a no-op save stays
	// byte-identical to the loaded file.
	OriginalSlotIndex int `json:"originalSlotIndex"`
}

EditableItem is the user-editable representation of an inventory record. All numeric IDs match the DB / game representation.

Pending* fields (Phase 1.7) carry RAM-only requests for weapon edits (Ash of War swap) that have NOT yet been encoded into the binary save. The eventual Save step (Phase 3+) reads these fields to drive the real GaItem patch / handle allocation. Until then they coexist with the existing ItemID/CurrentUpgrade/InfusionName fields, which DO get updated in place by UpdateWeapon when upgrade/infusion change (because those are pure ItemID re-encodings and don't need a handle).

CurrentAoW* fields (Phase 4A) are READ-ONLY mirrors of the AoW that is currently encoded into the weapon's GaItem.AoWGaItemHandle. They are populated by BuildSnapshot for editable weapons and never mutated by editor mutations — they describe slot state at snapshot time, not the user's pending edit. Use Pending* fields to express an unsaved AoW swap; the two coexist until Save resolves the pending request.

type HandlesByUID

type HandlesByUID map[string]uint32

HandlesByUID maps editor UIDs to allocated GaItem handles after a successful ApplyWorkspaceSave. UID format for existing items is "hnd:0x%08X" (handle never changes — kept stable across transfers and reorders), or "hnd:0x%08X:<container>:<slot>" for a handle that repeats across records in the same snapshot (e.g. several copies of the same talisman — see classifyRecord), and "new:N" for added items (handle freshly minted by AddItemsToSlotBatch). Removed items are absent from this map. Callers use this to follow the same item through subsequent edits.

func ApplyWorkspaceSave

func ApplyWorkspaceSave(slot *core.SaveSlot, snap *InventoryWorkspaceSnapshot, baseline map[string]ContainerKind) (HandlesByUID, error)

ApplyWorkspaceSave is the Phase 4B commit path. It validates the workspace, rejects out-of-scope or unsafe edits, then writes a reorder + add + transfer + remove + weapon-upgrade + pending-AoW plan into slot.Data via the wipe-and-replay layout plus targeted GaItem patches.

In scope for Phase 4B (extends Phase 3B):

  • reorder existing editable items within their original container
  • transfer existing editable items between Inventory and Storage — OriginalHandle is preserved, the GaItem stays in place, and only the inventory record is moved; old record is wiped by the layout rebuild
  • remove existing editable items from the workspace — their record is absent from both containers post-save; GaItem stays in slot.GaItems / slot.GaMap (conservative no-GC policy — see notes under "GaItem GC policy" below)
  • add new editable items (Source=Added) — allocates real handles and GaItem entries via core.AddItemsToSlotBatch
  • patch weapon ItemID for existing items with upgrade/infusion changes via core.PatchWeaponItemID (works correctly for transferred weapons too — patch keys on handle, not container)
  • apply pending AoW edits (Phase 4B):
  • PendingAoWItemID != 0 — allocate a fresh AoW GaItem via core.PatchWeaponAoW (never reuses a handle, so two weapons setting the same AoW item get distinct handles)
  • PendingAoWClear == true — patch weapon's AoWGaItemHandle field to the canonical no-custom sentinel via core.PatchWeaponAoWHandle (in-place, no rebuild)
  • preserve unsupported/pass-through records at their original physical SlotIndex

Still rejected with clear errors (slot.Data left untouched):

  • workspace validation errors (including the new CodePendingAoWConflict)
  • any pending AoW set whose AoW item is not category "ashes_of_war"
  • any pending AoW set whose (aow, weapon) compatibility is known and false, or unknown (fail-closed)
  • missing baseline data (session created without baseline tracking)
  • inventory or storage capacity exceeded by the final layout
  • pass-through SlotIndex collisions

GaItem GC policy (Phase 3B):

  • Removed items leave their GaItem record orphaned in slot.GaItems and slot.GaMap. No record in slot.Data references them after the layout rebuild, so they do not show up in any container.
  • We do NOT call RepairOrphanedGaItems automatically. The trade-off is a small amount of wasted GaItem array space versus the safety risk of an under-tested GC sweep mutating shared state.
  • Future Phase 4+ may add explicit GC when AoW work lands and the ownership model is fully understood.

Atomicity contract:

  • Callers MUST snapshot slot via core.SnapshotSlot BEFORE calling this function and call core.RestoreSlot on a non-nil error to roll back partial state.
  • This function does NOT manage its own undo. It only guarantees all rejection checks run BEFORE any mutation; if a check fails, slot.Data is byte-identical to the input.
  • Once writes begin (after the rejection block), an error means slot.Data has been partially mutated. Caller MUST roll back.

type InventoryEditSession

type InventoryEditSession struct {
	ID                      string                     `json:"id"`
	CharacterIndex          int                        `json:"characterIndex"`
	CreatedAt               time.Time                  `json:"createdAt"`
	BaseRevision            string                     `json:"baseRevision"`
	Workspace               InventoryWorkspaceSnapshot `json:"workspace"`
	BaselineEditableHandles map[string]ContainerKind   `json:"-"`
	// contains filtered or unexported fields
}

InventoryEditSession owns a single workspace snapshot for a character.

The session is held in-memory on App and never persisted to disk. BaselineEditableHandles captures the (record UID → container) state at Start time and is consumed by ApplyWorkspaceSave to detect transfers and removals against the original load. It is keyed by EditableItem.UID rather than OriginalHandle: a handle alone does not identify a physical record — talisman handles (and any other item-derived handle) are legitimately shared by multiple records in the same container or split across inventory/storage, and a handle-keyed map can only remember one of them. UID (container+slot+handle) disambiguates every such record. Phase 3B regenerates this after every successful save so subsequent edits diff against the freshly-reparsed state rather than the original load.

Concurrency contract:

  • Lifecycle (insert/replace/delete in the App registry) is governed by a registry-scoped mutex owned by the caller (package main).
  • Mutations and reads of Workspace, BaselineEditableHandles and BaseRevision MUST run under Acquire/Unlock so the multi-step SaveInventoryWorkspaceChanges flow cannot race with concurrent mutators or readers. Acquire also enforces the closed-after-Discard contract: callers that arrive after Close get ErrSessionClosed instead of touching an orphaned struct.

func StartSession

func StartSession(slot *core.SaveSlot, charIdx int) (*InventoryEditSession, error)

StartSession builds a fresh InventoryEditSession from the given slot. The slot is not mutated. Returns the populated session, ready to be stored in App's session map.

func (*InventoryEditSession) Acquire

func (s *InventoryEditSession) Acquire() error

Acquire locks the session for exclusive use. It returns ErrSessionClosed when the session has already been Close()-d, in which case the lock is NOT held and Unlock must NOT be called. On nil error the caller owns the lock and must release it with Unlock — typically via defer.

func (*InventoryEditSession) Close

func (s *InventoryEditSession) Close()

Close marks the session as invalidated. The caller MUST hold the session lock (acquired via Acquire) — Close itself does not lock so the discard path can mark-and-release in one critical section.

Subsequent Acquire calls return ErrSessionClosed. Operations that already passed Acquire continue safely against the (orphan) struct because they hold the lock; once they Unlock the struct is unreachable from the registry and becomes garbage.

func (*InventoryEditSession) IsClosed

func (s *InventoryEditSession) IsClosed() bool

IsClosed reports whether the session has been Close()-d. The check briefly takes the lock so the result reflects a committed state rather than a half-written one — fine for tests / diagnostics, not a substitute for the Acquire failure path on the mutator hot path.

func (*InventoryEditSession) Unlock

func (s *InventoryEditSession) Unlock()

Unlock releases the session lock acquired by Acquire. It is unsafe to call without a prior successful Acquire (matches sync.Mutex semantics).

type InventoryWorkspaceSnapshot

type InventoryWorkspaceSnapshot struct {
	SessionID                   string                    `json:"sessionID"`
	CharacterIndex              int                       `json:"characterIndex"`
	InventoryItems              []EditableItem            `json:"inventoryItems"`
	StorageItems                []EditableItem            `json:"storageItems"`
	UnsupportedInventoryRecords []RawInventoryRecord      `json:"unsupportedInventoryRecords"`
	UnsupportedStorageRecords   []RawInventoryRecord      `json:"unsupportedStorageRecords"`
	Dirty                       bool                      `json:"dirty"`
	Validation                  WorkspaceValidationReport `json:"validation"`
}

InventoryWorkspaceSnapshot is the read-only DTO returned across the Wails boundary. It is a single point-in-time view of an InventoryEditSession's Workspace field.

func BuildSnapshot

func BuildSnapshot(slot *core.SaveSlot, sessionID string, charIdx int) (InventoryWorkspaceSnapshot, error)

BuildSnapshot scans the slot's CommonItems for both containers and produces a sorted, classified InventoryWorkspaceSnapshot. The slot is not mutated.

Ordering matches the legacy GetInventoryOrder/GetStorageOrder behavior: editable items are sorted by AcquisitionIndex ascending and assigned sequential Position values starting at 0. Pass-through records keep their physical SlotIndex.

type ItemSource

type ItemSource string

ItemSource records how an item entered the workspace. Phase 1 only sees ItemSourceOriginal — ItemSourceAdded is reserved for future AddInventoryWorkspaceItem mutations.

const (
	ItemSourceOriginal ItemSource = "original"
	ItemSourceAdded    ItemSource = "added"
)

type RawInventoryRecord

type RawInventoryRecord struct {
	Container        ContainerKind `json:"container"`
	SlotIndex        int           `json:"slotIndex"`
	Handle           uint32        `json:"handle"`
	Quantity         uint32        `json:"quantity"`
	AcquisitionIndex uint32        `json:"acquisitionIndex"`
	ItemID           uint32        `json:"itemID"`
	Name             string        `json:"name,omitempty"`
	Category         string        `json:"category,omitempty"`
	Reason           string        `json:"reason"`
	HasGaItem        bool          `json:"hasGaItem"`
}

RawInventoryRecord captures a record the workspace cannot or should not edit. It carries enough metadata for a future rebuild step to write the bytes back into the same slot index without loss.

type WeaponPatch

type WeaponPatch struct {
	SetUpgrade      bool   `json:"setUpgrade"`
	Upgrade         int    `json:"upgrade"`
	SetInfusionName bool   `json:"setInfusionName"`
	InfusionName    string `json:"infusionName"`
	SetAoWItemID    bool   `json:"setAoWItemID"`
	AoWItemID       uint32 `json:"aowItemID"`
	ClearAoW        bool   `json:"clearAoW"`
}

WeaponPatch is the RAM-only request DTO for UpdateWeapon. Each Set* flag explicitly opts a sub-field into the patch — this avoids pointer fields (which Wails generates as nullable types that surface awkwardly in TypeScript bindings) while still distinguishing "field absent" from "field set to zero".

Semantics:

  • SetUpgrade: Upgrade replaces CurrentUpgrade; ItemID is re-encoded.
  • SetInfusionName: InfusionName replaces the current infusion; ItemID is re-encoded. "" / "Standard" map to the un-infused offset 0.
  • SetAoWItemID: AoWItemID != 0 stores it as PendingAoWItemID (with resolved PendingAoWName) and resets PendingAoWClear. AoWItemID == 0 is treated as a clear (same as ClearAoW): sets PendingAoWClear=true, clears PendingAoWItemID / PendingAoWName.
  • ClearAoW: sets PendingAoWClear=true and clears PendingAoWItemID / PendingAoWName. Distinct from "no pending edit" — Save will patch the weapon's AoWGaItemHandle to the no-custom sentinel.

Phase 4B contract:

  • Save consumes PendingAoWItemID (custom AoW set) and PendingAoWClear (custom AoW removal) and patches slot via core.PatchWeaponAoW / core.PatchWeaponAoWHandle.
  • PendingAoWItemID != 0 and PendingAoWClear == true at the same time is rejected by Validate.

type WorkspaceValidationIssue

type WorkspaceValidationIssue struct {
	Severity string `json:"severity"`
	Code     string `json:"code"`
	Message  string `json:"message"`
	UID      string `json:"uid,omitempty"`
	Handle   uint32 `json:"handle,omitempty"`
}

WorkspaceValidationIssue is one row in Errors or Warnings.

type WorkspaceValidationReport

type WorkspaceValidationReport struct {
	OK                        bool                       `json:"ok"`
	Errors                    []WorkspaceValidationIssue `json:"errors"`
	Warnings                  []WorkspaceValidationIssue `json:"warnings"`
	InventoryItemCount        int                        `json:"inventoryItemCount"`
	StorageItemCount          int                        `json:"storageItemCount"`
	UnsupportedInventoryCount int                        `json:"unsupportedInventoryCount"`
	UnsupportedStorageCount   int                        `json:"unsupportedStorageCount"`
	DuplicateUIDs             []string                   `json:"duplicateUIDs"`
	DuplicateHandles          []uint32                   `json:"duplicateHandles"`
}

WorkspaceValidationReport summarizes the result of Validate.

OK == false signals at least one Error; warnings never flip OK to false. Pass-through records are always surfaced as a warning so the UI can show "N items will round-trip unchanged" without blocking save.

func Validate

Validate runs Phase 1 dry-run checks. It does not touch the slot.

Errors:

  • duplicate UIDs across editable items
  • duplicate non-zero handles across editable items
  • unknown itemID / baseItemID (not in DB)
  • quantity == 0
  • CurrentUpgrade outside [0, MaxUpgrade]
  • editable category outside SupportedCategories (defensive — builder should have caught this already)

Warnings:

  • pass-through records present (count surfaced)
  • shared-AoW conflict detection not yet wired in Phase 1

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL