Documentation
¶
Index ¶
- Constants
- func ApplyRowPatch(baseJSON []byte, row ExmodRow) ([]byte, error)
- func Compile(basePakPath, exmodzPath, outputPakPath string) (err error)
- func MergeCompile(ctx context.Context, basePakPath string, sources []MergeSource, ...) (warnings []string, failed []source.MergeFailure, err error)
- func ValidateSource(sourceFilePath string) error
- type ExmodDiff
- type ExmodFileItem
- type ExmodRow
- type ExmodzBundle
- type Icarus
- func (s *Icarus) AuthURL() string
- func (s *Icarus) Capabilities() source.Capabilities
- func (s *Icarus) CheckUpdates(ctx context.Context, installed []domain.InstalledMod) ([]domain.Update, error)
- func (s *Icarus) ClassifyMergeSource(id string) (kind string, convertible bool)
- func (s *Icarus) ExchangeToken(ctx context.Context, code string) (*source.Token, error)
- func (s *Icarus) FingerprintBase(baseArtifactPath string) (string, error)
- func (s *Icarus) GetDependencies(ctx context.Context, mod *domain.Mod) ([]domain.ModReference, error)
- func (s *Icarus) GetDownloadURL(ctx context.Context, mod *domain.Mod, fileID string) (string, error)
- func (s *Icarus) GetMod(ctx context.Context, queryGameID, modID string) (*domain.Mod, error)
- func (s *Icarus) GetModFiles(ctx context.Context, mod *domain.Mod) ([]domain.DownloadableFile, error)
- func (s *Icarus) ID() string
- func (s *Icarus) IsConvertibleArtifact(fileName string) bool
- func (s *Icarus) IsNativeMergeSource(fileName string) bool
- func (s *Icarus) MergeCompile(ctx context.Context, basePakPath string, sources []MergeSource, ...) ([]string, []source.MergeFailure, error)
- func (s *Icarus) MergedArtifactLabel() string
- func (s *Icarus) MergedArtifactName() string
- func (s *Icarus) Name() string
- func (s *Icarus) ResolveBaseArtifact(game *domain.Game) (string, error)
- func (s *Icarus) RestoredArtifactName(modID string) string
- func (s *Icarus) Search(ctx context.Context, query source.SearchQuery) (source.SearchResult, error)
- func (s *Icarus) TypeLabel() string
- func (s *Icarus) ValidateSource(sourceFilePath string) error
- type MergeSource
Constants ¶
const ( MergeSourceExmodz = "exmodz" MergeSourcePak = "pak" )
Merge-source kinds (#221; moved here from internal/source in #256 - they are Icarus vocabulary, and core now treats kinds as opaque strings it obtains from ClassifyMergeSource). An empty Kind means MergeSourceExmodz: every pre-#221 constructor built exmodz-only sources and never set a kind, and stored merge fingerprints from that era carry no Kind field.
Variables ¶
This section is empty.
Functions ¶
func ApplyRowPatch ¶
ApplyRowPatch applies row's File_Items to baseJSON, a real Icarus DataTable JSON export — the standard Unreal Engine shape {"RowStruct": "...", "Defaults": {...}, "Rows": [{"Name": "...", ...fields}, ...]}, confirmed against a real installed data.pak (task-7-report.md); not the flat {name: {fields}} map this function originally assumed, which never matched real game data and was only ever exercised against synthetic fixtures.
Each File_Item is an upsert, not a strict patch: if its Name matches an existing entry in Rows, that row's fields are shallow-merged with the item's fields (item fields win, everything else on the row survives untouched); if no row has that Name, the item is appended to Rows verbatim as a brand-new row. This matches what real .EXMOD content actually does — most rows patch existing base stats, but a content-adding mod (e.g. a new mountable species) introduces rows the base game doesn't have yet, and erroring on that (the original patch-only design) made every such mod uncompilable. All other top-level keys on the base document (RowStruct, Defaults, and anything else) pass through re-serialization unchanged, since only doc["Rows"] is ever modified. Output is deterministic: encoding/json sorts map keys.
func Compile ¶
Compile reads exmodzPath's .EXMOD diff, applies it to the game's base data tables, bundles in any pre-built assets the .EXMODZ carries, and writes the result as a new pak at outputPakPath ready to deploy as-is.
Base tables are read directly out of basePakPath — the installed game's own Content/Data/data.pak — so they are always week-correct by construction and the whole operation is offline. That pak stores 40 tables uncompressed and compresses the other 258 with Zlib, all of which go-unrealpak reads with the standard library (#175). basePakPath is also what resolves a bare, hyphen-flattened CurrentFile to a real mount path.
There is no ctx parameter: every step is local file I/O over a ~2 MB pak, with no network call and no long-running loop to cancel. The source.MergeCompiler interface still takes one, for implementations that need it (MergeCompile, this package's own N-mod entry point, is one).
The compiled pak's mount point and table-entry paths (icarusContentMountPoint, icarusDataTablePrefix below) are Icarus-specific and deliberately live here rather than in go-unrealpak, which stays game-agnostic — see unrealpak.Writer's WithMountPoint. They are not guessed: both were confirmed against two real, working prebuilt Icarus mod paks (#178; see docs/plans/2026-08-01-icarus-zlib-pivot.md's pak-divergence-report.md).
func MergeCompile ¶
func MergeCompile(ctx context.Context, basePakPath string, sources []MergeSource, outputPakPath string) (warnings []string, failed []source.MergeFailure, err error)
MergeCompile applies every source's .EXMOD row upserts, IN ORDER, against the same evolving base tables - a merge is just Compile with N diffs instead of 1. Table conflicts compose at the FIELD level for free: ApplyRowPatch always shallow-merges an item's fields into whatever the target row currently holds, so feeding mod A's patched bytes back in as the "base" for mod B's row (instead of re-reading the pristine base table each time) is the entire merge algorithm - two mods patching DIFFERENT fields of the same row, or entirely different rows of the same table, both survive; only a genuine same-row-same-field write is last-wins (an ordinary, expected upsert outcome, not something to warn about). Bundled ASSET files cannot compose this way - a same-path asset collision is necessarily last-wins, so it is reported as a warning instead.
ctx is accepted only to satisfy source.MergeCompiler and is never read - every step here is local file I/O over small files (mirrors Compile's own doc comment, internal/source/icarus/compile.go:23-25).
A non-nil error always means outputPakPath does not exist (or does not contain a fully-written pak) - see the removal defer below, mirroring Compile's own fail-clean contract.
func ValidateSource ¶
ValidateSource parses sourceFilePath without compiling anything - the ingest-time check. .exmodz archives fully parse (#197); .pak files (#221) open + enumerate only - full conversion is checked at merge time BY DESIGN (the result depends on the current base pak, which changes weekly).
Types ¶
type ExmodDiff ¶
type ExmodDiff struct {
Name string
Author string
Version string
Description string
Rows []ExmodRow
}
ExmodDiff is the parsed .EXMOD manifest — a diff against the base game's JSON data tables, not a binary/compiled-asset diff (confirmed against a real sample; see docs/plans/2026-07-29-icarus-exmod-pak-research.md).
func ParseExmod ¶
type ExmodFileItem ¶
ExmodFileItem upserts fields on the base row named Name — patching it if it already exists, adding it as a new row otherwise (see ApplyRowPatch). Fields holds every key from the source JSON except "Name" itself, generically — the real schema nests arbitrary game-data shapes here (see package doc comment), so this deliberately does not enumerate them.
type ExmodRow ¶
type ExmodRow struct {
CurrentFile string
FileItems []ExmodFileItem
}
ExmodRow targets one base data-table file (e.g. "AI-D_AIGrowth.json").
type ExmodzBundle ¶
type ExmodzBundle struct {
Diff *ExmodDiff
Assets map[string][]byte // zip-internal path -> raw content, manifest/readme/image excluded
}
ExmodzBundle is a parsed .EXMODZ: the diff manifest plus any pre-built asset files the mod author already compiled (placed as-is into the output pak — never recompiled by LMM).
func ParseExmodz ¶
func ParseExmodz(zipData []byte) (*ExmodzBundle, error)
ParseExmodz unpacks zipData (an in-memory .EXMODZ) into its manifest and bundled assets. The manifest lives at "Extracted Mods/<name>.EXMOD" in every sample seen so far; this looks for any "*.EXMOD" file under an "Extracted Mods/" prefix rather than hard-coding the mod name, since that varies per mod. Matching (both the manifest's prefix/suffix and the asset extensions below) is done on the entry name with backslashes normalized to forward slashes and case folded — some .EXMODZ producers are Windows tools and zip entry casing is not guaranteed — but stored asset keys keep their original case, only the slash direction is normalized.
type Icarus ¶
type Icarus struct {
// contains filtered or unexported fields
}
Icarus is a ModSource backed by the public, unauthenticated Firestore REST API described in docs/plans/2026-07-29-icarus-exmod-pak-research.md.
func New ¶
New constructs an Icarus source. projectID is the Firestore project ID (from the Firebase console) — passed explicitly rather than hard-coded so tests can point at an httptest server and so the real value lives in one place at the call site (Task 9), not buried in this package.
func (*Icarus) Capabilities ¶
func (s *Icarus) Capabilities() source.Capabilities
func (*Icarus) CheckUpdates ¶
func (s *Icarus) CheckUpdates(ctx context.Context, installed []domain.InstalledMod) ([]domain.Update, error)
CheckUpdates compares each installed mod's stored version against the catalog's current version string (semantic-ish, per modinfo.json's "recommended" versioning note — not guaranteed strictly semver, so this uses domain.IsNewerVersion the same way custom.API does).
func (*Icarus) ClassifyMergeSource ¶ added in v1.30.1
ClassifyMergeSource maps a retained-source identity to its merge-source kind (#221) plus whether that kind is convertible (a raw prebuilt pak, as opposed to a native .exmodz diff). id is a retained-source fileID - download-path icarus fileIDs are literally "pak"/"exmodz", import-path fileIDs are the archive's own filename - or a Kind string previously recorded on a merge fingerprint (kind strings classify as themselves, and a legacy pre-#221 entry's empty Kind classifies as exmodz, the only kind that existed then). Unknown ids default to exmodz for the same reason.
func (*Icarus) ExchangeToken ¶
func (*Icarus) FingerprintBase ¶ added in v1.30.1
FingerprintBase opens the base pak and returns its footer IndexHash (#196) as an opaque fingerprint - cheap (footer + primary-index region only; unrealpak.Open never reads a pak's actual file payloads), matching the base pak MergeCompile itself already opens to read patched tables from, so this adds no new I/O pattern to the compile path.
func (*Icarus) GetDependencies ¶
func (s *Icarus) GetDependencies(ctx context.Context, mod *domain.Mod) ([]domain.ModReference, error)
GetDependencies: the modinfo.json v2 schema has no dependency field.
func (*Icarus) GetDownloadURL ¶
func (s *Icarus) GetDownloadURL(ctx context.Context, mod *domain.Mod, fileID string) (string, error)
GetDownloadURL re-fetches the mod document and returns the stored URL for fileID ("pak" or "exmodz") directly — no signing, matching a static-URL catalog rather than an OAuth-gated one.
func (*Icarus) GetModFiles ¶
func (s *Icarus) GetModFiles(ctx context.Context, mod *domain.Mod) ([]domain.DownloadableFile, error)
GetModFiles returns the mod's downloadable files (pak and/or exmodz — see modinfo.json v2 schema). When exactly one file exists, it is marked primary. When both variants are published, exmodz is marked primary (#211) — the pak remains explicitly selectable. All returned files have a Description set: "mergeable EXMOD - recommended" for exmodz, "prebuilt PAK" for pak.
func (*Icarus) IsConvertibleArtifact ¶ added in v1.30.1
IsConvertibleArtifact reports whether fileName names a prebuilt .pak this source can convert into a merge source (#221). Pure format test (case-insensitive ".pak" suffix, mirroring pre-#256 core's isConvertEligiblePakFile) - core owns the DeployCompile/ConvertPaks policy gates that decide whether a convertible file actually enters the merge-convert pipeline.
func (*Icarus) IsNativeMergeSource ¶ added in v1.30.1
IsNativeMergeSource reports whether fileName names this source's NATIVE merge-source format - a ".exmodz" archive (case-insensitive), the diff format MergeCompile consumes directly without conversion. Pure format test, the exact mirror of IsConvertibleArtifact (pre-#256 core's isExmodzFile); core owns the DeployCompile policy gates and the "native archive from a non-compile-capable source" hard error.
func (*Icarus) MergeCompile ¶
func (s *Icarus) MergeCompile(ctx context.Context, basePakPath string, sources []MergeSource, outputPakPath string) ([]string, []source.MergeFailure, error)
MergeCompile implements source.MergeCompiler by delegating to the package-level MergeCompile function. ctx is unused: merging is pure local file I/O against the installed game's own pak (#175/#197), with nothing to cancel.
func (*Icarus) MergedArtifactLabel ¶ added in v1.30.1
MergedArtifactLabel is the user-facing display name for the merged artifact's synthetic mod row (verify/update output).
func (*Icarus) MergedArtifactName ¶ added in v1.30.1
MergedArtifactName names the single merged output artifact deployed into the game's mods directory - see mergedPakFileName for why this exact name is a deploy contract.
func (*Icarus) ResolveBaseArtifact ¶ added in v1.30.1
ResolveBaseArtifact locates the currently-installed game's base pak - the artifact every merge applies against. One known pak filename pattern: the relative path below is Task 1's empirically-confirmed finding (docs/plans/icarus-pak-format-findings.md), recorded before this function was written, not an assumption made here: the JSON data tables live in Content/Data/data.pak, NOT in the Content/Paks pakchunks, which carry only cooked .uasset/.uexp assets and no JSON at all.
This pak is also the direct source of base table *content* (#175): MergeCompile reads each patched table straight out of it, so a compile is always week-correct by construction (there's no separate dump to go stale relative to the install) and works entirely offline.
func (*Icarus) RestoredArtifactName ¶ added in v1.30.1
RestoredArtifactName names the deployable raw-fallback copy synthesized when core heals a prune-damaged cache entry whose original artifact name is unrecoverable (#250: download-path fileIDs never carried it - the name came from the download URL's basename at ingest, and the convert flip erased the manifest that recorded it). A deterministic mod-scoped name in the "_P.pak" override convention (see mergedPakFileName's doc) keeps healed installs stable: the same mod always restores to the same name, which is on-disk state existing installs already depend on.
func (*Icarus) Search ¶
func (s *Icarus) Search(ctx context.Context, query source.SearchQuery) (source.SearchResult, error)
Search fetches the whole mods collection and filters client-side — this catalog has no server-side query support to speak of, matching project_daedalus's own ModsController#find_mods approach.
func (*Icarus) ValidateSource ¶
ValidateSource implements source.MergeCompiler by delegating to the package-level ValidateSource function.
type MergeSource ¶
type MergeSource = source.MergeSource
MergeSource is a type alias (not a distinct type) for source.MergeSource (Step 3 above). internal/core must NOT import this icarus package directly (established #136/#196 precedent - see service_icarus_compile_test.go's fakeCompilerSource doc comment), so it can only ever construct/consume source.MergeSource values - aliasing it here, rather than defining a second, structurally-similar type, is what lets *Icarus's MergeCompile method (Step 6) satisfy source.MergeCompiler at all: Go interface satisfaction requires identical types, and a type alias IS the same type, not a look-alike.