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
- Variables
- func AddItem(snap *InventoryWorkspaceSnapshot, spec AddItemSpec, ...) error
- func AutoRepairWorkspaceItem(snap *InventoryWorkspaceSnapshot, uid, code string) error
- func CanRepairCode(code string) bool
- func ClampUpgrade(requested, max int) int
- func ComputeBaseRevision(slot *core.SaveSlot) string
- func MoveItem(snap *InventoryWorkspaceSnapshot, uid string, targetContainer ContainerKind, ...) error
- func NewSessionID() string
- func RemoveItem(snap *InventoryWorkspaceSnapshot, uid string) error
- func ReorderItems(snap *InventoryWorkspaceSnapshot, inventoryUIDs, storageUIDs []string) error
- func RepairDescForIssue(snap *InventoryWorkspaceSnapshot, iss WorkspaceValidationIssue) string
- func UpdateWeapon(snap *InventoryWorkspaceSnapshot, uid string, patch WeaponPatch) error
- type AddItemSpec
- type ContainerKind
- type EditableItem
- type HandlesByUID
- type InventoryEditSession
- type InventoryWorkspaceSnapshot
- type ItemSource
- type RawInventoryRecord
- type WeaponPatch
- type WorkspaceValidationIssue
- type WorkspaceValidationReport
Constants ¶
const ( SeverityError = "error" SeverityWarning = "warning" )
Severity constants.
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" CodePendingAoWUnknown = "pending_aow_unknown" CodePendingAoWConflict = "pending_aow_conflict" CodeCurrentAoWMissing = "current_aow_missing" CodeCurrentAoWNonAoWCategory = "current_aow_non_aow_category" )
Validation issue codes. Frontend / tests use these as stable keys.
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.
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) )
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 ¶
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.
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
CanRepairCode reports whether code can be auto-repaired without binary patching (i.e. via AutoRepairWorkspaceItem).
func ClampUpgrade ¶
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 ¶
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"`
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 ¶
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 ¶
func Validate(snap InventoryWorkspaceSnapshot) WorkspaceValidationReport
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