z_mirror/fmm

git clone https://git.raiguard.me/z_mirror/fmm

Caleb HeuerInitial WIP811753c

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