using CringePlugins.Abstractions; using CringePlugins.Services; using ImGuiNET; using Microsoft.Extensions.DependencyInjection; using Plugin.ClientModLoader.Utils; using Sandbox; using Steamworks; using System.Collections.Immutable; using System.Net.Http.Json; using System.Text.Json; using VRage; using VRage.FileSystem; using static ImGuiNET.ImGui; namespace Plugin.ClientModLoader; public class ModListComponent : IRenderComponent { private readonly string _configPath; private readonly JsonSerializerOptions _options = new(JsonSerializerDefaults.Web); private readonly IImGuiImageService _imageService = MySandboxGame.Services.GetRequiredService(); private bool _visible; private string _searchQuery = string.Empty; private Task? _searchTask; private PublishedFileId_t[] _mods = []; private ImmutableDictionary _whitelist = ImmutableDictionary.Empty; private List _modList = []; private ModItem? _selected; public HashSet Installed { get; } public bool Visible { get => _visible; set => _visible = value; } public ModListComponent(string configPath) { _configPath = configPath; if (File.Exists(configPath)) { using var stream = File.OpenRead(configPath); Installed = JsonSerializer.Deserialize>(stream, _options)!; } else Installed = []; _searchTask = DownloadWhitelist(); } private async Task DownloadWhitelist() { using var client = new HttpClient(); var response = await client.GetFromJsonAsync( "https://github.com/sepluginloader/sepluginloader.github.io/raw/refs/heads/main/plugins.json"); if (response is null) return; var mods = new List(); var builder = ImmutableDictionary.CreateBuilder(); foreach (var item in response.Mods) { if (!ulong.TryParse(item.Id, out var id)) continue; mods.Add(new PublishedFileId_t(id)); builder.Add(id, item); } _mods = mods.ToArray(); _whitelist = builder.ToImmutable(); await RefreshAsync(); } public void OnFrame() { if (!Visible) return; SetNextWindowSize(new(700, 500), ImGuiCond.FirstUseEver); if (!Begin("Mod List", ref _visible)) return; try { InputText("##searchbox", ref _searchQuery, 256); SameLine(); if (Button("Search")) { _searchTask = RefreshAsync(); return; } Spacing(); switch (_searchTask) { case { IsCompleted: false }: TextDisabled("Loading..."); return; case { IsCompletedSuccessfully: false }: { TextDisabled("Failed to load plugins list"); if (_searchTask.Exception is null) return; foreach (var exception in _searchTask.Exception.InnerExceptions) { TextWrapped($"{exception.GetType()}: {exception.Message}"); } return; } } if (_modList.Count == 0) { TextDisabled("Nothing found"); return; } BeginChild("List", new(400, 0), ImGuiChildFlags.Border | ImGuiChildFlags.ResizeX); { if (BeginTable("AvailableModsTable", 3, ImGuiTableFlags.ScrollY | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp)) { TableSetupColumn("Name", ImGuiTableColumnFlags.None, .75f); TableSetupColumn("Installed", ImGuiTableColumnFlags.None, .25f); TableHeadersRow(); foreach (var mod in _modList) { TableNextRow(); TableNextColumn(); var selected = _selected == mod; if (Selectable(mod.Name, ref selected, ImGuiSelectableFlags.SpanAllColumns)) { _selected = selected ? mod : null; } if (!string.IsNullOrEmpty(mod.Summary) && IsItemHovered(ImGuiHoveredFlags.ForTooltip)) { SetTooltip(mod.Summary); } TableNextColumn(); var installed = Installed.Contains(mod.Id); TextColored(installed ? new(0f, 1f, 0f, 1f) : new(1f, 0f, 0f, 1f), installed ? "Installed" : "Not Installed"); } EndTable(); } EndChild(); } SameLine(); BeginGroup(); BeginChild("Mod View", new(0, -GetFrameHeightWithSpacing())); // Leave room for 1 line below us if (_selected is not null) { Text(_selected.Name); Separator(); if (_selected.ThubmnailTask.IsCompletedSuccessfully && _selected.ThubmnailTask.Result is { } thumbnail) { var img = _imageService.GetFromPath(thumbnail); var size = img.Size; if (size.X > 256 || size.Y > 256) { //scale down to fit var scale = Math.Min(256f / size.X, 256f / size.Y); size = new(size.X * scale, size.Y * scale); } Image(img, size); } if (BeginTabBar("##ModViewTabs")) { if (!string.IsNullOrEmpty(_selected.Description) && BeginTabItem("Description")) { TextWrapped(_selected.Description); EndTabItem(); } if (BeginTabItem("Mod Description")) { TextWrapped(string.IsNullOrEmpty(_selected.ModDescription) ? "Nothing." : _selected.ModDescription); EndTabItem(); } if (BeginTabItem("Details")) { TextLinkOpenURL("Mod URL", _selected.Url); Text("Author:"); SameLine(); TextWrapped(_selected.Author); Text("Created:"); SameLine(); TextWrapped(DateTimeOffset.FromUnixTimeSeconds(_selected.Created).ToString("g")); Text("Updated:"); SameLine(); TextWrapped(DateTimeOffset.FromUnixTimeSeconds(_selected.Updated).ToString("g")); EndTabItem(); } EndTabBar(); } } EndChild(); if (_selected is not null) { var installed = Installed.Contains(_selected.Id); if (Button(installed ? "Uninstall" : "Install")) { if (installed) Installed.Remove(_selected.Id); else Installed.Add(_selected.Id); Save(); } } EndGroup(); } finally { End(); } } private void Save() { using var stream = File.Create(_configPath); JsonSerializer.Serialize(stream, Installed, _options); } private async Task RefreshAsync() { var handle = SteamUGC.CreateQueryUGCDetailsRequest(_mods, (uint)_mods.Length); if (handle.m_UGCQueryHandle == ulong.MaxValue) throw new Exception("Failed to create UGC request"); try { SteamUGC.SetSearchText(handle, _searchQuery); SteamUGC.SetReturnLongDescription(handle, true); // TODO font support for other languages // SteamUGC.SetLanguage(handle, MapLanguage(MySandboxGame.Config.Language)); var call = SteamUGC.SendQueryUGCRequest(handle); var result = await CallResultAsync.Create(call); var list = new List((int)result.m_unNumResultsReturned); for (uint i = 0; i < result.m_unNumResultsReturned; i++) { if (!SteamUGC.GetQueryUGCResult(handle, i, out var details)) break; if (details.m_eResult != EResult.k_EResultOK) continue; var title = details.m_rgchTitle; if (!string.IsNullOrEmpty(_searchQuery) && !title.Contains(_searchQuery, StringComparison.OrdinalIgnoreCase)) continue; var item = _whitelist[details.m_nPublishedFileId.m_PublishedFileId]; var description = details.m_rgchDescription; list.Add(new(details.m_nPublishedFileId.m_PublishedFileId, title, string.IsNullOrEmpty(item.Tooltip) ? description[..Math.Min(description.Length, 255)] : item.Tooltip, item.Description, description, $"https://steamcommunity.com/sharedfiles/filedetails/?id={details.m_nPublishedFileId.m_PublishedFileId}", details.m_rtimeCreated, details.m_rtimeUpdated, item.Author, GetThumbnailImageAsync(details.m_hPreviewFile))); } _modList = list; } finally { SteamUGC.ReleaseQueryUGCRequest(handle); } } private static async Task GetThumbnailImageAsync(UGCHandle_t handle) { var dir = MyFileSystem.WorkshopBrowserPath; if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); var path = Path.Join(dir, $"{handle.m_UGCHandle}.png"); if (File.Exists(path)) { if (File.GetLastWriteTimeUtc(path) + TimeSpan.FromMinutes(10) < DateTime.UtcNow) File.Delete(path); else return path; } var call = SteamRemoteStorage.UGCDownloadToLocation(handle, path, 0); var result = await CallResultAsync.Create(call); if (result.m_eResult != EResult.k_EResultOK || result.m_hFile != handle || !File.Exists(path)) { return null; } File.SetLastWriteTimeUtc(path, DateTime.UtcNow); return path; } private static string MapLanguage(MyLanguagesEnum lang) => lang switch { MyLanguagesEnum.Czech => "czech", MyLanguagesEnum.German => "german", MyLanguagesEnum.Russian => "russian", MyLanguagesEnum.Spanish_Spain => "spanish", MyLanguagesEnum.French => "french", MyLanguagesEnum.Italian => "italian", MyLanguagesEnum.Danish => "danish", MyLanguagesEnum.Dutch => "dutch", MyLanguagesEnum.Polish => "polish", MyLanguagesEnum.Finnish => "finnish", MyLanguagesEnum.Hungarian => "hungarian", MyLanguagesEnum.Portuguese_Brazil => "brazilian", MyLanguagesEnum.Norwegian => "norwegian", MyLanguagesEnum.Spanish_HispanicAmerica => "latam", MyLanguagesEnum.Swedish => "swedish", MyLanguagesEnum.Romanian => "romanian", MyLanguagesEnum.Ukrainian => "ukrainian", MyLanguagesEnum.Turkish => "turkish", MyLanguagesEnum.ChineseChina => "schinese", MyLanguagesEnum.Japanese => "japanese", _ => "english", }; private record ModItem(ulong Id, string Name, string Summary, string? Description, string ModDescription, string Url, long Created, long Updated, string Author, Task ThubmnailTask); private record WhitelistItem(string Id, string? Tooltip, string Author, string? Description); private record Whitelist(ImmutableArray Mods); }