z_mirror/fmm
git clone https://git.raiguard.me/z_mirror/fmm
811753c
updating
1package fmm 2 3import ( 4"cmp" 5"encoding/json" 6"errors" 7"fmt" 8"os" 9"path/filepath" 10"slices" 11) 12 13// Manager manages mdos for a given game directory. A game directory is 14// considered valid if it has either a config-path.cfg file or a 15// config/config.ini file. 16type Manager struct { 17DoSave bool 18Portal ModPortal 19 20gamePath string 21internalModsPath string 22modListJsonPath string 23modsPath string 24 25mods map [string ]* Mod 26} 27 28type PlayerData struct { 29Token string 30Username string 31} 32 33// Creates a new Manager for the given game directory. A game directory is 34// considered valid if it has either a config-path.cfg file or a 35// config/config.ini file. The player's username and token will 36// be automatically retrieved from `player-data.json` if it exists. 37func NewManager (gamePath string ,modsPath string ) (* Manager ,error ) { 38if ! entryExists (gamePath ,"data" ,"base" ,"info.json" )|| ! entryExists (modsPath ) { 39return nil ,ErrInvalidGameDirectory 40 } 41 42m := Manager { 43DoSave :true , 44Portal :ModPortal { 45downloadPath :modsPath , 46mods :map [string ]* PortalModInfo {}, 47server :"https://mods.factorio.com" , 48 }, 49 50gamePath :gamePath , 51internalModsPath :filepath .Join (gamePath ,"data" ), 52modListJsonPath :filepath .Join (modsPath ,"mod-list.json" ), 53modsPath :modsPath , 54mods :map [string ]* Mod {}, 55 } 56 57if err := m .readPlayerData ();err != nil { 58return nil ,errors .Join (errors .New ("unable to get player data" ),err ) 59 } 60 61if ! entryExists (m .modsPath ) { 62if err := os .Mkdir ("mods" ,0755 );err != nil { 63return nil ,errors .Join (errors .New ("failed to create mods directory" ),err ) 64 } 65 } 66 67if err := m .parseInternalMods ();err != nil { 68return nil ,errors .Join (errors .New ("error parsing internal mods" ),err ) 69 } 70 71if err := m .parseMods ();err != nil { 72return nil ,errors .Join (errors .New ("error parsing mods" ),err ) 73 } 74 75for _ ,mod := range m .mods { 76slices .SortFunc (mod .releases ,func (a * Release ,b * Release )int { 77switch a .Version .Cmp (& b .Version ) { 78case VersionLt : 79return - 1 80case VersionGt : 81return 1 82case VersionEq : 83return 0 84// Should be unreachable 85default : 86return 0 87 } 88 }) 89 } 90 91if err := m .parseModList ();err != nil { 92return nil ,errors .Join (errors .New ("error parsing mod-list.json" ),err ) 93 } 94 95if base ,_ := m .GetMod ("base" );base != nil { 96m .Portal .baseVersion = & base .GetLatestRelease ().Version 97 } 98 99return & m ,nil 100} 101 102// Add downloads and enables the given mod. Returns the version that was added. 103func (m * Manager )Add (mod ModIdent ) (* Version ,error ) { 104ver ,err := m .Enable (mod ) 105if err == nil { 106return ver ,nil 107 } 108 109if ! errors .Is (err ,ErrModNotFoundLocal )&& ! errors .Is (err ,ErrNoCompatibleRelease ) { 110return nil ,err 111 } 112 113filepath ,err := m .Portal .DownloadRelease (mod .Name ,mod .Version ) 114if err != nil { 115return nil ,err 116 } 117release ,err := releaseFromFile (filepath ) 118if err != nil { 119return nil ,err 120 } 121m .addRelease (release ,false ) 122m .Enable (mod ) 123return & release .Version ,nil 124} 125 126// Requests the mod to be disabled. 127func (m * Manager )Disable (modName string )error { 128mod ,err := m .GetMod (modName ) 129if err != nil { 130return err 131 } 132if mod .Enabled == nil { 133return ErrModAlreadyDisabled 134 } 135mod .Enabled = nil 136return nil 137} 138 139// Requests all non-internal mods to be disabled. 140func (m * Manager )DisableAll () { 141for _ ,mod := range m .mods { 142// base is the only mod that is always enabled by default 143if mod .Name != "base" { 144mod .Enabled = nil 145 } 146 } 147} 148 149// Enable the given mod, if it exists. If version is nil, it will default to 150// the newest local release. Returns the version that was enabled, if any. 151func (m * Manager )Enable (ident ModIdent ) (* Version ,error ) { 152mod ,err := m .GetMod (ident .Name ) 153if err != nil { 154return nil ,err 155 } 156release := mod .GetRelease (ident .Version ) 157if release == nil { 158return nil ,ErrNoCompatibleRelease 159 } 160if mod .Enabled != nil && * mod .Enabled == release .Version { 161return nil ,nil 162 } 163toEnable := & release .Version 164mod .Enabled = toEnable 165return toEnable ,nil 166} 167 168// Retrieves the corresponding Mod object. 169func (m * Manager )GetMod (name string ) (* Mod ,error ) { 170mod := m .mods [name ] 171if mod == nil { 172return nil ,ErrModNotFoundLocal 173 } 174return mod ,nil 175} 176 177// GetMods returns a list of the mods managed by this Manager. 178func (m * Manager )GetMods () []ModIdent { 179mods := []ModIdent {} 180for _ ,mod := range m .mods { 181for _ ,release := range mod .releases { 182mods = append (mods ,ModIdent {Name :mod .Name ,Version :& release .Version }) 183 } 184 } 185return mods 186} 187 188// GetLatestMods gets a list of the newest mods managed by this Manager. 189func (m * Manager )GetLatestMods () []ModIdent { 190mods := []ModIdent {} 191for _ ,mod := range m .mods { 192mods = append (mods ,ModIdent {mod .Name ,& mod .releases [len (mod .releases )- 1 ].Version }) 193 } 194return mods 195} 196 197// Applies the requested modifications and saves to mod-list.json. 198func (m * Manager )Save ()error { 199if ! m .DoSave { 200return nil 201 } 202var ModListJson ModListJson 203for name ,mod := range m .mods { 204ModListJson .Mods = append (ModListJson .Mods ,ModListJsonMod { 205Name :name , 206Enabled :mod .Enabled != nil , 207Version :mod .Enabled , 208isInternal :mod .isInternal , 209 }) 210 } 211 212slices .SortFunc (ModListJson .Mods ,func (a ,b ModListJsonMod )int { 213if a .isInternal != b .isInternal { 214if a .isInternal { 215return - 1 216 }else { 217return 1 218 } 219 } 220if a .Name != b .Name { 221return cmp .Compare (a .Name ,b .Name ) 222 } 223switch a .Version .Cmp (b .Version ) { 224case VersionLt : 225return - 1 226case VersionGt : 227return 1 228default : 229return 0 230 } 231 }) 232 233marshaled ,err := json .MarshalIndent (ModListJson ,"" ," " ) 234if err != nil { 235return err 236 } 237return os .WriteFile (m .modListJsonPath ,marshaled ,0666 ) 238} 239 240// Returns the current upload API key. 241func (m * Manager )GetApiKey ()string { 242return m .Portal .apiKey 243} 244 245// Returns true if the Manager has an upload API key. 246func (m * Manager )HasApiKey ()bool { 247return m .Portal .apiKey != "" 248} 249 250// Sets the API key used for mod uploading. 251func (m * Manager )SetApiKey (key string ) { 252m .Portal .apiKey = key 253} 254 255// Returns the current player data. 256func (m * Manager )GetPlayerData ()PlayerData { 257return m .Portal .playerData 258} 259 260// Returns true if the Manager has valid player data. 261func (m * Manager )HasPlayerData ()bool { 262return m .Portal .playerData .Token != "" && m .Portal .playerData .Username != "" 263} 264 265// Sets the player data used for downloading mods. The player data will be 266// automatically retrieved from the game directory if it is available. 267func (m * Manager )SetPlayerData (playerData PlayerData ) { 268m .Portal .playerData = playerData 269} 270 271func (m * Manager )addRelease (release * Release ,isInternal bool ) { 272mod := m .mods [release .Name ] 273if mod == nil { 274mod = & Mod { 275Name :release .Name , 276releases : []* Release {}, 277isInternal :isInternal , 278 } 279m .mods [release .Name ]= mod 280 } 281mod .releases = append (mod .releases ,release ) 282} 283 284func (m * Manager )parseModList ()error { 285m .Enable (ModIdent {Name :"base" }) 286mlj ,err := ParseModListJson (m .modListJsonPath ) 287if err != nil { 288return err 289 } 290if mlj == nil { 291m .Enable (ModIdent {Name :"base" }) 292return nil 293 } 294 295for _ ,modEntry := range mlj .Mods { 296if ! modEntry .Enabled { 297continue 298 } 299mod := m .mods [modEntry .Name ] 300if mod == nil { 301continue 302 } 303if release := mod .GetRelease (modEntry .Version );release != nil { 304enabled := release .Version 305mod .Enabled = & enabled 306 } 307 } 308 309return nil 310} 311 312func (m * Manager )parseInternalMods ()error { 313entries ,err := os .ReadDir (m .internalModsPath ) 314if err != nil { 315return errors .Join (errors .New ("could not read internal mods directory" ),err ) 316 } 317 318for _ ,entry := range entries { 319filename := entry .Name () 320if filename == "core" || ! entry .Type ().IsDir () { 321continue 322 } 323// Not all directories are necessarily mods 324if _ ,err := os .Stat (filepath .Join (m .internalModsPath ,filename ,"info.json" ));err != nil { 325continue 326 } 327release ,err := releaseFromFile (filepath .Join (m .internalModsPath ,filename )) 328if err != nil { 329return errors .Join (errors .New (fmt .Sprint ("unable to parse " ,filename )),err ) 330 } 331m .addRelease (release ,true ) 332 } 333 334return nil 335} 336 337func (m * Manager )parseMods ()error { 338entries ,err := os .ReadDir (m .modsPath ) 339if err != nil { 340return errors .Join (errors .New ("could not read mods directory" ),err ) 341 } 342 343for _ ,entry := range entries { 344filename := entry .Name () 345if filename == "mod-list.json" || filename == "mod-settings.dat" { 346continue 347 } 348release ,err := releaseFromFile (filepath .Join (m .modsPath ,filename )) 349if err != nil { 350return errors .Join (errors .New (fmt .Sprint ("invalid mod " ,filename )),err ) 351 } 352m .addRelease (release ,false ) 353 } 354 355return nil 356} 357 358func (m * Manager )readPlayerData ()error { 359playerDataJsonPath := filepath .Join (m .gamePath ,"player-data.json" ) 360if ! entryExists (playerDataJsonPath ) { 361return nil 362 } 363 364data ,err := os .ReadFile (playerDataJsonPath ) 365if err != nil { 366return errors .Join (errors .New ("unable to read player-data.json" ),err ) 367 } 368var playerDataJson playerDataJson 369err = json .Unmarshal (data ,& playerDataJson ) 370if err != nil { 371return errors .Join (errors .New ("invalid player-data.json format" ),err ) 372 } 373if playerDataJson .ServiceToken != nil { 374m .Portal .playerData .Token = * playerDataJson .ServiceToken 375 } 376if playerDataJson .ServiceUsername != nil { 377m .Portal .playerData .Username = * playerDataJson .ServiceUsername 378 } 379 380return nil 381} 382 383func entryExists (pathParts ... string )bool { 384_ ,err := os .Stat (filepath .Join (pathParts ... )) 385return err == nil 386} 387 388func (m * Manager )ExpandDependencies (mods []ModIdent ,fetchFromPortal bool ) []ModIdent { 389visited := map [string ]bool {} 390toVisit := []Dependency {} 391for _ ,mod := range mods { 392toVisit = append (toVisit ,Dependency { 393Name :mod .Name , 394Version :mod .Version , 395Kind :DependencyRequired , 396Req :VersionEq , 397 }) 398 } 399output := []ModIdent {} 400 401for i := 0 ;i < len (toVisit );i += 1 { 402dep := toVisit [i ] 403if visited [dep .Name ] { 404continue 405 } 406visited [dep .Name ]= true 407var ident * ModIdent 408var deps []* Dependency 409mod ,err := m .GetMod (dep .Name ) 410if err != nil && err != ErrModNotFoundLocal { 411fmt .Println (err ) 412 } 413if mod != nil { 414release := mod .GetMatchingRelease (& dep ) 415if release != nil { 416ident = & ModIdent {Name :release .Name ,Version :& release .Version } 417deps = release .Dependencies 418 } 419 } 420if ident == nil && fetchFromPortal { 421var release * PortalModRelease 422release ,err = m .Portal .GetMatchingRelease (& dep ) 423if err == nil { 424ident = & ModIdent {dep .Name ,& release .Version } 425deps = release .InfoJson .Dependencies 426 } 427 } 428if err != nil { 429fmt .Println (err ) 430continue 431 } 432if ident == nil { 433panic ("Unreachable" ) 434 } 435output = append (output ,* ident ) 436for _ ,dep := range deps { 437if dep .Kind == DependencyRequired || dep .Kind == DependencyNoLoadOrder { 438toVisit = append (toVisit ,* dep ) 439 } 440 } 441 } 442 443return output 444} 445 446func (m * Manager )CheckDownloadUpdates (mods []ModIdent ) { 447if len (mods )== 0 { 448mods = m .GetLatestMods () 449 } 450// err := m.Portal.GetBulkModInfo(mods) 451// if err != nil { 452// fmt.Println(err) 453// return 454// } 455for _ ,mod := range mods { 456info ,err := m .Portal .GetModInfo (mod .Name ) 457if err != nil { 458fmt .Println (err ) 459continue 460 } 461if len (info .Releases )== 0 { 462continue // WTF? 463 } 464if info .Releases [len (info .Releases )- 1 ].Version .Cmp (mod .Version )== VersionGt { 465m .Portal .DownloadLatestRelease (mod .Name ) 466 } 467 } 468}