Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client and server-only mods #71

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading