375 lines
12 KiB
C#
375 lines
12 KiB
C#
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<IImGuiImageService>();
|
|
|
|
private bool _visible;
|
|
private string _searchQuery = string.Empty;
|
|
private Task? _searchTask;
|
|
|
|
private PublishedFileId_t[] _mods = [];
|
|
private ImmutableDictionary<ulong, WhitelistItem> _whitelist = ImmutableDictionary<ulong, WhitelistItem>.Empty;
|
|
|
|
private List<ModItem> _modList = [];
|
|
private ModItem? _selected;
|
|
public HashSet<ulong> 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<HashSet<ulong>>(stream, _options)!;
|
|
}
|
|
else Installed = [];
|
|
|
|
_searchTask = DownloadWhitelist();
|
|
}
|
|
|
|
private async Task DownloadWhitelist()
|
|
{
|
|
using var client = new HttpClient();
|
|
var response =
|
|
await client.GetFromJsonAsync<Whitelist>(
|
|
"https://github.com/sepluginloader/sepluginloader.github.io/raw/refs/heads/main/plugins.json");
|
|
|
|
if (response is null) return;
|
|
|
|
var mods = new List<PublishedFileId_t>();
|
|
var builder = ImmutableDictionary.CreateBuilder<ulong, WhitelistItem>();
|
|
|
|
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<SteamUGCQueryCompleted_t>.Create(call);
|
|
|
|
var list = new List<ModItem>((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<string?> 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<RemoteStorageDownloadUGCResult_t>.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<string?> ThubmnailTask);
|
|
|
|
private record WhitelistItem(string Id, string? Tooltip, string Author, string? Description);
|
|
|
|
private record Whitelist(ImmutableArray<WhitelistItem> Mods);
|
|
} |