2using System.Collections;
3using System.Collections.Generic;
6using HeathenEngineering.SteamworksIntegration;
7using HeathenEngineering.SteamworksIntegration.API;
18 public readonly WorkshopItem
item;
61 public override void Init(
string path,
string defaultPackage =
"_Elona")
63 base.Init(path, defaultPackage);
70 string key = elinIni.GetKey(
"path_workshop");
73 dirWorkshop =
new DirectoryInfo(key);
78 string path2 = Path.Combine(App.Client.GetAppInstallDirectory(SteamSettings.behaviour.settings.applicationId),
"../../workshop/content/2135150");
79 dirWorkshop =
new DirectoryInfo(path2);
80 elinIni.Global[
"path_workshop"] = dirWorkshop.FullName;
84 if ((!(dirWorkshop?.Exists)) ??
true)
88 Debug.Log(
"Workshop:" + dirWorkshop);
95 string arg = p.
dirInfo?.FullName;
96 int num = (p.willActivate ? 1 : 0);
97 if (ModLoadOrderPreset.IsValidId(p.
id))
99 return $"{arg},{num},{p.id.Trim()}";
110 List<string> list =
new List<string>();
113 string str = package.
dirInfo?.FullName;
114 if (!package.
builtin && !str.IsEmpty())
126 package.orderIndex = -1;
129 if (!File.Exists(pathLoadOrder))
133 Dictionary<string, BaseModPackage> dictionary =
new Dictionary<string, BaseModPackage>(StringComparer.OrdinalIgnoreCase);
134 Dictionary<string, BaseModPackage> dictionary2 =
new Dictionary<string, BaseModPackage>();
142 dictionary.TryAdd(text, package2);
145 if (!text2.IsEmpty())
147 dictionary2.TryAdd(text2, package2);
152 HashSet<BaseModPackage> hashSet =
new HashSet<BaseModPackage>();
153 string[] array = File.ReadAllLines(pathLoadOrder);
154 for (
int i = 0; i < array.Length; i++)
156 if (ParseLoadOrderLine(array[i], out var path, out var activate, out var
id))
162 if (value !=
null && hashSet.Add(value))
164 value.orderIndex = num;
165 value.willActivate = activate;
170 static bool ParseLoadOrderLine(
string line, out
string reference, out
bool reference2, out
string reference3)
175 if (line.IsEmpty() || line.Trim().IsEmpty())
179 int num2 = line.LastIndexOf(
',');
184 string text3 = line[(num2 + 1)..].Trim();
185 if (text3 ==
"0" || text3 ==
"1")
187 reference = line[..num2];
188 reference2 = text3 ==
"1";
189 return !reference.IsEmpty();
191 int num3 = line.LastIndexOf(
',', num2 - 1);
196 string text4 = line[(num3 + 1)..num2].Trim();
197 if (text4 !=
"0" && text4 !=
"1")
201 reference = line[..num3];
202 reference2 = text4 ==
"1";
204 return !reference.IsEmpty();
212 return path.NormalizePath().TrimEnd(
'/');
219 return CorePath.PathLoadOrderPreset + name.SanitizeFileName() +
".txt";
226 return new List<FileInfo>();
228 return new DirectoryInfo(
CorePath.
PathLoadOrderPreset).GetFiles(
"*.txt", SearchOption.TopDirectoryOnly).OrderBy((FileInfo f) => f.Name, StringComparer.OrdinalIgnoreCase).ToList();
240 File.WriteAllText(path, ModLoadOrderPreset.Serialize(preset));
244 public bool TryLoadPresetFile(FileInfo file, out ModLoadOrderPreset.Preset preset, out
string error)
250 text = File.ReadAllText(file.FullName);
257 if (!ModLoadOrderPreset.TryParse(text, out preset, out error))
261 if (preset.name.IsEmpty())
263 preset.name = Path.GetFileNameWithoutExtension(file.Name);
268 public ModLoadOrderPreset.ApplyResult
ApplyPreset(ModLoadOrderPreset.Preset preset,
bool disableOthers =
true)
270 ModLoadOrderPreset.ApplyResult result = ModLoadOrderPreset.Apply(packages, preset, disableOthers);
279 UnityEngine.Object.Destroy(
_loading.gameObject);
282 bool flag = !BaseCore.IsOffline && syncMods && UserGeneratedContent.Client.GetNumSubscribedItems() != 0;
283 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
310 yield
return awaiter;
313 BaseModManager.isInitialized =
true;
314 yield
return awaiter;
315 onComplete?.Invoke();
318 UnityEngine.Object.Destroy(
_loading.gameObject);
323 private void Halt(List<BaseModPackage> broken)
326 list.Add(
"Fix the package.xml, or unsubscribe / delete the mod, then restart the game.");
327 string text =
string.Join(
"\n", list);
328 Debug.LogError(
"#mod game will not start:\n" + text);
329 EGui.CreatePopup(() =>
new EGui.UpdateInfo(text), (EGui _) =>
false);
335 List<BaseModPackage> list = ModOrder.SolveOrder(packages, useLoadorder:
true);
336 if (list.Count != packages.Count)
338 Debug.LogError(
$"#mod order {list.Count} of {packages.Count} packages");
342 packages.AddRange(list);
357 dirInfo =
new DirectoryInfo(dir.FullName.NormalizePath()),
359 isInPackages = isInPackages,
360 Mapping =
new FileMapping(dir)
362 packages.Add(modPackage);
368 bool itemInstallInfo = UserGeneratedContent.Client.GetItemInstallInfo(
item.FileId, out var _, out var folderPath, out var _);
369 if (folderPath.IsEmpty())
371 folderPath = Path.Combine(
DirWorkshop.FullName,
item.FileId.ToString());
373 DirectoryInfo directoryInfo =
new DirectoryInfo(folderPath);
374 itemInstallInfo &= directoryInfo.Exists;
376 modPackage.installed = itemInstallInfo;
377 modPackage.banned =
item.IsBanned;
378 modPackage.workshopId =
item.FileId.ToString();
379 modPackage.item =
item;
392 Array.Reverse(directories);
393 DirectoryInfo[] array = directories;
394 foreach (DirectoryInfo directoryInfo
in array)
398 if (directoryInfo.Name ==
"Mod_FixedPackageLoader")
400 IO.DeleteDirectory(directoryInfo.FullName);
404 AddPackage(directoryInfo, isInPackages:
true);
413 DirectoryInfo[] directories =
new DirectoryInfo(
CorePath.
custom).GetDirectories();
415 DirectoryInfo[] array = directories;
416 foreach (DirectoryInfo dir
in array)
418 ParseExtra(dir, package);
424 Text status =
_loading.
Log(
"Initializing mods...");
425 HashSet<BaseModPackage> hashSet =
new HashSet<BaseModPackage>();
434 hashSet.Add(package);
435 Debug.LogWarning(
"Not a package: " + package.
dirInfo.FullName);
438 if (package.
parseError.IsEmpty() && package.
id.IsEmpty())
440 hashSet.Add(package);
441 Debug.LogWarning(
"No <id> in package.xml, skipped: " + package.
dirInfo.FullName);
444 if (!package.
id.IsEmpty())
446 mappedPackages[package.
id] =
package as ModPackage;
448 Debug.Log(package.ToString());
449 SetStatus(status,
$"Initializing mods {num}/{packages.Count}: {package.title}");
453 _loading.
Log(
"Mod " + package.
title +
"/" + package.
id +
" has failed to initialize, " + ex.Message);
457 if (hashSet.Count > 0)
459 packages.RemoveAll(hashSet.Contains);
460 _loading.
Log(
$"Ignored {hashSet.Count} folder(s) without a usable package.xml");
462 SetStatus(status,
$"Initialized {packages.Count} mods");
470 ModUtil.OnModsActivated();
471 ModUtil.LoadTypeFallback();
476 Dictionary<string, BaseModPackage> loaded =
new Dictionary<string, BaseModPackage>();
477 Dictionary<string, BaseModPackage> claimed =
new Dictionary<string, BaseModPackage>();
481 package.blockedBy =
null;
482 package.langDepError =
null;
484 Dictionary<string, BaseModPackage> dictionary =
new Dictionary<string, BaseModPackage>();
490 dictionary.TryAdd(text, package2);
497 if (!
item.IsValidVersion())
507 if (
item.CanActivate)
510 if (baseModPackage !=
null)
512 item.blockedBy = baseModPackage;
520 item.langDepError = text2;
531 _loading?.
Log(
"Failed to activate mod " +
item.title +
": " + ex.Message);
545 Debug.LogWarning(
$"#mod {num} package(s) skipped due to incompatibility");
556 foreach (
string[] array
in dependency)
558 if (array ==
null || array.Length == 0)
565 string[] array2 = array;
566 for (
int j = 0; j < array2.Length; j++)
573 if (loaded.ContainsKey(text))
578 if (!
installed.TryGetValue(text, out var value) || value == p)
582 if (packages.IndexOf(value) > packages.IndexOf(p))
584 if (baseModPackage ==
null)
586 baseModPackage = value;
589 else if (baseModPackage2 ==
null)
591 baseModPackage2 = value;
596 if (baseModPackage !=
null)
598 return "mod_info_dep_order\t" + baseModPackage.
title.IsEmpty(baseModPackage.
dirInfo?.Name);
600 if (baseModPackage2 !=
null)
602 return "mod_info_dep_failed\t" + baseModPackage2.
title.IsEmpty(baseModPackage2.
dirInfo?.Name);
604 return "mod_info_dep_missing\t" +
string.Join(
'\t', array);
622 if (incompatible !=
null)
624 string[][] array = incompatible;
625 foreach (
string[] array2
in array)
631 string[] array3 = array2;
632 for (
int j = 0; j < array3.Length; j++)
642 Debug.Log(
"#mod incompatible: '" + p.
id +
"' self decl, ignored");
644 else if (loaded.TryGetValue(text2, out value) && value !=
null)
650 Debug.Log(
"#mod incompatible: '" + p.
id +
"' targets builtin '" + value.
id +
"', ignored");
655 if (claimed.TryGetValue(text, out var value2) && value2 !=
null && !value2.builtin)
675 if (incompatible ==
null)
679 string[][] array = incompatible;
680 foreach (
string[] array2
in array)
686 string[] array3 = array2;
687 for (
int j = 0; j < array3.Length; j++)
690 if (text2 !=
null && !(text2 == text))
692 claimed.TryAdd(text2, p);
700 PublishedFileId_t[] subscribedItems = UserGeneratedContent.Client.GetSubscribedItems();
701 if (!subscribedItems.IsEmpty())
703 _subscribedItems = subscribedItems.Select((PublishedFileId_t
id) =>
id.m_PublishedFileId).ToHashSet();
708 yield
return new WaitForSecondsRealtime(3f);
715 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
716 Text status =
_loading.
Log(
"Fetching subscriptions...");
717 Dictionary<ulong, WorkshopItem> fetched =
new Dictionary<ulong, WorkshopItem>();
718 bool aborted =
false;
719 for (
int i = 0; i < subscribed.Length; i += 50)
721 PublishedFileId_t[] array = subscribed.Skip(i).Take(50).ToArray();
722 UgcQuery query = UgcQuery.Get(array);
723 bool completed =
false;
724 bool discard =
false;
725 SetStatus(status,
$"Fetching subscriptions {i + array.Length}/{subscribed.Length}...(Hit ESC to cancel)");
726 if (query ==
null || !query.Execute(OnQueryCompleted))
728 Debug.LogWarning(
"Workshop: steam rejected the subscription query");
733 float deadline = Time.realtimeSinceStartup + 30f;
743 yield
return awaiter;
747 Debug.LogWarning(
"Workshop: subscription query cancelled or timed out");
750 void OnQueryCompleted(UgcQuery result)
757 foreach (WorkshopItem results
in result.ResultsList)
759 SteamUGCDetails_t sourceItemDetails = results.SourceItemDetails;
760 if (sourceItemDetails.m_eResult == EResult.k_EResultOK && sourceItemDetails.m_nPublishedFileId.m_PublishedFileId != 0L)
762 fetched[sourceItemDetails.m_nPublishedFileId.m_PublishedFileId] = results;
767 foreach (WorkshopItem value
in fetched.Values)
779 SetStatus(status,
$"Fetched {fetched.Count}/{subscribed.Length} subscriptions");
784 for (
int j = 0; j < subscribed.Length; j++)
786 PublishedFileId_t publishedFileId_t = subscribed[j];
787 if (!fetched.ContainsKey(publishedFileId_t.m_PublishedFileId) && !Directory.Exists(Path.Combine(dirWorkshop.FullName, publishedFileId_t.ToString())))
796 float now = Time.realtimeSinceStartup;
797 List<WorkshopDownload> queue = (from p in packages
798 where p.item is WorkshopItem workshopItem && (!p.installed || workshopItem.IsNeedsUpdate)
800 if (queue.Count == 0)
804 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
805 Text status =
_loading.
Log(
$"Downloading {queue.Count} mods...");
806 Dictionary<ulong, EResult> results =
new Dictionary<ulong, EResult>();
807 int total = queue.Count;
809 float lastActivity = now;
810 float nextStatus = 0f;
811 UserGeneratedContent.Client.EventItemDownloaded.AddListener(OnItemDownloaded);
814 while (queue.Count > 0)
816 now = Time.realtimeSinceStartup;
817 for (
int num = queue.Count - 1; num >= 0; num--)
820 WorkshopItem
item = workshopDownload.
item;
821 ulong publishedFileId =
item.FileId.m_PublishedFileId;
823 string reason =
null;
828 workshopDownload.package.downloadStarted =
true;
829 workshopDownload.lastChange = now;
830 if (
item.DownloadItem(highPriority:
true))
832 Debug.Log(
"Start downloading: " + workshopDownload.
Title +
" | " +
$"Installed={item.IsInstalled}, " +
$"Update={item.IsNeedsUpdate}, " +
$"Downloading={item.IsDownloading}, " +
$"Pending={item.IsDownloadPending}");
835 reason =
"cannot start the download";
837 else if (results.TryGetValue(publishedFileId, out value) && value != EResult.k_EResultOK)
839 reason =
$"download failed ({value})";
847 if (
item.IsDownloadPending)
849 workshopDownload.lastChange = now;
852 if (
item.IsDownloading)
854 float downloadCompletion =
item.DownloadCompletion;
855 if (downloadCompletion > workshopDownload.
progress)
857 workshopDownload.progress = downloadCompletion;
859 lastActivity = (lastChange = now);
860 workshopDownload.lastChange = lastChange;
864 if (!(now - workshopDownload.
lastChange > 20f))
868 reason =
"cannot start the download";
882 if (queue.Count == 0)
891 if (now - lastActivity > 90f)
896 if (now >= nextStatus)
898 nextStatus = now + 0.5f;
900 SetStatus(status,
$"Downloading mods {completed}/{total}: " +
$"{workshopDownload2.Title} {Mathf.FloorToInt(workshopDownload2.progress * 100f)}%...(Hit ESC to cancel)");
902 yield
return awaiter;
907 UserGeneratedContent.Client.EventItemDownloaded.RemoveListener(OnItemDownloaded);
909 SetStatus(status,
$"Downloaded {completed}/{total} mods");
911 void OnItemDownloaded(DownloadItemResult_t result)
913 results[result.m_nPublishedFileId.m_PublishedFileId] = result.m_eResult;
919 HashSet<string> hashSet = packages.Select((
BaseModPackage p) => p.
dirInfo.FullName).ToHashSet(StringComparer.OrdinalIgnoreCase);
920 DirectoryInfo[] directories = dirWorkshop.GetDirectories();
921 foreach (DirectoryInfo directoryInfo
in directories)
924 bool flag = ulong.TryParse(directoryInfo.Name, out result);
930 modPackage.workshopId = directoryInfo.Name;
939 package.installed =
true;
940 package.dirInfo =
new DirectoryInfo(path.NormalizePath());
943 modPackage.Mapping =
new FileMapping(package.
dirInfo);
952 Debug.LogWarning(
"Workshop update failed: " + download.
Title +
", " + reason);
956 Debug.LogWarning(
"Workshop download failed: " + download.
Title +
", " + reason);
971 if (!
item.IsInstalled ||
item.IsNeedsUpdate ||
item.IsDownloading ||
item.IsDownloadPending)
975 if (UserGeneratedContent.Client.GetItemInstallInfo(
item.FileId, out var _, out path, out var _) && !path.IsEmpty())
977 return Directory.Exists(path);
984 return UnityEngine.Input.GetKey(KeyCode.Escape);
987 private static void SetStatus(Text status,
string message)
991 status.text = message;
998 switch (dir.Name.ToLower())
1015 case "texture replace":
static bool isInitialized
static string rootDefaultPacakge
static BaseModManager Instance
static List< string > listChainLoad
static string NormalizeId(string s)
static bool IsBuiltinDir(string dirName)
static string PathLoadOrderPreset
static string PathLoadOrder
static void SaveElinIni(IniData ini)
static IniData GetElinIni()
readonly BaseModPackage package
WorkshopDownload(BaseModPackage package, float now)
readonly WorkshopItem item
static new ModManager Instance
override void ParseExtra(DirectoryInfo dir, BaseModPackage package)
void InstallWorkshopPackage(WorkshopDownload download, string path)
string UnmetDependency(BaseModPackage p, Dictionary< string, BaseModPackage > loaded, Dictionary< string, BaseModPackage > installed)
bool TryLoadPresetFile(FileInfo file, out ModLoadOrderPreset.Preset preset, out string error)
void LoadCachedWorkshopPackages()
static string FormatLoadOrder(BaseModPackage p)
static bool IsInitialized
IEnumerator QuerySubscriptions(PublishedFileId_t[] subscribed)
readonly HashSet< ulong > _blockedItems
static string NormalizeOrderPath(string path)
static List< string > ListChainLoad
static BaseModPackage FindIncompatible(BaseModPackage p, Dictionary< string, BaseModPackage > loaded, Dictionary< string, BaseModPackage > claimed)
List< FileInfo > replaceFiles
HashSet< ulong > _subscribedItems
ModPackage AddWorkshopPackage(WorkshopItem item, bool isInPackages=false)
ModLoadOrderPreset.ApplyResult ApplyPreset(ModLoadOrderPreset.Preset preset, bool disableOthers=true)
static bool IsItemReady(WorkshopItem item, out string path)
static string GetPresetPath(string name)
override void Init(string path, string defaultPackage="_Elona")
IEnumerator RefreshMods(Action onComplete, bool syncMods)
void AbortDownloads(List< WorkshopDownload > queue, string reason)
IEnumerator LoadWorkshopPackages()
static void RegisterLoaded(BaseModPackage p, Dictionary< string, BaseModPackage > loaded, Dictionary< string, BaseModPackage > claimed)
IEnumerator DownloadSubscriptions()
void FailWorkshopPackage(WorkshopDownload download, string reason)
void ActivatePackageList()
static bool IsCancelRequested()
ModPackage AddPackage(DirectoryInfo dir, bool isInPackages=false)
void Halt(List< BaseModPackage > broken)
static List< object > ListPluginObject
void MapActivatedPackages()
bool SavePreset(string name, out string path)
List< FileInfo > ListPresets()
static void SetStatus(Text status, string message)
static DirectoryInfo DirWorkshop
IReadOnlyList< FileInfo > ParseTalkText(DirectoryInfo dir)
void ParseLangMod(DirectoryInfo dir)
IReadOnlyList< FileInfo > ParseTexture(DirectoryInfo dir)
void AddOrUpdateLang(DirectoryInfo dir)
IReadOnlyList< FileInfo > ParseMapPiece(DirectoryInfo dir, bool addToList=true)
IReadOnlyList< FileInfo > ParseTextureReplace(DirectoryInfo dir)
IReadOnlyList< FileInfo > ParsePortrait(DirectoryInfo dir)
IReadOnlyList< FileInfo > ParseMap(DirectoryInfo dir, bool addToList=true)
IReadOnlyList< FileInfo > ParseSound(DirectoryInfo dir)