Skip to content
Open
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
14 changes: 6 additions & 8 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ func BuildRootCommand() *cobra.Command {

var userHome, _ = os.UserHomeDir()

var configFile, serverName, shortName, gameMode, startupMap, gameDifficulty, gameLength,
password, adminName, adminMail, adminPassword, motd, specimenType, mutators,
serverMutators, redirectURL, mapList, allTradersMessage, kfunflectURL, kfpatcherURL,
logLevel, logFilePath, logFileFormat, steamRootDir, steamAppInstallDir string
var configFile, modsFile, serverName, shortName, gameMode, startupMap, gameDifficulty,
gameLength, password, adminName, adminMail, adminPassword, motd, specimenType, mutators,
serverMutators, redirectURL, mapList, allTradersMessage, logLevel, logFilePath,
logFileFormat, steamRootDir, steamAppInstallDir string

var gamePort, webadminPort, gamespyPort, maxPlayers, maxSpectators, region,
mapVoteRepeatLimit, logMaxSize, logMaxBackups, logMaxAge int
Expand All @@ -42,6 +42,7 @@ func BuildRootCommand() *cobra.Command {
Desc string
Default interface{}
}{
"mods": {&modsFile, "mods file", settings.DefaultModsFile},
"config": {&configFile, "configuration file", settings.DefaultConfigFile},
"servername": {&serverName, "server name", settings.DefaultServerName},
"shortname": {&shortName, "server short name", settings.DefaultShortName},
Expand Down Expand Up @@ -86,8 +87,6 @@ func BuildRootCommand() *cobra.Command {
"buyeverywhere": {&enableBuyEverywhere, "(KFPatcher) allow players to shop whenever", settings.DefaultKFPBuyEverywhere},
"alltraders": {&enableAllTraders, "(KFPatcher) make all trader's spots accessible", settings.DefaultKFPEnableAllTraders},
"alltraders-message": {&allTradersMessage, "(KFPatcher) All traders screen message", settings.DefaultKFPAllTradersMessage},
"kfunflect-url": {&kfunflectURL, "(KFPatcher) KFUnflect URL", settings.DefaultKFUnflectURL},
"kfpatcher-url": {&kfpatcherURL, "(KFPatcher) archive URL", settings.DefaultKFPatcherURL},
"log-to-file": {&enableFileLogging, "enable file logging", settings.DefaultLogToFile},
"log-level": {&logLevel, "log level (info, debug, warn, error)", settings.DefaultLogLevel},
"log-file": {&logFilePath, "log file path", settings.DefaultLogFile},
Expand Down Expand Up @@ -154,6 +153,7 @@ func runRootCommand(cmd *cobra.Command, args []string) error {

func registerArguments(sett *settings.KFDSLSettings) {
sett.ConfigFile = arguments.NewArgument("Config File", viper.GetString("config"), nil, nil, false)
sett.ModsFile = arguments.NewArgument("Mods File", viper.GetString("mods"), nil, nil, false)
sett.ServerName = arguments.NewArgument("Server Name", viper.GetString("servername"), arguments.ParseNonEmptyStr, nil, false)
sett.ShortName = arguments.NewArgument("Short Name", viper.GetString("shortname"), arguments.ParseNonEmptyStr, nil, false)
sett.GamePort = arguments.NewArgument("Game Port", viper.GetInt("port"), arguments.ParsePort, nil, false)
Expand Down Expand Up @@ -197,8 +197,6 @@ func registerArguments(sett *settings.KFDSLSettings) {
sett.KFPBuyEverywhere = arguments.NewArgument("KFP Buy Everywhere", viper.GetBool("buyeverywhere"), nil, arguments.FormatBool, false)
sett.KFPEnableAllTraders = arguments.NewArgument("KFP All Traders", viper.GetBool("alltraders"), nil, arguments.FormatBool, false)
sett.KFPAllTradersMessage = arguments.NewArgument("KFP All Traders Msg", viper.GetString("alltraders-message"), nil, nil, false)
sett.KFPatcherURL = arguments.NewArgument("KFPatcher URL", viper.GetString("kfpatcher-url"), arguments.ParseURL, nil, false)
sett.KFUnflectURL = arguments.NewArgument("KFUnflect URL", viper.GetString("kfunflect-url"), arguments.ParseURL, nil, false)
sett.LogToFile = arguments.NewArgument("Log to File", viper.GetBool("log-to-file"), nil, arguments.FormatBool, false)
sett.LogLevel = arguments.NewArgument("Log Level", viper.GetString("log-level"), arguments.ParseLogLevel, nil, false)
sett.LogFile = arguments.NewArgument("Log File", viper.GetString("log-file"), nil, nil, false)
Expand Down
244 changes: 244 additions & 0 deletions internal/mods/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package mods

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"sync"

"github.com/K4rian/kfdsl/internal/log"
"github.com/K4rian/kfdsl/internal/utils"
)

type Author struct {
Name string `json:"name"`
Website string `json:"website"`
}

type InstallItem struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
Checksum string `json:"checksum,omitempty"`
}

type Mod struct {
Version string `json:"version"`
Description string `json:"description"`
Authors []Author `json:"authors"`
License string `json:"license"`
ProjectURL string `json:"project_url"`
DownloadURL string `json:"download_url"`
Checksum string `json:"checksum,omitempty"`
Extract bool `json:"extract"`
InstallItems []InstallItem `json:"install"`
DependOn []string `json:"depend_on"`
Enabled bool `json:"enabled,omitempty"`
}

type installError struct {
name string
err error
}

var mu sync.Mutex

func (m *Mod) isDownloadRequired(dir string) bool {
for _, item := range m.InstallItems {
if item.Checksum == "" {
continue
}

itemPath := filepath.Join(dir, item.Path, item.Name)
log.Logger.Debug("Checking mod file", "path", itemPath, "checksum", item.Checksum)

if !utils.FileExists(itemPath) {
return true
}

if match, err := utils.FileMatchesChecksum(itemPath, item.Checksum); err != nil || !match {
log.Logger.Debug("Checksum mismatch, download required", "path", itemPath, "checksum", item.Checksum)
return true
}
}

return false
}

func (m *Mod) download(dir, name string) (string, error) {
if !m.isDownloadRequired(dir) {
return "", nil
}

log.Logger.Debug("Downloading mod", "name", name, "url", m.DownloadURL)
filename, err := utils.DownloadFile(m.DownloadURL, m.Checksum)
if err != nil {
return "", fmt.Errorf("failed to download %s: %w", name, err)
}
log.Logger.Debug("Mod download complete", "name", name)
return filename, nil
}

func (m *Mod) installFile(dir, filename string, item InstallItem) error {
log.Logger.Debug("Installing mod file", "name", item.Name, "dir", dir, "path", item.Path, "from", filename)
path, err := utils.CreateDirIfNotExists(dir, item.Path)
if err != nil {
return err
}
return utils.MoveFile(filename, filepath.Join(path, item.Name), item.Checksum)
}

func (m *Mod) installFiles(dir, filename string) error {
if len(m.InstallItems) > 1 && !m.Extract {
return fmt.Errorf("mod contains multiple files but is not marked for extraction")
}

log.Logger.Debug("Installing mod files")
if len(m.InstallItems) == 1 {
return m.installFile(dir, filename, m.InstallItems[0])
}
return m.installArchive(dir, filename)
}

func (m *Mod) installArchive(dir, archive string) error {
// Unpack item in temporary directory then move them one by one
tempDir, err := os.MkdirTemp("", "*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)

log.Logger.Debug("Extracting mod archive", "archive", archive, "to", tempDir)
if err := utils.UnzipFile(archive, tempDir); err != nil {
return err
}

for _, item := range m.InstallItems {
if err := m.installFile(dir, filepath.Join(tempDir, item.Name), item); err != nil {
return err
}
}
return nil
}

func (m *Mod) install(dir string, name string) error {
if !m.Enabled {
log.Logger.Debug("Skipping installation of mod, it is disabled", "name", name)
return nil
}

if !m.isDownloadRequired(dir) {
log.Logger.Debug("Skipping installation of mod, it is already installed", "name", name)
return nil
}

log.Logger.Debug("Installing mod", "name", name)

filename, err := m.download(dir, name)
if err != nil {
return err
}

if filename != "" {
if err := m.installFiles(dir, filename); err != nil {
return err
}
}

return nil
}

func (m *Mod) resolveDependencies(mods map[string]*Mod) []string {
if m.DependOn == nil {
return nil
}

deps := make([]string, 0)
for _, name := range m.DependOn {
if dep, ok := mods[name]; ok {
// Enable mod for installation if it is a dependency
dep.Enabled = true
deps = append(deps, dep.resolveDependencies(mods)...)
} else {
log.Logger.Warn("Dependency not found", "name", name)
}
}
return deps
}

func resolveModsToInstall(mods map[string]*Mod) []string {
m := make([]string, 0)
for name, mod := range mods {
m = append(m, name)
m = append(m, mod.resolveDependencies(mods)...)
}
return utils.RemoveDuplicates(m)
}

func InstallMods(dir string, mods map[string]*Mod, installed *[]string) error {
toInstall := resolveModsToInstall(mods)
log.Logger.Debug("Mods to install", "mods", strings.Join(toInstall, " / "))

var installWg, processWg sync.WaitGroup
installations := make(chan string, len(toInstall))
errors := make(chan installError, len(toInstall))

for _, name := range toInstall {
mod := mods[name]

installWg.Add(1)
go func(name string, mod *Mod) {
defer installWg.Done()

err := mod.install(dir, name)
if err != nil {
errors <- installError{name, err}
} else {
installations <- name
}
}(name, mod)
}

processWg.Add(1)
go func() {
defer processWg.Done()
for installedMod := range installations {
mu.Lock()
*installed = append(*installed, installedMod)
mu.Unlock()
}
}()

processWg.Add(1)
go func() {
defer processWg.Done()
for err := range errors {
log.Logger.Error("Failed to install mod", "name", err.name, "error", err.err)
}
}()

installWg.Wait()
close(installations)
close(errors)
processWg.Wait()

return nil
}

func ParseModsFile(filename string) (map[string]*Mod, error) {
jsonFile, err := os.Open(filename)
if err != nil {
return nil, err
}
defer jsonFile.Close()

var items map[string]*Mod
err = json.NewDecoder(jsonFile).Decode(&items)
if err != nil {
return nil, err
}

return items, nil
}
6 changes: 1 addition & 5 deletions internal/settings/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package settings

const (
DefaultConfigFile = "KillingFloor.ini"
DefaultModsFile = "mods.json"
DefaultServerName = "Killing Floor Server"
DefaultShortName = "KF Server"
DefaultGamePort = 7707
Expand Down Expand Up @@ -62,8 +63,3 @@ const (
DefaultInternalSpecimenType = "ET_None"
DefaultMaxInternetClientRate = 10000
)

const (
DefaultKFUnflectURL = "https://github.com/InsultingPros/KFUnflect/releases/download/1.0.0/KFUnflect.u"
DefaultKFPatcherURL = "https://github.com/InsultingPros/KFPatcher/releases/download/1.4.0/KFPatcher.zip"
)
3 changes: 1 addition & 2 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

type KFDSLSettings struct {
ConfigFile *arguments.Argument[string] // Server Configuration File
ModsFile *arguments.Argument[string] // File defining which mods to install
ServerName *arguments.Argument[string] // Server Name
ShortName *arguments.Argument[string] // Server Alias
GamePort *arguments.Argument[int] // Port
Expand Down Expand Up @@ -53,8 +54,6 @@ type KFDSLSettings struct {
KFPBuyEverywhere *arguments.Argument[bool] // KFPatcher: Allows opening the buy menu anywhere (untested)
KFPEnableAllTraders *arguments.Argument[bool] // KFPatcher: All of the trader's spots are accessible after each wave
KFPAllTradersMessage *arguments.Argument[string] // KFPatcher: All traders open message
KFPatcherURL *arguments.Argument[string] // KFPatcher: archive URL
KFUnflectURL *arguments.Argument[string] // KFPatcher: KFUnflect URL
LogToFile *arguments.Argument[bool] // Enable file logging
LogLevel *arguments.Argument[string] // Log level (info, debug, warn, error)
LogFile *arguments.Argument[string] // Log file path
Expand Down
13 changes: 13 additions & 0 deletions internal/utils/data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package utils

func RemoveDuplicates[T comparable](sliceList []T) []T {
keys := make(map[T]bool)
l := []T{}
for _, item := range sliceList {
if _, value := keys[item]; !value {
keys[item] = true
l = append(l, item)
}
}
return l
}
Loading