Skip to content

Commit

Permalink
feat: client and server-only mods (#71)
Browse files Browse the repository at this point in the history
* feat: local registry database migrations

* chore: store raw API responses in local registry

* feat: update ficsit-resolver

* feat: client and server-only mods

* fix: remove mods that no longer support the current target
  • Loading branch information
mircearoata authored Oct 4, 2024
1 parent bf6d6b0 commit 3640e5e
Show file tree
Hide file tree
Showing 11 changed files with 392 additions and 111 deletions.
13 changes: 11 additions & 2 deletions cli/installations.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,13 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
var deleteWait errgroup.Group
for _, entry := range dir {
if entry.IsDir() {
if _, ok := lockfile.Mods[entry.Name()]; !ok {
modName := entry.Name()
mod, hasMod := lockfile.Mods[modName]
if hasMod {
_, hasTarget := mod.Targets[platform.TargetName]
hasMod = hasTarget
}
if !hasMod {
modName := entry.Name()
modDir := filepath.Join(modsDirectory, modName)
deleteWait.Go(func() error {
Expand Down Expand Up @@ -493,7 +499,10 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)

target, ok := version.Targets[platform.TargetName]
if !ok {
return fmt.Errorf("%s@%s not available for %s", modReference, version.Version, platform.TargetName)
// The resolver validates that the resulting lockfile mods can be installed on the sides where they are required
// so if the mod is missing this target, it means it is not required on this target
slog.Info("skipping mod not available for target", slog.String("mod_reference", modReference), slog.String("version", version.Version), slog.String("target", platform.TargetName))
return nil
}

// Only install if a link is provided, otherwise assume mod is already installed
Expand Down
106 changes: 106 additions & 0 deletions cli/localregistry/migrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package localregistry

import (
"database/sql"
"fmt"
)

var migrations = []func(*sql.Tx) error{
initialSetup,
addRequiredOnRemote,
}

func applyMigrations(db *sql.DB) error {
// user_version will store the 1-indexed migration that was last applied
var nextMigration int
err := db.QueryRow("PRAGMA user_version;").Scan(&nextMigration)
if err != nil {
return fmt.Errorf("failed to get user_version: %w", err)
}

for i := nextMigration; i < len(migrations); i++ {
err := applyMigration(db, i)
if err != nil {
return fmt.Errorf("failed to apply migration %d: %w", i, err)
}
}

return nil
}

func applyMigration(db *sql.DB, migrationIndex int) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
// Will noop if the transaction was committed
defer tx.Rollback() //nolint:errcheck

err = migrations[migrationIndex](tx)
if err != nil {
return err
}

_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d;", migrationIndex+1))
if err != nil {
return fmt.Errorf("failed to set user_version: %w", err)
}

err = tx.Commit()
if err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}

return nil
}

func initialSetup(tx *sql.Tx) error {
// Create the initial user
_, err := tx.Exec(`
CREATE TABLE IF NOT EXISTS "versions" (
"id" TEXT NOT NULL PRIMARY KEY,
"mod_reference" TEXT NOT NULL,
"version" TEXT NOT NULL,
"game_version" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS "mod_reference" ON "versions" ("mod_reference");
CREATE UNIQUE INDEX IF NOT EXISTS "mod_version" ON "versions" ("mod_reference", "version");
CREATE TABLE IF NOT EXISTS "dependencies" (
"version_id" TEXT NOT NULL,
"dependency" TEXT NOT NULL,
"condition" TEXT NOT NULL,
"optional" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "dependency")
);
CREATE TABLE IF NOT EXISTS "targets" (
"version_id" TEXT NOT NULL,
"target_name" TEXT NOT NULL,
"link" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"size" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "target_name")
);
`)

if err != nil {
return fmt.Errorf("failed to create initial tables: %w", err)
}

return nil
}

func addRequiredOnRemote(tx *sql.Tx) error {
_, err := tx.Exec(`
ALTER TABLE "versions" ADD COLUMN "required_on_remote" INT NOT NULL DEFAULT 1;
`)

if err != nil {
return fmt.Errorf("failed to add required_on_remote column: %w", err)
}

return nil
}
64 changes: 21 additions & 43 deletions cli/localregistry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"path/filepath"
"sync"

resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"

"github.com/satisfactorymodding/ficsit-cli/ficsit"

// sqlite driver
_ "modernc.org/sqlite"
)
Expand All @@ -36,43 +37,20 @@ func Init() error {
PRAGMA journal_mode = WAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
CREATE TABLE IF NOT EXISTS "versions" (
"id" TEXT NOT NULL PRIMARY KEY,
"mod_reference" TEXT NOT NULL,
"version" TEXT NOT NULL,
"game_version" TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS "mod_reference" ON "versions" ("mod_reference");
CREATE UNIQUE INDEX IF NOT EXISTS "mod_version" ON "versions" ("mod_reference", "version");
CREATE TABLE IF NOT EXISTS "dependencies" (
"version_id" TEXT NOT NULL,
"dependency" TEXT NOT NULL,
"condition" TEXT NOT NULL,
"optional" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "dependency")
);
CREATE TABLE IF NOT EXISTS "targets" (
"version_id" TEXT NOT NULL,
"target_name" TEXT NOT NULL,
"link" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"size" INT NOT NULL,
FOREIGN KEY ("version_id") REFERENCES "versions" ("id") ON DELETE CASCADE,
PRIMARY KEY ("version_id", "target_name")
);
`)
if err != nil {
return fmt.Errorf("failed to setup tables: %w", err)
return fmt.Errorf("failed to setup connection pragmas: %w", err)
}

err = applyMigrations(db)
if err != nil {
return fmt.Errorf("failed to apply migrations: %w", err)
}

return nil
}

func Add(modReference string, modVersions []resolver.ModVersion) {
func Add(modReference string, modVersions []ficsit.ModVersion) {
dbWriteMutex.Lock()
defer dbWriteMutex.Unlock()

Expand All @@ -93,7 +71,7 @@ func Add(modReference string, modVersions []resolver.ModVersion) {
for _, modVersion := range modVersions {
l := slog.With(slog.String("mod", modReference), slog.String("version", modVersion.Version))

_, err = tx.Exec("INSERT INTO versions (id, mod_reference, version, game_version) VALUES (?, ?, ?, ?)", modVersion.ID, modReference, modVersion.Version, modVersion.GameVersion)
_, err = tx.Exec("INSERT INTO versions (id, mod_reference, version, game_version, required_on_remote) VALUES (?, ?, ?, ?, ?)", modVersion.ID, modReference, modVersion.Version, modVersion.GameVersion, modVersion.RequiredOnRemote)
if err != nil {
l.Error("failed to insert mod version into local registry", slog.Any("err", err))
return
Expand Down Expand Up @@ -121,17 +99,17 @@ func Add(modReference string, modVersions []resolver.ModVersion) {
}
}

func GetModVersions(modReference string) ([]resolver.ModVersion, error) {
versionRows, err := db.Query("SELECT id, version, game_version FROM versions WHERE mod_reference = ?", modReference)
func GetModVersions(modReference string) ([]ficsit.ModVersion, error) {
versionRows, err := db.Query("SELECT id, version, game_version, required_on_remote FROM versions WHERE mod_reference = ?", modReference)
if err != nil {
return nil, fmt.Errorf("failed to fetch mod versions from local registry: %w", err)
}
defer versionRows.Close()

var versions []resolver.ModVersion
var versions []ficsit.ModVersion
for versionRows.Next() {
var version resolver.ModVersion
err = versionRows.Scan(&version.ID, &version.Version, &version.GameVersion)
var version ficsit.ModVersion
err = versionRows.Scan(&version.ID, &version.Version, &version.GameVersion, &version.RequiredOnRemote)
if err != nil {
return nil, fmt.Errorf("failed to scan version row: %w", err)
}
Expand All @@ -156,16 +134,16 @@ func GetModVersions(modReference string) ([]resolver.ModVersion, error) {
return versions, nil
}

func getVersionDependencies(versionID string) ([]resolver.Dependency, error) {
var dependencies []resolver.Dependency
func getVersionDependencies(versionID string) ([]ficsit.Dependency, error) {
var dependencies []ficsit.Dependency
dependencyRows, err := db.Query("SELECT dependency, condition, optional FROM dependencies WHERE version_id = ?", versionID)
if err != nil {
return nil, fmt.Errorf("failed to fetch dependencies from local registry: %w", err)
}
defer dependencyRows.Close()

for dependencyRows.Next() {
var dependency resolver.Dependency
var dependency ficsit.Dependency
err = dependencyRows.Scan(&dependency.ModID, &dependency.Condition, &dependency.Optional)
if err != nil {
return nil, fmt.Errorf("failed to scan dependency row: %w", err)
Expand All @@ -176,16 +154,16 @@ func getVersionDependencies(versionID string) ([]resolver.Dependency, error) {
return dependencies, nil
}

func getVersionTargets(versionID string) ([]resolver.Target, error) {
var targets []resolver.Target
func getVersionTargets(versionID string) ([]ficsit.Target, error) {
var targets []ficsit.Target
targetRows, err := db.Query("SELECT target_name, link, hash, size FROM targets WHERE version_id = ?", versionID)
if err != nil {
return nil, fmt.Errorf("failed to fetch targets from local registry: %w", err)
}
defer targetRows.Close()

for targetRows.Next() {
var target resolver.Target
var target ficsit.Target
err = targetRows.Scan(&target.TargetName, &target.Link, &target.Hash, &target.Size)
if err != nil {
return nil, fmt.Errorf("failed to scan target row: %w", err)
Expand Down
41 changes: 41 additions & 0 deletions cli/provider/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package provider

import (
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"

"github.com/satisfactorymodding/ficsit-cli/ficsit"
)

func convertFicsitVersionsToResolver(versions []ficsit.ModVersion) []resolver.ModVersion {
modVersions := make([]resolver.ModVersion, len(versions))
for i, modVersion := range versions {
dependencies := make([]resolver.Dependency, len(modVersion.Dependencies))
for j, dependency := range modVersion.Dependencies {
dependencies[j] = resolver.Dependency{
ModID: dependency.ModID,
Condition: dependency.Condition,
Optional: dependency.Optional,
}
}

targets := make([]resolver.Target, len(modVersion.Targets))
for j, target := range modVersion.Targets {
targets[j] = resolver.Target{
TargetName: resolver.TargetName(target.TargetName),
Link: viper.GetString("api-base") + target.Link,
Hash: target.Hash,
Size: target.Size,
}
}

modVersions[i] = resolver.ModVersion{
Version: modVersion.Version,
GameVersion: modVersion.GameVersion,
Dependencies: dependencies,
Targets: targets,
RequiredOnRemote: modVersion.RequiredOnRemote,
}
}
return modVersions
}
35 changes: 2 additions & 33 deletions cli/provider/ficsit.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

"github.com/Khan/genqlient/graphql"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"

"github.com/satisfactorymodding/ficsit-cli/cli/localregistry"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
Expand Down Expand Up @@ -40,39 +39,9 @@ func (p FicsitProvider) ModVersionsWithDependencies(_ context.Context, modID str
return nil, errors.New(response.Error.Message)
}

modVersions := make([]resolver.ModVersion, len(response.Data))
for i, modVersion := range response.Data {
dependencies := make([]resolver.Dependency, len(modVersion.Dependencies))
for j, dependency := range modVersion.Dependencies {
dependencies[j] = resolver.Dependency{
ModID: dependency.ModID,
Condition: dependency.Condition,
Optional: dependency.Optional,
}
}
localregistry.Add(modID, response.Data)

targets := make([]resolver.Target, len(modVersion.Targets))
for j, target := range modVersion.Targets {
targets[j] = resolver.Target{
TargetName: resolver.TargetName(target.TargetName),
Link: viper.GetString("api-base") + target.Link,
Hash: target.Hash,
Size: target.Size,
}
}

modVersions[i] = resolver.ModVersion{
ID: modVersion.ID,
Version: modVersion.Version,
GameVersion: modVersion.GameVersion,
Dependencies: dependencies,
Targets: targets,
}
}

localregistry.Add(modID, modVersions)

return modVersions, err
return convertFicsitVersionsToResolver(response.Data), nil
}

func (p FicsitProvider) GetModName(context context.Context, modReference string) (*resolver.ModName, error) {
Expand Down
2 changes: 1 addition & 1 deletion cli/provider/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func (p LocalProvider) ModVersionsWithDependencies(_ context.Context, modID stri

// TODO: only list as available the versions that have at least one target cached

return modVersions, nil
return convertFicsitVersionsToResolver(modVersions), nil
}

func (p LocalProvider) GetModName(_ context.Context, modReference string) (*resolver.ModName, error) {
Expand Down
Loading

0 comments on commit 3640e5e

Please sign in to comment.