2using System.Collections;
3using System.Collections.Generic;
6using HeathenEngineering.SteamworksIntegration;
7using HeathenEngineering.SteamworksIntegration.API;
18 public readonly WorkshopItem
item;
47 public readonly List<ModGroup>
emptyGroups =
new List<ModGroup>();
63 public override void Init(
string path,
string defaultPackage =
"_Elona")
65 base.Init(path, defaultPackage);
72 string key = elinIni.GetKey(
"path_workshop");
75 dirWorkshop =
new DirectoryInfo(key);
80 string path2 = Path.Combine(App.Client.GetAppInstallDirectory(SteamSettings.behaviour.settings.applicationId),
"../../workshop/content/2135150");
81 dirWorkshop =
new DirectoryInfo(path2);
82 elinIni.Global[
"path_workshop"] = dirWorkshop.FullName;
86 if ((!(dirWorkshop?.Exists)) ??
true)
90 Debug.Log(
"Workshop:" + dirWorkshop);
97 string arg = p.
dirInfo?.FullName;
98 int num = (p.willActivate ? 1 : 0);
99 if (ModLoadOrderPreset.IsValidId(p.
id))
101 return $"{arg},{num},{p.id.Trim()}";
113 List<string> list =
new List<string>();
117 if (emptyGroup.
after ==
null)
119 list.Add((modGroup = emptyGroup).ToString());
124 string str = package.
dirInfo?.FullName;
125 string text = ((package.builtin || str.IsEmpty()) ?
"" :
FormatLoadOrder(package));
128 if (package.
group != modGroup)
130 modGroup = package.
group;
131 list.Add(modGroup?.ToString() ??
"#group");
137 if (emptyGroup2.
after == package)
139 list.Add((modGroup = emptyGroup2).ToString());
148 HashSet<ModGroup> hashSet =
new HashSet<ModGroup>();
153 if (package.
group != modGroup)
155 modGroup = package.
group;
156 modGroup2 = ((modGroup ==
null || hashSet.Add(modGroup)) ? modGroup : modGroup.Clone());
158 package.group = modGroup2;
166 emptyGroup.after = package2;
177 package.orderIndex = -1;
178 package.group =
null;
181 if (!File.Exists(pathLoadOrder))
185 Dictionary<string, BaseModPackage> dictionary =
new Dictionary<string, BaseModPackage>(StringComparer.OrdinalIgnoreCase);
186 Dictionary<string, BaseModPackage> dictionary2 =
new Dictionary<string, BaseModPackage>();
194 dictionary.TryAdd(text, package2);
197 if (!text2.IsEmpty())
199 dictionary2.TryAdd(text2, package2);
207 HashSet<BaseModPackage> hashSet =
new HashSet<BaseModPackage>();
208 foreach (
string item in File.ReadAllLines(pathLoadOrder).Append(
"#group"))
215 if (modGroup !=
null && num2 == 0)
217 modGroup.after = after;
223 else if (ParseLoadOrderLine(
item, out path, out activate, out
id))
230 if (value !=
null && hashSet.Add(value))
232 value.orderIndex = num;
233 value.willActivate = activate;
234 value.group = modGroup;
240 static bool ParseLoadOrderLine(
string line, out
string reference, out
bool reference2, out
string reference3)
245 if (line.IsEmpty() || line.Trim().IsEmpty())
249 int num3 = line.LastIndexOf(
',');
254 string text3 = line[(num3 + 1)..].Trim();
255 if (text3 ==
"0" || text3 ==
"1")
257 reference = line[..num3];
258 reference2 = text3 ==
"1";
259 return !reference.IsEmpty();
261 int num4 = line.LastIndexOf(
',', num3 - 1);
266 string text4 = line[(num4 + 1)..num3].Trim();
267 if (text4 !=
"0" && text4 !=
"1")
271 reference = line[..num4];
272 reference2 = text4 ==
"1";
274 return !reference.IsEmpty();
282 return path.NormalizePath().TrimEnd(
'/');
289 return CorePath.PathLoadOrderPreset + name.SanitizeFileName() +
".txt";
296 return new List<FileInfo>();
298 return new DirectoryInfo(
CorePath.
PathLoadOrderPreset).GetFiles(
"*.txt", SearchOption.TopDirectoryOnly).OrderBy((FileInfo f) => f.Name, StringComparer.OrdinalIgnoreCase).ToList();
310 File.WriteAllText(path, ModLoadOrderPreset.Serialize(preset));
314 public bool TryLoadPresetFile(FileInfo file, out ModLoadOrderPreset.Preset preset, out
string error)
320 text = File.ReadAllText(file.FullName);
327 if (!ModLoadOrderPreset.TryParse(text, out preset, out error))
331 if (preset.name.IsEmpty())
333 preset.name = Path.GetFileNameWithoutExtension(file.Name);
338 public ModLoadOrderPreset.ApplyResult
ApplyPreset(ModLoadOrderPreset.Preset preset,
bool disableOthers =
true)
340 ModLoadOrderPreset.ApplyResult result = ModLoadOrderPreset.Apply(packages, preset, disableOthers);
349 UnityEngine.Object.Destroy(
_loading.gameObject);
352 bool flag = !BaseCore.IsOffline && syncMods && UserGeneratedContent.Client.GetNumSubscribedItems() != 0;
353 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
380 yield
return awaiter;
383 BaseModManager.isInitialized =
true;
384 yield
return awaiter;
385 onComplete?.Invoke();
388 UnityEngine.Object.Destroy(
_loading.gameObject);
393 private void Halt(List<BaseModPackage> broken)
396 list.Add(
"Fix the package.xml, or unsubscribe / delete the mod, then restart the game.");
397 string text =
string.Join(
"\n", list);
398 Debug.LogError(
"#mod game will not start:\n" + text);
399 EGui.CreatePopup(() =>
new EGui.UpdateInfo(text), (EGui _) =>
false);
405 List<BaseModPackage> list = ModOrder.SolveOrder(packages, useLoadorder:
true);
406 if (list.Count != packages.Count)
408 Debug.LogError(
$"#mod order {list.Count} of {packages.Count} packages");
412 packages.AddRange(list);
428 dirInfo =
new DirectoryInfo(dir.FullName.NormalizePath()),
430 isInPackages = isInPackages,
431 Mapping =
new FileMapping(dir)
433 packages.Add(modPackage);
439 bool itemInstallInfo = UserGeneratedContent.Client.GetItemInstallInfo(
item.FileId, out var _, out var folderPath, out var _);
440 if (folderPath.IsEmpty())
442 folderPath = Path.Combine(
DirWorkshop.FullName,
item.FileId.ToString());
444 DirectoryInfo directoryInfo =
new DirectoryInfo(folderPath);
445 itemInstallInfo &= directoryInfo.Exists;
447 modPackage.installed = itemInstallInfo;
448 modPackage.banned =
item.IsBanned;
449 modPackage.workshopId =
item.FileId.ToString();
450 modPackage.item =
item;
463 Array.Reverse(directories);
464 DirectoryInfo[] array = directories;
465 foreach (DirectoryInfo directoryInfo
in array)
469 if (directoryInfo.Name ==
"Mod_FixedPackageLoader")
471 IO.DeleteDirectory(directoryInfo.FullName);
475 AddPackage(directoryInfo, isInPackages:
true);
484 DirectoryInfo[] directories =
new DirectoryInfo(
CorePath.
custom).GetDirectories();
486 DirectoryInfo[] array = directories;
487 foreach (DirectoryInfo dir
in array)
489 ParseExtra(dir, package);
495 Text status =
_loading.
Log(
"Initializing mods...");
496 HashSet<BaseModPackage> hashSet =
new HashSet<BaseModPackage>();
505 hashSet.Add(package);
506 Debug.LogWarning(
"Not a package: " + package.
dirInfo.FullName);
509 if (package.
parseError.IsEmpty() && package.
id.IsEmpty())
511 hashSet.Add(package);
512 Debug.LogWarning(
"No <id> in package.xml, skipped: " + package.
dirInfo.FullName);
515 if (!package.
id.IsEmpty())
517 mappedPackages[package.
id] =
package as ModPackage;
519 Debug.Log(package.ToString());
520 SetStatus(status,
$"Initializing mods {num}/{packages.Count}: {package.title}");
524 _loading.
Log(
"Mod " + package.
title +
"/" + package.
id +
" has failed to initialize, " + ex.Message);
528 if (hashSet.Count > 0)
530 packages.RemoveAll(hashSet.Contains);
531 _loading.
Log(
$"Ignored {hashSet.Count} folder(s) without a usable package.xml");
533 SetStatus(status,
$"Initialized {packages.Count} mods");
541 ModUtil.OnModsActivated();
542 ModUtil.LoadTypeFallback();
547 Dictionary<string, BaseModPackage> loaded =
new Dictionary<string, BaseModPackage>();
548 Dictionary<string, BaseModPackage> claimed =
new Dictionary<string, BaseModPackage>();
552 package.blockedBy =
null;
553 package.langDepError =
null;
555 Dictionary<string, BaseModPackage> dictionary =
new Dictionary<string, BaseModPackage>();
561 dictionary.TryAdd(text, package2);
568 if (!
item.IsValidVersion())
578 if (
item.CanActivate)
581 if (baseModPackage !=
null)
583 item.blockedBy = baseModPackage;
591 item.langDepError = text2;
602 _loading?.
Log(
"Failed to activate mod " +
item.title +
": " + ex.Message);
616 Debug.LogWarning(
$"#mod {num} package(s) skipped due to incompatibility");
627 foreach (
string[] array
in dependency)
629 if (array ==
null || array.Length == 0)
636 string[] array2 = array;
637 for (
int j = 0; j < array2.Length; j++)
644 if (loaded.ContainsKey(text))
649 if (!
installed.TryGetValue(text, out var value) || value == p)
653 if (packages.IndexOf(value) > packages.IndexOf(p))
655 if (baseModPackage ==
null)
657 baseModPackage = value;
660 else if (baseModPackage2 ==
null)
662 baseModPackage2 = value;
667 if (baseModPackage !=
null)
669 return "mod_info_dep_order\t" + baseModPackage.
title.IsEmpty(baseModPackage.
dirInfo?.Name);
671 if (baseModPackage2 !=
null)
673 return "mod_info_dep_failed\t" + baseModPackage2.
title.IsEmpty(baseModPackage2.
dirInfo?.Name);
675 return "mod_info_dep_missing\t" +
string.Join(
'\t', array);
693 if (incompatible !=
null)
695 string[][] array = incompatible;
696 foreach (
string[] array2
in array)
702 string[] array3 = array2;
703 for (
int j = 0; j < array3.Length; j++)
713 Debug.Log(
"#mod incompatible: '" + p.
id +
"' self decl, ignored");
715 else if (loaded.TryGetValue(text2, out value) && value !=
null)
721 Debug.Log(
"#mod incompatible: '" + p.
id +
"' targets builtin '" + value.
id +
"', ignored");
726 if (claimed.TryGetValue(text, out var value2) && value2 !=
null && !value2.builtin)
746 if (incompatible ==
null)
750 string[][] array = incompatible;
751 foreach (
string[] array2
in array)
757 string[] array3 = array2;
758 for (
int j = 0; j < array3.Length; j++)
761 if (text2 !=
null && !(text2 == text))
763 claimed.TryAdd(text2, p);
771 PublishedFileId_t[] subscribedItems = UserGeneratedContent.Client.GetSubscribedItems();
772 if (!subscribedItems.IsEmpty())
774 _subscribedItems = subscribedItems.Select((PublishedFileId_t
id) =>
id.m_PublishedFileId).ToHashSet();
779 yield
return new WaitForSecondsRealtime(3f);
786 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
787 Text status =
_loading.
Log(
"Fetching subscriptions...");
788 Dictionary<ulong, WorkshopItem> fetched =
new Dictionary<ulong, WorkshopItem>();
789 bool aborted =
false;
790 for (
int i = 0; i < subscribed.Length; i += 50)
792 PublishedFileId_t[] array = subscribed.Skip(i).Take(50).ToArray();
793 UgcQuery query = UgcQuery.Get(array);
794 bool completed =
false;
795 bool discard =
false;
796 SetStatus(status,
$"Fetching subscriptions {i + array.Length}/{subscribed.Length}...(Hit ESC to cancel)");
797 if (query ==
null || !query.Execute(OnQueryCompleted))
799 Debug.LogWarning(
"Workshop: steam rejected the subscription query");
804 float deadline = Time.realtimeSinceStartup + 30f;
814 yield
return awaiter;
818 Debug.LogWarning(
"Workshop: subscription query cancelled or timed out");
821 void OnQueryCompleted(UgcQuery result)
828 foreach (WorkshopItem results
in result.ResultsList)
830 SteamUGCDetails_t sourceItemDetails = results.SourceItemDetails;
831 if (sourceItemDetails.m_eResult == EResult.k_EResultOK && sourceItemDetails.m_nPublishedFileId.m_PublishedFileId != 0L)
833 fetched[sourceItemDetails.m_nPublishedFileId.m_PublishedFileId] = results;
838 foreach (WorkshopItem value
in fetched.Values)
850 SetStatus(status,
$"Fetched {fetched.Count}/{subscribed.Length} subscriptions");
855 for (
int j = 0; j < subscribed.Length; j++)
857 PublishedFileId_t publishedFileId_t = subscribed[j];
858 if (!fetched.ContainsKey(publishedFileId_t.m_PublishedFileId) && !Directory.Exists(Path.Combine(dirWorkshop.FullName, publishedFileId_t.ToString())))
867 float now = Time.realtimeSinceStartup;
868 List<WorkshopDownload> queue = (from p in packages
869 where p.item is WorkshopItem workshopItem && (!p.installed || workshopItem.IsNeedsUpdate)
871 if (queue.Count == 0)
875 WaitForEndOfFrame awaiter =
new WaitForEndOfFrame();
876 Text status =
_loading.
Log(
$"Downloading {queue.Count} mods...");
877 Dictionary<ulong, EResult> results =
new Dictionary<ulong, EResult>();
878 int total = queue.Count;
880 float lastActivity = now;
881 float nextStatus = 0f;
882 UserGeneratedContent.Client.EventItemDownloaded.AddListener(OnItemDownloaded);
885 while (queue.Count > 0)
887 now = Time.realtimeSinceStartup;
888 for (
int num = queue.Count - 1; num >= 0; num--)
891 WorkshopItem
item = workshopDownload.
item;
892 ulong publishedFileId =
item.FileId.m_PublishedFileId;
894 string reason =
null;
899 workshopDownload.package.downloadStarted =
true;
900 workshopDownload.lastChange = now;
901 if (
item.DownloadItem(highPriority:
true))
903 Debug.Log(
"Start downloading: " + workshopDownload.
Title +
" | " +
$"Installed={item.IsInstalled}, " +
$"Update={item.IsNeedsUpdate}, " +
$"Downloading={item.IsDownloading}, " +
$"Pending={item.IsDownloadPending}");
906 reason =
"cannot start the download";
908 else if (results.TryGetValue(publishedFileId, out value) && value != EResult.k_EResultOK)
910 reason =
$"download failed ({value})";
918 if (
item.IsDownloadPending)
920 workshopDownload.lastChange = now;
923 if (
item.IsDownloading)
925 float downloadCompletion =
item.DownloadCompletion;
926 if (downloadCompletion > workshopDownload.
progress)
928 workshopDownload.progress = downloadCompletion;
930 lastActivity = (lastChange = now);
931 workshopDownload.lastChange = lastChange;
935 if (!(now - workshopDownload.
lastChange > 20f))
939 reason =
"cannot start the download";
953 if (queue.Count == 0)
962 if (now - lastActivity > 90f)
967 if (now >= nextStatus)
969 nextStatus = now + 0.5f;
971 SetStatus(status,
$"Downloading mods {completed}/{total}: " +
$"{workshopDownload2.Title} {Mathf.FloorToInt(workshopDownload2.progress * 100f)}%...(Hit ESC to cancel)");
973 yield
return awaiter;
978 UserGeneratedContent.Client.EventItemDownloaded.RemoveListener(OnItemDownloaded);
980 SetStatus(status,
$"Downloaded {completed}/{total} mods");
982 void OnItemDownloaded(DownloadItemResult_t result)
984 results[result.m_nPublishedFileId.m_PublishedFileId] = result.m_eResult;
990 HashSet<string> hashSet = packages.Select((
BaseModPackage p) => p.
dirInfo.FullName).ToHashSet(StringComparer.OrdinalIgnoreCase);
991 DirectoryInfo[] directories = dirWorkshop.GetDirectories();
992 foreach (DirectoryInfo directoryInfo
in directories)
995 bool flag = ulong.TryParse(directoryInfo.Name, out result);
1001 modPackage.workshopId = directoryInfo.Name;
1010 package.installed =
true;
1011 package.dirInfo =
new DirectoryInfo(path.NormalizePath());
1014 modPackage.Mapping =
new FileMapping(package.
dirInfo);
1023 Debug.LogWarning(
"Workshop update failed: " + download.
Title +
", " + reason);
1027 Debug.LogWarning(
"Workshop download failed: " + download.
Title +
", " + reason);
1042 if (!
item.IsInstalled ||
item.IsNeedsUpdate ||
item.IsDownloading ||
item.IsDownloadPending)
1046 if (UserGeneratedContent.Client.GetItemInstallInfo(
item.FileId, out var _, out path, out var _) && !path.IsEmpty())
1048 return Directory.Exists(path);
1055 return UnityEngine.Input.GetKey(KeyCode.Escape);
1062 status.text = message;
1069 switch (dir.Name.ToLower())
1086 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()
static bool TryParse(string line, out ModGroup group)
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)
readonly List< ModGroup > emptyGroups
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)